this repo has no description
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 r.Route("/{did}", func(r chi.Router) {
76 r.Route("/{name}", func(r chi.Router) {
77 // routes for git operations
78 r.Get("/info/refs", h.InfoRefs)
79 r.Post("/git-upload-pack", h.UploadPack)
80 r.Post("/git-receive-pack", h.ReceivePack)
81 })
82 })
83
84 // xrpc apis
85 r.Route("/xrpc", func(r chi.Router) {
86 r.Get("/_health", h.Version)
87 r.Mount("/", h.XrpcRouter())
88 })
89
90 // Socket that streams git oplogs
91 r.Get("/events", h.Events)
92
93 return r, nil
94}
95
96func (h *Knot) XrpcRouter() http.Handler {
97 logger := tlog.New("knots")
98
99 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
100
101 xrpc := &xrpc.Xrpc{
102 Config: h.c,
103 Db: h.db,
104 Ingester: h.jc,
105 Enforcer: h.e,
106 Logger: logger,
107 Notifier: h.n,
108 Resolver: h.resolver,
109 ServiceAuth: serviceAuth,
110 }
111 return xrpc.Router()
112}
113
114// version is set during build time.
115var version string
116
117func (h *Knot) Version(w http.ResponseWriter, r *http.Request) {
118 if version == "" {
119 info, ok := debug.ReadBuildInfo()
120 if !ok {
121 http.Error(w, "failed to read build info", http.StatusInternalServerError)
122 return
123 }
124
125 var modVer string
126 var sha string
127 var modified bool
128
129 for _, mod := range info.Deps {
130 if mod.Path == "tangled.sh/tangled.sh/knotserver" {
131 modVer = mod.Version
132 break
133 }
134 }
135
136 for _, setting := range info.Settings {
137 switch setting.Key {
138 case "vcs.revision":
139 sha = setting.Value
140 case "vcs.modified":
141 modified = setting.Value == "true"
142 }
143 }
144
145 if modVer == "" {
146 modVer = "unknown"
147 }
148
149 if sha == "" {
150 version = modVer
151 } else if modified {
152 version = fmt.Sprintf("%s (%s with modifications)", modVer, sha)
153 } else {
154 version = fmt.Sprintf("%s (%s)", modVer, sha)
155 }
156 }
157
158 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
159 fmt.Fprintf(w, "knotserver/%s", version)
160}
161
162func (h *Knot) configureOwner() error {
163 cfgOwner := h.c.Server.Owner
164
165 rbacDomain := "thisserver"
166
167 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
168 if err != nil {
169 return err
170 }
171
172 switch len(existing) {
173 case 0:
174 // no owner configured, continue
175 case 1:
176 // find existing owner
177 existingOwner := existing[0]
178
179 // no ownership change, this is okay
180 if existingOwner == h.c.Server.Owner {
181 break
182 }
183
184 // remove existing owner
185 if err = h.db.RemoveDid(existingOwner); err != nil {
186 return err
187 }
188 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
189 return err
190 }
191
192 default:
193 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
194 }
195
196 if err = h.db.AddDid(cfgOwner); err != nil {
197 return fmt.Errorf("failed to add owner to DB: %w", err)
198 }
199 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
200 return fmt.Errorf("failed to add owner to RBAC: %w", err)
201 }
202
203 return nil
204}