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