You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

http.go 16KB

10 years ago
9 years ago
10 years ago
10 years ago
8 years ago
10 years ago
10 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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. "fmt"
  10. "net/http"
  11. "os"
  12. "os/exec"
  13. "path"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "code.gitea.io/gitea/models"
  19. "code.gitea.io/gitea/modules/auth"
  20. "code.gitea.io/gitea/modules/base"
  21. "code.gitea.io/gitea/modules/context"
  22. "code.gitea.io/gitea/modules/git"
  23. "code.gitea.io/gitea/modules/log"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/timeutil"
  26. )
  27. // HTTP implmentation git smart HTTP protocol
  28. func HTTP(ctx *context.Context) {
  29. if len(setting.Repository.AccessControlAllowOrigin) > 0 {
  30. allowedOrigin := setting.Repository.AccessControlAllowOrigin
  31. // Set CORS headers for browser-based git clients
  32. ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
  33. ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
  34. // Handle preflight OPTIONS request
  35. if ctx.Req.Method == "OPTIONS" {
  36. if allowedOrigin == "*" {
  37. ctx.Status(http.StatusOK)
  38. } else if allowedOrigin == "null" {
  39. ctx.Status(http.StatusForbidden)
  40. } else {
  41. origin := ctx.Req.Header.Get("Origin")
  42. if len(origin) > 0 && origin == allowedOrigin {
  43. ctx.Status(http.StatusOK)
  44. } else {
  45. ctx.Status(http.StatusForbidden)
  46. }
  47. }
  48. return
  49. }
  50. }
  51. username := ctx.Params(":username")
  52. reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
  53. if ctx.Query("go-get") == "1" {
  54. context.EarlyResponseForGoGetMeta(ctx)
  55. return
  56. }
  57. var isPull bool
  58. service := ctx.Query("service")
  59. if service == "git-receive-pack" ||
  60. strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
  61. isPull = false
  62. } else if service == "git-upload-pack" ||
  63. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
  64. isPull = true
  65. } else if service == "git-upload-archive" ||
  66. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
  67. isPull = true
  68. } else {
  69. isPull = (ctx.Req.Method == "GET")
  70. }
  71. var accessMode models.AccessMode
  72. if isPull {
  73. accessMode = models.AccessModeRead
  74. } else {
  75. accessMode = models.AccessModeWrite
  76. }
  77. isWiki := false
  78. var unitType = models.UnitTypeCode
  79. if strings.HasSuffix(reponame, ".wiki") {
  80. isWiki = true
  81. unitType = models.UnitTypeWiki
  82. reponame = reponame[:len(reponame)-5]
  83. }
  84. owner, err := models.GetUserByName(username)
  85. if err != nil {
  86. ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err)
  87. return
  88. }
  89. repo, err := models.GetRepositoryByName(owner.ID, reponame)
  90. if err != nil {
  91. if models.IsErrRepoNotExist(err) {
  92. redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame)
  93. if err == nil {
  94. context.RedirectToRepo(ctx, redirectRepoID)
  95. } else {
  96. ctx.NotFoundOrServerError("GetRepositoryByName", models.IsErrRepoRedirectNotExist, err)
  97. }
  98. } else {
  99. ctx.ServerError("GetRepositoryByName", err)
  100. }
  101. return
  102. }
  103. // Don't allow pushing if the repo is archived
  104. if repo.IsArchived && !isPull {
  105. ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
  106. return
  107. }
  108. // Only public pull don't need auth.
  109. isPublicPull := !repo.IsPrivate && isPull
  110. var (
  111. askAuth = !isPublicPull || setting.Service.RequireSignInView
  112. authUser *models.User
  113. authUsername string
  114. authPasswd string
  115. environ []string
  116. )
  117. // check access
  118. if askAuth {
  119. authUsername = ctx.Req.Header.Get(setting.ReverseProxyAuthUser)
  120. if setting.Service.EnableReverseProxyAuth && len(authUsername) > 0 {
  121. authUser, err = models.GetUserByName(authUsername)
  122. if err != nil {
  123. ctx.HandleText(401, "reverse proxy login error, got error while running GetUserByName")
  124. return
  125. }
  126. } else {
  127. authHead := ctx.Req.Header.Get("Authorization")
  128. if len(authHead) == 0 {
  129. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
  130. ctx.Error(http.StatusUnauthorized)
  131. return
  132. }
  133. auths := strings.Fields(authHead)
  134. // currently check basic auth
  135. // TODO: support digit auth
  136. // FIXME: middlewares/context.go did basic auth check already,
  137. // maybe could use that one.
  138. if len(auths) != 2 || auths[0] != "Basic" {
  139. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  140. return
  141. }
  142. authUsername, authPasswd, err = base.BasicAuthDecode(auths[1])
  143. if err != nil {
  144. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  145. return
  146. }
  147. // Check if username or password is a token
  148. isUsernameToken := len(authPasswd) == 0 || authPasswd == "x-oauth-basic"
  149. // Assume username is token
  150. authToken := authUsername
  151. if !isUsernameToken {
  152. // Assume password is token
  153. authToken = authPasswd
  154. }
  155. uid := auth.CheckOAuthAccessToken(authToken)
  156. if uid != 0 {
  157. ctx.Data["IsApiToken"] = true
  158. authUser, err = models.GetUserByID(uid)
  159. if err != nil {
  160. ctx.ServerError("GetUserByID", err)
  161. return
  162. }
  163. }
  164. // Assume password is a token.
  165. token, err := models.GetAccessTokenBySHA(authToken)
  166. if err == nil {
  167. if isUsernameToken {
  168. authUser, err = models.GetUserByID(token.UID)
  169. if err != nil {
  170. ctx.ServerError("GetUserByID", err)
  171. return
  172. }
  173. } else {
  174. authUser, err = models.GetUserByName(authUsername)
  175. if err != nil {
  176. if models.IsErrUserNotExist(err) {
  177. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  178. } else {
  179. ctx.ServerError("GetUserByName", err)
  180. }
  181. return
  182. }
  183. if authUser.ID != token.UID {
  184. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  185. return
  186. }
  187. }
  188. token.UpdatedUnix = timeutil.TimeStampNow()
  189. if err = models.UpdateAccessToken(token); err != nil {
  190. ctx.ServerError("UpdateAccessToken", err)
  191. }
  192. } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
  193. log.Error("GetAccessTokenBySha: %v", err)
  194. }
  195. if authUser == nil {
  196. // Check username and password
  197. authUser, err = models.UserSignIn(authUsername, authPasswd)
  198. if err != nil {
  199. if models.IsErrUserProhibitLogin(err) {
  200. ctx.HandleText(http.StatusForbidden, "User is not permitted to login")
  201. return
  202. } else if !models.IsErrUserNotExist(err) {
  203. ctx.ServerError("UserSignIn error: %v", err)
  204. return
  205. }
  206. }
  207. if authUser == nil {
  208. ctx.HandleText(http.StatusUnauthorized, "invalid credentials")
  209. return
  210. }
  211. _, err = models.GetTwoFactorByUID(authUser.ID)
  212. if err == nil {
  213. // 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
  214. 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")
  215. return
  216. } else if !models.IsErrTwoFactorNotEnrolled(err) {
  217. ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
  218. return
  219. }
  220. }
  221. }
  222. perm, err := models.GetUserRepoPermission(repo, authUser)
  223. if err != nil {
  224. ctx.ServerError("GetUserRepoPermission", err)
  225. return
  226. }
  227. if !perm.CanAccess(accessMode, unitType) {
  228. ctx.HandleText(http.StatusForbidden, "User permission denied")
  229. return
  230. }
  231. if !isPull && repo.IsMirror {
  232. ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
  233. return
  234. }
  235. environ = []string{
  236. models.EnvRepoUsername + "=" + username,
  237. models.EnvRepoName + "=" + reponame,
  238. models.EnvPusherName + "=" + authUser.Name,
  239. models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
  240. models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID),
  241. }
  242. if !authUser.KeepEmailPrivate {
  243. environ = append(environ, models.EnvPusherEmail+"="+authUser.Email)
  244. }
  245. if isWiki {
  246. environ = append(environ, models.EnvRepoIsWiki+"=true")
  247. } else {
  248. environ = append(environ, models.EnvRepoIsWiki+"=false")
  249. }
  250. }
  251. HTTPBackend(ctx, &serviceConfig{
  252. UploadPack: true,
  253. ReceivePack: true,
  254. Env: environ,
  255. })(ctx.Resp, ctx.Req.Request)
  256. }
  257. type serviceConfig struct {
  258. UploadPack bool
  259. ReceivePack bool
  260. Env []string
  261. }
  262. type serviceHandler struct {
  263. cfg *serviceConfig
  264. w http.ResponseWriter
  265. r *http.Request
  266. dir string
  267. file string
  268. environ []string
  269. }
  270. func (h *serviceHandler) setHeaderNoCache() {
  271. h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  272. h.w.Header().Set("Pragma", "no-cache")
  273. h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  274. }
  275. func (h *serviceHandler) setHeaderCacheForever() {
  276. now := time.Now().Unix()
  277. expires := now + 31536000
  278. h.w.Header().Set("Date", fmt.Sprintf("%d", now))
  279. h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
  280. h.w.Header().Set("Cache-Control", "public, max-age=31536000")
  281. }
  282. func (h *serviceHandler) sendFile(contentType string) {
  283. reqFile := path.Join(h.dir, h.file)
  284. fi, err := os.Stat(reqFile)
  285. if os.IsNotExist(err) {
  286. h.w.WriteHeader(http.StatusNotFound)
  287. return
  288. }
  289. h.w.Header().Set("Content-Type", contentType)
  290. h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
  291. h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
  292. http.ServeFile(h.w, h.r, reqFile)
  293. }
  294. type route struct {
  295. reg *regexp.Regexp
  296. method string
  297. handler func(serviceHandler)
  298. }
  299. var routes = []route{
  300. {regexp.MustCompile(`(.*?)/git-upload-pack$`), "POST", serviceUploadPack},
  301. {regexp.MustCompile(`(.*?)/git-receive-pack$`), "POST", serviceReceivePack},
  302. {regexp.MustCompile(`(.*?)/info/refs$`), "GET", getInfoRefs},
  303. {regexp.MustCompile(`(.*?)/HEAD$`), "GET", getTextFile},
  304. {regexp.MustCompile(`(.*?)/objects/info/alternates$`), "GET", getTextFile},
  305. {regexp.MustCompile(`(.*?)/objects/info/http-alternates$`), "GET", getTextFile},
  306. {regexp.MustCompile(`(.*?)/objects/info/packs$`), "GET", getInfoPacks},
  307. {regexp.MustCompile(`(.*?)/objects/info/[^/]*$`), "GET", getTextFile},
  308. {regexp.MustCompile(`(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$`), "GET", getLooseObject},
  309. {regexp.MustCompile(`(.*?)/objects/pack/pack-[0-9a-f]{40}\.pack$`), "GET", getPackFile},
  310. {regexp.MustCompile(`(.*?)/objects/pack/pack-[0-9a-f]{40}\.idx$`), "GET", getIdxFile},
  311. }
  312. func getGitConfig(option, dir string) string {
  313. out, err := git.NewCommand("config", option).RunInDir(dir)
  314. if err != nil {
  315. log.Error("%v - %s", err, out)
  316. }
  317. return out[0 : len(out)-1]
  318. }
  319. func getConfigSetting(service, dir string) bool {
  320. service = strings.Replace(service, "-", "", -1)
  321. setting := getGitConfig("http."+service, dir)
  322. if service == "uploadpack" {
  323. return setting != "false"
  324. }
  325. return setting == "true"
  326. }
  327. func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
  328. if checkContentType {
  329. if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
  330. return false
  331. }
  332. }
  333. if !(service == "upload-pack" || service == "receive-pack") {
  334. return false
  335. }
  336. if service == "receive-pack" {
  337. return h.cfg.ReceivePack
  338. }
  339. if service == "upload-pack" {
  340. return h.cfg.UploadPack
  341. }
  342. return getConfigSetting(service, h.dir)
  343. }
  344. func serviceRPC(h serviceHandler, service string) {
  345. defer func() {
  346. if err := h.r.Body.Close(); err != nil {
  347. log.Error("serviceRPC: Close: %v", err)
  348. }
  349. }()
  350. if !hasAccess(service, h, true) {
  351. h.w.WriteHeader(http.StatusUnauthorized)
  352. return
  353. }
  354. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  355. var err error
  356. var reqBody = h.r.Body
  357. // Handle GZIP.
  358. if h.r.Header.Get("Content-Encoding") == "gzip" {
  359. reqBody, err = gzip.NewReader(reqBody)
  360. if err != nil {
  361. log.Error("Fail to create gzip reader: %v", err)
  362. h.w.WriteHeader(http.StatusInternalServerError)
  363. return
  364. }
  365. }
  366. // set this for allow pre-receive and post-receive execute
  367. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  368. var stderr bytes.Buffer
  369. cmd := exec.Command(git.GitExecutable, service, "--stateless-rpc", h.dir)
  370. cmd.Dir = h.dir
  371. if service == "receive-pack" {
  372. cmd.Env = append(os.Environ(), h.environ...)
  373. }
  374. cmd.Stdout = h.w
  375. cmd.Stdin = reqBody
  376. cmd.Stderr = &stderr
  377. if err := cmd.Run(); err != nil {
  378. log.Error("Fail to serve RPC(%s): %v - %s", service, err, stderr.String())
  379. return
  380. }
  381. }
  382. func serviceUploadPack(h serviceHandler) {
  383. serviceRPC(h, "upload-pack")
  384. }
  385. func serviceReceivePack(h serviceHandler) {
  386. serviceRPC(h, "receive-pack")
  387. }
  388. func getServiceType(r *http.Request) string {
  389. serviceType := r.FormValue("service")
  390. if !strings.HasPrefix(serviceType, "git-") {
  391. return ""
  392. }
  393. return strings.Replace(serviceType, "git-", "", 1)
  394. }
  395. func updateServerInfo(dir string) []byte {
  396. out, err := git.NewCommand("update-server-info").RunInDirBytes(dir)
  397. if err != nil {
  398. log.Error(fmt.Sprintf("%v - %s", err, string(out)))
  399. }
  400. return out
  401. }
  402. func packetWrite(str string) []byte {
  403. s := strconv.FormatInt(int64(len(str)+4), 16)
  404. if len(s)%4 != 0 {
  405. s = strings.Repeat("0", 4-len(s)%4) + s
  406. }
  407. return []byte(s + str)
  408. }
  409. func getInfoRefs(h serviceHandler) {
  410. h.setHeaderNoCache()
  411. if hasAccess(getServiceType(h.r), h, false) {
  412. service := getServiceType(h.r)
  413. refs, err := git.NewCommand(service, "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(h.dir)
  414. if err != nil {
  415. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  416. }
  417. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  418. h.w.WriteHeader(http.StatusOK)
  419. _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
  420. _, _ = h.w.Write([]byte("0000"))
  421. _, _ = h.w.Write(refs)
  422. } else {
  423. updateServerInfo(h.dir)
  424. h.sendFile("text/plain; charset=utf-8")
  425. }
  426. }
  427. func getTextFile(h serviceHandler) {
  428. h.setHeaderNoCache()
  429. h.sendFile("text/plain")
  430. }
  431. func getInfoPacks(h serviceHandler) {
  432. h.setHeaderCacheForever()
  433. h.sendFile("text/plain; charset=utf-8")
  434. }
  435. func getLooseObject(h serviceHandler) {
  436. h.setHeaderCacheForever()
  437. h.sendFile("application/x-git-loose-object")
  438. }
  439. func getPackFile(h serviceHandler) {
  440. h.setHeaderCacheForever()
  441. h.sendFile("application/x-git-packed-objects")
  442. }
  443. func getIdxFile(h serviceHandler) {
  444. h.setHeaderCacheForever()
  445. h.sendFile("application/x-git-packed-objects-toc")
  446. }
  447. func getGitRepoPath(subdir string) (string, error) {
  448. if !strings.HasSuffix(subdir, ".git") {
  449. subdir += ".git"
  450. }
  451. fpath := path.Join(setting.RepoRootPath, subdir)
  452. if _, err := os.Stat(fpath); os.IsNotExist(err) {
  453. return "", err
  454. }
  455. return fpath, nil
  456. }
  457. // HTTPBackend middleware for git smart HTTP protocol
  458. func HTTPBackend(ctx *context.Context, cfg *serviceConfig) http.HandlerFunc {
  459. return func(w http.ResponseWriter, r *http.Request) {
  460. for _, route := range routes {
  461. r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
  462. if m := route.reg.FindStringSubmatch(r.URL.Path); m != nil {
  463. if setting.Repository.DisableHTTPGit {
  464. w.WriteHeader(http.StatusForbidden)
  465. _, err := w.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
  466. if err != nil {
  467. log.Error(err.Error())
  468. }
  469. return
  470. }
  471. if route.method != r.Method {
  472. if r.Proto == "HTTP/1.1" {
  473. w.WriteHeader(http.StatusMethodNotAllowed)
  474. _, err := w.Write([]byte("Method Not Allowed"))
  475. if err != nil {
  476. log.Error(err.Error())
  477. }
  478. } else {
  479. w.WriteHeader(http.StatusBadRequest)
  480. _, err := w.Write([]byte("Bad Request"))
  481. if err != nil {
  482. log.Error(err.Error())
  483. }
  484. }
  485. return
  486. }
  487. file := strings.Replace(r.URL.Path, m[1]+"/", "", 1)
  488. dir, err := getGitRepoPath(m[1])
  489. if err != nil {
  490. log.Error(err.Error())
  491. ctx.NotFound("HTTPBackend", err)
  492. return
  493. }
  494. route.handler(serviceHandler{cfg, w, r, dir, file, cfg.Env})
  495. return
  496. }
  497. }
  498. ctx.NotFound("HTTPBackend", nil)
  499. }
  500. }