diff --git a/appview/repo/archive.go b/appview/repo/archive.go --- a/appview/repo/archive.go +++ b/appview/repo/archive.go @@ -54,16 +54,19 @@ return } defer resp.Body.Close() - // pass through headers from upstream response - if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" { - w.Header().Set("Content-Disposition", contentDisposition) + // force application/gzip here + w.Header().Set("Content-Type", "application/gzip") + + filename := "" + if cd := resp.Header.Get("Content-Disposition"); strings.HasPrefix(cd, "attachment;") { + filename = cd // knot has already set the attachment CD } - if contentType := resp.Header.Get("Content-Type"); contentType != "" { - w.Header().Set("Content-Type", contentType) + if filename == "" { + filename = fmt.Sprintf("attachment; filename=\"%s-%s.tar.gz\"", f.Name, ref) } - if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { - w.Header().Set("Content-Length", contentLength) - } + w.Header().Set("Content-Disposition", filename) + w.Header().Set("X-Content-Type-Options", "nosniff") + if link := resp.Header.Get("Link"); link != "" { if resolvedRef, err := extractImmutableLink(link); err == nil { newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", diff --git a/appview/state/git_http.go b/appview/state/git_http.go --- a/appview/state/git_http.go +++ b/appview/state/git_http.go @@ -3,7 +3,6 @@ import ( "fmt" "io" - "maps" "net/http" "github.com/bluesky-social/indigo/atproto/identity" @@ -11,6 +10,32 @@ "github.com/go-chi/chi/v5" "tangled.org/core/appview/models" ) +// allowedResponseHeaders is the set of headers we will forward from the knot +// back to the client. everything else is stripped. +var allowedResponseHeaders = map[string]bool{ + "Content-Encoding": true, + "Transfer-Encoding": true, + "Cache-Control": true, + "Expires": true, + "Pragma": true, +} + +func copyAllowedHeaders(dst, src http.Header) { + for k, vv := range src { + if allowedResponseHeaders[http.CanonicalHeaderKey(k)] { + for _, v := range vv { + dst.Add(k, v) + } + } + } +} + +func setGitHeaders(w http.ResponseWriter, contentType string) { + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Disposition", "attachment") + w.Header().Set("X-Content-Type-Options", "nosniff") +} + func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("resolvedId").(identity.Identity) repo := r.Context().Value("repo").(*models.Repo) @@ -20,9 +45,19 @@ if s.config.Core.Dev { scheme = "http" } - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) - s.proxyRequest(w, r, targetURL) + // check for the 'service' url param + service := r.URL.Query().Get("service") + var contentType string + switch service { + case "git-receive-pack": + contentType = "application/x-git-receive-pack-advertisement" + default: + // git-upload-pack is the default service for git-clone / git-fetch. + contentType = "application/x-git-upload-pack-advertisement" + } + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) + s.proxyRequest(w, r, targetURL, contentType) } func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { @@ -39,7 +74,7 @@ scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) - s.proxyRequest(w, r, targetURL) + s.proxyRequest(w, r, targetURL, "application/x-git-upload-archive-result") } func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { @@ -56,7 +91,7 @@ scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) - s.proxyRequest(w, r, targetURL) + s.proxyRequest(w, r, targetURL, "application/x-git-upload-pack-result") } func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { @@ -73,26 +108,23 @@ scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) - s.proxyRequest(w, r, targetURL) + s.proxyRequest(w, r, targetURL, "application/x-git-receive-pack-result") } -func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { +func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string, contentType string) { client := &http.Client{} - // Create new request proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - // Copy original headers - proxyReq.Header = r.Header + proxyReq.Header = r.Header.Clone() repoOwnerHandle := chi.URLParam(r, "user") - proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) + proxyReq.Header.Set("x-tangled-repo-owner-handle", repoOwnerHandle) - // Execute request resp, err := client.Do(proxyReq) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -100,13 +132,13 @@ return } defer resp.Body.Close() - // Copy response headers - maps.Copy(w.Header(), resp.Header) + // selectively copy only allowed headers + copyAllowedHeaders(w.Header(), resp.Header) - // Set response status code + setGitHeaders(w, contentType) + w.WriteHeader(resp.StatusCode) - // Copy response body if _, err := io.Copy(w, resp.Body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return