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