1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "runtime/debug"
9 "time"
10
11 "github.com/bluesky-social/indigo/api/atproto"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 lexutil "github.com/bluesky-social/indigo/lex/util"
14 "github.com/bluesky-social/indigo/xrpc"
15 "github.com/go-chi/chi/v5"
16 "tangled.sh/tangled.sh/core/api/tangled"
17 "tangled.sh/tangled.sh/core/jetstream"
18 "tangled.sh/tangled.sh/core/knotserver/config"
19 "tangled.sh/tangled.sh/core/knotserver/db"
20 "tangled.sh/tangled.sh/core/rbac"
21 "tangled.sh/tangled.sh/core/resolver"
22)
23
24const (
25 ThisServer = "thisserver" // resource identifier for rbac enforcement
26)
27
28type Handle struct {
29 c *config.Config
30 db *db.DB
31 jc *jetstream.JetstreamClient
32 e *rbac.Enforcer
33 l *slog.Logger
34 clock syntax.TIDClock
35}
36
37func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger) (http.Handler, error) {
38 h := Handle{
39 c: c,
40 db: db,
41 e: e,
42 l: l,
43 jc: jc,
44 }
45
46 err := e.AddDomain(ThisServer)
47 if err != nil {
48 return nil, fmt.Errorf("failed to setup enforcer: %w", err)
49 }
50
51 // if this knot does not already have an owner, publish it
52 if _, err := h.db.Owner(); err != nil {
53 l.Info("publishing this knot ...", "owner", h.c.Owner.Did)
54 err = h.Publish()
55 if err != nil {
56 return nil, fmt.Errorf("failed to announce knot: %w", err)
57 }
58 }
59
60 l.Info("this knot has been published", "owner", h.c.Owner.Did)
61
62 err = h.jc.StartJetstream(ctx, h.processMessages)
63 if err != nil {
64 return nil, fmt.Errorf("failed to start jetstream: %w", err)
65 }
66
67 dids, err := db.GetAllDids()
68 if err != nil {
69 return nil, fmt.Errorf("failed to get all Dids: %w", err)
70 }
71
72 for _, d := range dids {
73 h.jc.AddDid(d)
74 }
75
76 r := chi.NewRouter()
77
78 r.Get("/", h.Index)
79 r.Get("/capabilities", h.Capabilities)
80 r.Get("/version", h.Version)
81 r.Route("/{did}", func(r chi.Router) {
82 // Repo routes
83 r.Route("/{name}", func(r chi.Router) {
84 r.Route("/collaborator", func(r chi.Router) {
85 r.Use(h.VerifySignature)
86 r.Post("/add", h.AddRepoCollaborator)
87 })
88
89 r.Route("/languages", func(r chi.Router) {
90 r.With(h.VerifySignature)
91 r.Get("/", h.RepoLanguages)
92 r.Get("/{ref}", h.RepoLanguages)
93 })
94
95 r.Get("/", h.RepoIndex)
96 r.Get("/info/refs", h.InfoRefs)
97 r.Post("/git-upload-pack", h.UploadPack)
98 r.Post("/git-receive-pack", h.ReceivePack)
99 r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
100
101 r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef)
102
103 r.Route("/merge", func(r chi.Router) {
104 r.With(h.VerifySignature)
105 r.Post("/", h.Merge)
106 r.Post("/check", h.MergeCheck)
107 })
108
109 r.Route("/tree/{ref}", func(r chi.Router) {
110 r.Get("/", h.RepoIndex)
111 r.Get("/*", h.RepoTree)
112 })
113
114 r.Route("/blob/{ref}", func(r chi.Router) {
115 r.Get("/*", h.Blob)
116 })
117
118 r.Route("/raw/{ref}", func(r chi.Router) {
119 r.Get("/*", h.BlobRaw)
120 })
121
122 r.Get("/log/{ref}", h.Log)
123 r.Get("/archive/{file}", h.Archive)
124 r.Get("/commit/{ref}", h.Diff)
125 r.Get("/tags", h.Tags)
126 r.Route("/branches", func(r chi.Router) {
127 r.Get("/", h.Branches)
128 r.Get("/{branch}", h.Branch)
129 r.Route("/default", func(r chi.Router) {
130 r.Get("/", h.DefaultBranch)
131 r.With(h.VerifySignature).Put("/", h.SetDefaultBranch)
132 })
133 })
134 })
135 })
136
137 // Create a new repository.
138 r.Route("/repo", func(r chi.Router) {
139 r.Use(h.VerifySignature)
140 r.Put("/new", h.NewRepo)
141 r.Delete("/", h.RemoveRepo)
142 r.Route("/fork", func(r chi.Router) {
143 r.Post("/", h.RepoFork)
144 r.Post("/sync/{branch}", h.RepoForkSync)
145 r.Get("/sync/{branch}", h.RepoForkAheadBehind)
146 })
147 })
148
149 r.Route("/member", func(r chi.Router) {
150 r.Use(h.VerifySignature)
151 r.Put("/add", h.AddMember)
152 })
153
154 // Initialize the knot with an owner and public key.
155 r.With(h.VerifySignature).Post("/init", h.Init)
156
157 // Health check. Used for two-way verification with appview.
158 r.With(h.VerifySignature).Get("/health", h.Health)
159
160 // Return did of the owner of this knot
161 r.Get("/owner", h.Owner)
162
163 // All public keys on the knot.
164 r.Get("/keys", h.Keys)
165
166 return r, nil
167}
168
169// version is set during build time.
170var version string
171
172func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
173 if version == "" {
174 info, ok := debug.ReadBuildInfo()
175 if !ok {
176 http.Error(w, "failed to read build info", http.StatusInternalServerError)
177 return
178 }
179
180 var modVer string
181 for _, mod := range info.Deps {
182 if mod.Path == "tangled.sh/tangled.sh/knotserver" {
183 version = mod.Version
184 break
185 }
186 }
187
188 if modVer == "" {
189 version = "unknown"
190 }
191 }
192
193 w.Header().Set("Content-Type", "text/plain")
194 fmt.Fprintf(w, "knotserver/%s", version)
195}
196
197func (h *Handle) Publish() error {
198 ownerDid := h.c.Owner.Did
199 appPassword := h.c.Owner.AppPassword
200
201 res := resolver.DefaultResolver()
202 ident, err := res.ResolveIdent(context.Background(), ownerDid)
203 if err != nil {
204 return err
205 }
206
207 client := xrpc.Client{
208 Host: ident.PDSEndpoint(),
209 }
210
211 resp, err := atproto.ServerCreateSession(context.Background(), &client, &atproto.ServerCreateSession_Input{
212 Identifier: ownerDid,
213 Password: appPassword,
214 })
215 if err != nil {
216 return err
217 }
218
219 authClient := xrpc.Client{
220 Host: ident.PDSEndpoint(),
221 Auth: &xrpc.AuthInfo{
222 AccessJwt: resp.AccessJwt,
223 RefreshJwt: resp.RefreshJwt,
224 Handle: resp.Handle,
225 Did: resp.Did,
226 },
227 }
228
229 rkey := h.TID()
230
231 // write a "knot" record to the owners's pds
232 _, err = atproto.RepoPutRecord(context.Background(), &authClient, &atproto.RepoPutRecord_Input{
233 Collection: tangled.KnotNSID,
234 Repo: ownerDid,
235 Rkey: rkey,
236 Record: &lexutil.LexiconTypeDecoder{
237 Val: &tangled.Knot{
238 CreatedAt: time.Now().Format(time.RFC3339),
239 Host: h.c.Server.Hostname,
240 },
241 },
242 })
243 if err != nil {
244 return err
245 }
246
247 err = h.db.SetOwner(ownerDid, rkey)
248 if err != nil {
249 return err
250 }
251
252 err = h.db.AddDid(ownerDid)
253 if err != nil {
254 return err
255 }
256
257 return nil
258}
259
260func (h *Handle) TID() string {
261 return h.clock.Next().String()
262}