Monorepo for Tangled tangled.org
3

Configure Feed

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

at icy/ytnwlw 5.6 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/bluesky-social/indigo/atproto/syntax" 13 "github.com/go-chi/chi/v5" 14 "tangled.org/core/idresolver" 15 "tangled.org/core/jetstream" 16 "tangled.org/core/knotserver/config" 17 "tangled.org/core/knotserver/db" 18 "tangled.org/core/knotserver/keys" 19 "tangled.org/core/knotserver/sandbox" 20 "tangled.org/core/knotserver/xrpc" 21 "tangled.org/core/log" 22 "tangled.org/core/notifier" 23 "tangled.org/core/rbac" 24 "tangled.org/core/xrpc/serviceauth" 25) 26 27//go:embed motd 28var defaultMotd []byte 29 30type Knot struct { 31 c *config.Config 32 db *db.DB 33 jc *jetstream.JetstreamClient 34 e *rbac.Enforcer 35 l *slog.Logger 36 n *notifier.Notifier 37 resolver *idresolver.Resolver 38 sandbox sandbox.Backend 39 motd []byte 40 motdMu sync.RWMutex 41} 42 43func 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) { 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 sandbox: sb, 53 motd: defaultMotd, 54 } 55 56 err := e.AddKnot(rbac.ThisServer) 57 if err != nil { 58 return nil, fmt.Errorf("failed to setup enforcer: %w", err) 59 } 60 61 // configure owner 62 if err = h.configureOwner(ctx); err != nil { 63 return nil, err 64 } 65 h.l.Info("owner set", "did", h.c.Server.Owner) 66 h.jc.AddDid(h.c.Server.Owner) 67 68 // configure known-dids in jetstream consumer 69 dids, err := h.db.GetAllDids() 70 if err != nil { 71 return nil, fmt.Errorf("failed to get all dids: %w", err) 72 } 73 for _, d := range dids { 74 jc.AddDid(d) 75 } 76 77 err = h.jc.StartJetstream(ctx, h.processMessages) 78 if err != nil { 79 return nil, fmt.Errorf("failed to start jetstream: %w", err) 80 } 81 82 return h.Router(), nil 83} 84 85func (h *Knot) Router() http.Handler { 86 r := chi.NewRouter() 87 88 r.Use(h.CORS) 89 r.Use(h.RequestLogger) 90 91 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 92 w.Write(h.GetMotdContent()) 93 }) 94 95 r.Route("/{did}", func(r chi.Router) { 96 r.Use(h.resolveDidRedirect) 97 98 r.Get("/info/refs", h.InfoRefs) 99 r.Post("/git-upload-archive", h.UploadArchive) 100 r.Post("/git-upload-pack", h.UploadPack) 101 r.Post("/git-receive-pack", h.ReceivePack) 102 103 r.Route("/{name}", func(r chi.Router) { 104 r.Get("/info/refs", h.InfoRefs) 105 r.Post("/git-upload-archive", h.UploadArchive) 106 r.Post("/git-upload-pack", h.UploadPack) 107 r.Post("/git-receive-pack", h.ReceivePack) 108 }) 109 }) 110 111 // xrpc apis 112 x := h.newXrpc() 113 r.Mount("/xrpc", x.Router()) 114 r.Mount("/admin", x.AdminRouter()) 115 116 // Socket that streams git oplogs 117 r.Get("/events", h.Events) 118 119 return r 120} 121 122// SetMotdContent sets custom MOTD content, replacing the embedded default. 123func (h *Knot) SetMotdContent(content []byte) { 124 h.motdMu.Lock() 125 defer h.motdMu.Unlock() 126 h.motd = content 127} 128 129// GetMotdContent returns the current MOTD content. 130func (h *Knot) GetMotdContent() []byte { 131 h.motdMu.RLock() 132 defer h.motdMu.RUnlock() 133 return h.motd 134} 135 136func (h *Knot) newXrpc() *xrpc.Xrpc { 137 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver.Directory(), h.c.Server.Did().String()) 138 139 l := log.SubLogger(h.l, "xrpc") 140 141 return &xrpc.Xrpc{ 142 Config: h.c, 143 Db: h.db, 144 Ingester: h.jc, 145 Enforcer: h.e, 146 Logger: l, 147 Notifier: h.n, 148 Resolver: h.resolver, 149 ServiceAuth: serviceAuth, 150 Sandbox: h.sandbox, 151 } 152} 153 154func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { 155 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 156 didOrHandle := chi.URLParam(r, "did") 157 if strings.HasPrefix(didOrHandle, "did:") { 158 next.ServeHTTP(w, r) 159 return 160 } 161 162 trimmed := strings.TrimPrefix(didOrHandle, "@") 163 id, err := h.resolver.ResolveIdent(r.Context(), trimmed) 164 if err != nil { 165 // invalid did or handle 166 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err) 167 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError) 168 return 169 } 170 171 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 172 newPath := "/" + id.DID.String() + suffix 173 if r.URL.RawQuery != "" { 174 newPath += "?" + r.URL.RawQuery 175 } 176 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 177 }) 178} 179 180func (h *Knot) configureOwner(ctx context.Context) error { 181 cfgOwner := h.c.Server.Owner 182 183 rbacDomain := "thisserver" 184 185 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 186 if err != nil { 187 return err 188 } 189 190 switch len(existing) { 191 case 0: 192 // no owner configured, continue 193 case 1: 194 // find existing owner 195 existingOwner := existing[0] 196 197 // no ownership change, this is okay 198 if existingOwner == h.c.Server.Owner { 199 break 200 } 201 202 // remove existing owner 203 if err = db.RemoveDid(h.db, existingOwner); err != nil { 204 return err 205 } 206 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil { 207 return err 208 } 209 210 default: 211 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 212 } 213 214 if err = db.AddDid(h.db, cfgOwner); err != nil { 215 return fmt.Errorf("failed to add owner to DB: %w", err) 216 } 217 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil { 218 return fmt.Errorf("failed to add owner to RBAC: %w", err) 219 } 220 221 err = keys.FetchAndStore(ctx, h.resolver.Directory(), h.db, syntax.DID(cfgOwner)) 222 if err != nil { 223 h.l.Error("fetching and adding owners public keys", "error", err, "did", cfgOwner) 224 } 225 226 return nil 227}