A vibe coded tangled fork which supports pijul.
at sl/lvnwqspuwzom 227 lines 5.8 kB view raw
1package knotserver 2 3import ( 4 "context" 5 "fmt" 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 "tangled.org/core/idresolver" 14 "tangled.org/core/jetstream" 15 "tangled.org/core/knotserver/config" 16 "tangled.org/core/knotserver/db" 17 "tangled.org/core/knotserver/xrpc" 18 "tangled.org/core/log" 19 "tangled.org/core/notifier" 20 "tangled.org/core/rbac" 21 "tangled.org/core/xrpc/serviceauth" 22) 23 24type Knot struct { 25 c *config.Config 26 db *db.DB 27 jc *jetstream.JetstreamClient 28 e *rbac.Enforcer 29 l *slog.Logger 30 n *notifier.Notifier 31 resolver *idresolver.Resolver 32} 33 34func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) { 35 h := Knot{ 36 c: c, 37 db: db, 38 e: e, 39 l: log.FromContext(ctx), 40 jc: jc, 41 n: n, 42 resolver: idresolver.DefaultResolver(c.Server.PlcUrl), 43 } 44 45 err := e.AddKnot(rbac.ThisServer) 46 if err != nil { 47 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 48 } 49 50 // configure owner 51 if err = h.configureOwner(); err != nil { 52 return nil, err 53 } 54 h.l.Info("owner set", "did", h.c.Server.Owner) 55 h.jc.AddDid(h.c.Server.Owner) 56 57 // configure known-dids in jetstream consumer 58 dids, err := h.db.GetAllDids() 59 if err != nil { 60 return nil, fmt.Errorf("failed to get all dids: %w", err) 61 } 62 for _, d := range dids { 63 jc.AddDid(d) 64 } 65 66 err = h.jc.StartJetstream(ctx, h.processMessages) 67 if err != nil { 68 return nil, fmt.Errorf("failed to start jetstream: %w", err) 69 } 70 71 return h.Router(), nil 72} 73 74func (h *Knot) Router() http.Handler { 75 r := chi.NewRouter() 76 77 r.Use(h.CORS) 78 r.Use(h.RequestLogger) 79 80 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 81 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 82 }) 83 84 r.Route("/{did}", func(r chi.Router) { 85 r.Use(h.resolveDidRedirect) 86 r.Use(h.resolveRepo) 87 r.Route("/{name}", func(r chi.Router) { 88 // routes for git operations 89 r.Get("/info/refs", h.InfoRefs) 90 r.Post("/git-upload-archive", h.UploadArchive) 91 r.Post("/git-upload-pack", h.UploadPack) 92 r.Post("/git-receive-pack", h.ReceivePack) 93 }) 94 }) 95 96 // xrpc apis 97 r.Mount("/xrpc", h.XrpcRouter()) 98 99 // Socket that streams git oplogs 100 r.Get("/events", h.Events) 101 102 return r 103} 104 105func (h *Knot) XrpcRouter() http.Handler { 106 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 107 108 l := log.SubLogger(h.l, "xrpc") 109 110 xrpc := &xrpc.Xrpc{ 111 Config: h.c, 112 Db: h.db, 113 Ingester: h.jc, 114 Enforcer: h.e, 115 Logger: l, 116 Notifier: h.n, 117 Resolver: h.resolver, 118 ServiceAuth: serviceAuth, 119 } 120 121 return xrpc.Router() 122} 123 124func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { 125 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 126 didOrHandle := chi.URLParam(r, "did") 127 if strings.HasPrefix(didOrHandle, "did:") { 128 next.ServeHTTP(w, r) 129 return 130 } 131 132 trimmed := strings.TrimPrefix(didOrHandle, "@") 133 id, err := h.resolver.ResolveIdent(r.Context(), trimmed) 134 if err != nil { 135 // invalid did or handle 136 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err) 137 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError) 138 return 139 } 140 141 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 142 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 143 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 144 }) 145} 146 147type ctxRepoPathKey struct{} 148 149func repoPathFromcontext(ctx context.Context) (string, bool) { 150 v, ok := ctx.Value(ctxRepoPathKey{}).(string) 151 return v, ok 152} 153 154// resolveRepo is a http middleware that constructs git repo path from given did & name pair. 155// It will reject the requests to unknown repos (when dir doesn't exist) 156func (h *Knot) resolveRepo(next http.Handler) http.Handler { 157 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 did := chi.URLParam(r, "did") 159 name := chi.URLParam(r, "name") 160 // TODO: resolve repository, get repoPath path, ensure repository 161 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 162 if err != nil { 163 w.WriteHeader(http.StatusNotFound) 164 w.Write([]byte("Repository not found")) 165 return 166 } 167 168 exist, err := isDir(repoPath) 169 if err != nil { 170 w.WriteHeader(http.StatusInternalServerError) 171 w.Write([]byte("Failed to check repository path")) 172 return 173 } 174 if !exist { 175 w.WriteHeader(http.StatusNotFound) 176 w.Write([]byte("Repository not found")) 177 return 178 } 179 180 ctx := context.WithValue(r.Context(), "repoPath", repoPath) 181 next.ServeHTTP(w, r.WithContext(ctx)) 182 }) 183} 184 185func (h *Knot) configureOwner() error { 186 cfgOwner := h.c.Server.Owner 187 188 rbacDomain := "thisserver" 189 190 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 191 if err != nil { 192 return err 193 } 194 195 switch len(existing) { 196 case 0: 197 // no owner configured, continue 198 case 1: 199 // find existing owner 200 existingOwner := existing[0] 201 202 // no ownership change, this is okay 203 if existingOwner == h.c.Server.Owner { 204 break 205 } 206 207 // remove existing owner 208 if err = h.db.RemoveDid(existingOwner); err != nil { 209 return err 210 } 211 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil { 212 return err 213 } 214 215 default: 216 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 217 } 218 219 if err = h.db.AddDid(cfgOwner); err != nil { 220 return fmt.Errorf("failed to add owner to DB: %w", err) 221 } 222 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil { 223 return fmt.Errorf("failed to add owner to RBAC: %w", err) 224 } 225 226 return nil 227}