A vibe coded tangled fork which supports pijul.
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 // TODO: resolve repository, get repoPath path, ensure repository
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(), "repoPath", 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}