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