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.

478 lines
13 KiB

10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 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
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 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
  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. "net/http"
  10. "os"
  11. "os/exec"
  12. "path"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "code.gitea.io/gitea/models"
  18. "code.gitea.io/gitea/modules/base"
  19. "code.gitea.io/gitea/modules/context"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/setting"
  22. )
  23. // HTTP implmentation git smart HTTP protocol
  24. func HTTP(ctx *context.Context) {
  25. username := ctx.Params(":username")
  26. reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
  27. var isPull bool
  28. service := ctx.Query("service")
  29. if service == "git-receive-pack" ||
  30. strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
  31. isPull = false
  32. } else if service == "git-upload-pack" ||
  33. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
  34. isPull = true
  35. } else if service == "git-upload-archive" ||
  36. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
  37. isPull = true
  38. } else {
  39. isPull = (ctx.Req.Method == "GET")
  40. }
  41. var accessMode models.AccessMode
  42. if isPull {
  43. accessMode = models.AccessModeRead
  44. } else {
  45. accessMode = models.AccessModeWrite
  46. }
  47. isWiki := false
  48. if strings.HasSuffix(reponame, ".wiki") {
  49. isWiki = true
  50. reponame = reponame[:len(reponame)-5]
  51. }
  52. repoUser, err := models.GetUserByName(username)
  53. if err != nil {
  54. if models.IsErrUserNotExist(err) {
  55. ctx.Handle(http.StatusNotFound, "GetUserByName", nil)
  56. } else {
  57. ctx.Handle(http.StatusInternalServerError, "GetUserByName", err)
  58. }
  59. return
  60. }
  61. repo, err := models.GetRepositoryByName(repoUser.ID, reponame)
  62. if err != nil {
  63. if models.IsErrRepoNotExist(err) {
  64. ctx.Handle(http.StatusNotFound, "GetRepositoryByName", nil)
  65. } else {
  66. ctx.Handle(http.StatusInternalServerError, "GetRepositoryByName", err)
  67. }
  68. return
  69. }
  70. // Only public pull don't need auth.
  71. isPublicPull := !repo.IsPrivate && isPull
  72. var (
  73. askAuth = !isPublicPull || setting.Service.RequireSignInView
  74. authUser *models.User
  75. authUsername string
  76. authPasswd string
  77. environ []string
  78. )
  79. // check access
  80. if askAuth {
  81. if setting.Service.EnableReverseProxyAuth {
  82. authUsername = ctx.Req.Header.Get(setting.ReverseProxyAuthUser)
  83. if len(authUsername) == 0 {
  84. ctx.HandleText(401, "reverse proxy login error. authUsername empty")
  85. return
  86. }
  87. authUser, err = models.GetUserByName(authUsername)
  88. if err != nil {
  89. ctx.HandleText(401, "reverse proxy login error, got error while running GetUserByName")
  90. return
  91. }
  92. } else {
  93. authHead := ctx.Req.Header.Get("Authorization")
  94. if len(authHead) == 0 {
  95. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
  96. ctx.Error(http.StatusUnauthorized)
  97. return
  98. }
  99. auths := strings.Fields(authHead)
  100. // currently check basic auth
  101. // TODO: support digit auth
  102. // FIXME: middlewares/context.go did basic auth check already,
  103. // maybe could use that one.
  104. if len(auths) != 2 || auths[0] != "Basic" {
  105. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  106. return
  107. }
  108. authUsername, authPasswd, err = base.BasicAuthDecode(auths[1])
  109. if err != nil {
  110. ctx.HandleText(http.StatusUnauthorized, "no basic auth and digit auth")
  111. return
  112. }
  113. authUser, err = models.UserSignIn(authUsername, authPasswd)
  114. if err != nil {
  115. if !models.IsErrUserNotExist(err) {
  116. ctx.Handle(http.StatusInternalServerError, "UserSignIn error: %v", err)
  117. return
  118. }
  119. // Assume username now is a token.
  120. token, err := models.GetAccessTokenBySHA(authUsername)
  121. if err != nil {
  122. if models.IsErrAccessTokenNotExist(err) || models.IsErrAccessTokenEmpty(err) {
  123. ctx.HandleText(http.StatusUnauthorized, "invalid token")
  124. } else {
  125. ctx.Handle(http.StatusInternalServerError, "GetAccessTokenBySha", err)
  126. }
  127. return
  128. }
  129. token.Updated = time.Now()
  130. if err = models.UpdateAccessToken(token); err != nil {
  131. ctx.Handle(http.StatusInternalServerError, "UpdateAccessToken", err)
  132. }
  133. authUser, err = models.GetUserByID(token.UID)
  134. if err != nil {
  135. ctx.Handle(http.StatusInternalServerError, "GetUserByID", err)
  136. return
  137. }
  138. }
  139. if !isPublicPull {
  140. has, err := models.HasAccess(authUser, repo, accessMode)
  141. if err != nil {
  142. ctx.Handle(http.StatusInternalServerError, "HasAccess", err)
  143. return
  144. } else if !has {
  145. if accessMode == models.AccessModeRead {
  146. has, err = models.HasAccess(authUser, repo, models.AccessModeWrite)
  147. if err != nil {
  148. ctx.Handle(http.StatusInternalServerError, "HasAccess2", err)
  149. return
  150. } else if !has {
  151. ctx.HandleText(http.StatusForbidden, "User permission denied")
  152. return
  153. }
  154. } else {
  155. ctx.HandleText(http.StatusForbidden, "User permission denied")
  156. return
  157. }
  158. }
  159. if !isPull && repo.IsMirror {
  160. ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
  161. return
  162. }
  163. }
  164. }
  165. environ = []string{
  166. models.EnvRepoUsername + "=" + username,
  167. models.EnvRepoName + "=" + reponame,
  168. models.EnvRepoUserSalt + "=" + repoUser.Salt,
  169. models.EnvPusherName + "=" + authUser.Name,
  170. models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID),
  171. models.ProtectedBranchRepoID + fmt.Sprintf("=%d", repo.ID),
  172. }
  173. if isWiki {
  174. environ = append(environ, models.EnvRepoIsWiki+"=true")
  175. } else {
  176. environ = append(environ, models.EnvRepoIsWiki+"=false")
  177. }
  178. }
  179. HTTPBackend(ctx, &serviceConfig{
  180. UploadPack: true,
  181. ReceivePack: true,
  182. Env: environ,
  183. })(ctx.Resp, ctx.Req.Request)
  184. }
  185. type serviceConfig struct {
  186. UploadPack bool
  187. ReceivePack bool
  188. Env []string
  189. }
  190. type serviceHandler struct {
  191. cfg *serviceConfig
  192. w http.ResponseWriter
  193. r *http.Request
  194. dir string
  195. file string
  196. environ []string
  197. }
  198. func (h *serviceHandler) setHeaderNoCache() {
  199. h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  200. h.w.Header().Set("Pragma", "no-cache")
  201. h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  202. }
  203. func (h *serviceHandler) setHeaderCacheForever() {
  204. now := time.Now().Unix()
  205. expires := now + 31536000
  206. h.w.Header().Set("Date", fmt.Sprintf("%d", now))
  207. h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
  208. h.w.Header().Set("Cache-Control", "public, max-age=31536000")
  209. }
  210. func (h *serviceHandler) sendFile(contentType string) {
  211. reqFile := path.Join(h.dir, h.file)
  212. fi, err := os.Stat(reqFile)
  213. if os.IsNotExist(err) {
  214. h.w.WriteHeader(http.StatusNotFound)
  215. return
  216. }
  217. h.w.Header().Set("Content-Type", contentType)
  218. h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
  219. h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
  220. http.ServeFile(h.w, h.r, reqFile)
  221. }
  222. type route struct {
  223. reg *regexp.Regexp
  224. method string
  225. handler func(serviceHandler)
  226. }
  227. var routes = []route{
  228. {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack},
  229. {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack},
  230. {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs},
  231. {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile},
  232. {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile},
  233. {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile},
  234. {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks},
  235. {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile},
  236. {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject},
  237. {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile},
  238. {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile},
  239. }
  240. // FIXME: use process module
  241. func gitCommand(dir string, args ...string) []byte {
  242. cmd := exec.Command("git", args...)
  243. cmd.Dir = dir
  244. out, err := cmd.Output()
  245. if err != nil {
  246. log.GitLogger.Error(4, fmt.Sprintf("%v - %s", err, out))
  247. }
  248. return out
  249. }
  250. func getGitConfig(option, dir string) string {
  251. out := string(gitCommand(dir, "config", option))
  252. return out[0 : len(out)-1]
  253. }
  254. func getConfigSetting(service, dir string) bool {
  255. service = strings.Replace(service, "-", "", -1)
  256. setting := getGitConfig("http."+service, dir)
  257. if service == "uploadpack" {
  258. return setting != "false"
  259. }
  260. return setting == "true"
  261. }
  262. func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
  263. if checkContentType {
  264. if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
  265. return false
  266. }
  267. }
  268. if !(service == "upload-pack" || service == "receive-pack") {
  269. return false
  270. }
  271. if service == "receive-pack" {
  272. return h.cfg.ReceivePack
  273. }
  274. if service == "upload-pack" {
  275. return h.cfg.UploadPack
  276. }
  277. return getConfigSetting(service, h.dir)
  278. }
  279. func serviceRPC(h serviceHandler, service string) {
  280. defer h.r.Body.Close()
  281. if !hasAccess(service, h, true) {
  282. h.w.WriteHeader(http.StatusUnauthorized)
  283. return
  284. }
  285. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  286. var err error
  287. var reqBody = h.r.Body
  288. // Handle GZIP.
  289. if h.r.Header.Get("Content-Encoding") == "gzip" {
  290. reqBody, err = gzip.NewReader(reqBody)
  291. if err != nil {
  292. log.GitLogger.Error(2, "fail to create gzip reader: %v", err)
  293. h.w.WriteHeader(http.StatusInternalServerError)
  294. return
  295. }
  296. }
  297. // set this for allow pre-receive and post-receive execute
  298. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  299. var stderr bytes.Buffer
  300. cmd := exec.Command("git", service, "--stateless-rpc", h.dir)
  301. cmd.Dir = h.dir
  302. if service == "receive-pack" {
  303. cmd.Env = append(os.Environ(), h.environ...)
  304. }
  305. cmd.Stdout = h.w
  306. cmd.Stdin = reqBody
  307. cmd.Stderr = &stderr
  308. if err := cmd.Run(); err != nil {
  309. log.GitLogger.Error(2, "fail to serve RPC(%s): %v - %v", service, err, stderr)
  310. h.w.WriteHeader(http.StatusInternalServerError)
  311. return
  312. }
  313. }
  314. func serviceUploadPack(h serviceHandler) {
  315. serviceRPC(h, "upload-pack")
  316. }
  317. func serviceReceivePack(h serviceHandler) {
  318. serviceRPC(h, "receive-pack")
  319. }
  320. func getServiceType(r *http.Request) string {
  321. serviceType := r.FormValue("service")
  322. if !strings.HasPrefix(serviceType, "git-") {
  323. return ""
  324. }
  325. return strings.Replace(serviceType, "git-", "", 1)
  326. }
  327. func updateServerInfo(dir string) []byte {
  328. return gitCommand(dir, "update-server-info")
  329. }
  330. func packetWrite(str string) []byte {
  331. s := strconv.FormatInt(int64(len(str)+4), 16)
  332. if len(s)%4 != 0 {
  333. s = strings.Repeat("0", 4-len(s)%4) + s
  334. }
  335. return []byte(s + str)
  336. }
  337. func getInfoRefs(h serviceHandler) {
  338. h.setHeaderNoCache()
  339. if hasAccess(getServiceType(h.r), h, false) {
  340. service := getServiceType(h.r)
  341. refs := gitCommand(h.dir, service, "--stateless-rpc", "--advertise-refs", ".")
  342. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  343. h.w.WriteHeader(http.StatusOK)
  344. h.w.Write(packetWrite("# service=git-" + service + "\n"))
  345. h.w.Write([]byte("0000"))
  346. h.w.Write(refs)
  347. } else {
  348. updateServerInfo(h.dir)
  349. h.sendFile("text/plain; charset=utf-8")
  350. }
  351. }
  352. func getTextFile(h serviceHandler) {
  353. h.setHeaderNoCache()
  354. h.sendFile("text/plain")
  355. }
  356. func getInfoPacks(h serviceHandler) {
  357. h.setHeaderCacheForever()
  358. h.sendFile("text/plain; charset=utf-8")
  359. }
  360. func getLooseObject(h serviceHandler) {
  361. h.setHeaderCacheForever()
  362. h.sendFile("application/x-git-loose-object")
  363. }
  364. func getPackFile(h serviceHandler) {
  365. h.setHeaderCacheForever()
  366. h.sendFile("application/x-git-packed-objects")
  367. }
  368. func getIdxFile(h serviceHandler) {
  369. h.setHeaderCacheForever()
  370. h.sendFile("application/x-git-packed-objects-toc")
  371. }
  372. func getGitRepoPath(subdir string) (string, error) {
  373. if !strings.HasSuffix(subdir, ".git") {
  374. subdir += ".git"
  375. }
  376. fpath := path.Join(setting.RepoRootPath, subdir)
  377. if _, err := os.Stat(fpath); os.IsNotExist(err) {
  378. return "", err
  379. }
  380. return fpath, nil
  381. }
  382. // HTTPBackend middleware for git smart HTTP protocol
  383. func HTTPBackend(ctx *context.Context, cfg *serviceConfig) http.HandlerFunc {
  384. return func(w http.ResponseWriter, r *http.Request) {
  385. for _, route := range routes {
  386. r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
  387. if m := route.reg.FindStringSubmatch(r.URL.Path); m != nil {
  388. if setting.Repository.DisableHTTPGit {
  389. w.WriteHeader(http.StatusForbidden)
  390. w.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
  391. return
  392. }
  393. if route.method != r.Method {
  394. if r.Proto == "HTTP/1.1" {
  395. w.WriteHeader(http.StatusMethodNotAllowed)
  396. w.Write([]byte("Method Not Allowed"))
  397. } else {
  398. w.WriteHeader(http.StatusBadRequest)
  399. w.Write([]byte("Bad Request"))
  400. }
  401. return
  402. }
  403. file := strings.Replace(r.URL.Path, m[1]+"/", "", 1)
  404. dir, err := getGitRepoPath(m[1])
  405. if err != nil {
  406. log.GitLogger.Error(4, err.Error())
  407. ctx.Handle(http.StatusNotFound, "HTTPBackend", err)
  408. return
  409. }
  410. route.handler(serviceHandler{cfg, w, r, dir, file, cfg.Env})
  411. return
  412. }
  413. }
  414. ctx.Handle(http.StatusNotFound, "HTTPBackend", nil)
  415. return
  416. }
  417. }