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.

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