Monorepo for Tangled
at push-pkuzytwlwptp 201 lines 6.3 kB view raw
1package knotserver 2 3import ( 4 "compress/gzip" 5 "fmt" 6 "io" 7 "net/http" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 "tangled.org/core/knotserver/git/service" 12) 13 14func (h *Knot) resolveRepoPath(r *http.Request) (string, string, error) { 15 did := chi.URLParam(r, "did") 16 name := chi.URLParam(r, "name") 17 18 if name == "" && strings.HasPrefix(did, "did:") { 19 repoPath, _, repoName, err := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, did) 20 if err != nil { 21 return "", "", fmt.Errorf("unknown repo DID: %w", err) 22 } 23 return repoPath, repoName, nil 24 } 25 26 repoPath, _, err := h.db.ResolveRepoOnDisk(h.c.Repo.ScanPath, did, name) 27 if err != nil { 28 return "", "", fmt.Errorf("repo not found: %w", err) 29 } 30 return repoPath, name, nil 31} 32 33func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 34 repoPath, name, err := h.resolveRepoPath(r) 35 if err != nil { 36 gitError(w, "repository not found", http.StatusNotFound) 37 h.l.Error("git: failed to resolve repo path", "handler", "InfoRefs", "error", err) 38 return 39 } 40 41 cmd := service.ServiceCommand{ 42 GitProtocol: r.Header.Get("Git-Protocol"), 43 Dir: repoPath, 44 Stdout: w, 45 } 46 47 serviceName := r.URL.Query().Get("service") 48 switch serviceName { 49 case "git-upload-pack": 50 w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement") 51 w.Header().Set("Connection", "Keep-Alive") 52 w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 53 w.WriteHeader(http.StatusOK) 54 55 if err := cmd.InfoRefs(); err != nil { 56 gitError(w, err.Error(), http.StatusInternalServerError) 57 h.l.Error("git: process failed", "handler", "InfoRefs", "service", serviceName, "error", err) 58 return 59 } 60 case "git-receive-pack": 61 h.RejectPush(w, r, name) 62 default: 63 gitError(w, fmt.Sprintf("service unsupported: '%s'", serviceName), http.StatusForbidden) 64 } 65} 66 67func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 68 repo, _, err := h.resolveRepoPath(r) 69 if err != nil { 70 gitError(w, "repository not found", http.StatusNotFound) 71 h.l.Error("git: failed to resolve repo path", "handler", "UploadArchive", "error", err) 72 return 73 } 74 75 const expectedContentType = "application/x-git-upload-archive-request" 76 contentType := r.Header.Get("Content-Type") 77 if contentType != expectedContentType { 78 gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) 79 } 80 81 var bodyReader io.ReadCloser = r.Body 82 if r.Header.Get("Content-Encoding") == "gzip" { 83 gzipReader, err := gzip.NewReader(r.Body) 84 if err != nil { 85 gitError(w, err.Error(), http.StatusInternalServerError) 86 h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err) 87 return 88 } 89 defer gzipReader.Close() 90 bodyReader = gzipReader 91 } 92 93 w.Header().Set("Content-Type", "application/x-git-upload-archive-result") 94 95 h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo) 96 97 cmd := service.ServiceCommand{ 98 GitProtocol: r.Header.Get("Git-Protocol"), 99 Dir: repo, 100 Stdout: w, 101 Stdin: bodyReader, 102 } 103 104 w.WriteHeader(http.StatusOK) 105 106 if err := cmd.UploadArchive(); err != nil { 107 h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 108 return 109 } 110} 111 112func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 113 repo, _, err := h.resolveRepoPath(r) 114 if err != nil { 115 gitError(w, "repository not found", http.StatusNotFound) 116 h.l.Error("git: failed to resolve repo path", "handler", "UploadPack", "error", err) 117 return 118 } 119 120 const expectedContentType = "application/x-git-upload-pack-request" 121 contentType := r.Header.Get("Content-Type") 122 if contentType != expectedContentType { 123 gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) 124 } 125 126 var bodyReader io.ReadCloser = r.Body 127 if r.Header.Get("Content-Encoding") == "gzip" { 128 gzipReader, err := gzip.NewReader(r.Body) 129 if err != nil { 130 gitError(w, err.Error(), http.StatusInternalServerError) 131 h.l.Error("git: failed to create gzip reader", "handler", "UploadPack", "error", err) 132 return 133 } 134 defer gzipReader.Close() 135 bodyReader = gzipReader 136 } 137 138 w.Header().Set("Content-Type", "application/x-git-upload-pack-result") 139 w.Header().Set("Connection", "Keep-Alive") 140 w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 141 142 h.l.Info("git: executing git-upload-pack", "handler", "UploadPack", "repo", repo) 143 144 cmd := service.ServiceCommand{ 145 GitProtocol: r.Header.Get("Git-Protocol"), 146 Dir: repo, 147 Stdout: w, 148 Stdin: bodyReader, 149 } 150 151 w.WriteHeader(http.StatusOK) 152 153 if err := cmd.UploadPack(); err != nil { 154 h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 155 return 156 } 157} 158 159func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 160 _, name, err := h.resolveRepoPath(r) 161 if err != nil { 162 gitError(w, "repository not found", http.StatusNotFound) 163 h.l.Error("git: failed to resolve repo path", "handler", "ReceivePack", "error", err) 164 return 165 } 166 167 h.RejectPush(w, r, name) 168} 169 170func (h *Knot) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRepoName string) { 171 // A text/plain response will cause git to print each line of the body 172 // prefixed with "remote: ". 173 w.Header().Set("content-type", "text/plain; charset=UTF-8") 174 w.WriteHeader(http.StatusForbidden) 175 176 fmt.Fprintf(w, "Pushes are only supported over SSH.") 177 178 // If the appview gave us the repository owner's handle we can attempt to 179 // construct the correct ssh url. 180 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle") 181 ownerHandle = strings.TrimPrefix(ownerHandle, "@") 182 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") { 183 hostname := h.c.Server.Hostname 184 if strings.Contains(hostname, ":") { 185 hostname = strings.Split(hostname, ":")[0] 186 } 187 188 if hostname == "knot1.tangled.sh" { 189 hostname = "tangled.sh" 190 } 191 192 fmt.Fprintf(w, " Try:\ngit remote set-url --push origin git@%s:%s/%s\n\n... and push again.", hostname, ownerHandle, unqualifiedRepoName) 193 } 194 fmt.Fprintf(w, "\n\n") 195} 196 197func gitError(w http.ResponseWriter, msg string, status int) { 198 w.Header().Set("content-type", "text/plain; charset=UTF-8") 199 w.WriteHeader(status) 200 fmt.Fprintf(w, "%s\n", msg) 201}