forked from
tangled.org/core
Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "path/filepath"
9 "strings"
10
11 securejoin "github.com/cyphar/filepath-securejoin"
12 "github.com/go-chi/chi/v5"
13 "tangled.org/core/idresolver"
14 "tangled.org/core/jetstream"
15 "tangled.org/core/knotserver/config"
16 "tangled.org/core/knotserver/db"
17 "tangled.org/core/knotserver/xrpc"
18 "tangled.org/core/log"
19 "tangled.org/core/notifier"
20 "tangled.org/core/rbac"
21 "tangled.org/core/xrpc/serviceauth"
22)
23
24type Knot struct {
25 c *config.Config
26 db *db.DB
27 jc *jetstream.JetstreamClient
28 e *rbac.Enforcer
29 l *slog.Logger
30 n *notifier.Notifier
31 resolver *idresolver.Resolver
32}
33
34func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) {
35 h := Knot{
36 c: c,
37 db: db,
38 e: e,
39 l: log.FromContext(ctx),
40 jc: jc,
41 n: n,
42 resolver: idresolver.DefaultResolver(c.Server.PlcUrl),
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 return h.Router(), nil
72}
73
74func (h *Knot) Router() http.Handler {
75 r := chi.NewRouter()
76
77 r.Use(h.CORS)
78 r.Use(h.RequestLogger)
79
80 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
81 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
82 })
83
84 r.Route("/{did}", func(r chi.Router) {
85 r.Use(h.resolveDidRedirect)
86 r.Route("/{name}", func(r chi.Router) {
87 r.Use(h.resolveRepo)
88
89 // routes for git operations
90 r.Get("/info/refs", h.InfoRefs)
91 r.Post("/git-upload-archive", h.UploadArchive)
92 r.Post("/git-upload-pack", h.UploadPack)
93 r.Post("/git-receive-pack", h.ReceivePack)
94 })
95 })
96
97 // xrpc apis
98 r.Mount("/xrpc", h.XrpcRouter())
99
100 // Socket that streams git oplogs
101 r.Get("/events", h.Events)
102
103 return r
104}
105
106func (h *Knot) XrpcRouter() http.Handler {
107 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
108
109 l := log.SubLogger(h.l, "xrpc")
110
111 xrpc := &xrpc.Xrpc{
112 Config: h.c,
113 Db: h.db,
114 Ingester: h.jc,
115 Enforcer: h.e,
116 Logger: l,
117 Notifier: h.n,
118 Resolver: h.resolver,
119 ServiceAuth: serviceAuth,
120 }
121
122 return xrpc.Router()
123}
124
125func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler {
126 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127 didOrHandle := chi.URLParam(r, "did")
128 if strings.HasPrefix(didOrHandle, "did:") {
129 next.ServeHTTP(w, r)
130 return
131 }
132
133 trimmed := strings.TrimPrefix(didOrHandle, "@")
134 id, err := h.resolver.ResolveIdent(r.Context(), trimmed)
135 if err != nil {
136 // invalid did or handle
137 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err)
138 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError)
139 return
140 }
141
142 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle)
143 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery)
144 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect)
145 })
146}
147
148type ctxRepoPathKey struct{}
149
150func repoPathFromcontext(ctx context.Context) (string, bool) {
151 v, ok := ctx.Value(ctxRepoPathKey{}).(string)
152 return v, ok
153}
154
155// resolveRepo is a http middleware that constructs git repo path from given did & name pair.
156// It will reject the requests to unknown repos (when dir doesn't exist)
157func (h *Knot) resolveRepo(next http.Handler) http.Handler {
158 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159 did := chi.URLParam(r, "did")
160 name := chi.URLParam(r, "name")
161 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name))
162 if err != nil {
163 w.WriteHeader(http.StatusNotFound)
164 w.Write([]byte("Repository not found"))
165 return
166 }
167
168 exist, err := isDir(repoPath)
169 if err != nil {
170 w.WriteHeader(http.StatusInternalServerError)
171 w.Write([]byte("Failed to check repository path"))
172 return
173 }
174 if !exist {
175 w.WriteHeader(http.StatusNotFound)
176 w.Write([]byte("Repository not found"))
177 return
178 }
179
180 ctx := context.WithValue(r.Context(), ctxRepoPathKey{}, repoPath)
181 next.ServeHTTP(w, r.WithContext(ctx))
182 })
183}
184
185func (h *Knot) configureOwner() error {
186 cfgOwner := h.c.Server.Owner
187
188 rbacDomain := "thisserver"
189
190 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
191 if err != nil {
192 return err
193 }
194
195 switch len(existing) {
196 case 0:
197 // no owner configured, continue
198 case 1:
199 // find existing owner
200 existingOwner := existing[0]
201
202 // no ownership change, this is okay
203 if existingOwner == h.c.Server.Owner {
204 break
205 }
206
207 // remove existing owner
208 if err = h.db.RemoveDid(existingOwner); err != nil {
209 return err
210 }
211 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
212 return err
213 }
214
215 default:
216 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
217 }
218
219 if err = h.db.AddDid(cfgOwner); err != nil {
220 return fmt.Errorf("failed to add owner to DB: %w", err)
221 }
222 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
223 return fmt.Errorf("failed to add owner to RBAC: %w", err)
224 }
225
226 return nil
227}