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.

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