forked from
tangled.org/core
Monorepo for Tangled
1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "strings"
9
10 "github.com/go-chi/chi/v5"
11 "tangled.org/core/idresolver"
12 "tangled.org/core/jetstream"
13 "tangled.org/core/knotserver/config"
14 "tangled.org/core/knotserver/db"
15 "tangled.org/core/knotserver/xrpc"
16 "tangled.org/core/log"
17 "tangled.org/core/notifier"
18 "tangled.org/core/rbac"
19 "tangled.org/core/xrpc/serviceauth"
20)
21
22type Knot struct {
23 c *config.Config
24 db *db.DB
25 jc *jetstream.JetstreamClient
26 e *rbac.Enforcer
27 l *slog.Logger
28 n *notifier.Notifier
29 resolver *idresolver.Resolver
30}
31
32// devPDSURL is the default PDS endpoint for local development.
33// In dev mode, PLC/PDS/Jetstream all run on localhost inside the VM.
34const devPDSURL = "http://localhost:2583"
35
36func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) {
37 var resolver *idresolver.Resolver
38 if c.Server.Dev {
39 resolver = idresolver.DefaultDevResolver(c.Server.PlcUrl, devPDSURL)
40 } else {
41 resolver = idresolver.DefaultResolver(c.Server.PlcUrl)
42 }
43
44 h := Knot{
45 c: c,
46 db: db,
47 e: e,
48 l: log.FromContext(ctx),
49 jc: jc,
50 n: n,
51 resolver: resolver,
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(); 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([]byte("This is a knot server. More info at https://tangled.sh"))
91 })
92
93 r.Route("/{did}", func(r chi.Router) {
94 r.Use(h.resolveDidRedirect)
95 r.Route("/{name}", func(r chi.Router) {
96 // routes for git operations
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 })
103
104 // xrpc apis
105 r.Mount("/xrpc", h.XrpcRouter())
106
107 // Socket that streams git oplogs
108 r.Get("/events", h.Events)
109
110 return r
111}
112
113func (h *Knot) XrpcRouter() http.Handler {
114 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
115
116 l := log.SubLogger(h.l, "xrpc")
117
118 xrpc := &xrpc.Xrpc{
119 Config: h.c,
120 Db: h.db,
121 Ingester: h.jc,
122 Enforcer: h.e,
123 Logger: l,
124 Notifier: h.n,
125 Resolver: h.resolver,
126 ServiceAuth: serviceAuth,
127 }
128
129 return xrpc.Router()
130}
131
132func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler {
133 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
134 didOrHandle := chi.URLParam(r, "did")
135 if strings.HasPrefix(didOrHandle, "did:") {
136 next.ServeHTTP(w, r)
137 return
138 }
139
140 trimmed := strings.TrimPrefix(didOrHandle, "@")
141 id, err := h.resolver.ResolveIdent(r.Context(), trimmed)
142 if err != nil {
143 // invalid did or handle
144 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err)
145 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError)
146 return
147 }
148
149 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle)
150 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery)
151 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect)
152 })
153}
154
155func (h *Knot) configureOwner() error {
156 cfgOwner := h.c.Server.Owner
157
158 rbacDomain := "thisserver"
159
160 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
161 if err != nil {
162 return err
163 }
164
165 switch len(existing) {
166 case 0:
167 // no owner configured, continue
168 case 1:
169 // find existing owner
170 existingOwner := existing[0]
171
172 // no ownership change, this is okay
173 if existingOwner == h.c.Server.Owner {
174 break
175 }
176
177 // remove existing owner
178 if err = h.db.RemoveDid(existingOwner); err != nil {
179 return err
180 }
181 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
182 return err
183 }
184
185 default:
186 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
187 }
188
189 if err = h.db.AddDid(cfgOwner); err != nil {
190 return fmt.Errorf("failed to add owner to DB: %w", err)
191 }
192 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
193 return fmt.Errorf("failed to add owner to RBAC: %w", err)
194 }
195
196 return nil
197}