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.

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