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/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}