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