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