You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

http.go 14 kB

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