Monorepo for Tangled
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}