1package knotserver
2
3import (
4 "compress/gzip"
5 "fmt"
6 "io"
7 "net/http"
8 "path/filepath"
9
10 securejoin "github.com/cyphar/filepath-securejoin"
11 "github.com/go-chi/chi/v5"
12 "tangled.sh/tangled.sh/core/knotserver/git/service"
13)
14
15func (d *Handle) InfoRefs(w http.ResponseWriter, r *http.Request) {
16 did := chi.URLParam(r, "did")
17 name := chi.URLParam(r, "name")
18 repoName, err := securejoin.SecureJoin(did, name)
19 if err != nil {
20 gitError(w, "repository not found", http.StatusNotFound)
21 d.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
22 return
23 }
24
25 repoPath, err := securejoin.SecureJoin(d.c.Repo.ScanPath, repoName)
26 if err != nil {
27 gitError(w, "repository not found", http.StatusNotFound)
28 d.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
29 return
30 }
31
32 cmd := service.ServiceCommand{
33 Dir: repoPath,
34 Stdout: w,
35 }
36
37 serviceName := r.URL.Query().Get("service")
38 switch serviceName {
39 case "git-upload-pack":
40 w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
41 w.WriteHeader(http.StatusOK)
42
43 if err := cmd.InfoRefs(); err != nil {
44 gitError(w, err.Error(), http.StatusInternalServerError)
45 d.l.Error("git: process failed", "handler", "InfoRefs", "service", serviceName, "error", err)
46 return
47 }
48 case "git-receive-pack":
49 d.RejectPush(w, r, name)
50 default:
51 gitError(w, fmt.Sprintf("service unsupported: '%s'", serviceName), http.StatusForbidden)
52 }
53}
54
55func (d *Handle) UploadPack(w http.ResponseWriter, r *http.Request) {
56 did := chi.URLParam(r, "did")
57 name := chi.URLParam(r, "name")
58 repo, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name))
59 if err != nil {
60 writeError(w, err.Error(), 500)
61 d.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err)
62 return
63 }
64
65 var bodyReader io.ReadCloser = r.Body
66 if r.Header.Get("Content-Encoding") == "gzip" {
67 gzipReader, err := gzip.NewReader(r.Body)
68 if err != nil {
69 writeError(w, err.Error(), 500)
70 d.l.Error("git: failed to create gzip reader", "handler", "UploadPack", "error", err)
71 return
72 }
73 defer gzipReader.Close()
74 bodyReader = gzipReader
75 }
76
77 w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
78 w.Header().Set("Connection", "Keep-Alive")
79
80 d.l.Info("git: executing git-upload-pack", "handler", "UploadPack", "repo", repo)
81
82 cmd := service.ServiceCommand{
83 Dir: repo,
84 Stdout: w,
85 Stdin: bodyReader,
86 }
87
88 w.WriteHeader(http.StatusOK)
89
90 if err := cmd.UploadPack(); err != nil {
91 d.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err)
92 return
93 }
94}
95
96func (d *Handle) ReceivePack(w http.ResponseWriter, r *http.Request) {
97 did := chi.URLParam(r, "did")
98 name := chi.URLParam(r, "name")
99 _, err := securejoin.SecureJoin(d.c.Repo.ScanPath, filepath.Join(did, name))
100 if err != nil {
101 gitError(w, err.Error(), http.StatusForbidden)
102 d.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err)
103 return
104 }
105
106 d.RejectPush(w, r, name)
107}
108
109func (d *Handle) RejectPush(w http.ResponseWriter, r *http.Request, unqualifiedRepoName string) {
110 // A text/plain response will cause git to print each line of the body
111 // prefixed with "remote: ".
112 w.Header().Set("content-type", "text/plain; charset=UTF-8")
113 w.WriteHeader(http.StatusForbidden)
114
115 fmt.Fprintf(w, "Welcome to Tangled.sh!\n\nPushes are currently only supported over SSH.")
116 fmt.Fprintf(w, "\n\n")
117}
118
119func gitError(w http.ResponseWriter, msg string, status int) {
120 w.Header().Set("content-type", "text/plain; charset=UTF-8")
121 w.WriteHeader(status)
122 fmt.Fprintf(w, "%s\n", msg)
123}