Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

at icy/kxpzqo 5.4 kB View raw
1package knotserver 2 3import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "strings" 10 "sync" 11 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/sandbox" 18 "tangled.org/core/knotserver/xrpc" 19 "tangled.org/core/log" 20 "tangled.org/core/notifier" 21 "tangled.org/core/rbac" 22 "tangled.org/core/xrpc/serviceauth" 23) 24 25//go:embed motd 26var defaultMotd []byte 27 28type Knot struct { 29 c *config.Config 30 db *db.DB 31 jc *jetstream.JetstreamClient 32 e *rbac.Enforcer 33 l *slog.Logger 34 n *notifier.Notifier 35 resolver *idresolver.Resolver 36 sandbox sandbox.Backend 37 motd []byte 38 motdMu sync.RWMutex 39} 40 41func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier, resolver *idresolver.Resolver, sb sandbox.Backend) (http.Handler, error) { 42 h := Knot{ 43 c: c, 44 db: db, 45 e: e, 46 l: log.FromContext(ctx), 47 jc: jc, 48 n: n, 49 resolver: resolver, 50 sandbox: sb, 51 motd: defaultMotd, 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(ctx); 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(h.GetMotdContent()) 91 }) 92 93 r.Route("/{did}", func(r chi.Router) { 94 r.Use(h.resolveDidRedirect) 95 96 r.Get("/info/refs", h.InfoRefs) 97 r.Post("/git-upload-archive", h.UploadArchive) 98 r.Post("/git-upload-pack", h.UploadPack) 99 r.Post("/git-receive-pack", h.ReceivePack) 100 101 r.Route("/{name}", func(r chi.Router) { 102 r.Get("/info/refs", h.InfoRefs) 103 r.Post("/git-upload-archive", h.UploadArchive) 104 r.Post("/git-upload-pack", h.UploadPack) 105 r.Post("/git-receive-pack", h.ReceivePack) 106 }) 107 }) 108 109 // xrpc apis 110 r.Mount("/xrpc", h.XrpcRouter()) 111 112 // Socket that streams git oplogs 113 r.Get("/events", h.Events) 114 115 return r 116} 117 118// SetMotdContent sets custom MOTD content, replacing the embedded default. 119func (h *Knot) SetMotdContent(content []byte) { 120 h.motdMu.Lock() 121 defer h.motdMu.Unlock() 122 h.motd = content 123} 124 125// GetMotdContent returns the current MOTD content. 126func (h *Knot) GetMotdContent() []byte { 127 h.motdMu.RLock() 128 defer h.motdMu.RUnlock() 129 return h.motd 130} 131 132func (h *Knot) XrpcRouter() http.Handler { 133 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 134 135 l := log.SubLogger(h.l, "xrpc") 136 137 xrpc := &xrpc.Xrpc{ 138 Config: h.c, 139 Db: h.db, 140 Ingester: h.jc, 141 Enforcer: h.e, 142 Logger: l, 143 Notifier: h.n, 144 Resolver: h.resolver, 145 ServiceAuth: serviceAuth, 146 Sandbox: h.sandbox, 147 } 148 149 return xrpc.Router() 150} 151 152func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { 153 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 didOrHandle := chi.URLParam(r, "did") 155 if strings.HasPrefix(didOrHandle, "did:") { 156 next.ServeHTTP(w, r) 157 return 158 } 159 160 trimmed := strings.TrimPrefix(didOrHandle, "@") 161 id, err := h.resolver.ResolveIdent(r.Context(), trimmed) 162 if err != nil { 163 // invalid did or handle 164 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err) 165 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError) 166 return 167 } 168 169 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 170 newPath := "/" + id.DID.String() + suffix 171 if r.URL.RawQuery != "" { 172 newPath += "?" + r.URL.RawQuery 173 } 174 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 175 }) 176} 177 178func (h *Knot) configureOwner(ctx context.Context) error { 179 cfgOwner := h.c.Server.Owner 180 181 rbacDomain := "thisserver" 182 183 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 184 if err != nil { 185 return err 186 } 187 188 switch len(existing) { 189 case 0: 190 // no owner configured, continue 191 case 1: 192 // find existing owner 193 existingOwner := existing[0] 194 195 // no ownership change, this is okay 196 if existingOwner == h.c.Server.Owner { 197 break 198 } 199 200 // remove existing owner 201 if err = db.RemoveDid(h.db, existingOwner); err != nil { 202 return err 203 } 204 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil { 205 return err 206 } 207 208 default: 209 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 210 } 211 212 if err = db.AddDid(h.db, cfgOwner); err != nil { 213 return fmt.Errorf("failed to add owner to DB: %w", err) 214 } 215 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil { 216 return fmt.Errorf("failed to add owner to RBAC: %w", err) 217 } 218 219 err = h.fetchAndAddKeys(ctx, cfgOwner) 220 if err != nil { 221 h.l.Error("fetching and adding owners public keys", "error", err, "did", cfgOwner) 222 } 223 224 return nil 225}