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.

516 lines
13 KiB

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