Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

10 anos atrás
9 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
Add single sign-on support via SSPI on Windows (#8463) * Add single sign-on support via SSPI on Windows * Ensure plugins implement interface * Ensure plugins implement interface * Move functions used only by the SSPI auth method to sspi_windows.go * Field SSPISeparatorReplacement of AuthenticationForm should not be required via binding, as binding will insist the field is non-empty even if another login type is selected * Fix breaking of oauth authentication on download links. Do not create new session with SSPI authentication on download links. * Update documentation for the new 'SPNEGO with SSPI' login source * Mention in documentation that ROOT_URL should contain the FQDN of the server * Make sure that Contexter is not checking for active login sources when the ORM engine is not initialized (eg. when installing) * Always initialize and free SSO methods, even if they are not enabled, as a method can be activated while the app is running (from Authentication sources) * Add option in SSPIConfig for removing of domains from logon names * Update helper text for StripDomainNames option * Make sure handleSignIn() is called after a new user object is created by SSPI auth method * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Only make a query to the DB to check if SSPI is enabled on handlers that need that information for templates * Remove code duplication * Log errors in ActiveLoginSources Co-Authored-By: Lauris BH <lauris@nix.lv> * Revert suffix of randomly generated E-mails for Reverse proxy authentication Co-Authored-By: Lauris BH <lauris@nix.lv> * Revert unneeded white-space change in template Co-Authored-By: Lauris BH <lauris@nix.lv> * Add copyright comments at the top of new files * Use loopback name for randomly generated emails * Add locale tag for the SSPISeparatorReplacement field with proper casing * Revert casing of SSPISeparatorReplacement field in locale file, moving it up, next to other form fields * Update docs/content/doc/features/authentication.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Remove Priority() method and define the order in which SSO auth methods should be executed in one place * Log authenticated username only if it's not empty * Rephrase helper text for automatic creation of users * Return error if more than one active SSPI auth source is found * Change newUser() function to return error, letting caller log/handle the error * Move isPublicResource, isPublicPage and handleSignIn functions outside SSPI auth method to allow other SSO methods to reuse them if needed * Refactor initialization of the list containing SSO auth methods * Validate SSPI settings on POST * Change SSPI to only perform authentication on its own login page, API paths and download links. Leave Toggle middleware to redirect non authenticated users to login page * Make 'Default language' in SSPI config empty, unless changed by admin * Show error if admin tries to add a second authentication source of type SSPI * Simplify declaration of global variable * Rebuild gitgraph.js on Linux * Make sure config values containing only whitespace are not accepted
4 anos atrás
10 anos atrás
8 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
8 anos atrás
8 anos atrás
8 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
Add single sign-on support via SSPI on Windows (#8463) * Add single sign-on support via SSPI on Windows * Ensure plugins implement interface * Ensure plugins implement interface * Move functions used only by the SSPI auth method to sspi_windows.go * Field SSPISeparatorReplacement of AuthenticationForm should not be required via binding, as binding will insist the field is non-empty even if another login type is selected * Fix breaking of oauth authentication on download links. Do not create new session with SSPI authentication on download links. * Update documentation for the new 'SPNEGO with SSPI' login source * Mention in documentation that ROOT_URL should contain the FQDN of the server * Make sure that Contexter is not checking for active login sources when the ORM engine is not initialized (eg. when installing) * Always initialize and free SSO methods, even if they are not enabled, as a method can be activated while the app is running (from Authentication sources) * Add option in SSPIConfig for removing of domains from logon names * Update helper text for StripDomainNames option * Make sure handleSignIn() is called after a new user object is created by SSPI auth method * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Remove default value from text of form field helper Co-Authored-By: Lauris BH <lauris@nix.lv> * Only make a query to the DB to check if SSPI is enabled on handlers that need that information for templates * Remove code duplication * Log errors in ActiveLoginSources Co-Authored-By: Lauris BH <lauris@nix.lv> * Revert suffix of randomly generated E-mails for Reverse proxy authentication Co-Authored-By: Lauris BH <lauris@nix.lv> * Revert unneeded white-space change in template Co-Authored-By: Lauris BH <lauris@nix.lv> * Add copyright comments at the top of new files * Use loopback name for randomly generated emails * Add locale tag for the SSPISeparatorReplacement field with proper casing * Revert casing of SSPISeparatorReplacement field in locale file, moving it up, next to other form fields * Update docs/content/doc/features/authentication.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Remove Priority() method and define the order in which SSO auth methods should be executed in one place * Log authenticated username only if it's not empty * Rephrase helper text for automatic creation of users * Return error if more than one active SSPI auth source is found * Change newUser() function to return error, letting caller log/handle the error * Move isPublicResource, isPublicPage and handleSignIn functions outside SSPI auth method to allow other SSO methods to reuse them if needed * Refactor initialization of the list containing SSO auth methods * Validate SSPI settings on POST * Change SSPI to only perform authentication on its own login page, API paths and download links. Leave Toggle middleware to redirect non authenticated users to login page * Make 'Default language' in SSPI config empty, unless changed by admin * Show error if admin tries to add a second authentication source of type SSPI * Simplify declaration of global variable * Rebuild gitgraph.js on Linux * Make sure config values containing only whitespace are not accepted
4 anos atrás
10 anos atrás
10 anos atrás
8 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
8 anos atrás
8 anos atrás
8 anos atrás
8 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
9 anos atrás
9 anos atrás
9 anos atrás
9 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
10 anos atrás
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "bytes"
  8. "compress/gzip"
  9. gocontext "context"
  10. "fmt"
  11. "io/ioutil"
  12. "net/http"
  13. "os"
  14. "os/exec"
  15. "path"
  16. "regexp"
  17. "strconv"
  18. "strings"
  19. "sync"
  20. "time"
  21. "code.gitea.io/gitea/models"
  22. "code.gitea.io/gitea/modules/auth/sso"
  23. "code.gitea.io/gitea/modules/base"
  24. "code.gitea.io/gitea/modules/context"
  25. "code.gitea.io/gitea/modules/git"
  26. "code.gitea.io/gitea/modules/log"
  27. "code.gitea.io/gitea/modules/process"
  28. "code.gitea.io/gitea/modules/setting"
  29. "code.gitea.io/gitea/modules/structs"
  30. "code.gitea.io/gitea/modules/timeutil"
  31. repo_service "code.gitea.io/gitea/services/repository"
  32. )
  33. // HTTP implmentation git smart HTTP protocol
  34. func HTTP(ctx *context.Context) {
  35. if len(setting.Repository.AccessControlAllowOrigin) > 0 {
  36. allowedOrigin := setting.Repository.AccessControlAllowOrigin
  37. // Set CORS headers for browser-based git clients
  38. ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
  39. ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
  40. // Handle preflight OPTIONS request
  41. if ctx.Req.Method == "OPTIONS" {
  42. if allowedOrigin == "*" {
  43. ctx.Status(http.StatusOK)
  44. } else if allowedOrigin == "null" {
  45. ctx.Status(http.StatusForbidden)
  46. } else {
  47. origin := ctx.Req.Header.Get("Origin")
  48. if len(origin) > 0 && origin == allowedOrigin {
  49. ctx.Status(http.StatusOK)
  50. } else {
  51. ctx.Status(http.StatusForbidden)
  52. }
  53. }
  54. return
  55. }
  56. }
  57. username := ctx.Params(":username")
  58. reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
  59. if ctx.Query("go-get") == "1" {
  60. context.EarlyResponseForGoGetMeta(ctx)
  61. return
  62. }
  63. var isPull, receivePack bool
  64. service := ctx.Query("service")
  65. if service == "git-receive-pack" ||
  66. strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
  67. isPull = false
  68. receivePack = true
  69. } else if service == "git-upload-pack" ||
  70. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
  71. isPull = true
  72. } else if service == "git-upload-archive" ||
  73. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
  74. isPull = true
  75. } else {
  76. isPull = (ctx.Req.Method == "GET")
  77. }
  78. var accessMode models.AccessMode
  79. if isPull {
  80. accessMode = models.AccessModeRead
  81. } else {
  82. accessMode = models.AccessModeWrite
  83. }
  84. isWiki := false
  85. var unitType = models.UnitTypeCode
  86. if strings.HasSuffix(reponame, ".wiki") {
  87. isWiki = true
  88. unitType = models.UnitTypeWiki
  89. reponame = reponame[:len(reponame)-5]
  90. }
  91. owner, err := models.GetUserByName(username)
  92. if err != nil {
  93. ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err)
  94. return
  95. }
  96. repoExist := true
  97. repo, err := models.GetRepositoryByName(owner.ID, reponame)
  98. if err != nil {
  99. if models.IsErrRepoNotExist(err) {
  100. if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
  101. context.RedirectToRepo(ctx, redirectRepoID)
  102. return
  103. }
  104. repoExist = false
  105. } else {
  106. ctx.ServerError("GetRepositoryByName", err)
  107. return
  108. }
  109. }
  110. // Don't allow pushing if the repo is archived
  111. if repoExist && repo.IsArchived && !isPull {
  112. ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
  113. return
  114. }
  115. // Only public pull don't need auth.
  116. isPublicPull := repoExist && !repo.IsPrivate && isPull
  117. var (
  118. askAuth = !isPublicPull || setting.Service.RequireSignInView
  119. authUser *models.User
  120. authUsername string
  121. authPasswd string
  122. environ []string
  123. )
  124. // don't allow anonymous pulls if organization is not public
  125. if isPublicPull {
  126. if err := repo.GetOwner(); err != nil {
  127. ctx.ServerError("GetOwner", err)
  128. return
  129. }
  130. askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
  131. }
  132. // check access
  133. if askAuth {
  134. authUsername = ctx.Req.Header.Get(setting.ReverseProxyAuthUser)
  135. if setting.Service.EnableReverseProxyAuth && len(authUsername) > 0 {
  136. authUser, err = models.GetUserByName(authUsername)
  137. if err != nil {
  138. ctx.HandleText(401, "reverse proxy login error, got error while running GetUserByName")
  139. return
  140. }
  141. } else {
  142. authHead := ctx.Req.Header.Get("Authorization")
  143. if len(authHead) == 0 {
  144. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
  145. ctx.Error(http.StatusUnauthorized)
  146. return
  147. }
  148. auths := strings.Fields(authHead)
  149. // currently check basic auth
  150. // TODO: support digit auth
  151. // FIXME: middlewares/context.go did basic auth check already,
  152. // maybe could use that one.
  153. if len(auths) != 2 || auths[0] != "Basic" {
  154. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  155. return
  156. }
  157. authUsername, authPasswd, err = base.BasicAuthDecode(auths[1])
  158. if err != nil {
  159. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  160. return
  161. }
  162. // Check if username or password is a token
  163. isUsernameToken := len(authPasswd) == 0 || authPasswd == "x-oauth-basic"
  164. // Assume username is token
  165. authToken := authUsername
  166. if !isUsernameToken {
  167. // Assume password is token
  168. authToken = authPasswd
  169. }
  170. uid := sso.CheckOAuthAccessToken(authToken)
  171. if uid != 0 {
  172. ctx.Data["IsApiToken"] = true
  173. authUser, err = models.GetUserByID(uid)
  174. if err != nil {
  175. ctx.ServerError("GetUserByID", err)
  176. return
  177. }
  178. }
  179. // Assume password is a token.
  180. token, err := models.GetAccessTokenBySHA(authToken)
  181. if err == nil {
  182. if isUsernameToken {
  183. authUser, err = models.GetUserByID(token.UID)
  184. if err != nil {
  185. ctx.ServerError("GetUserByID", err)
  186. return
  187. }
  188. } else {
  189. authUser, err = models.GetUserByName(authUsername)
  190. if err != nil {
  191. if models.IsErrUserNotExist(err) {
  192. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  193. } else {
  194. ctx.ServerError("GetUserByName", err)
  195. }
  196. return
  197. }
  198. if authUser.ID != token.UID {
  199. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  200. return
  201. }
  202. }
  203. token.UpdatedUnix = timeutil.TimeStampNow()
  204. if err = models.UpdateAccessToken(token); err != nil {
  205. ctx.ServerError("UpdateAccessToken", err)
  206. }
  207. } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
  208. log.Error("GetAccessTokenBySha: %v", err)
  209. }
  210. if authUser == nil {
  211. // Check username and password
  212. authUser, err = models.UserSignIn(authUsername, authPasswd)
  213. if err != nil {
  214. if models.IsErrUserProhibitLogin(err) {
  215. ctx.HandleText(http.StatusForbidden, "User is not permitted to login")
  216. return
  217. } else if !models.IsErrUserNotExist(err) {
  218. ctx.ServerError("UserSignIn error: %v", err)
  219. return
  220. }
  221. }
  222. if authUser == nil {
  223. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  224. return
  225. }
  226. _, err = models.GetTwoFactorByUID(authUser.ID)
  227. if err == nil {
  228. // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
  229. ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
  230. return
  231. } else if !models.IsErrTwoFactorNotEnrolled(err) {
  232. ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
  233. return
  234. }
  235. }
  236. }
  237. if repoExist {
  238. perm, err := models.GetUserRepoPermission(repo, authUser)
  239. if err != nil {
  240. ctx.ServerError("GetUserRepoPermission", err)
  241. return
  242. }
  243. if !perm.CanAccess(accessMode, unitType) {
  244. ctx.HandleText(http.StatusForbidden, "User permission denied")
  245. return
  246. }
  247. if !isPull && repo.IsMirror {
  248. ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
  249. return
  250. }
  251. }
  252. environ = []string{
  253. models.EnvRepoUsername + "=" + username,
  254. models.EnvRepoName + "=" + reponame,
  255. models.EnvPusherName + "=" + authUser.Name,
  256. models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
  257. models.EnvIsDeployKey + "=false",
  258. }
  259. if !authUser.KeepEmailPrivate {
  260. environ = append(environ, models.EnvPusherEmail+"="+authUser.Email)
  261. }
  262. if isWiki {
  263. environ = append(environ, models.EnvRepoIsWiki+"=true")
  264. } else {
  265. environ = append(environ, models.EnvRepoIsWiki+"=false")
  266. }
  267. }
  268. if !repoExist {
  269. if !receivePack {
  270. ctx.HandleText(http.StatusNotFound, "Repository not found")
  271. return
  272. }
  273. if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
  274. ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
  275. return
  276. }
  277. if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
  278. ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
  279. return
  280. }
  281. // Return dummy payload if GET receive-pack
  282. if ctx.Req.Method == http.MethodGet {
  283. dummyInfoRefs(ctx)
  284. return
  285. }
  286. repo, err = repo_service.PushCreateRepo(authUser, owner, reponame)
  287. if err != nil {
  288. log.Error("pushCreateRepo: %v", err)
  289. ctx.Status(http.StatusNotFound)
  290. return
  291. }
  292. }
  293. if isWiki {
  294. // Ensure the wiki is enabled before we allow access to it
  295. if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil {
  296. if models.IsErrUnitTypeNotExist(err) {
  297. ctx.HandleText(http.StatusForbidden, "repository wiki is disabled")
  298. return
  299. }
  300. log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
  301. ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
  302. return
  303. }
  304. }
  305. environ = append(environ, models.ProtectedBranchRepoID+fmt.Sprintf("=%d", repo.ID))
  306. w := ctx.Resp
  307. r := ctx.Req.Request
  308. cfg := &serviceConfig{
  309. UploadPack: true,
  310. ReceivePack: true,
  311. Env: environ,
  312. }
  313. for _, route := range routes {
  314. r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
  315. if m := route.reg.FindStringSubmatch(r.URL.Path); m != nil {
  316. if setting.Repository.DisableHTTPGit {
  317. w.WriteHeader(http.StatusForbidden)
  318. _, err := w.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
  319. if err != nil {
  320. log.Error(err.Error())
  321. }
  322. return
  323. }
  324. if route.method != r.Method {
  325. if r.Proto == "HTTP/1.1" {
  326. w.WriteHeader(http.StatusMethodNotAllowed)
  327. _, err := w.Write([]byte("Method Not Allowed"))
  328. if err != nil {
  329. log.Error(err.Error())
  330. }
  331. } else {
  332. w.WriteHeader(http.StatusBadRequest)
  333. _, err := w.Write([]byte("Bad Request"))
  334. if err != nil {
  335. log.Error(err.Error())
  336. }
  337. }
  338. return
  339. }
  340. file := strings.Replace(r.URL.Path, m[1]+"/", "", 1)
  341. dir, err := getGitRepoPath(m[1])
  342. if err != nil {
  343. log.Error(err.Error())
  344. ctx.NotFound("Smart Git HTTP", err)
  345. return
  346. }
  347. route.handler(serviceHandler{cfg, w, r, dir, file, cfg.Env})
  348. return
  349. }
  350. }
  351. ctx.NotFound("Smart Git HTTP", nil)
  352. }
  353. var (
  354. infoRefsCache []byte
  355. infoRefsOnce sync.Once
  356. )
  357. func dummyInfoRefs(ctx *context.Context) {
  358. infoRefsOnce.Do(func() {
  359. tmpDir, err := ioutil.TempDir(os.TempDir(), "gitea-info-refs-cache")
  360. if err != nil {
  361. log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
  362. return
  363. }
  364. defer func() {
  365. if err := os.RemoveAll(tmpDir); err != nil {
  366. log.Error("RemoveAll: %v", err)
  367. }
  368. }()
  369. if err := git.InitRepository(tmpDir, true); err != nil {
  370. log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
  371. return
  372. }
  373. refs, err := git.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(tmpDir)
  374. if err != nil {
  375. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  376. }
  377. log.Debug("populating infoRefsCache: \n%s", string(refs))
  378. infoRefsCache = refs
  379. })
  380. ctx.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  381. ctx.Header().Set("Pragma", "no-cache")
  382. ctx.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  383. ctx.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement")
  384. _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
  385. _, _ = ctx.Write([]byte("0000"))
  386. _, _ = ctx.Write(infoRefsCache)
  387. }
  388. type serviceConfig struct {
  389. UploadPack bool
  390. ReceivePack bool
  391. Env []string
  392. }
  393. type serviceHandler struct {
  394. cfg *serviceConfig
  395. w http.ResponseWriter
  396. r *http.Request
  397. dir string
  398. file string
  399. environ []string
  400. }
  401. func (h *serviceHandler) setHeaderNoCache() {
  402. h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  403. h.w.Header().Set("Pragma", "no-cache")
  404. h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  405. }
  406. func (h *serviceHandler) setHeaderCacheForever() {
  407. now := time.Now().Unix()
  408. expires := now + 31536000
  409. h.w.Header().Set("Date", fmt.Sprintf("%d", now))
  410. h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
  411. h.w.Header().Set("Cache-Control", "public, max-age=31536000")
  412. }
  413. func (h *serviceHandler) sendFile(contentType string) {
  414. reqFile := path.Join(h.dir, h.file)
  415. fi, err := os.Stat(reqFile)
  416. if os.IsNotExist(err) {
  417. h.w.WriteHeader(http.StatusNotFound)
  418. return
  419. }
  420. h.w.Header().Set("Content-Type", contentType)
  421. h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
  422. h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
  423. http.ServeFile(h.w, h.r, reqFile)
  424. }
  425. type route struct {
  426. reg *regexp.Regexp
  427. method string
  428. handler func(serviceHandler)
  429. }
  430. var routes = []route{
  431. {regexp.MustCompile(`(.*?)/git-upload-pack$`), "POST", serviceUploadPack},
  432. {regexp.MustCompile(`(.*?)/git-receive-pack$`), "POST", serviceReceivePack},
  433. {regexp.MustCompile(`(.*?)/info/refs$`), "GET", getInfoRefs},
  434. {regexp.MustCompile(`(.*?)/HEAD$`), "GET", getTextFile},
  435. {regexp.MustCompile(`(.*?)/objects/info/alternates$`), "GET", getTextFile},
  436. {regexp.MustCompile(`(.*?)/objects/info/http-alternates$`), "GET", getTextFile},
  437. {regexp.MustCompile(`(.*?)/objects/info/packs$`), "GET", getInfoPacks},
  438. {regexp.MustCompile(`(.*?)/objects/info/[^/]*$`), "GET", getTextFile},
  439. {regexp.MustCompile(`(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$`), "GET", getLooseObject},
  440. {regexp.MustCompile(`(.*?)/objects/pack/pack-[0-9a-f]{40}\.pack$`), "GET", getPackFile},
  441. {regexp.MustCompile(`(.*?)/objects/pack/pack-[0-9a-f]{40}\.idx$`), "GET", getIdxFile},
  442. }
  443. func getGitConfig(option, dir string) string {
  444. out, err := git.NewCommand("config", option).RunInDir(dir)
  445. if err != nil {
  446. log.Error("%v - %s", err, out)
  447. }
  448. return out[0 : len(out)-1]
  449. }
  450. func getConfigSetting(service, dir string) bool {
  451. service = strings.Replace(service, "-", "", -1)
  452. setting := getGitConfig("http."+service, dir)
  453. if service == "uploadpack" {
  454. return setting != "false"
  455. }
  456. return setting == "true"
  457. }
  458. func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
  459. if checkContentType {
  460. if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
  461. return false
  462. }
  463. }
  464. if !(service == "upload-pack" || service == "receive-pack") {
  465. return false
  466. }
  467. if service == "receive-pack" {
  468. return h.cfg.ReceivePack
  469. }
  470. if service == "upload-pack" {
  471. return h.cfg.UploadPack
  472. }
  473. return getConfigSetting(service, h.dir)
  474. }
  475. func serviceRPC(h serviceHandler, service string) {
  476. defer func() {
  477. if err := h.r.Body.Close(); err != nil {
  478. log.Error("serviceRPC: Close: %v", err)
  479. }
  480. }()
  481. if !hasAccess(service, h, true) {
  482. h.w.WriteHeader(http.StatusUnauthorized)
  483. return
  484. }
  485. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  486. var err error
  487. var reqBody = h.r.Body
  488. // Handle GZIP.
  489. if h.r.Header.Get("Content-Encoding") == "gzip" {
  490. reqBody, err = gzip.NewReader(reqBody)
  491. if err != nil {
  492. log.Error("Fail to create gzip reader: %v", err)
  493. h.w.WriteHeader(http.StatusInternalServerError)
  494. return
  495. }
  496. }
  497. // set this for allow pre-receive and post-receive execute
  498. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  499. ctx, cancel := gocontext.WithCancel(git.DefaultContext)
  500. defer cancel()
  501. var stderr bytes.Buffer
  502. cmd := exec.CommandContext(ctx, git.GitExecutable, service, "--stateless-rpc", h.dir)
  503. cmd.Dir = h.dir
  504. if service == "receive-pack" {
  505. cmd.Env = append(os.Environ(), h.environ...)
  506. }
  507. cmd.Stdout = h.w
  508. cmd.Stdin = reqBody
  509. cmd.Stderr = &stderr
  510. pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir), cancel)
  511. defer process.GetManager().Remove(pid)
  512. if err := cmd.Run(); err != nil {
  513. log.Error("Fail to serve RPC(%s): %v - %s", service, err, stderr.String())
  514. return
  515. }
  516. }
  517. func serviceUploadPack(h serviceHandler) {
  518. serviceRPC(h, "upload-pack")
  519. }
  520. func serviceReceivePack(h serviceHandler) {
  521. serviceRPC(h, "receive-pack")
  522. }
  523. func getServiceType(r *http.Request) string {
  524. serviceType := r.FormValue("service")
  525. if !strings.HasPrefix(serviceType, "git-") {
  526. return ""
  527. }
  528. return strings.Replace(serviceType, "git-", "", 1)
  529. }
  530. func updateServerInfo(dir string) []byte {
  531. out, err := git.NewCommand("update-server-info").RunInDirBytes(dir)
  532. if err != nil {
  533. log.Error(fmt.Sprintf("%v - %s", err, string(out)))
  534. }
  535. return out
  536. }
  537. func packetWrite(str string) []byte {
  538. s := strconv.FormatInt(int64(len(str)+4), 16)
  539. if len(s)%4 != 0 {
  540. s = strings.Repeat("0", 4-len(s)%4) + s
  541. }
  542. return []byte(s + str)
  543. }
  544. func getInfoRefs(h serviceHandler) {
  545. h.setHeaderNoCache()
  546. if hasAccess(getServiceType(h.r), h, false) {
  547. service := getServiceType(h.r)
  548. refs, err := git.NewCommand(service, "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(h.dir)
  549. if err != nil {
  550. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  551. }
  552. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  553. h.w.WriteHeader(http.StatusOK)
  554. _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
  555. _, _ = h.w.Write([]byte("0000"))
  556. _, _ = h.w.Write(refs)
  557. } else {
  558. updateServerInfo(h.dir)
  559. h.sendFile("text/plain; charset=utf-8")
  560. }
  561. }
  562. func getTextFile(h serviceHandler) {
  563. h.setHeaderNoCache()
  564. h.sendFile("text/plain")
  565. }
  566. func getInfoPacks(h serviceHandler) {
  567. h.setHeaderCacheForever()
  568. h.sendFile("text/plain; charset=utf-8")
  569. }
  570. func getLooseObject(h serviceHandler) {
  571. h.setHeaderCacheForever()
  572. h.sendFile("application/x-git-loose-object")
  573. }
  574. func getPackFile(h serviceHandler) {
  575. h.setHeaderCacheForever()
  576. h.sendFile("application/x-git-packed-objects")
  577. }
  578. func getIdxFile(h serviceHandler) {
  579. h.setHeaderCacheForever()
  580. h.sendFile("application/x-git-packed-objects-toc")
  581. }
  582. func getGitRepoPath(subdir string) (string, error) {
  583. if !strings.HasSuffix(subdir, ".git") {
  584. subdir += ".git"
  585. }
  586. fpath := path.Join(setting.RepoRootPath, subdir)
  587. if _, err := os.Stat(fpath); os.IsNotExist(err) {
  588. return "", err
  589. }
  590. return fpath, nil
  591. }