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 Handle 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 := Handle{
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("/", h.Index)
72 r.Get("/capabilities", h.Capabilities)
73 r.Get("/version", h.Version)
74 r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
75 w.Write([]byte(h.c.Server.Owner))
76 })
77 r.Route("/{did}", func(r chi.Router) {
78 // Repo routes
79 r.Route("/{name}", func(r chi.Router) {
80
81 r.Route("/languages", func(r chi.Router) {
82 r.Get("/", h.RepoLanguages)
83 r.Get("/{ref}", h.RepoLanguages)
84 })
85
86 r.Get("/", h.RepoIndex)
87 r.Get("/info/refs", h.InfoRefs)
88 r.Post("/git-upload-pack", h.UploadPack)
89 r.Post("/git-receive-pack", h.ReceivePack)
90 r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
91
92 r.Route("/tree/{ref}", func(r chi.Router) {
93 r.Get("/", h.RepoIndex)
94 r.Get("/*", h.RepoTree)
95 })
96
97 r.Route("/blob/{ref}", func(r chi.Router) {
98 r.Get("/*", h.Blob)
99 })
100
101 r.Route("/raw/{ref}", func(r chi.Router) {
102 r.Get("/*", h.BlobRaw)
103 })
104
105 r.Get("/log/{ref}", h.Log)
106 r.Get("/archive/{file}", h.Archive)
107 r.Get("/commit/{ref}", h.Diff)
108 r.Get("/tags", h.Tags)
109 r.Route("/branches", func(r chi.Router) {
110 r.Get("/", h.Branches)
111 r.Get("/{branch}", h.Branch)
112 r.Get("/default", h.DefaultBranch)
113 })
114 })
115 })
116
117 // xrpc apis
118 r.Mount("/xrpc", h.XrpcRouter())
119
120 // Socket that streams git oplogs
121 r.Get("/events", h.Events)
122
123 // All public keys on the knot.
124 r.Get("/keys", h.Keys)
125
126 return r, nil
127}
128
129func (h *Handle) XrpcRouter() http.Handler {
130 logger := tlog.New("knots")
131
132 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
133
134 xrpc := &xrpc.Xrpc{
135 Config: h.c,
136 Db: h.db,
137 Ingester: h.jc,
138 Enforcer: h.e,
139 Logger: logger,
140 Notifier: h.n,
141 Resolver: h.resolver,
142 ServiceAuth: serviceAuth,
143 }
144 return xrpc.Router()
145}
146
147// version is set during build time.
148var version string
149
150func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
151 if version == "" {
152 info, ok := debug.ReadBuildInfo()
153 if !ok {
154 http.Error(w, "failed to read build info", http.StatusInternalServerError)
155 return
156 }
157
158 var modVer string
159 var sha string
160 var modified bool
161
162 for _, mod := range info.Deps {
163 if mod.Path == "tangled.sh/tangled.sh/knotserver" {
164 modVer = mod.Version
165 break
166 }
167 }
168
169 for _, setting := range info.Settings {
170 switch setting.Key {
171 case "vcs.revision":
172 sha = setting.Value
173 case "vcs.modified":
174 modified = setting.Value == "true"
175 }
176 }
177
178 if modVer == "" {
179 modVer = "unknown"
180 }
181
182 if sha == "" {
183 version = modVer
184 } else if modified {
185 version = fmt.Sprintf("%s (%s with modifications)", modVer, sha)
186 } else {
187 version = fmt.Sprintf("%s (%s)", modVer, sha)
188 }
189 }
190
191 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
192 fmt.Fprintf(w, "knotserver/%s", version)
193}
194
195func (h *Handle) configureOwner() error {
196 cfgOwner := h.c.Server.Owner
197
198 rbacDomain := "thisserver"
199
200 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
201 if err != nil {
202 return err
203 }
204
205 switch len(existing) {
206 case 0:
207 // no owner configured, continue
208 case 1:
209 // find existing owner
210 existingOwner := existing[0]
211
212 // no ownership change, this is okay
213 if existingOwner == h.c.Server.Owner {
214 break
215 }
216
217 // remove existing owner
218 if err = h.db.RemoveDid(existingOwner); err != nil {
219 return err
220 }
221 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
222 return err
223 }
224
225 default:
226 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
227 }
228
229 if err = h.db.AddDid(cfgOwner); err != nil {
230 return fmt.Errorf("failed to add owner to DB: %w", err)
231 }
232 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
233 return fmt.Errorf("failed to add owner to RBAC: %w", err)
234 }
235
236 return nil
237}