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.

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