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