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.

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