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/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 h.l.Error("failed to parse workflow", "err", err, "path", fpath) 190 continue 191 } 192 193 pipeline = append(pipeline, wf) 194 } 195 196 trigger := tangled.Pipeline_PushTriggerData{ 197 Ref: line.Ref, 198 OldSha: line.OldSha.String(), 199 NewSha: line.NewSha.String(), 200 } 201 202 compiler := workflow.Compiler{ 203 Trigger: tangled.Pipeline_TriggerMetadata{ 204 Kind: string(workflow.TriggerKindPush), 205 Push: &trigger, 206 Repo: &tangled.Pipeline_TriggerRepo{ 207 Did: repoDid, 208 Knot: h.c.Server.Hostname, 209 Repo: repoName, 210 }, 211 }, 212 } 213 214 // TODO: send the diagnostics back to the user here via stderr 215 cp := compiler.Compile(pipeline) 216 eventJson, err := json.Marshal(cp) 217 if err != nil { 218 return err 219 } 220 221 // do not run empty pipelines 222 if cp.Workflows == nil { 223 return nil 224 } 225 226 event := db.Event{ 227 Rkey: TID(), 228 Nsid: tangled.PipelineNSID, 229 EventJson: string(eventJson), 230 } 231 232 return h.db.InsertEvent(event, h.n) 233} 234 235func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler { 236 r := chi.NewRouter() 237 238 h := InternalHandle{ 239 db, 240 c, 241 e, 242 l, 243 n, 244 } 245 246 r.Get("/push-allowed", h.PushAllowed) 247 r.Get("/keys", h.InternalKeys) 248 r.Post("/hooks/post-receive", h.PostReceiveHook) 249 r.Mount("/debug", middleware.Profiler()) 250 251 return r 252}