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.

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