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