1package knotserver
2
3import (
4 "bufio"
5 "context"
6 "log/slog"
7 "net/http"
8 "path/filepath"
9 "strings"
10
11 "github.com/go-chi/chi/v5"
12 "github.com/go-chi/chi/v5/middleware"
13 "tangled.sh/tangled.sh/core/knotserver/config"
14 "tangled.sh/tangled.sh/core/knotserver/db"
15 "tangled.sh/tangled.sh/core/knotserver/notifier"
16 "tangled.sh/tangled.sh/core/rbac"
17)
18
19type InternalHandle struct {
20 db *db.DB
21 c *config.Config
22 e *rbac.Enforcer
23 l *slog.Logger
24 n *notifier.Notifier
25}
26
27func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) {
28 user := r.URL.Query().Get("user")
29 repo := r.URL.Query().Get("repo")
30
31 if user == "" || repo == "" {
32 w.WriteHeader(http.StatusBadRequest)
33 return
34 }
35
36 ok, err := h.e.IsPushAllowed(user, ThisServer, repo)
37 if err != nil || !ok {
38 w.WriteHeader(http.StatusForbidden)
39 return
40 }
41
42 w.WriteHeader(http.StatusNoContent)
43 return
44}
45
46func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) {
47 keys, err := h.db.GetAllPublicKeys()
48 if err != nil {
49 writeError(w, err.Error(), http.StatusInternalServerError)
50 return
51 }
52
53 data := make([]map[string]interface{}, 0)
54 for _, key := range keys {
55 j := key.JSON()
56 data = append(data, j)
57 }
58 writeJSON(w, data)
59 return
60}
61
62func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) {
63 l := h.l.With("handler", "PostReceiveHook")
64
65 gitAbsoluteDir := r.Header.Get("X-Git-Dir")
66 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir)
67 if err != nil {
68 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir)
69 return
70 }
71 gitUserDid := r.Header.Get("X-Git-User-Did")
72
73 var ops []db.Op
74 scanner := bufio.NewScanner(r.Body)
75 for scanner.Scan() {
76 line := scanner.Text()
77 parts := strings.SplitN(line, " ", 3)
78 if len(parts) != 3 {
79 l.Error("invalid payload", "parts", parts)
80 continue
81 }
82
83 tid := TID()
84 oldSha := parts[0]
85 newSha := parts[1]
86 ref := parts[2]
87 op := db.Op{
88 Tid: tid,
89 Did: gitUserDid,
90 Repo: gitRelativeDir,
91 OldSha: oldSha,
92 NewSha: newSha,
93 Ref: ref,
94 }
95 ops = append(ops, op)
96 }
97
98 if err := scanner.Err(); err != nil {
99 l.Error("failed to read payload", "err", err)
100 return
101 }
102
103 for _, op := range ops {
104 err := h.db.InsertOp(op, h.n)
105 if err != nil {
106 l.Error("failed to insert op", "err", err, "op", op)
107 continue
108 }
109 }
110
111 return
112}
113
114func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler {
115 r := chi.NewRouter()
116
117 h := InternalHandle{
118 db,
119 c,
120 e,
121 l,
122 n,
123 }
124
125 r.Get("/push-allowed", h.PushAllowed)
126 r.Get("/keys", h.InternalKeys)
127 r.Post("/hooks/post-receive", h.PostReceiveHook)
128 r.Mount("/debug", middleware.Profiler())
129
130 return r
131}