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 // All public keys on the knot.
161 r.Get("/keys", h.Keys)
162
163 return r, nil
164}
165
166// version is set during build time.
167var version string
168
169func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
170 if version == "" {
171 info, ok := debug.ReadBuildInfo()
172 if !ok {
173 http.Error(w, "failed to read build info", http.StatusInternalServerError)
174 return
175 }
176
177 var modVer string
178 for _, mod := range info.Deps {
179 if mod.Path == "tangled.sh/tangled.sh/knotserver" {
180 version = mod.Version
181 break
182 }
183 }
184
185 if modVer == "" {
186 version = "unknown"
187 }
188 }
189
190 w.Header().Set("Content-Type", "text/plain")
191 fmt.Fprintf(w, "knotserver/%s", version)
192}
193
194func (h *Handle) Publish() error {
195 ownerDid := h.c.Owner.Did
196 appPassword := h.c.Owner.AppPassword
197
198 res := resolver.DefaultResolver()
199 ident, err := res.ResolveIdent(context.Background(), ownerDid)
200 if err != nil {
201 return err
202 }
203
204 client := xrpc.Client{
205 Host: ident.PDSEndpoint(),
206 }
207
208 resp, err := atproto.ServerCreateSession(context.Background(), &client, &atproto.ServerCreateSession_Input{
209 Identifier: ownerDid,
210 Password: appPassword,
211 })
212 if err != nil {
213 return err
214 }
215
216 authClient := xrpc.Client{
217 Host: ident.PDSEndpoint(),
218 Auth: &xrpc.AuthInfo{
219 AccessJwt: resp.AccessJwt,
220 RefreshJwt: resp.RefreshJwt,
221 Handle: resp.Handle,
222 Did: resp.Did,
223 },
224 }
225
226 rkey := h.TID()
227
228 // write a "knot" record to the owners's pds
229 _, err = atproto.RepoPutRecord(context.Background(), &authClient, &atproto.RepoPutRecord_Input{
230 Collection: tangled.KnotNSID,
231 Repo: ownerDid,
232 Rkey: rkey,
233 Record: &lexutil.LexiconTypeDecoder{
234 Val: &tangled.Knot{
235 CreatedAt: time.Now().Format(time.RFC3339),
236 Host: h.c.Server.Hostname,
237 },
238 },
239 })
240 if err != nil {
241 return err
242 }
243
244 err = h.db.SetOwner(ownerDid)
245 if err != nil {
246 return err
247 }
248
249 err = h.db.AddDid(ownerDid)
250 if err != nil {
251 return err
252 }
253
254 return nil
255}
256
257func (h *Handle) TID() string {
258 return h.clock.Next().String()
259}