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.sh/tangled.sh/core/knotserver/git/service"
14)
15
16func (d *Handle) 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 d.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
23 return
24 }
25
26 repoPath, err := securejoin.SecureJoin(d.c.Repo.ScanPath, repoName)
27 if err != nil {
28 gitError(w, "repository not found", http.StatusNotFound)
29 d.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 d.l.Error("git: process failed", "handler", "InfoRefs", "service", serviceName, "error", err)
50 return
51 }
52 case "git-receive-pack":
53 d.RejectPush(w, r, name)
54 default:
55 gitError(w, fmt.Sprintf("service unsupported: '%s'", serviceName), http.StatusForbidden)
56 }
57}
58
59func (d *Handle) UploadPack(w http.ResponseWriter, r *http.Request) {
60 did := chi.URLParam(r, "did")
61 name := chi.URLParam(r, "name")
62 repo, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name))
63 if err != nil {
64 gitError(w, err.Error(), http.StatusInternalServerError)
65 d.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err)
66 return
67 }
68
69 const expectedContentType = "application/x-git-upload-pack-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 d.l.Error("git: failed to create gzip reader", "handler", "UploadPack", "error", err)
81 return
82 }
83 defer gzipReader.Close()
84 bodyReader = gzipReader
85 }
86
87 w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
88 w.Header().Set("Connection", "Keep-Alive")
89 w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
90
91 d.l.Info("git: executing git-upload-pack", "handler", "UploadPack", "repo", repo)
92
93 cmd := service.ServiceCommand{
94 GitProtocol: r.Header.Get("Git-Protocol"),
95 Dir: repo,
96 Stdout: w,
97 Stdin: bodyReader,
98 }
99
100 w.WriteHeader(http.StatusOK)
101
102 if err := cmd.UploadPack(); err != nil {
103 d.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err)
104 return
105 }
106}
107
108func (d *Handle) ReceivePack(w http.ResponseWriter, r *http.Request) {
109 did := chi.URLParam(r, "did")
110 name := chi.URLParam(r, "name")
111 _, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name))
112 if err != nil {
113 gitError(w, err.Error(), http.StatusForbidden)
114 d.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err)
115 return
116 }
117
118 d.RejectPush(w, r, name)
119}
120
121func (d *Handle) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRepoName string) {
122 // A text/plain response will cause git to print each line of the body
123 // prefixed with "remote: ".
124 w.Header().Set("content-type", "text/plain; charset=UTF-8")
125 w.WriteHeader(http.StatusForbidden)
126
127 fmt.Fprintf(w, "Pushes are only supported over SSH.")
128
129 // If the appview gave us the repository owner's handle we can attempt to
130 // construct the correct ssh url.
131 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle")
132 ownerHandle = strings.TrimPrefix(ownerHandle, "@")
133 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") {
134 hostname := d.c.Server.Hostname
135 if strings.Contains(hostname, ":") {
136 hostname = strings.Split(hostname, ":")[0]
137 }
138
139 if hostname == "knot1.tangled.sh" {
140 hostname = "tangled.sh"
141 }
142
143 fmt.Fprintf(w, " Try:\ngit remote set-url --push origin git@%s:%s/%s\n\n... and push again.", hostname, ownerHandle, unqualifiedRepoName)
144 }
145 fmt.Fprintf(w, "\n\n")
146}
147
148func gitError(w http.ResponseWriter, msg string, status int) {
149 w.Header().Set("content-type", "text/plain; charset=UTF-8")
150 w.WriteHeader(status)
151 fmt.Fprintf(w, "%s\n", msg)
152}