forked from tangled.org/core
this repo has no description
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 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}