forked from
tangled.org/core
Monorepo for Tangled
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.Use(h.resolveRepo)
87 r.Route("/{name}", func(r chi.Router) {
88 // routes for git operations
89 r.Get("/info/refs", h.InfoRefs)
90 r.Post("/git-upload-archive", h.UploadArchive)
91 r.Post("/git-upload-pack", h.UploadPack)
92 r.Post("/git-receive-pack", h.ReceivePack)
93 })
94 })
95
96 // xrpc apis
97 r.Mount("/xrpc", h.XrpcRouter())
98
99 // Socket that streams git oplogs
100 r.Get("/events", h.Events)
101
102 return r
103}
104
105func (h *Knot) XrpcRouter() http.Handler {
106 serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
107
108 l := log.SubLogger(h.l, "xrpc")
109
110 xrpc := &xrpc.Xrpc{
111 Config: h.c,
112 Db: h.db,
113 Ingester: h.jc,
114 Enforcer: h.e,
115 Logger: l,
116 Notifier: h.n,
117 Resolver: h.resolver,
118 ServiceAuth: serviceAuth,
119 }
120
121 return xrpc.Router()
122}
123
124func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler {
125 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126 didOrHandle := chi.URLParam(r, "did")
127 if strings.HasPrefix(didOrHandle, "did:") {
128 next.ServeHTTP(w, r)
129 return
130 }
131
132 trimmed := strings.TrimPrefix(didOrHandle, "@")
133 id, err := h.resolver.ResolveIdent(r.Context(), trimmed)
134 if err != nil {
135 // invalid did or handle
136 h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err)
137 http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError)
138 return
139 }
140
141 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle)
142 newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery)
143 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect)
144 })
145}
146
147type ctxRepoPathKey struct{}
148
149func repoPathFromcontext(ctx context.Context) (string, bool) {
150 v, ok := ctx.Value(ctxRepoPathKey{}).(string)
151 return v, ok
152}
153
154// resolveRepo is a http middleware that constructs git repo path from given did & name pair.
155// It will reject the requests to unknown repos (when dir doesn't exist)
156func (h *Knot) resolveRepo(next http.Handler) http.Handler {
157 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158 did := chi.URLParam(r, "did")
159 name := chi.URLParam(r, "name")
160 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name))
161 if err != nil {
162 w.WriteHeader(http.StatusNotFound)
163 w.Write([]byte("Repository not found"))
164 return
165 }
166
167 exist, err := isDir(repoPath)
168 if err != nil {
169 w.WriteHeader(http.StatusInternalServerError)
170 w.Write([]byte("Failed to check repository path"))
171 return
172 }
173 if !exist {
174 w.WriteHeader(http.StatusNotFound)
175 w.Write([]byte("Repository not found"))
176 return
177 }
178
179 ctx := context.WithValue(r.Context(), "repoPath", repoPath)
180 next.ServeHTTP(w, r.WithContext(ctx))
181 })
182}
183
184func (h *Knot) configureOwner() error {
185 cfgOwner := h.c.Server.Owner
186
187 rbacDomain := "thisserver"
188
189 existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
190 if err != nil {
191 return err
192 }
193
194 switch len(existing) {
195 case 0:
196 // no owner configured, continue
197 case 1:
198 // find existing owner
199 existingOwner := existing[0]
200
201 // no ownership change, this is okay
202 if existingOwner == h.c.Server.Owner {
203 break
204 }
205
206 // remove existing owner
207 if err = h.db.RemoveDid(existingOwner); err != nil {
208 return err
209 }
210 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil {
211 return err
212 }
213
214 default:
215 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
216 }
217
218 if err = h.db.AddDid(cfgOwner); err != nil {
219 return fmt.Errorf("failed to add owner to DB: %w", err)
220 }
221 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
222 return fmt.Errorf("failed to add owner to RBAC: %w", err)
223 }
224
225 return nil
226}