From 2fa885d421548e4958861c5bade5d663118b33f2 Mon Sep 17 00:00:00 2001 From: tjh Date: Fri, 12 Dec 2025 15:47:52 +0000 Subject: [PATCH] knotserver: support git-upload-archive service over http Change-Id: knmpmozlkyqontpwrzpmxrqtoukzrspk Allows a knotserver-hosted repository to be the target of a `git archive --remote=https://` command. Signed-off-by: tjh --- knotserver/git.go | 47 +++++++++++++++++++++++++++++++ knotserver/git/service/service.go | 13 +++++++++ knotserver/router.go | 1 + 3 files changed, 61 insertions(+) diff --git a/knotserver/git.go b/knotserver/git.go index 4c89a90a..e2018c46 100644 --- a/knotserver/git.go +++ b/knotserver/git.go @@ -56,6 +56,53 @@ func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { } } +func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { + did := chi.URLParam(r, "did") + name := chi.URLParam(r, "name") + repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) + if err != nil { + gitError(w, err.Error(), http.StatusInternalServerError) + h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) + return + } + + const expectedContentType = "application/x-git-upload-archive-request" + contentType := r.Header.Get("Content-Type") + if contentType != expectedContentType { + gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) + } + + var bodyReader io.ReadCloser = r.Body + if r.Header.Get("Content-Encoding") == "gzip" { + gzipReader, err := gzip.NewReader(r.Body) + if err != nil { + gitError(w, err.Error(), http.StatusInternalServerError) + h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err) + return + } + defer gzipReader.Close() + bodyReader = gzipReader + } + + w.Header().Set("Content-Type", "application/x-git-upload-archive-result") + + h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo) + + cmd := service.ServiceCommand{ + GitProtocol: r.Header.Get("Git-Protocol"), + Dir: repo, + Stdout: w, + Stdin: bodyReader, + } + + w.WriteHeader(http.StatusOK) + + if err := cmd.UploadArchive(); err != nil { + h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) + return + } +} + func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { did := chi.URLParam(r, "did") name := chi.URLParam(r, "name") diff --git a/knotserver/git/service/service.go b/knotserver/git/service/service.go index bd6438b2..24411afc 100644 --- a/knotserver/git/service/service.go +++ b/knotserver/git/service/service.go @@ -95,6 +95,19 @@ func (c *ServiceCommand) InfoRefs() error { return c.RunService(cmd) } +func (c *ServiceCommand) UploadArchive() error { + cmd := exec.Command("git", []string{ + "upload-archive", + ".", + }...) + + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol)) + cmd.Dir = c.Dir + + return c.RunService(cmd) +} + func (c *ServiceCommand) UploadPack() error { cmd := exec.Command("git", []string{ "-c", "uploadpack.allowFilter=true", diff --git a/knotserver/router.go b/knotserver/router.go index 482f932d..3b27d832 100644 --- a/knotserver/router.go +++ b/knotserver/router.go @@ -82,6 +82,7 @@ func (h *Knot) Router() http.Handler { r.Route("/{name}", func(r chi.Router) { // routes for git operations r.Get("/info/refs", h.InfoRefs) + r.Post("/git-upload-archive", h.UploadArchive) r.Post("/git-upload-pack", h.UploadPack) r.Post("/git-receive-pack", h.ReceivePack) }) -- 2.51.1.dirty