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.

http.go 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package repo
  5. import (
  6. "bytes"
  7. "compress/gzip"
  8. gocontext "context"
  9. "fmt"
  10. "net/http"
  11. "os"
  12. "path"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "time"
  18. actions_model "code.gitea.io/gitea/models/actions"
  19. "code.gitea.io/gitea/models/auth"
  20. "code.gitea.io/gitea/models/perm"
  21. access_model "code.gitea.io/gitea/models/perm/access"
  22. repo_model "code.gitea.io/gitea/models/repo"
  23. "code.gitea.io/gitea/models/unit"
  24. "code.gitea.io/gitea/modules/context"
  25. "code.gitea.io/gitea/modules/git"
  26. "code.gitea.io/gitea/modules/log"
  27. repo_module "code.gitea.io/gitea/modules/repository"
  28. "code.gitea.io/gitea/modules/setting"
  29. "code.gitea.io/gitea/modules/structs"
  30. "code.gitea.io/gitea/modules/util"
  31. repo_service "code.gitea.io/gitea/services/repository"
  32. )
  33. // httpBase implementation git smart HTTP protocol
  34. func httpBase(ctx *context.Context) (h *serviceHandler) {
  35. if setting.Repository.DisableHTTPGit {
  36. ctx.Resp.WriteHeader(http.StatusForbidden)
  37. _, err := ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
  38. if err != nil {
  39. log.Error(err.Error())
  40. }
  41. return
  42. }
  43. if len(setting.Repository.AccessControlAllowOrigin) > 0 {
  44. allowedOrigin := setting.Repository.AccessControlAllowOrigin
  45. // Set CORS headers for browser-based git clients
  46. ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
  47. ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
  48. // Handle preflight OPTIONS request
  49. if ctx.Req.Method == "OPTIONS" {
  50. if allowedOrigin == "*" {
  51. ctx.Status(http.StatusOK)
  52. } else if allowedOrigin == "null" {
  53. ctx.Status(http.StatusForbidden)
  54. } else {
  55. origin := ctx.Req.Header.Get("Origin")
  56. if len(origin) > 0 && origin == allowedOrigin {
  57. ctx.Status(http.StatusOK)
  58. } else {
  59. ctx.Status(http.StatusForbidden)
  60. }
  61. }
  62. return
  63. }
  64. }
  65. username := ctx.Params(":username")
  66. reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
  67. if ctx.FormString("go-get") == "1" {
  68. context.EarlyResponseForGoGetMeta(ctx)
  69. return
  70. }
  71. var isPull, receivePack bool
  72. service := ctx.FormString("service")
  73. if service == "git-receive-pack" ||
  74. strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
  75. isPull = false
  76. receivePack = true
  77. } else if service == "git-upload-pack" ||
  78. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
  79. isPull = true
  80. } else if service == "git-upload-archive" ||
  81. strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
  82. isPull = true
  83. } else {
  84. isPull = ctx.Req.Method == "GET"
  85. }
  86. var accessMode perm.AccessMode
  87. if isPull {
  88. accessMode = perm.AccessModeRead
  89. } else {
  90. accessMode = perm.AccessModeWrite
  91. }
  92. isWiki := false
  93. unitType := unit.TypeCode
  94. var wikiRepoName string
  95. if strings.HasSuffix(reponame, ".wiki") {
  96. isWiki = true
  97. unitType = unit.TypeWiki
  98. wikiRepoName = reponame
  99. reponame = reponame[:len(reponame)-5]
  100. }
  101. owner := ctx.ContextUser
  102. if !owner.IsOrganization() && !owner.IsActive {
  103. ctx.PlainText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.")
  104. return
  105. }
  106. repoExist := true
  107. repo, err := repo_model.GetRepositoryByName(owner.ID, reponame)
  108. if err != nil {
  109. if repo_model.IsErrRepoNotExist(err) {
  110. if redirectRepoID, err := repo_model.LookupRedirect(owner.ID, reponame); err == nil {
  111. context.RedirectToRepo(ctx, redirectRepoID)
  112. return
  113. }
  114. repoExist = false
  115. } else {
  116. ctx.ServerError("GetRepositoryByName", err)
  117. return
  118. }
  119. }
  120. // Don't allow pushing if the repo is archived
  121. if repoExist && repo.IsArchived && !isPull {
  122. ctx.PlainText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
  123. return
  124. }
  125. // Only public pull don't need auth.
  126. isPublicPull := repoExist && !repo.IsPrivate && isPull
  127. var (
  128. askAuth = !isPublicPull || setting.Service.RequireSignInView
  129. environ []string
  130. )
  131. // don't allow anonymous pulls if organization is not public
  132. if isPublicPull {
  133. if err := repo.GetOwner(ctx); err != nil {
  134. ctx.ServerError("GetOwner", err)
  135. return
  136. }
  137. askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
  138. }
  139. // check access
  140. if askAuth {
  141. // rely on the results of Contexter
  142. if !ctx.IsSigned {
  143. // TODO: support digit auth - which would be Authorization header with digit
  144. ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
  145. ctx.Error(http.StatusUnauthorized)
  146. return
  147. }
  148. if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
  149. _, err = auth.GetTwoFactorByUID(ctx.Doer.ID)
  150. if err == nil {
  151. // 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
  152. ctx.PlainText(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")
  153. return
  154. } else if !auth.IsErrTwoFactorNotEnrolled(err) {
  155. ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
  156. return
  157. }
  158. }
  159. if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
  160. ctx.PlainText(http.StatusForbidden, "Your account is disabled.")
  161. return
  162. }
  163. environ = []string{
  164. repo_module.EnvRepoUsername + "=" + username,
  165. repo_module.EnvRepoName + "=" + reponame,
  166. repo_module.EnvPusherName + "=" + ctx.Doer.Name,
  167. repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
  168. repo_module.EnvAppURL + "=" + setting.AppURL,
  169. }
  170. if repoExist {
  171. // Because of special ref "refs/for" .. , need delay write permission check
  172. if git.SupportProcReceive {
  173. accessMode = perm.AccessModeRead
  174. }
  175. if ctx.Data["IsActionsToken"] == true {
  176. taskID := ctx.Data["ActionsTaskID"].(int64)
  177. task, err := actions_model.GetTaskByID(ctx, taskID)
  178. if err != nil {
  179. ctx.ServerError("GetTaskByID", err)
  180. return
  181. }
  182. if task.RepoID != repo.ID {
  183. ctx.PlainText(http.StatusForbidden, "User permission denied")
  184. return
  185. }
  186. if task.IsForkPullRequest {
  187. if accessMode > perm.AccessModeRead {
  188. ctx.PlainText(http.StatusForbidden, "User permission denied")
  189. return
  190. }
  191. environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
  192. } else {
  193. if accessMode > perm.AccessModeWrite {
  194. ctx.PlainText(http.StatusForbidden, "User permission denied")
  195. return
  196. }
  197. environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
  198. }
  199. } else {
  200. p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
  201. if err != nil {
  202. ctx.ServerError("GetUserRepoPermission", err)
  203. return
  204. }
  205. if !p.CanAccess(accessMode, unitType) {
  206. ctx.PlainText(http.StatusForbidden, "User permission denied")
  207. return
  208. }
  209. }
  210. if !isPull && repo.IsMirror {
  211. ctx.PlainText(http.StatusForbidden, "mirror repository is read-only")
  212. return
  213. }
  214. }
  215. if !ctx.Doer.KeepEmailPrivate {
  216. environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
  217. }
  218. if isWiki {
  219. environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
  220. } else {
  221. environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
  222. }
  223. }
  224. if !repoExist {
  225. if !receivePack {
  226. ctx.PlainText(http.StatusNotFound, "Repository not found")
  227. return
  228. }
  229. if isWiki { // you cannot send wiki operation before create the repository
  230. ctx.PlainText(http.StatusNotFound, "Repository not found")
  231. return
  232. }
  233. if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
  234. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for organizations.")
  235. return
  236. }
  237. if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
  238. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for users.")
  239. return
  240. }
  241. // Return dummy payload if GET receive-pack
  242. if ctx.Req.Method == http.MethodGet {
  243. dummyInfoRefs(ctx)
  244. return
  245. }
  246. repo, err = repo_service.PushCreateRepo(ctx.Doer, owner, reponame)
  247. if err != nil {
  248. log.Error("pushCreateRepo: %v", err)
  249. ctx.Status(http.StatusNotFound)
  250. return
  251. }
  252. }
  253. if isWiki {
  254. // Ensure the wiki is enabled before we allow access to it
  255. if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
  256. if repo_model.IsErrUnitTypeNotExist(err) {
  257. ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
  258. return
  259. }
  260. log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
  261. ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
  262. return
  263. }
  264. }
  265. environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
  266. w := ctx.Resp
  267. r := ctx.Req
  268. cfg := &serviceConfig{
  269. UploadPack: true,
  270. ReceivePack: true,
  271. Env: environ,
  272. }
  273. r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
  274. dir := repo_model.RepoPath(username, reponame)
  275. if isWiki {
  276. dir = repo_model.RepoPath(username, wikiRepoName)
  277. }
  278. return &serviceHandler{cfg, w, r, dir, cfg.Env}
  279. }
  280. var (
  281. infoRefsCache []byte
  282. infoRefsOnce sync.Once
  283. )
  284. func dummyInfoRefs(ctx *context.Context) {
  285. infoRefsOnce.Do(func() {
  286. tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-info-refs-cache")
  287. if err != nil {
  288. log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
  289. return
  290. }
  291. defer func() {
  292. if err := util.RemoveAll(tmpDir); err != nil {
  293. log.Error("RemoveAll: %v", err)
  294. }
  295. }()
  296. if err := git.InitRepository(ctx, tmpDir, true); err != nil {
  297. log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
  298. return
  299. }
  300. refs, _, err := git.NewCommand(ctx, "receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Dir: tmpDir})
  301. if err != nil {
  302. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  303. }
  304. log.Debug("populating infoRefsCache: \n%s", string(refs))
  305. infoRefsCache = refs
  306. })
  307. ctx.RespHeader().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  308. ctx.RespHeader().Set("Pragma", "no-cache")
  309. ctx.RespHeader().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  310. ctx.RespHeader().Set("Content-Type", "application/x-git-receive-pack-advertisement")
  311. _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
  312. _, _ = ctx.Write([]byte("0000"))
  313. _, _ = ctx.Write(infoRefsCache)
  314. }
  315. type serviceConfig struct {
  316. UploadPack bool
  317. ReceivePack bool
  318. Env []string
  319. }
  320. type serviceHandler struct {
  321. cfg *serviceConfig
  322. w http.ResponseWriter
  323. r *http.Request
  324. dir string
  325. environ []string
  326. }
  327. func (h *serviceHandler) setHeaderNoCache() {
  328. h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  329. h.w.Header().Set("Pragma", "no-cache")
  330. h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  331. }
  332. func (h *serviceHandler) setHeaderCacheForever() {
  333. now := time.Now().Unix()
  334. expires := now + 31536000
  335. h.w.Header().Set("Date", fmt.Sprintf("%d", now))
  336. h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
  337. h.w.Header().Set("Cache-Control", "public, max-age=31536000")
  338. }
  339. func containsParentDirectorySeparator(v string) bool {
  340. if !strings.Contains(v, "..") {
  341. return false
  342. }
  343. for _, ent := range strings.FieldsFunc(v, isSlashRune) {
  344. if ent == ".." {
  345. return true
  346. }
  347. }
  348. return false
  349. }
  350. func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
  351. func (h *serviceHandler) sendFile(contentType, file string) {
  352. if containsParentDirectorySeparator(file) {
  353. log.Error("request file path contains invalid path: %v", file)
  354. h.w.WriteHeader(http.StatusBadRequest)
  355. return
  356. }
  357. reqFile := path.Join(h.dir, file)
  358. fi, err := os.Stat(reqFile)
  359. if os.IsNotExist(err) {
  360. h.w.WriteHeader(http.StatusNotFound)
  361. return
  362. }
  363. h.w.Header().Set("Content-Type", contentType)
  364. h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
  365. h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
  366. http.ServeFile(h.w, h.r, reqFile)
  367. }
  368. // one or more key=value pairs separated by colons
  369. var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
  370. func getGitConfig(ctx gocontext.Context, option, dir string) string {
  371. out, _, err := git.NewCommand(ctx, "config").AddDynamicArguments(option).RunStdString(&git.RunOpts{Dir: dir})
  372. if err != nil {
  373. log.Error("%v - %s", err, out)
  374. }
  375. return out[0 : len(out)-1]
  376. }
  377. func getConfigSetting(ctx gocontext.Context, service, dir string) bool {
  378. service = strings.ReplaceAll(service, "-", "")
  379. setting := getGitConfig(ctx, "http."+service, dir)
  380. if service == "uploadpack" {
  381. return setting != "false"
  382. }
  383. return setting == "true"
  384. }
  385. func hasAccess(ctx gocontext.Context, service string, h serviceHandler, checkContentType bool) bool {
  386. if checkContentType {
  387. if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
  388. return false
  389. }
  390. }
  391. if !(service == "upload-pack" || service == "receive-pack") {
  392. return false
  393. }
  394. if service == "receive-pack" {
  395. return h.cfg.ReceivePack
  396. }
  397. if service == "upload-pack" {
  398. return h.cfg.UploadPack
  399. }
  400. return getConfigSetting(ctx, service, h.dir)
  401. }
  402. func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) {
  403. defer func() {
  404. if err := h.r.Body.Close(); err != nil {
  405. log.Error("serviceRPC: Close: %v", err)
  406. }
  407. }()
  408. if !hasAccess(ctx, service, h, true) {
  409. h.w.WriteHeader(http.StatusUnauthorized)
  410. return
  411. }
  412. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  413. var err error
  414. reqBody := h.r.Body
  415. // Handle GZIP.
  416. if h.r.Header.Get("Content-Encoding") == "gzip" {
  417. reqBody, err = gzip.NewReader(reqBody)
  418. if err != nil {
  419. log.Error("Fail to create gzip reader: %v", err)
  420. h.w.WriteHeader(http.StatusInternalServerError)
  421. return
  422. }
  423. }
  424. // set this for allow pre-receive and post-receive execute
  425. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  426. if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  427. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  428. }
  429. var stderr bytes.Buffer
  430. cmd := git.NewCommand(h.r.Context(), git.CmdArgCheck(service), "--stateless-rpc").AddDynamicArguments(h.dir)
  431. cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir))
  432. if err := cmd.Run(&git.RunOpts{
  433. Dir: h.dir,
  434. Env: append(os.Environ(), h.environ...),
  435. Stdout: h.w,
  436. Stdin: reqBody,
  437. Stderr: &stderr,
  438. UseContextTimeout: true,
  439. }); err != nil {
  440. if err.Error() != "signal: killed" {
  441. log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
  442. }
  443. return
  444. }
  445. }
  446. // ServiceUploadPack implements Git Smart HTTP protocol
  447. func ServiceUploadPack(ctx *context.Context) {
  448. h := httpBase(ctx)
  449. if h != nil {
  450. serviceRPC(ctx, *h, "upload-pack")
  451. }
  452. }
  453. // ServiceReceivePack implements Git Smart HTTP protocol
  454. func ServiceReceivePack(ctx *context.Context) {
  455. h := httpBase(ctx)
  456. if h != nil {
  457. serviceRPC(ctx, *h, "receive-pack")
  458. }
  459. }
  460. func getServiceType(r *http.Request) string {
  461. serviceType := r.FormValue("service")
  462. if !strings.HasPrefix(serviceType, "git-") {
  463. return ""
  464. }
  465. return strings.Replace(serviceType, "git-", "", 1)
  466. }
  467. func updateServerInfo(ctx gocontext.Context, dir string) []byte {
  468. out, _, err := git.NewCommand(ctx, "update-server-info").RunStdBytes(&git.RunOpts{Dir: dir})
  469. if err != nil {
  470. log.Error(fmt.Sprintf("%v - %s", err, string(out)))
  471. }
  472. return out
  473. }
  474. func packetWrite(str string) []byte {
  475. s := strconv.FormatInt(int64(len(str)+4), 16)
  476. if len(s)%4 != 0 {
  477. s = strings.Repeat("0", 4-len(s)%4) + s
  478. }
  479. return []byte(s + str)
  480. }
  481. // GetInfoRefs implements Git dumb HTTP
  482. func GetInfoRefs(ctx *context.Context) {
  483. h := httpBase(ctx)
  484. if h == nil {
  485. return
  486. }
  487. h.setHeaderNoCache()
  488. if hasAccess(ctx, getServiceType(h.r), *h, false) {
  489. service := getServiceType(h.r)
  490. if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  491. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  492. }
  493. h.environ = append(os.Environ(), h.environ...)
  494. refs, _, err := git.NewCommand(ctx, git.CmdArgCheck(service), "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir})
  495. if err != nil {
  496. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  497. }
  498. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  499. h.w.WriteHeader(http.StatusOK)
  500. _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
  501. _, _ = h.w.Write([]byte("0000"))
  502. _, _ = h.w.Write(refs)
  503. } else {
  504. updateServerInfo(ctx, h.dir)
  505. h.sendFile("text/plain; charset=utf-8", "info/refs")
  506. }
  507. }
  508. // GetTextFile implements Git dumb HTTP
  509. func GetTextFile(p string) func(*context.Context) {
  510. return func(ctx *context.Context) {
  511. h := httpBase(ctx)
  512. if h != nil {
  513. h.setHeaderNoCache()
  514. file := ctx.Params("file")
  515. if file != "" {
  516. h.sendFile("text/plain", "objects/info/"+file)
  517. } else {
  518. h.sendFile("text/plain", p)
  519. }
  520. }
  521. }
  522. }
  523. // GetInfoPacks implements Git dumb HTTP
  524. func GetInfoPacks(ctx *context.Context) {
  525. h := httpBase(ctx)
  526. if h != nil {
  527. h.setHeaderCacheForever()
  528. h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
  529. }
  530. }
  531. // GetLooseObject implements Git dumb HTTP
  532. func GetLooseObject(ctx *context.Context) {
  533. h := httpBase(ctx)
  534. if h != nil {
  535. h.setHeaderCacheForever()
  536. h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
  537. ctx.Params("head"), ctx.Params("hash")))
  538. }
  539. }
  540. // GetPackFile implements Git dumb HTTP
  541. func GetPackFile(ctx *context.Context) {
  542. h := httpBase(ctx)
  543. if h != nil {
  544. h.setHeaderCacheForever()
  545. h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
  546. }
  547. }
  548. // GetIdxFile implements Git dumb HTTP
  549. func GetIdxFile(ctx *context.Context) {
  550. h := httpBase(ctx)
  551. if h != nil {
  552. h.setHeaderCacheForever()
  553. h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
  554. }
  555. }