Monorepo for Tangled tangled.org
761
fork

Configure Feed

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

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