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