This repository has no description
0

Configure Feed

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

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