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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "bytes"
  8. "compress/gzip"
  9. gocontext "context"
  10. "fmt"
  11. "net/http"
  12. "os"
  13. "path"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "time"
  19. "code.gitea.io/gitea/models"
  20. "code.gitea.io/gitea/models/auth"
  21. "code.gitea.io/gitea/models/perm"
  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 {
  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. if repoExist {
  164. p, err := models.GetUserRepoPermission(ctx, repo, ctx.Doer)
  165. if err != nil {
  166. ctx.ServerError("GetUserRepoPermission", err)
  167. return
  168. }
  169. // Because of special ref "refs/for" .. , need delay write permission check
  170. if git.SupportProcReceive {
  171. accessMode = perm.AccessModeRead
  172. }
  173. if !p.CanAccess(accessMode, unitType) {
  174. ctx.PlainText(http.StatusForbidden, "User permission denied")
  175. return
  176. }
  177. if !isPull && repo.IsMirror {
  178. ctx.PlainText(http.StatusForbidden, "mirror repository is read-only")
  179. return
  180. }
  181. }
  182. environ = []string{
  183. repo_module.EnvRepoUsername + "=" + username,
  184. repo_module.EnvRepoName + "=" + reponame,
  185. repo_module.EnvPusherName + "=" + ctx.Doer.Name,
  186. repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
  187. repo_module.EnvAppURL + "=" + setting.AppURL,
  188. }
  189. if !ctx.Doer.KeepEmailPrivate {
  190. environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
  191. }
  192. if isWiki {
  193. environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
  194. } else {
  195. environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
  196. }
  197. }
  198. if !repoExist {
  199. if !receivePack {
  200. ctx.PlainText(http.StatusNotFound, "Repository not found")
  201. return
  202. }
  203. if isWiki { // you cannot send wiki operation before create the repository
  204. ctx.PlainText(http.StatusNotFound, "Repository not found")
  205. return
  206. }
  207. if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
  208. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for organizations.")
  209. return
  210. }
  211. if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
  212. ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for users.")
  213. return
  214. }
  215. // Return dummy payload if GET receive-pack
  216. if ctx.Req.Method == http.MethodGet {
  217. dummyInfoRefs(ctx)
  218. return
  219. }
  220. repo, err = repo_service.PushCreateRepo(ctx.Doer, owner, reponame)
  221. if err != nil {
  222. log.Error("pushCreateRepo: %v", err)
  223. ctx.Status(http.StatusNotFound)
  224. return
  225. }
  226. }
  227. if isWiki {
  228. // Ensure the wiki is enabled before we allow access to it
  229. if _, err := repo.GetUnit(unit.TypeWiki); err != nil {
  230. if repo_model.IsErrUnitTypeNotExist(err) {
  231. ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
  232. return
  233. }
  234. log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
  235. ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
  236. return
  237. }
  238. }
  239. environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
  240. w := ctx.Resp
  241. r := ctx.Req
  242. cfg := &serviceConfig{
  243. UploadPack: true,
  244. ReceivePack: true,
  245. Env: environ,
  246. }
  247. r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
  248. dir := repo_model.RepoPath(username, reponame)
  249. if isWiki {
  250. dir = repo_model.RepoPath(username, wikiRepoName)
  251. }
  252. return &serviceHandler{cfg, w, r, dir, cfg.Env}
  253. }
  254. var (
  255. infoRefsCache []byte
  256. infoRefsOnce sync.Once
  257. )
  258. func dummyInfoRefs(ctx *context.Context) {
  259. infoRefsOnce.Do(func() {
  260. tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-info-refs-cache")
  261. if err != nil {
  262. log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
  263. return
  264. }
  265. defer func() {
  266. if err := util.RemoveAll(tmpDir); err != nil {
  267. log.Error("RemoveAll: %v", err)
  268. }
  269. }()
  270. if err := git.InitRepository(ctx, tmpDir, true); err != nil {
  271. log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
  272. return
  273. }
  274. refs, _, err := git.NewCommand(ctx, "receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Dir: tmpDir})
  275. if err != nil {
  276. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  277. }
  278. log.Debug("populating infoRefsCache: \n%s", string(refs))
  279. infoRefsCache = refs
  280. })
  281. ctx.RespHeader().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  282. ctx.RespHeader().Set("Pragma", "no-cache")
  283. ctx.RespHeader().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  284. ctx.RespHeader().Set("Content-Type", "application/x-git-receive-pack-advertisement")
  285. _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
  286. _, _ = ctx.Write([]byte("0000"))
  287. _, _ = ctx.Write(infoRefsCache)
  288. }
  289. type serviceConfig struct {
  290. UploadPack bool
  291. ReceivePack bool
  292. Env []string
  293. }
  294. type serviceHandler struct {
  295. cfg *serviceConfig
  296. w http.ResponseWriter
  297. r *http.Request
  298. dir string
  299. environ []string
  300. }
  301. func (h *serviceHandler) setHeaderNoCache() {
  302. h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
  303. h.w.Header().Set("Pragma", "no-cache")
  304. h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
  305. }
  306. func (h *serviceHandler) setHeaderCacheForever() {
  307. now := time.Now().Unix()
  308. expires := now + 31536000
  309. h.w.Header().Set("Date", fmt.Sprintf("%d", now))
  310. h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
  311. h.w.Header().Set("Cache-Control", "public, max-age=31536000")
  312. }
  313. func containsParentDirectorySeparator(v string) bool {
  314. if !strings.Contains(v, "..") {
  315. return false
  316. }
  317. for _, ent := range strings.FieldsFunc(v, isSlashRune) {
  318. if ent == ".." {
  319. return true
  320. }
  321. }
  322. return false
  323. }
  324. func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
  325. func (h *serviceHandler) sendFile(contentType, file string) {
  326. if containsParentDirectorySeparator(file) {
  327. log.Error("request file path contains invalid path: %v", file)
  328. h.w.WriteHeader(http.StatusBadRequest)
  329. return
  330. }
  331. reqFile := path.Join(h.dir, file)
  332. fi, err := os.Stat(reqFile)
  333. if os.IsNotExist(err) {
  334. h.w.WriteHeader(http.StatusNotFound)
  335. return
  336. }
  337. h.w.Header().Set("Content-Type", contentType)
  338. h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
  339. h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
  340. http.ServeFile(h.w, h.r, reqFile)
  341. }
  342. // one or more key=value pairs separated by colons
  343. var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
  344. func getGitConfig(ctx gocontext.Context, option, dir string) string {
  345. out, _, err := git.NewCommand(ctx, "config", option).RunStdString(&git.RunOpts{Dir: dir})
  346. if err != nil {
  347. log.Error("%v - %s", err, out)
  348. }
  349. return out[0 : len(out)-1]
  350. }
  351. func getConfigSetting(ctx gocontext.Context, service, dir string) bool {
  352. service = strings.ReplaceAll(service, "-", "")
  353. setting := getGitConfig(ctx, "http."+service, dir)
  354. if service == "uploadpack" {
  355. return setting != "false"
  356. }
  357. return setting == "true"
  358. }
  359. func hasAccess(ctx gocontext.Context, service string, h serviceHandler, checkContentType bool) bool {
  360. if checkContentType {
  361. if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
  362. return false
  363. }
  364. }
  365. if !(service == "upload-pack" || service == "receive-pack") {
  366. return false
  367. }
  368. if service == "receive-pack" {
  369. return h.cfg.ReceivePack
  370. }
  371. if service == "upload-pack" {
  372. return h.cfg.UploadPack
  373. }
  374. return getConfigSetting(ctx, service, h.dir)
  375. }
  376. func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) {
  377. defer func() {
  378. if err := h.r.Body.Close(); err != nil {
  379. log.Error("serviceRPC: Close: %v", err)
  380. }
  381. }()
  382. if !hasAccess(ctx, service, h, true) {
  383. h.w.WriteHeader(http.StatusUnauthorized)
  384. return
  385. }
  386. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
  387. var err error
  388. reqBody := h.r.Body
  389. // Handle GZIP.
  390. if h.r.Header.Get("Content-Encoding") == "gzip" {
  391. reqBody, err = gzip.NewReader(reqBody)
  392. if err != nil {
  393. log.Error("Fail to create gzip reader: %v", err)
  394. h.w.WriteHeader(http.StatusInternalServerError)
  395. return
  396. }
  397. }
  398. // set this for allow pre-receive and post-receive execute
  399. h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
  400. if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  401. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  402. }
  403. var stderr bytes.Buffer
  404. cmd := git.NewCommand(h.r.Context(), service, "--stateless-rpc", h.dir)
  405. cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir))
  406. if err := cmd.Run(&git.RunOpts{
  407. Dir: h.dir,
  408. Env: append(os.Environ(), h.environ...),
  409. Stdout: h.w,
  410. Stdin: reqBody,
  411. Stderr: &stderr,
  412. }); err != nil {
  413. if err.Error() != "signal: killed" {
  414. log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
  415. }
  416. return
  417. }
  418. }
  419. // ServiceUploadPack implements Git Smart HTTP protocol
  420. func ServiceUploadPack(ctx *context.Context) {
  421. h := httpBase(ctx)
  422. if h != nil {
  423. serviceRPC(ctx, *h, "upload-pack")
  424. }
  425. }
  426. // ServiceReceivePack implements Git Smart HTTP protocol
  427. func ServiceReceivePack(ctx *context.Context) {
  428. h := httpBase(ctx)
  429. if h != nil {
  430. serviceRPC(ctx, *h, "receive-pack")
  431. }
  432. }
  433. func getServiceType(r *http.Request) string {
  434. serviceType := r.FormValue("service")
  435. if !strings.HasPrefix(serviceType, "git-") {
  436. return ""
  437. }
  438. return strings.Replace(serviceType, "git-", "", 1)
  439. }
  440. func updateServerInfo(ctx gocontext.Context, dir string) []byte {
  441. out, _, err := git.NewCommand(ctx, "update-server-info").RunStdBytes(&git.RunOpts{Dir: dir})
  442. if err != nil {
  443. log.Error(fmt.Sprintf("%v - %s", err, string(out)))
  444. }
  445. return out
  446. }
  447. func packetWrite(str string) []byte {
  448. s := strconv.FormatInt(int64(len(str)+4), 16)
  449. if len(s)%4 != 0 {
  450. s = strings.Repeat("0", 4-len(s)%4) + s
  451. }
  452. return []byte(s + str)
  453. }
  454. // GetInfoRefs implements Git dumb HTTP
  455. func GetInfoRefs(ctx *context.Context) {
  456. h := httpBase(ctx)
  457. if h == nil {
  458. return
  459. }
  460. h.setHeaderNoCache()
  461. if hasAccess(ctx, getServiceType(h.r), *h, false) {
  462. service := getServiceType(h.r)
  463. if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
  464. h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
  465. }
  466. h.environ = append(os.Environ(), h.environ...)
  467. refs, _, err := git.NewCommand(ctx, service, "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir})
  468. if err != nil {
  469. log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
  470. }
  471. h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
  472. h.w.WriteHeader(http.StatusOK)
  473. _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
  474. _, _ = h.w.Write([]byte("0000"))
  475. _, _ = h.w.Write(refs)
  476. } else {
  477. updateServerInfo(ctx, h.dir)
  478. h.sendFile("text/plain; charset=utf-8", "info/refs")
  479. }
  480. }
  481. // GetTextFile implements Git dumb HTTP
  482. func GetTextFile(p string) func(*context.Context) {
  483. return func(ctx *context.Context) {
  484. h := httpBase(ctx)
  485. if h != nil {
  486. h.setHeaderNoCache()
  487. file := ctx.Params("file")
  488. if file != "" {
  489. h.sendFile("text/plain", "objects/info/"+file)
  490. } else {
  491. h.sendFile("text/plain", p)
  492. }
  493. }
  494. }
  495. }
  496. // GetInfoPacks implements Git dumb HTTP
  497. func GetInfoPacks(ctx *context.Context) {
  498. h := httpBase(ctx)
  499. if h != nil {
  500. h.setHeaderCacheForever()
  501. h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
  502. }
  503. }
  504. // GetLooseObject implements Git dumb HTTP
  505. func GetLooseObject(ctx *context.Context) {
  506. h := httpBase(ctx)
  507. if h != nil {
  508. h.setHeaderCacheForever()
  509. h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
  510. ctx.Params("head"), ctx.Params("hash")))
  511. }
  512. }
  513. // GetPackFile implements Git dumb HTTP
  514. func GetPackFile(ctx *context.Context) {
  515. h := httpBase(ctx)
  516. if h != nil {
  517. h.setHeaderCacheForever()
  518. h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
  519. }
  520. }
  521. // GetIdxFile implements Git dumb HTTP
  522. func GetIdxFile(ctx *context.Context) {
  523. h := httpBase(ctx)
  524. if h != nil {
  525. h.setHeaderCacheForever()
  526. h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
  527. }
  528. }