Monorepo for Tangled tangled.org
3

Configure Feed

Select the types of activity you want to include in your feed.

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