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