code.gitea.io/gitea@v1.21.7/routers/web/repo/githttp.go (about)

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