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