1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8
9 "github.com/go-chi/chi/v5"
10 "tangled.sh/tangled.sh/core/idresolver"
11 "tangled.sh/tangled.sh/core/jetstream"
12 "tangled.sh/tangled.sh/core/knotserver/config"
13 "tangled.sh/tangled.sh/core/knotserver/db"
14 "tangled.sh/tangled.sh/core/knotserver/xrpc"
15 tlog "tangled.sh/tangled.sh/core/log"
16 "tangled.sh/tangled.sh/core/notifier"
17 "tangled.sh/tangled.sh/core/rbac"
18 "tangled.sh/tangled.sh/core/xrpc/serviceauth"
19)
20
21type Knot struct {
22 c *config.Config
23 db *db.DB
24 jc *jetstream.JetstreamClient
25 e *rbac.Enforcer
26 l *slog.Logger
27 n *notifier.Notifier
28 resolver *idresolver.Resolver
29}
30
31func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
32 r := chi.NewRouter()
33
34 h := Knot{
35 c: c,
36 db: db,
37 e: e,
38 l: l,
39 jc: jc,
40 n: n,
41 resolver: idresolver.DefaultResolver(),
42 }
43
44 err := e.AddKnot(rbac.ThisServer)
45 if err != nil {
46 return nil, fmt.Errorf("failed to setup enforcer: %w", err)
47 }
48
49 // configure owner
50 if err = h.configureOwner(); err != nil {
51 return nil, err
52 }
53 h.l.Info("owner set", "did", h.c.Server.Owner)
54 h.jc.AddDid(h.c.Server.Owner)
55
56 // configure known-dids in jetstream consumer
57 dids, err := h.db.GetAllDids()
58 if err != nil {
59 return nil, fmt.Errorf("failed to get all dids: %w", err)
60 }
61 for _, d := range dids {
62 jc.AddDid(d)
63 }
64
65 err = h.jc.StartJetstream(ctx, h.processMessages)
66 if err != nil {
67 return nil, fmt.Errorf("failed to start jetstream: %w", err)
68 }
69
70 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
71 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
72 })
73
74 r.Route("/{did}", func(r chi.Router) {
75 r.Route("/{name}", func(r chi.Router) {
76 // routes for git operations
77 r.Get("/info/refs", h.InfoRefs)
78 r.Post("/git-upload-pack", h.UploadPack)
79 r.Post("/git-receive-pack", h.ReceivePack)
80 })
81 })
82
83 // xrpc apis
84 r.Mount("/xrpc", h.XrpcRouter())
85
86 // Socket that streams git oplogs
87 r.Get("/events", h.Events)
88
89 return r, nil
90}
91
92func (h *Knot) XrpcRouter() http.Handler {
93 logger := tlog.New("knots")
94
95 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
96
97 xrpc := &xrpc.Xrpc{
98 Config: h.c,
99 Db: h.db,
100 Ingester: h.jc,
101 Enforcer: h.e,
102 Logger: logger,
103 Notifier: h.n,
104 Resolver: h.resolver,
105 ServiceAuth: serviceAuth,
106 }
107 return xrpc.Router()
108}
109
110func (h *Knot) configureOwner() error {
111 cfgOwner := h.c.Server.Owner
112
113 rbacDomain := "thisserver"
114
115 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
116 if err != nil {
117 return err
118 }
119
120 switch len(existing) {
121 case 0:
122 // no owner configured, continue
123 case 1:
124 // find existing owner
125 existingOwner := existing[0]
126
127 // no ownership change, this is okay
128 if existingOwner == h.c.Server.Owner {
129 break
130 }
131
132 // remove existing owner
133 if err = h.db.RemoveDid(existingOwner); err != nil {
134 return err
135 }
136 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
137 return err
138 }
139
140 default:
141 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
142 }
143
144 if err = h.db.AddDid(cfgOwner); err != nil {
145 return fmt.Errorf("failed to add owner to DB: %w", err)
146 }
147 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
148 return fmt.Errorf("failed to add owner to RBAC: %w", err)
149 }
150
151 return nil
152}