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