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

http.go 15 kB

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