From 54d47fd751d275005189278afed1beb8a8f361ec Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Fri, 16 Jan 2026 17:42:17 +0100 Subject: [PATCH 01/11] add support for archive-upload rpc --- routers/web/githttp.go | 1 + routers/web/repo/githttp.go | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index ed3c56b07b0c1..4cb573e315657 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -22,5 +22,6 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) + m.Post("/git-upload-archive", repo.ServiceUploadArchive) }, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb()) } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index c7b53dcbfb1b5..5689e6a1b276e 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -387,6 +387,9 @@ func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) { if service == ServiceTypeUploadPack { return gitcmd.NewCommand(ServiceTypeUploadPack), nil } + if service == ServiceTypeUploadArchive { + return gitcmd.NewCommand(ServiceTypeUploadArchive), nil + } return nil, fmt.Errorf("service %q is not allowed", service) } @@ -435,7 +438,10 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { } var stderr bytes.Buffer - if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", "."). + if service != ServiceTypeUploadArchive { + cmd.AddArguments("--stateless-rpc") + } + if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("."). WithEnv(append(os.Environ(), h.environ...)). WithStderr(&stderr). WithStdin(reqBody). @@ -444,13 +450,13 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { if !git.IsErrCanceledOrKilled(err) { log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getStorageRepo().RelativePath(), err, stderr.String()) } - return } } const ( - ServiceTypeUploadPack = "upload-pack" - ServiceTypeReceivePack = "receive-pack" + ServiceTypeUploadPack = "upload-pack" + ServiceTypeReceivePack = "receive-pack" + ServiceTypeUploadArchive = "upload-archive" ) // ServiceUploadPack implements Git Smart HTTP protocol @@ -461,6 +467,13 @@ func ServiceUploadPack(ctx *context.Context) { } } +func ServiceUploadArchive(ctx *context.Context) { + h := httpBase(ctx) + if h != nil { + serviceRPC(ctx, h, ServiceTypeUploadArchive) + } +} + // ServiceReceivePack implements Git Smart HTTP protocol func ServiceReceivePack(ctx *context.Context) { h := httpBase(ctx) @@ -475,6 +488,8 @@ func getServiceType(ctx *context.Context) string { return ServiceTypeUploadPack case "git-" + ServiceTypeReceivePack: return ServiceTypeReceivePack + case "git-" + ServiceTypeUploadArchive: + return ServiceTypeUploadArchive } return "" } From 1763aef3f7f16888da2a1e6d09fe460d8d546991 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Fri, 16 Jan 2026 18:28:56 +0100 Subject: [PATCH 02/11] group with other post routes --- routers/web/githttp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 4cb573e315657..4ba766c2c7e4f 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -13,6 +13,7 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) + m.Post("/git-upload-archive", repo.ServiceUploadArchive) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD")) m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates")) @@ -22,6 +23,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - m.Post("/git-upload-archive", repo.ServiceUploadArchive) }, repo.HTTPGitEnabledHandler, repo.CorsHandler(), optSignInFromAnyOrigin, context.UserAssignmentWeb()) } From 1cac1828b9afbfbd56daed97286b62a4fa5e72eb Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Fri, 16 Jan 2026 22:09:14 +0100 Subject: [PATCH 03/11] add integration test for service --- tests/integration/git_helper_for_declarative_test.go | 12 ++++++++++++ tests/integration/git_smart_http_test.go | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 197a20bc8ce6a..0e4163041a168 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -229,3 +229,15 @@ func doGitPull(dstPath string, args ...string) func(*testing.T) { assert.NoError(t, err) } } + +// doGitRemoteArchive runs a git archive command requesting an archive from remote +// and verifies that the command did not error out and returned only normal output +func doGitRemoteArchive(remote string, args ...string) func(*testing.T) { + return func(t *testing.T) { + stdout, stderr, err := gitcmd.NewCommand("archive").AddOptionValues("--remote", remote).AddArguments(gitcmd.ToTrustedCmdArgs(args)...). + RunStdString(t.Context()) + require.NoError(t, err) + assert.Empty(t, stderr) + assert.NotEmpty(t, stdout) + } +} diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index e984fd3aaddec..7e08304b67f29 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,6 +20,7 @@ func TestGitSmartHTTP(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { testGitSmartHTTP(t, u) testRenamedRepoRedirect(t) + testGitArchiveRemote(t, u) }) } @@ -96,3 +96,10 @@ func testRenamedRepoRedirect(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1") } + +func testGitArchiveRemote(t *testing.T, u *url.URL) { + u = u.JoinPath("user27/repo49.git") + t.Run("Fetch HEAD archive", doGitRemoteArchive(u.String(), "HEAD")) + t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test")) + t.Run("list compression options", doGitRemoteArchive(u.String(), "--list")) +} From e4a3f34e8efa9c990f20e2b5ae255b845c99418c Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Fri, 16 Jan 2026 22:25:00 +0100 Subject: [PATCH 04/11] fmt --- tests/integration/git_smart_http_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 7e08304b67f29..b74eb528c0874 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) From be3f7790d192c9c0971e704e053a54f85efe587e Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sat, 17 Jan 2026 09:29:10 +0100 Subject: [PATCH 05/11] lock out info/refs from calling upload-archive --- routers/web/githttp.go | 2 +- routers/web/repo/githttp.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 4ba766c2c7e4f..1b6ef455a7026 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -13,7 +13,7 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) - m.Post("/git-upload-archive", repo.ServiceUploadArchive) + m.Methods("POST,OPTIONS", "/git-upload-archive", repo.ServiceUploadArchive) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD")) m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates")) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 5689e6a1b276e..915057e5f181e 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -438,7 +438,8 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { } var stderr bytes.Buffer - if service != ServiceTypeUploadArchive { + // git upload-archive does not have a -- stateless-rpc option + if service == ServiceTypeUploadArchive || service == ServiceTypeReceivePack { cmd.AddArguments("--stateless-rpc") } if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("."). @@ -510,6 +511,10 @@ func GetInfoRefs(ctx *context.Context) { } setHeaderNoCache(ctx) service := getServiceType(ctx) + if !(service == ServiceTypeUploadPack || service == ServiceTypeReceivePack) { + ctx.Resp.WriteHeader(http.StatusBadRequest) + return + } cmd, err := prepareGitCmdWithAllowedService(service) if err == nil { if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { From f8b418d07d75e614ce067e669586250ce3ef63b2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 19:40:57 +0800 Subject: [PATCH 06/11] fix git service handling --- routers/web/githttp.go | 9 +++-- routers/web/repo/githttp.go | 70 ++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 1b6ef455a7026..794c2944bb173 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -10,10 +10,13 @@ import ( ) func addOwnerRepoGitHTTPRouters(m *web.Router) { + presetGitService := func(service string) func(ctx *context.Context) { + return func(ctx *context.Context) { ctx.SetPathParam("preset-git-service", service) } + } m.Group("/{username}/{reponame}", func() { - m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) - m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) - m.Methods("POST,OPTIONS", "/git-upload-archive", repo.ServiceUploadArchive) + m.Methods("POST,OPTIONS", "/git-upload-pack", presetGitService("git-upload-pack"), repo.ServiceUploadPack) + m.Methods("POST,OPTIONS", "/git-receive-pack", presetGitService("git-receive-pack"), repo.ServiceReceivePack) + m.Methods("POST,OPTIONS", "/git-upload-archive", presetGitService("git-upload-archive"), repo.ServiceUploadArchive) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD")) m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates")) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 915057e5f181e..3e5de9c0e9218 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -66,16 +66,13 @@ func httpBase(ctx *context.Context) *serviceHandler { } var isPull, receivePack bool - service := ctx.FormString("service") - if service == "git-receive-pack" || - strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") { + gitService := ctx.FormString("service", ctx.PathParam("preset-git-service")) + if gitService == "git-receive-pack" { isPull = false receivePack = true - } else if service == "git-upload-pack" || - strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { + } else if gitService == "git-upload-pack" { isPull = true - } else if service == "git-upload-archive" || - strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") { + } else if gitService == "git-upload-archive" { isPull = true } else { isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet @@ -380,39 +377,35 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string // 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 == ServiceTypeReceivePack { - return gitcmd.NewCommand(ServiceTypeReceivePack), nil - } - if service == ServiceTypeUploadPack { - return gitcmd.NewCommand(ServiceTypeUploadPack), nil +func prepareGitCmdWithAllowedService(service string, allowedServices []string) *gitcmd.Command { + if !slices.Contains(allowedServices, service) { + return nil } - if service == ServiceTypeUploadArchive { - return gitcmd.NewCommand(ServiceTypeUploadArchive), 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, h *serviceHandler, service string) { - defer func() { - if err := ctx.Req.Body.Close(); err != nil { - log.Error("serviceRPC: Close: %v", err) - } - }() + defer ctx.Req.Body.Close() 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) - // FIXME: why it's 401 if the content type is unexpected? - ctx.Resp.WriteHeader(http.StatusUnauthorized) + ctx.Resp.WriteHeader(http.StatusBadRequest) return } - cmd, err := prepareGitCmdWithAllowedService(service) - if err != nil { - log.Error("Failed to prepareGitCmdWithService: %v", err) - // FIXME: why it's 401 if the service type doesn't supported? - ctx.Resp.WriteHeader(http.StatusUnauthorized) + cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack, ServiceTypeUploadArchive}) + if cmd == nil { + ctx.Resp.WriteHeader(http.StatusBadRequest) return } @@ -422,10 +415,10 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, 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 } } @@ -438,7 +431,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { } var stderr bytes.Buffer - // git upload-archive does not have a -- stateless-rpc option + // git upload-archive does not have a "--stateless-rpc" option if service == ServiceTypeUploadArchive || service == ServiceTypeReceivePack { cmd.AddArguments("--stateless-rpc") } @@ -484,7 +477,8 @@ func ServiceReceivePack(ctx *context.Context) { } func getServiceType(ctx *context.Context) string { - switch ctx.Req.FormValue("service") { + gitService := ctx.Req.FormValue("service") + switch gitService { case "git-" + ServiceTypeUploadPack: return ServiceTypeUploadPack case "git-" + ServiceTypeReceivePack: @@ -511,12 +505,8 @@ func GetInfoRefs(ctx *context.Context) { } setHeaderNoCache(ctx) service := getServiceType(ctx) - if !(service == ServiceTypeUploadPack || service == ServiceTypeReceivePack) { - ctx.Resp.WriteHeader(http.StatusBadRequest) - return - } - cmd, err := prepareGitCmdWithAllowedService(service) - if err == nil { + cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack}) + if cmd != nil { if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { h.environ = append(h.environ, "GIT_PROTOCOL="+protocol) } @@ -533,11 +523,13 @@ func GetInfoRefs(ctx *context.Context) { _, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n")) _, _ = ctx.Resp.Write([]byte("0000")) _, _ = ctx.Resp.Write(refs) - } else { + } else if service == "" { if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil { log.Error("Failed to update server info: %v", err) } h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs") + } else { + ctx.Resp.WriteHeader(http.StatusBadRequest) } } From 0ebc3e4c0fc4df253cd08691ef84f6f8b3aa3398 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 19:46:37 +0800 Subject: [PATCH 07/11] fix error handling, don't treat user's bad input as error log --- routers/web/repo/githttp.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 3e5de9c0e9218..3cd7dd876116b 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -274,7 +274,6 @@ func httpBase(ctx *context.Context) *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 } @@ -364,7 +363,7 @@ 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 } @@ -398,7 +397,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { 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) + log.Debug("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType) ctx.Resp.WriteHeader(http.StatusBadRequest) return } @@ -512,10 +511,11 @@ func GetInfoRefs(ctx *context.Context) { } h.environ = append(os.Environ(), h.environ...) - refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd.AddArguments("--stateless-rpc", "--advertise-refs", "."). - WithEnv(h.environ)) + cmd = cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").WithEnv(h.environ) + refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd) if err != nil { - log.Error(fmt.Sprintf("%v - %s", err, string(refs))) + ctx.ServerError("RunGitServiceAdvertiseRefs", err) + return } ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) @@ -525,7 +525,8 @@ func GetInfoRefs(ctx *context.Context) { _, _ = ctx.Resp.Write(refs) } else if service == "" { if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil { - log.Error("Failed to update server info: %v", err) + ctx.ServerError("UpdateServerInfo", err) + return } h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs") } else { From 6802f5e0b63943489ee245ae945ca3e101403005 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 20:33:03 +0800 Subject: [PATCH 08/11] refactor git service type parsing --- routers/web/repo/githttp.go | 92 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 3cd7dd876116b..3b7d460625ded 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -65,16 +65,20 @@ func httpBase(ctx *context.Context) *serviceHandler { return nil } + var serviceType string var isPull, receivePack bool gitService := ctx.FormString("service", ctx.PathParam("preset-git-service")) - if gitService == "git-receive-pack" { - isPull = false + switch gitService { + case "git-receive-pack": + serviceType = ServiceTypeReceivePack receivePack = true - } else if gitService == "git-upload-pack" { + case "git-upload-pack": + serviceType = ServiceTypeUploadPack isPull = true - } else if gitService == "git-upload-archive" { + case "git-upload-archive": + serviceType = ServiceTypeUploadArchive isPull = true - } else { + default: isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet } @@ -281,9 +285,7 @@ func httpBase(ctx *context.Context) *serviceHandler { environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID)) - 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 ( @@ -326,6 +328,8 @@ func dummyInfoRefs(ctx *context.Context) { } type serviceHandler struct { + serviceType string + repo *repo_model.Repository isWiki bool environ []string @@ -460,13 +464,6 @@ func ServiceUploadPack(ctx *context.Context) { } } -func ServiceUploadArchive(ctx *context.Context) { - h := httpBase(ctx) - if h != nil { - serviceRPC(ctx, h, ServiceTypeUploadArchive) - } -} - // ServiceReceivePack implements Git Smart HTTP protocol func ServiceReceivePack(ctx *context.Context) { h := httpBase(ctx) @@ -475,17 +472,11 @@ func ServiceReceivePack(ctx *context.Context) { } } -func getServiceType(ctx *context.Context) string { - gitService := ctx.Req.FormValue("service") - switch gitService { - case "git-" + ServiceTypeUploadPack: - return ServiceTypeUploadPack - case "git-" + ServiceTypeReceivePack: - return ServiceTypeReceivePack - case "git-" + ServiceTypeUploadArchive: - return ServiceTypeUploadArchive - } - return "" +func ServiceUploadArchive(ctx *context.Context) { + h := httpBase(ctx) + if h != nil { + serviceRPC(ctx, h, ServiceTypeUploadArchive) + } } func packetWrite(str string) []byte { @@ -503,35 +494,40 @@ func GetInfoRefs(ctx *context.Context) { return } setHeaderNoCache(ctx) - service := getServiceType(ctx) - cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack}) - if cmd != nil { - 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", service)) - ctx.Resp.WriteHeader(http.StatusOK) - _, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n")) - _, _ = ctx.Resp.Write([]byte("0000")) - _, _ = ctx.Resp.Write(refs) - } else if service == "" { + 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.sendFile(ctx, "text/plain; charset=utf-8", "info/refs") - } else { + 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 From 6e161a1d2e5d5223af7daff41d6460088386f1c6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 20:51:04 +0800 Subject: [PATCH 09/11] fix comments --- routers/web/githttp.go | 2 ++ routers/web/repo/githttp.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 794c2944bb173..a1e11a4cd7de5 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -13,6 +13,8 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { presetGitService := func(service string) func(ctx *context.Context) { return func(ctx *context.Context) { ctx.SetPathParam("preset-git-service", service) } } + // Some users want to use "web-based git client" to access Gitea's repositories, + // so the CORS handler and OPTIONS method are used. m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", presetGitService("git-upload-pack"), repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", presetGitService("git-receive-pack"), repo.ServiceReceivePack) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 3b7d460625ded..f1a01145c2361 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -189,7 +189,7 @@ func httpBase(ctx *context.Context) *serviceHandler { } if repoExist { - // Because of special ref "refs/for" .. , need delay write permission check + // Because of special ref "refs/for" (agit) , need delay write permission check if git.DefaultFeatures().SupportProcReceive { accessMode = perm.AccessModeRead } @@ -350,7 +350,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") From c2860491c266adfe1f14e9583b9c6c14fdb12fb7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 21:12:49 +0800 Subject: [PATCH 10/11] fix typo --- routers/web/repo/githttp.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index f1a01145c2361..bddfd55229ebc 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -411,6 +411,10 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { 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)) @@ -434,10 +438,6 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { } var stderr bytes.Buffer - // git upload-archive does not have a "--stateless-rpc" option - if service == ServiceTypeUploadArchive || service == ServiceTypeReceivePack { - cmd.AddArguments("--stateless-rpc") - } if err := gitrepo.RunCmd(ctx, h.getStorageRepo(), cmd.AddArguments("."). WithEnv(append(os.Environ(), h.environ...)). WithStderr(&stderr). From c2e9a7dfb86cb6fd92c2b85109167686eb393ed4 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 17 Jan 2026 21:32:17 +0800 Subject: [PATCH 11/11] fix httpBase logic --- routers/web/githttp.go | 9 +++------ routers/web/repo/githttp.go | 37 ++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/routers/web/githttp.go b/routers/web/githttp.go index a1e11a4cd7de5..43d318c1a1f60 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -10,15 +10,12 @@ import ( ) func addOwnerRepoGitHTTPRouters(m *web.Router) { - presetGitService := func(service string) func(ctx *context.Context) { - return func(ctx *context.Context) { ctx.SetPathParam("preset-git-service", service) } - } // Some users want to use "web-based git client" to access Gitea's repositories, // so the CORS handler and OPTIONS method are used. m.Group("/{username}/{reponame}", func() { - m.Methods("POST,OPTIONS", "/git-upload-pack", presetGitService("git-upload-pack"), repo.ServiceUploadPack) - m.Methods("POST,OPTIONS", "/git-receive-pack", presetGitService("git-receive-pack"), repo.ServiceReceivePack) - m.Methods("POST,OPTIONS", "/git-upload-archive", presetGitService("git-upload-archive"), repo.ServiceUploadArchive) + m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) + m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) + m.Methods("POST,OPTIONS", "/git-upload-archive", repo.ServiceUploadArchive) m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs) m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD")) m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates")) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index bddfd55229ebc..325fe85f12e53 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -30,6 +30,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" repo_service "code.gitea.io/gitea/services/repository" @@ -55,8 +56,9 @@ func CorsHandler() func(next http.Handler) http.Handler { } } -// httpBase implementation git smart HTTP protocol -func httpBase(ctx *context.Context) *serviceHandler { +// 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") @@ -67,8 +69,7 @@ func httpBase(ctx *context.Context) *serviceHandler { var serviceType string var isPull, receivePack bool - gitService := ctx.FormString("service", ctx.PathParam("preset-git-service")) - switch gitService { + switch util.OptionalArg(optGitService) { case "git-receive-pack": serviceType = ServiceTypeReceivePack receivePack = true @@ -78,8 +79,11 @@ func httpBase(ctx *context.Context) *serviceHandler { 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 @@ -396,8 +400,12 @@ func prepareGitCmdWithAllowedService(service string, allowedServices []string) * } } -func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { +func serviceRPC(ctx *context.Context, service string) { defer ctx.Req.Body.Close() + h := httpBase(ctx, "git-"+service) + if h == nil { + return + } expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) if ctx.Req.Header.Get("Content-Type") != expectedContentType { @@ -458,25 +466,16 @@ const ( // ServiceUploadPack implements Git Smart HTTP protocol func ServiceUploadPack(ctx *context.Context) { - h := httpBase(ctx) - if h != nil { - serviceRPC(ctx, h, ServiceTypeUploadPack) - } + serviceRPC(ctx, ServiceTypeUploadPack) } // ServiceReceivePack implements Git Smart HTTP protocol func ServiceReceivePack(ctx *context.Context) { - h := httpBase(ctx) - if h != nil { - serviceRPC(ctx, h, ServiceTypeReceivePack) - } + serviceRPC(ctx, ServiceTypeReceivePack) } func ServiceUploadArchive(ctx *context.Context) { - h := httpBase(ctx) - if h != nil { - serviceRPC(ctx, h, ServiceTypeUploadArchive) - } + serviceRPC(ctx, ServiceTypeUploadArchive) } func packetWrite(str string) []byte { @@ -489,7 +488,7 @@ func packetWrite(str string) []byte { // GetInfoRefs implements Git dumb HTTP func GetInfoRefs(ctx *context.Context) { - h := httpBase(ctx) + h := httpBase(ctx, ctx.FormString("service")) // git http protocol: "?service=git-" if h == nil { return }