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.

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