Monorepo for Tangled tangled.org
5

Configure Feed

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

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