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 13KB

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