feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
+115 -152
View File
@@ -5,13 +5,11 @@
package repo
import (
"bytes"
"compress/gzip"
gocontext "context"
"fmt"
"net/http"
"os"
"path/filepath"
"path"
"regexp"
"slices"
"strconv"
@@ -19,7 +17,6 @@ import (
"sync"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
@@ -27,6 +24,7 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -57,9 +55,9 @@ func CorsHandler() func(next http.Handler) http.Handler {
}
}
// httpBase implementation git smart HTTP protocol
// httpBase does the common work for git http services,
// including early response, authentication, repository lookup and permission check.
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
username := ctx.PathParam("username")
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
if ctx.FormString("go-get") == "1" {
@@ -67,16 +65,23 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
var serviceType string
var isPull, receivePack bool
switch util.OptionalArg(optGitService) {
case "git-receive-pack":
serviceType = ServiceTypeReceivePack
receivePack = true
case "git-upload-pack":
serviceType = ServiceTypeUploadPack
isPull = true
case "git-upload-archive":
serviceType = ServiceTypeUploadArchive
isPull = true
default:
case "":
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
default: // unknown service
ctx.Resp.WriteHeader(http.StatusBadRequest)
return nil
}
var accessMode perm.AccessMode
@@ -124,10 +129,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string
)
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
// don't allow anonymous pulls if organization is not public
if isPublicPull {
@@ -160,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil {
// 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
@@ -177,56 +179,21 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
environ = []string{
repo_module.EnvRepoUsername + "=" + username,
repo_module.EnvRepoName + "=" + reponame,
repo_module.EnvPusherName + "=" + ctx.Doer.Name,
repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
repo_module.EnvAppURL + "=" + setting.AppURL,
}
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
if git.DefaultFeatures().SupportProcReceive {
// Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks.
if git.DefaultFeatures().SupportProcReceive && !isWiki {
accessMode = perm.AccessModeRead
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
ctx.ServerError("GetTaskByID", err)
return nil
}
if task.RepoID != repo.ID {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
p, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetDoerRepoPermission", err)
return nil
}
if task.IsForkPullRequest {
if accessMode > perm.AccessModeRead {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
} else {
if accessMode > perm.AccessModeWrite {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
}
} else {
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return nil
}
if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return nil
}
if !isPull && repo.IsMirror {
@@ -234,16 +201,6 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
}
if !ctx.Doer.KeepEmailPrivate {
environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
}
if isWiki {
environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
} else {
environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
}
}
if !repoExist {
@@ -287,17 +244,18 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
return nil
}
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
return nil
}
}
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
var environ []string
if !isPull {
// if not "pull", then must be "push", and doer must exist
environ = repo_module.DoerPushingEnvironment(ctx.Doer, repo, isWiki)
}
ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
return &serviceHandler{repo, isWiki, environ}
return &serviceHandler{serviceType, repo, isWiki, environ}
}
var (
@@ -319,7 +277,9 @@ func dummyInfoRefs(ctx *context.Context) {
return
}
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: tmpDir})
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").
WithDir(tmpDir).
RunStdBytes(ctx)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
@@ -338,16 +298,18 @@ func dummyInfoRefs(ctx *context.Context) {
}
type serviceHandler struct {
serviceType string
repo *repo_model.Repository
isWiki bool
environ []string
}
func (h *serviceHandler) getRepoDir() string {
func (h *serviceHandler) getStorageRepo() gitrepo.Repository {
if h.isWiki {
return h.repo.WikiPath()
return h.repo.WikiStorageRepo()
}
return h.repo.RepoPath()
return h.repo
}
func setHeaderNoCache(ctx *context.Context) {
@@ -358,7 +320,7 @@ func setHeaderNoCache(ctx *context.Context) {
func setHeaderCacheForever(ctx *context.Context) {
now := time.Now().Unix()
expires := now + 31536000
expires := now + 365*86400 // 365 days
ctx.Resp.Header().Set("Date", strconv.FormatInt(now, 10))
ctx.Resp.Header().Set("Expires", strconv.FormatInt(expires, 10))
ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
@@ -375,65 +337,58 @@ func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
if containsParentDirectorySeparator(file) {
log.Error("request file path contains invalid path: %v", file)
log.Debug("request file path contains invalid path: %v", file)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
reqFile := filepath.Join(h.getRepoDir(), file)
fi, err := os.Stat(reqFile)
if os.IsNotExist(err) {
ctx.Resp.WriteHeader(http.StatusNotFound)
return
}
fs := gitrepo.GetRepoFS(h.getStorageRepo())
ctx.Resp.Header().Set("Content-Type", contentType)
ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
http.ServeFileFS(ctx.Resp, ctx.Req, fs, path.Clean(file))
}
// one or more key=value pairs separated by colons
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
if service == "receive-pack" {
return gitcmd.NewCommand("receive-pack"), nil
func prepareGitCmdWithAllowedService(service string, allowedServices []string) *gitcmd.Command {
if !slices.Contains(allowedServices, service) {
return nil
}
if service == "upload-pack" {
return gitcmd.NewCommand("upload-pack"), nil
switch service {
case ServiceTypeReceivePack:
return gitcmd.NewCommand(ServiceTypeReceivePack)
case ServiceTypeUploadPack:
return gitcmd.NewCommand(ServiceTypeUploadPack)
case ServiceTypeUploadArchive:
return gitcmd.NewCommand(ServiceTypeUploadArchive)
default:
return nil
}
return nil, fmt.Errorf("service %q is not allowed", service)
}
func serviceRPC(ctx *context.Context, service string) {
defer func() {
if err := ctx.Req.Body.Close(); err != nil {
log.Error("serviceRPC: Close: %v", err)
}
}()
defer ctx.Req.Body.Close()
h := httpBase(ctx, "git-"+service)
if h == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
ctx.Resp.WriteHeader(http.StatusUnauthorized)
log.Debug("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
cmd, err := prepareGitCmdWithAllowedService(service)
if err != nil {
log.Error("Failed to prepareGitCmdWithService: %v", err)
ctx.Resp.WriteHeader(http.StatusUnauthorized)
cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack, ServiceTypeUploadArchive})
if cmd == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
// git upload-archive does not have a "--stateless-rpc" option
if service == ServiceTypeUploadPack || service == ServiceTypeReceivePack {
cmd.AddArguments("--stateless-rpc")
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
@@ -441,10 +396,10 @@ func serviceRPC(ctx *context.Context, service string) {
// Handle GZIP.
if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
var err error
reqBody, err = gzip.NewReader(reqBody)
if err != nil {
log.Error("Fail to create gzip reader: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
}
@@ -456,39 +411,35 @@ func serviceRPC(ctx *context.Context, service string) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
var stderr bytes.Buffer
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
if err := cmd.Run(ctx, &gitcmd.RunOpts{
Dir: h.getRepoDir(),
Env: append(os.Environ(), h.environ...),
Stdout: ctx.Resp,
Stdin: reqBody,
Stderr: &stderr,
UseContextTimeout: true,
}); err != nil {
if !git.IsErrCanceledOrKilled(err) {
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
if err := gitrepo.RunCmdWithStderr(ctx, h.getStorageRepo(), cmd.AddArguments(".").
WithEnv(append(os.Environ(), h.environ...)).
WithStdinCopy(reqBody).
WithStdoutCopy(ctx.Resp),
); err != nil {
if !gitcmd.IsErrorCanceledOrKilled(err) {
log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err)
}
return
}
}
const (
ServiceTypeUploadPack = "upload-pack"
ServiceTypeReceivePack = "receive-pack"
ServiceTypeUploadArchive = "upload-archive"
)
// ServiceUploadPack implements Git Smart HTTP protocol
func ServiceUploadPack(ctx *context.Context) {
serviceRPC(ctx, "upload-pack")
serviceRPC(ctx, ServiceTypeUploadPack)
}
// ServiceReceivePack implements Git Smart HTTP protocol
func ServiceReceivePack(ctx *context.Context) {
serviceRPC(ctx, "receive-pack")
serviceRPC(ctx, ServiceTypeReceivePack)
}
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
out, _, err := gitcmd.NewCommand("update-server-info").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: dir})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
}
return out
func ServiceUploadArchive(ctx *context.Context) {
serviceRPC(ctx, ServiceTypeUploadArchive)
}
func packetWrite(str string) []byte {
@@ -501,33 +452,45 @@ func packetWrite(str string) []byte {
// GetInfoRefs implements Git dumb HTTP
func GetInfoRefs(ctx *context.Context) {
service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
h := httpBase(ctx, "git-"+service)
h := httpBase(ctx, ctx.FormString("service")) // git http protocol: "?service=git-<service>"
if h == nil {
return
}
setHeaderNoCache(ctx)
cmd, err := prepareGitCmdWithAllowedService(service)
if err == nil {
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
if h.serviceType == "" {
// it's said that some legacy git clients will send requests to "/info/refs" without "service" parameter,
// although there should be no such case client in the modern days. TODO: not quite sure why we need this UpdateServerInfo logic
if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil {
ctx.ServerError("UpdateServerInfo", err)
return
}
h.environ = append(os.Environ(), h.environ...)
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
_, _ = ctx.Resp.Write([]byte("0000"))
_, _ = ctx.Resp.Write(refs)
} else {
updateServerInfo(ctx, h.getRepoDir())
h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
return
}
cmd := prepareGitCmdWithAllowedService(h.serviceType, []string{ServiceTypeUploadPack, ServiceTypeReceivePack})
if cmd == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
h.environ = append(os.Environ(), h.environ...)
cmd = cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").WithEnv(h.environ)
refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd)
if err != nil {
ctx.ServerError("RunGitServiceAdvertiseRefs", err)
return
}
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", h.serviceType))
ctx.Resp.WriteHeader(http.StatusOK)
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + h.serviceType + "\n"))
_, _ = ctx.Resp.Write([]byte("0000"))
_, _ = ctx.Resp.Write(refs)
}
// GetTextFile implements Git dumb HTTP