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}