forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package knotserver
2
3import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "net/http"
8 "path/filepath"
9 "strings"
10
11 securejoin "github.com/cyphar/filepath-securejoin"
12 "github.com/go-chi/chi/v5"
13 "github.com/go-chi/chi/v5/middleware"
14 "tangled.sh/tangled.sh/core/api/tangled"
15 "tangled.sh/tangled.sh/core/knotserver/config"
16 "tangled.sh/tangled.sh/core/knotserver/db"
17 "tangled.sh/tangled.sh/core/knotserver/git"
18 "tangled.sh/tangled.sh/core/knotserver/notifier"
19 "tangled.sh/tangled.sh/core/rbac"
20 "tangled.sh/tangled.sh/core/workflow"
21)
22
23type InternalHandle struct {
24 db *db.DB
25 c *config.Config
26 e *rbac.Enforcer
27 l *slog.Logger
28 n *notifier.Notifier
29}
30
31func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) {
32 user := r.URL.Query().Get("user")
33 repo := r.URL.Query().Get("repo")
34
35 if user == "" || repo == "" {
36 w.WriteHeader(http.StatusBadRequest)
37 return
38 }
39
40 ok, err := h.e.IsPushAllowed(user, ThisServer, repo)
41 if err != nil || !ok {
42 w.WriteHeader(http.StatusForbidden)
43 return
44 }
45
46 w.WriteHeader(http.StatusNoContent)
47 return
48}
49
50func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) {
51 keys, err := h.db.GetAllPublicKeys()
52 if err != nil {
53 writeError(w, err.Error(), http.StatusInternalServerError)
54 return
55 }
56
57 data := make([]map[string]interface{}, 0)
58 for _, key := range keys {
59 j := key.JSON()
60 data = append(data, j)
61 }
62 writeJSON(w, data)
63 return
64}
65
66func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) {
67 l := h.l.With("handler", "PostReceiveHook")
68
69 gitAbsoluteDir := r.Header.Get("X-Git-Dir")
70 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir)
71 if err != nil {
72 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir)
73 return
74 }
75
76 parts := strings.SplitN(gitRelativeDir, "/", 2)
77 if len(parts) != 2 {
78 l.Error("invalid git dir", "gitRelativeDir", gitRelativeDir)
79 return
80 }
81 repoDid := parts[0]
82 repoName := parts[1]
83
84 gitUserDid := r.Header.Get("X-Git-User-Did")
85
86 lines, err := git.ParsePostReceive(r.Body)
87 if err != nil {
88 l.Error("failed to parse post-receive payload", "err", err)
89 // non-fatal
90 }
91
92 for _, line := range lines {
93 err := h.insertRefUpdate(line, gitUserDid, repoDid, repoName)
94 if err != nil {
95 l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir)
96 // non-fatal
97 }
98
99 err = h.triggerPipeline(line, gitUserDid, repoDid, repoName)
100 if err != nil {
101 l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir)
102 // non-fatal
103 }
104 }
105}
106
107func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
108 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName)
109 if err != nil {
110 return err
111 }
112
113 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo)
114 if err != nil {
115 return err
116 }
117
118 gr, err := git.PlainOpen(repoPath)
119 if err != nil {
120 return err
121 }
122
123 meta := gr.RefUpdateMeta(line)
124 metaRecord := meta.AsRecord()
125
126 refUpdate := tangled.GitRefUpdate{
127 OldSha: line.OldSha.String(),
128 NewSha: line.NewSha.String(),
129 Ref: line.Ref,
130 CommitterDid: gitUserDid,
131 RepoDid: repoDid,
132 RepoName: repoName,
133 Meta: &metaRecord,
134 }
135 eventJson, err := json.Marshal(refUpdate)
136 if err != nil {
137 return err
138 }
139
140 event := db.Event{
141 Rkey: TID(),
142 Nsid: tangled.GitRefUpdateNSID,
143 EventJson: string(eventJson),
144 }
145
146 return h.db.InsertEvent(event, h.n)
147}
148
149func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error {
150 const (
151 WorkflowDir = ".tangled/workflows"
152 )
153
154 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName)
155 if err != nil {
156 return err
157 }
158
159 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo)
160 if err != nil {
161 return err
162 }
163
164 gr, err := git.Open(repoPath, line.Ref)
165 if err != nil {
166 return err
167 }
168
169 workflowDir, err := gr.FileTree(context.Background(), WorkflowDir)
170 if err != nil {
171 return err
172 }
173
174 var pipeline workflow.Pipeline
175 for _, e := range workflowDir {
176 if !e.IsFile {
177 continue
178 }
179
180 fpath := filepath.Join(WorkflowDir, e.Name)
181 contents, err := gr.RawContent(fpath)
182 if err != nil {
183 continue
184 }
185
186 wf, err := workflow.FromFile(e.Name, contents)
187 if err != nil {
188 // TODO: log here, respond to client that is pushing
189 continue
190 }
191
192 pipeline = append(pipeline, wf)
193 }
194
195 trigger := tangled.Pipeline_PushTriggerData{
196 Ref: line.Ref,
197 OldSha: line.OldSha.String(),
198 NewSha: line.NewSha.String(),
199 }
200
201 compiler := workflow.Compiler{
202 Trigger: tangled.Pipeline_TriggerMetadata{
203 Kind: string(workflow.TriggerKindPush),
204 Push: &trigger,
205 Repo: &tangled.Pipeline_TriggerRepo{
206 Did: repoDid,
207 Knot: h.c.Server.Hostname,
208 Repo: repoName,
209 },
210 },
211 }
212
213 // TODO: send the diagnostics back to the user here via stderr
214 cp := compiler.Compile(pipeline)
215 eventJson, err := json.Marshal(cp)
216 if err != nil {
217 return err
218 }
219
220 event := db.Event{
221 Rkey: TID(),
222 Nsid: tangled.PipelineNSID,
223 EventJson: string(eventJson),
224 }
225
226 return h.db.InsertEvent(event, h.n)
227}
228
229func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler {
230 r := chi.NewRouter()
231
232 h := InternalHandle{
233 db,
234 c,
235 e,
236 l,
237 n,
238 }
239
240 r.Get("/push-allowed", h.PushAllowed)
241 r.Get("/keys", h.InternalKeys)
242 r.Post("/hooks/post-receive", h.PostReceiveHook)
243 r.Mount("/debug", middleware.Profiler())
244
245 return r
246}