Monorepo for Tangled
at local-dev 197 lines 4.8 kB view raw
1package knotserver 2 3import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 "tangled.org/core/idresolver" 12 "tangled.org/core/jetstream" 13 "tangled.org/core/knotserver/config" 14 "tangled.org/core/knotserver/db" 15 "tangled.org/core/knotserver/xrpc" 16 "tangled.org/core/log" 17 "tangled.org/core/notifier" 18 "tangled.org/core/rbac" 19 "tangled.org/core/xrpc/serviceauth" 20) 21 22type Knot struct { 23 c *config.Config 24 db *db.DB 25 jc *jetstream.JetstreamClient 26 e *rbac.Enforcer 27 l *slog.Logger 28 n *notifier.Notifier 29 resolver *idresolver.Resolver 30} 31 32// devPDSURL is the default PDS endpoint for local development. 33// In dev mode, PLC/PDS/Jetstream all run on localhost inside the VM. 34const devPDSURL = "http://localhost:2583" 35 36func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) { 37 var resolver *idresolver.Resolver 38 if c.Server.Dev { 39 resolver = idresolver.DefaultDevResolver(c.Server.PlcUrl, devPDSURL) 40 } else { 41 resolver = idresolver.DefaultResolver(c.Server.PlcUrl) 42 } 43 44 h := Knot{ 45 c: c, 46 db: db, 47 e: e, 48 l: log.FromContext(ctx), 49 jc: jc, 50 n: n, 51 resolver: resolver, 52 } 53 54 err := e.AddKnot(rbac.ThisServer) 55 if err != nil { 56 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 57 } 58 59 // configure owner 60 if err = h.configureOwner(); err != nil { 61 return nil, err 62 } 63 h.l.Info("owner set", "did", h.c.Server.Owner) 64 h.jc.AddDid(h.c.Server.Owner) 65 66 // configure known-dids in jetstream consumer 67 dids, err := h.db.GetAllDids() 68 if err != nil { 69 return nil, fmt.Errorf("failed to get all dids: %w", err) 70 } 71 for _, d := range dids { 72 jc.AddDid(d) 73 } 74 75 err = h.jc.StartJetstream(ctx, h.processMessages) 76 if err != nil { 77 return nil, fmt.Errorf("failed to start jetstream: %w", err) 78 } 79 80 return h.Router(), nil 81} 82 83func (h *Knot) Router() http.Handler { 84 r := chi.NewRouter() 85 86 r.Use(h.CORS) 87 r.Use(h.RequestLogger) 88 89 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 90 w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 91 }) 92 93 r.Route("/{did}", func(r chi.Router) { 94 r.Use(h.resolveDidRedirect) 95 r.Route("/{name}", func(r chi.Router) { 96 // routes for git operations 97 r.Get("/info/refs", h.InfoRefs) 98 r.Post("/git-upload-archive", h.UploadArchive) 99 r.Post("/git-upload-pack", h.UploadPack) 100 r.Post("/git-receive-pack", h.ReceivePack) 101 }) 102 }) 103 104 // xrpc apis 105 r.Mount("/xrpc", h.XrpcRouter()) 106 107 // Socket that streams git oplogs 108 r.Get("/events", h.Events) 109 110 return r 111} 112 113func (h *Knot) XrpcRouter() http.Handler { 114 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 115 116 l := log.SubLogger(h.l, "xrpc") 117 118 xrpc := &xrpc.Xrpc{ 119 Config: h.c, 120 Db: h.db, 121 Ingester: h.jc, 122 Enforcer: h.e, 123 Logger: l, 124 Notifier: h.n, 125 Resolver: h.resolver, 126 ServiceAuth: serviceAuth, 127 } 128 129 return xrpc.Router() 130} 131 132func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { 133 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 134 didOrHandle := chi.URLParam(r, "did") 135 if strings.HasPrefix(didOrHandle, "did:") { 136 next.ServeHTTP(w, r) 137 return 138 } 139 140 trimmed := strings.TrimPrefix(didOrHandle, "@") 141 id, err := h.resolver.ResolveIdent(r.Context(), trimmed) 142 if err != nil { 143 // invalid did or handle 144 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err) 145 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError) 146 return 147 } 148 149 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 150 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 151 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 152 }) 153} 154 155func (h *Knot) configureOwner() error { 156 cfgOwner := h.c.Server.Owner 157 158 rbacDomain := "thisserver" 159 160 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 161 if err != nil { 162 return err 163 } 164 165 switch len(existing) { 166 case 0: 167 // no owner configured, continue 168 case 1: 169 // find existing owner 170 existingOwner := existing[0] 171 172 // no ownership change, this is okay 173 if existingOwner == h.c.Server.Owner { 174 break 175 } 176 177 // remove existing owner 178 if err = h.db.RemoveDid(existingOwner); err != nil { 179 return err 180 } 181 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil { 182 return err 183 } 184 185 default: 186 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 187 } 188 189 if err = h.db.AddDid(cfgOwner); err != nil { 190 return fmt.Errorf("failed to add owner to DB: %w", err) 191 } 192 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil { 193 return fmt.Errorf("failed to add owner to RBAC: %w", err) 194 } 195 196 return nil 197}