Monorepo for Tangled
tangled.org
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}