forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}