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/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 refUpdate := tangled.GitRefUpdate{ 109 OldSha: line.OldSha, 110 NewSha: line.NewSha, 111 Ref: line.Ref, 112 CommitterDid: gitUserDid, 113 RepoDid: repoDid, 114 RepoName: repoName, 115 } 116 eventJson, err := json.Marshal(refUpdate) 117 if err != nil { 118 return err 119 } 120 121 event := db.Event{ 122 Rkey: TID(), 123 Nsid: tangled.GitRefUpdateNSID, 124 EventJson: string(eventJson), 125 } 126 127 return h.db.InsertEvent(event, h.n) 128} 129 130func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error { 131 const ( 132 WorkflowDir = ".tangled/workflows" 133 ) 134 135 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 136 if err != nil { 137 return err 138 } 139 140 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 141 if err != nil { 142 return err 143 } 144 145 gr, err := git.Open(repoPath, line.Ref) 146 if err != nil { 147 return err 148 } 149 150 workflowDir, err := gr.FileTree(context.Background(), WorkflowDir) 151 if err != nil { 152 return err 153 } 154 155 var pipeline workflow.Pipeline 156 for _, e := range workflowDir { 157 if !e.IsFile { 158 continue 159 } 160 161 fpath := filepath.Join(WorkflowDir, e.Name) 162 contents, err := gr.RawContent(fpath) 163 if err != nil { 164 continue 165 } 166 167 wf, err := workflow.FromFile(e.Name, contents) 168 if err != nil { 169 // TODO: log here, respond to client that is pushing 170 continue 171 } 172 173 pipeline = append(pipeline, wf) 174 } 175 176 trigger := tangled.Pipeline_PushTriggerData{ 177 Ref: line.Ref, 178 OldSha: line.OldSha, 179 NewSha: line.NewSha, 180 } 181 182 compiler := workflow.Compiler{ 183 Trigger: tangled.Pipeline_TriggerMetadata{ 184 Kind: string(workflow.TriggerKindPush), 185 Push: &trigger, 186 Repo: &tangled.Pipeline_TriggerRepo{ 187 Did: repoDid, 188 Knot: h.c.Server.Hostname, 189 Repo: repoName, 190 }, 191 }, 192 } 193 194 // TODO: send the diagnostics back to the user here via stderr 195 cp := compiler.Compile(pipeline) 196 eventJson, err := json.Marshal(cp) 197 if err != nil { 198 return err 199 } 200 201 event := db.Event{ 202 Rkey: TID(), 203 Nsid: tangled.PipelineNSID, 204 EventJson: string(eventJson), 205 } 206 207 return h.db.InsertEvent(event, h.n) 208} 209 210func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler { 211 r := chi.NewRouter() 212 213 h := InternalHandle{ 214 db, 215 c, 216 e, 217 l, 218 n, 219 } 220 221 r.Get("/push-allowed", h.PushAllowed) 222 r.Get("/keys", h.InternalKeys) 223 r.Post("/hooks/post-receive", h.PostReceiveHook) 224 r.Mount("/debug", middleware.Profiler()) 225 226 return r 227}