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