1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "runtime/debug"
9
10 "github.com/go-chi/chi/v5"
11 "tangled.sh/tangled.sh/core/idresolver"
12 "tangled.sh/tangled.sh/core/jetstream"
13 "tangled.sh/tangled.sh/core/knotserver/config"
14 "tangled.sh/tangled.sh/core/knotserver/db"
15 "tangled.sh/tangled.sh/core/knotserver/xrpc"
16 tlog "tangled.sh/tangled.sh/core/log"
17 "tangled.sh/tangled.sh/core/notifier"
18 "tangled.sh/tangled.sh/core/rbac"
19 "tangled.sh/tangled.sh/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, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
33 r := chi.NewRouter()
34
35 h := Knot{
36 c: c,
37 db: db,
38 e: e,
39 l: l,
40 jc: jc,
41 n: n,
42 resolver: idresolver.DefaultResolver(),
43 }
44
45 err := e.AddKnot(rbac.ThisServer)
46 if err != nil {
47 return nil, fmt.Errorf("failed to setup enforcer: %w", err)
48 }
49
50 // configure owner
51 if err = h.configureOwner(); err != nil {
52 return nil, err
53 }
54 h.l.Info("owner set", "did", h.c.Server.Owner)
55 h.jc.AddDid(h.c.Server.Owner)
56
57 // configure known-dids in jetstream consumer
58 dids, err := h.db.GetAllDids()
59 if err != nil {
60 return nil, fmt.Errorf("failed to get all dids: %w", err)
61 }
62 for _, d := range dids {
63 jc.AddDid(d)
64 }
65
66 err = h.jc.StartJetstream(ctx, h.processMessages)
67 if err != nil {
68 return nil, fmt.Errorf("failed to start jetstream: %w", err)
69 }
70
71 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
72 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
73 })
74
75 owner := func(w http.ResponseWriter, r *http.Request) {
76 w.Write([]byte(h.c.Server.Owner))
77 }
78 // Deprecated: the sh.tangled.knot.owner xrpc call should be used instead
79 r.Get("/owner", owner)
80
81 r.Route("/{did}", func(r chi.Router) {
82 r.Route("/{name}", func(r chi.Router) {
83 // routes for git operations
84 r.Get("/info/refs", h.InfoRefs)
85 r.Post("/git-upload-pack", h.UploadPack)
86 r.Post("/git-receive-pack", h.ReceivePack)
87 })
88 })
89
90 // xrpc apis
91 r.Route("/xrpc", func(r chi.Router) {
92 r.Get("/_health", h.Version)
93 r.Get("/sh.tangled.knot.owner", owner)
94 r.Mount("/", h.XrpcRouter())
95 })
96
97 // Socket that streams git oplogs
98 r.Get("/events", h.Events)
99
100 return r, nil
101}
102
103func (h *Knot) XrpcRouter() http.Handler {
104 logger := tlog.New("knots")
105
106 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
107
108 xrpc := &xrpc.Xrpc{
109 Config: h.c,
110 Db: h.db,
111 Ingester: h.jc,
112 Enforcer: h.e,
113 Logger: logger,
114 Notifier: h.n,
115 Resolver: h.resolver,
116 ServiceAuth: serviceAuth,
117 }
118 return xrpc.Router()
119}
120
121// version is set during build time.
122var version string
123
124func (h *Knot) Version(w http.ResponseWriter, r *http.Request) {
125 if version == "" {
126 info, ok := debug.ReadBuildInfo()
127 if !ok {
128 http.Error(w, "failed to read build info", http.StatusInternalServerError)
129 return
130 }
131
132 var modVer string
133 var sha string
134 var modified bool
135
136 for _, mod := range info.Deps {
137 if mod.Path == "tangled.sh/tangled.sh/knotserver" {
138 modVer = mod.Version
139 break
140 }
141 }
142
143 for _, setting := range info.Settings {
144 switch setting.Key {
145 case "vcs.revision":
146 sha = setting.Value
147 case "vcs.modified":
148 modified = setting.Value == "true"
149 }
150 }
151
152 if modVer == "" {
153 modVer = "unknown"
154 }
155
156 if sha == "" {
157 version = modVer
158 } else if modified {
159 version = fmt.Sprintf("%s (%s with modifications)", modVer, sha)
160 } else {
161 version = fmt.Sprintf("%s (%s)", modVer, sha)
162 }
163 }
164
165 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
166 fmt.Fprintf(w, "knotserver/%s", version)
167}
168
169func (h *Knot) configureOwner() error {
170 cfgOwner := h.c.Server.Owner
171
172 rbacDomain := "thisserver"
173
174 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
175 if err != nil {
176 return err
177 }
178
179 switch len(existing) {
180 case 0:
181 // no owner configured, continue
182 case 1:
183 // find existing owner
184 existingOwner := existing[0]
185
186 // no ownership change, this is okay
187 if existingOwner == h.c.Server.Owner {
188 break
189 }
190
191 // remove existing owner
192 if err = h.db.RemoveDid(existingOwner); err != nil {
193 return err
194 }
195 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
196 return err
197 }
198
199 default:
200 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
201 }
202
203 if err = h.db.AddDid(cfgOwner); err != nil {
204 return fmt.Errorf("failed to add owner to DB: %w", err)
205 }
206 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
207 return fmt.Errorf("failed to add owner to RBAC: %w", err)
208 }
209
210 return nil
211}