A vibe coded tangled fork which supports pijul.

appview/state: harden knot proxying

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

+48 -16
+48 -16
appview/state/git_http.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 - "maps" 7 6 "net/http" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/identity" ··· 11 10 "tangled.org/core/appview/models" 12 11 ) 13 12 13 + // allowedResponseHeaders is the set of headers we will forward from the knot 14 + // back to the client. everything else is stripped. 15 + var allowedResponseHeaders = map[string]bool{ 16 + "Content-Encoding": true, 17 + "Transfer-Encoding": true, 18 + "Cache-Control": true, 19 + "Expires": true, 20 + "Pragma": true, 21 + } 22 + 23 + func copyAllowedHeaders(dst, src http.Header) { 24 + for k, vv := range src { 25 + if allowedResponseHeaders[http.CanonicalHeaderKey(k)] { 26 + for _, v := range vv { 27 + dst.Add(k, v) 28 + } 29 + } 30 + } 31 + } 32 + 33 + func setGitHeaders(w http.ResponseWriter, contentType string) { 34 + w.Header().Set("Content-Type", contentType) 35 + w.Header().Set("Content-Disposition", "attachment") 36 + w.Header().Set("X-Content-Type-Options", "nosniff") 37 + } 38 + 14 39 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 40 user := r.Context().Value("resolvedId").(identity.Identity) 16 41 repo := r.Context().Value("repo").(*models.Repo) ··· 20 45 scheme = "http" 21 46 } 22 47 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 - s.proxyRequest(w, r, targetURL) 48 + // check for the 'service' url param 49 + service := r.URL.Query().Get("service") 50 + var contentType string 51 + switch service { 52 + case "git-receive-pack": 53 + contentType = "application/x-git-receive-pack-advertisement" 54 + default: 55 + // git-upload-pack is the default service for git-clone / git-fetch. 56 + contentType = "application/x-git-upload-pack-advertisement" 57 + } 25 58 59 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 60 + s.proxyRequest(w, r, targetURL, contentType) 26 61 } 27 62 28 63 func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { ··· 39 74 } 40 75 41 76 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 - s.proxyRequest(w, r, targetURL) 77 + s.proxyRequest(w, r, targetURL, "application/x-git-upload-archive-result") 43 78 } 44 79 45 80 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { ··· 56 91 } 57 92 58 93 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 - s.proxyRequest(w, r, targetURL) 94 + s.proxyRequest(w, r, targetURL, "application/x-git-upload-pack-result") 60 95 } 61 96 62 97 func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { ··· 73 108 } 74 109 75 110 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 76 - s.proxyRequest(w, r, targetURL) 111 + s.proxyRequest(w, r, targetURL, "application/x-git-receive-pack-result") 77 112 } 78 113 79 - func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 114 + func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string, contentType string) { 80 115 client := &http.Client{} 81 116 82 - // Create new request 83 117 proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 84 118 if err != nil { 85 119 http.Error(w, err.Error(), http.StatusInternalServerError) 86 120 return 87 121 } 88 122 89 - // Copy original headers 90 - proxyReq.Header = r.Header 123 + proxyReq.Header = r.Header.Clone() 91 124 92 125 repoOwnerHandle := chi.URLParam(r, "user") 93 - proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 126 + proxyReq.Header.Set("x-tangled-repo-owner-handle", repoOwnerHandle) 94 127 95 - // Execute request 96 128 resp, err := client.Do(proxyReq) 97 129 if err != nil { 98 130 http.Error(w, err.Error(), http.StatusInternalServerError) ··· 100 132 } 101 133 defer resp.Body.Close() 102 134 103 - // Copy response headers 104 - maps.Copy(w.Header(), resp.Header) 135 + // selectively copy only allowed headers 136 + copyAllowedHeaders(w.Header(), resp.Header) 105 137 106 - // Set response status code 138 + setGitHeaders(w, contentType) 139 + 107 140 w.WriteHeader(resp.StatusCode) 108 141 109 - // Copy response body 110 142 if _, err := io.Copy(w, resp.Body); err != nil { 111 143 http.Error(w, err.Error(), http.StatusInternalServerError) 112 144 return