777 lines
19 kB
1
package state
2
3
import (
4
"context"
5
"crypto/hmac"
6
"crypto/sha256"
7
"encoding/hex"
8
"encoding/json"
9
"fmt"
10
"log"
11
"log/slog"
12
"net/http"
13
"strings"
14
"time"
15
16
comatproto "github.com/bluesky-social/indigo/api/atproto"
17
"github.com/bluesky-social/indigo/atproto/syntax"
18
lexutil "github.com/bluesky-social/indigo/lex/util"
19
"github.com/bluesky-social/jetstream/pkg/models"
20
securejoin "github.com/cyphar/filepath-securejoin"
21
"github.com/go-chi/chi/v5"
22
tangled "github.com/sotangled/tangled/api/tangled"
23
"github.com/sotangled/tangled/appview"
24
"github.com/sotangled/tangled/appview/auth"
25
"github.com/sotangled/tangled/appview/db"
26
"github.com/sotangled/tangled/appview/pages"
27
"github.com/sotangled/tangled/jetstream"
28
"github.com/sotangled/tangled/rbac"
29
)
30
31
type State struct {
32
db *db.DB
33
auth *auth.Auth
34
enforcer *rbac.Enforcer
35
tidClock *syntax.TIDClock
36
pages *pages.Pages
37
resolver *appview.Resolver
38
jc *jetstream.JetstreamClient
39
}
40
41
func Make() (*State, error) {
42
db, err := db.Make(appview.SqliteDbPath)
43
if err != nil {
44
return nil, err
45
}
46
47
auth, err := auth.Make()
48
if err != nil {
49
return nil, err
50
}
51
52
enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath)
53
if err != nil {
54
return nil, err
55
}
56
57
clock := syntax.NewTIDClock(0)
58
59
pgs := pages.NewPages()
60
61
resolver := appview.NewResolver()
62
63
jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false)
64
if err != nil {
65
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
66
}
67
err = jc.StartJetstream(context.Background(), func(ctx context.Context, e *models.Event) error {
68
if e.Kind != models.EventKindCommit {
69
return nil
70
}
71
72
did := e.Did
73
fmt.Println("got event", e.Commit.Collection, e.Commit.RKey, e.Commit.Record)
74
raw := json.RawMessage(e.Commit.Record)
75
76
switch e.Commit.Collection {
77
case tangled.GraphFollowNSID:
78
record := tangled.GraphFollow{}
79
err := json.Unmarshal(raw, &record)
80
if err != nil {
81
return err
82
}
83
err = db.AddFollow(did, record.Subject, e.Commit.RKey)
84
if err != nil {
85
return fmt.Errorf("failed to add follow to db: %w", err)
86
}
87
return db.UpdateLastTimeUs(e.TimeUS)
88
}
89
90
return nil
91
})
92
if err != nil {
93
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
94
}
95
96
state := &State{
97
db,
98
auth,
99
enforcer,
100
clock,
101
pgs,
102
resolver,
103
jc,
104
}
105
106
return state, nil
107
}
108
109
func (s *State) TID() string {
110
return s.tidClock.Next().String()
111
}
112
113
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
114
ctx := r.Context()
115
116
switch r.Method {
117
case http.MethodGet:
118
err := s.pages.Login(w, pages.LoginParams{})
119
if err != nil {
120
log.Printf("rendering login page: %s", err)
121
}
122
return
123
case http.MethodPost:
124
handle := strings.TrimPrefix(r.FormValue("handle"), "@")
125
appPassword := r.FormValue("app_password")
126
127
resolved, err := s.resolver.ResolveIdent(ctx, handle)
128
if err != nil {
129
log.Println("failed to resolve handle:", err)
130
s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
131
return
132
}
133
134
atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
135
if err != nil {
136
s.pages.Notice(w, "login-msg", "Invalid handle or password.")
137
return
138
}
139
sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
140
141
err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
142
if err != nil {
143
s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
144
return
145
}
146
147
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
148
s.pages.HxRedirect(w, "/")
149
return
150
}
151
}
152
153
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
154
user := s.auth.GetUser(r)
155
s.pages.Timeline(w, pages.TimelineParams{
156
LoggedInUser: user,
157
})
158
return
159
}
160
161
// requires auth
162
func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
163
switch r.Method {
164
case http.MethodGet:
165
// list open registrations under this did
166
167
return
168
case http.MethodPost:
169
session, err := s.auth.Store.Get(r, appview.SessionName)
170
if err != nil || session.IsNew {
171
log.Println("unauthorized attempt to generate registration key")
172
http.Error(w, "Forbidden", http.StatusUnauthorized)
173
return
174
}
175
176
did := session.Values[appview.SessionDid].(string)
177
178
// check if domain is valid url, and strip extra bits down to just host
179
domain := r.FormValue("domain")
180
if domain == "" {
181
http.Error(w, "Invalid form", http.StatusBadRequest)
182
return
183
}
184
185
key, err := s.db.GenerateRegistrationKey(domain, did)
186
187
if err != nil {
188
log.Println(err)
189
http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
190
return
191
}
192
193
w.Write([]byte(key))
194
}
195
}
196
197
func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
198
user := chi.URLParam(r, "user")
199
user = strings.TrimPrefix(user, "@")
200
201
if user == "" {
202
w.WriteHeader(http.StatusBadRequest)
203
return
204
}
205
206
id, err := s.resolver.ResolveIdent(r.Context(), user)
207
if err != nil {
208
w.WriteHeader(http.StatusInternalServerError)
209
return
210
}
211
212
pubKeys, err := s.db.GetPublicKeys(id.DID.String())
213
if err != nil {
214
w.WriteHeader(http.StatusNotFound)
215
return
216
}
217
218
if len(pubKeys) == 0 {
219
w.WriteHeader(http.StatusNotFound)
220
return
221
}
222
223
for _, k := range pubKeys {
224
key := strings.TrimRight(k.Key, "\n")
225
w.Write([]byte(fmt.Sprintln(key)))
226
}
227
}
228
229
// create a signed request and check if a node responds to that
230
func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
231
user := s.auth.GetUser(r)
232
233
domain := chi.URLParam(r, "domain")
234
if domain == "" {
235
http.Error(w, "malformed url", http.StatusBadRequest)
236
return
237
}
238
log.Println("checking ", domain)
239
240
secret, err := s.db.GetRegistrationKey(domain)
241
if err != nil {
242
log.Printf("no key found for domain %s: %s\n", domain, err)
243
return
244
}
245
246
client, err := NewSignedClient(domain, secret)
247
if err != nil {
248
log.Println("failed to create client to ", domain)
249
}
250
251
resp, err := client.Init(user.Did)
252
if err != nil {
253
w.Write([]byte("no dice"))
254
log.Println("domain was unreachable after 5 seconds")
255
return
256
}
257
258
if resp.StatusCode == http.StatusConflict {
259
log.Println("status conflict", resp.StatusCode)
260
w.Write([]byte("already registered, sorry!"))
261
return
262
}
263
264
if resp.StatusCode != http.StatusNoContent {
265
log.Println("status nok", resp.StatusCode)
266
w.Write([]byte("no dice"))
267
return
268
}
269
270
// verify response mac
271
signature := resp.Header.Get("X-Signature")
272
signatureBytes, err := hex.DecodeString(signature)
273
if err != nil {
274
return
275
}
276
277
expectedMac := hmac.New(sha256.New, []byte(secret))
278
expectedMac.Write([]byte("ok"))
279
280
if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
281
log.Printf("response body signature mismatch: %x\n", signatureBytes)
282
return
283
}
284
285
// mark as registered
286
err = s.db.Register(domain)
287
if err != nil {
288
log.Println("failed to register domain", err)
289
http.Error(w, err.Error(), http.StatusInternalServerError)
290
return
291
}
292
293
// set permissions for this did as owner
294
reg, err := s.db.RegistrationByDomain(domain)
295
if err != nil {
296
log.Println("failed to register domain", err)
297
http.Error(w, err.Error(), http.StatusInternalServerError)
298
return
299
}
300
301
// add basic acls for this domain
302
err = s.enforcer.AddDomain(domain)
303
if err != nil {
304
log.Println("failed to setup owner of domain", err)
305
http.Error(w, err.Error(), http.StatusInternalServerError)
306
return
307
}
308
309
// add this did as owner of this domain
310
err = s.enforcer.AddOwner(domain, reg.ByDid)
311
if err != nil {
312
log.Println("failed to setup owner of domain", err)
313
http.Error(w, err.Error(), http.StatusInternalServerError)
314
return
315
}
316
317
w.Write([]byte("check success"))
318
}
319
320
func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
321
domain := chi.URLParam(r, "domain")
322
if domain == "" {
323
http.Error(w, "malformed url", http.StatusBadRequest)
324
return
325
}
326
327
user := s.auth.GetUser(r)
328
reg, err := s.db.RegistrationByDomain(domain)
329
if err != nil {
330
w.Write([]byte("failed to pull up registration info"))
331
return
332
}
333
334
var members []string
335
if reg.Registered != nil {
336
members, err = s.enforcer.GetUserByRole("server:member", domain)
337
if err != nil {
338
w.Write([]byte("failed to fetch member list"))
339
return
340
}
341
}
342
343
ok, err := s.enforcer.IsServerOwner(user.Did, domain)
344
isOwner := err == nil && ok
345
346
p := pages.KnotParams{
347
LoggedInUser: user,
348
Registration: reg,
349
Members: members,
350
IsOwner: isOwner,
351
}
352
353
s.pages.Knot(w, p)
354
}
355
356
// get knots registered by this user
357
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
358
// for now, this is just pubkeys
359
user := s.auth.GetUser(r)
360
registrations, err := s.db.RegistrationsByDid(user.Did)
361
if err != nil {
362
log.Println(err)
363
}
364
365
s.pages.Knots(w, pages.KnotsParams{
366
LoggedInUser: user,
367
Registrations: registrations,
368
})
369
}
370
371
// list members of domain, requires auth and requires owner status
372
func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
373
domain := chi.URLParam(r, "domain")
374
if domain == "" {
375
http.Error(w, "malformed url", http.StatusBadRequest)
376
return
377
}
378
379
// list all members for this domain
380
memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
381
if err != nil {
382
w.Write([]byte("failed to fetch member list"))
383
return
384
}
385
386
w.Write([]byte(strings.Join(memberDids, "\n")))
387
return
388
}
389
390
// add member to domain, requires auth and requires invite access
391
func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
392
domain := chi.URLParam(r, "domain")
393
if domain == "" {
394
http.Error(w, "malformed url", http.StatusBadRequest)
395
return
396
}
397
398
memberDid := r.FormValue("member")
399
if memberDid == "" {
400
http.Error(w, "malformed form", http.StatusBadRequest)
401
return
402
}
403
404
memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid)
405
if err != nil {
406
w.Write([]byte("failed to resolve member did to a handle"))
407
return
408
}
409
log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
410
411
// announce this relation into the firehose, store into owners' pds
412
client, _ := s.auth.AuthorizedClient(r)
413
currentUser := s.auth.GetUser(r)
414
addedAt := time.Now().Format(time.RFC3339)
415
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
416
Collection: tangled.KnotMemberNSID,
417
Repo: currentUser.Did,
418
Rkey: s.TID(),
419
Record: &lexutil.LexiconTypeDecoder{
420
Val: &tangled.KnotMember{
421
Member: memberIdent.DID.String(),
422
Domain: domain,
423
AddedAt: &addedAt,
424
}},
425
})
426
427
// invalid record
428
if err != nil {
429
log.Printf("failed to create record: %s", err)
430
return
431
}
432
log.Println("created atproto record: ", resp.Uri)
433
434
secret, err := s.db.GetRegistrationKey(domain)
435
if err != nil {
436
log.Printf("no key found for domain %s: %s\n", domain, err)
437
return
438
}
439
440
ksClient, err := NewSignedClient(domain, secret)
441
if err != nil {
442
log.Println("failed to create client to ", domain)
443
return
444
}
445
446
ksResp, err := ksClient.AddMember(memberIdent.DID.String())
447
if err != nil {
448
log.Printf("failed to make request to %s: %s", domain, err)
449
return
450
}
451
452
if ksResp.StatusCode != http.StatusNoContent {
453
w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
454
return
455
}
456
457
err = s.enforcer.AddMember(domain, memberIdent.DID.String())
458
if err != nil {
459
w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
460
return
461
}
462
463
w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
464
}
465
466
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
467
}
468
469
func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
470
switch r.Method {
471
case http.MethodGet:
472
user := s.auth.GetUser(r)
473
knots, err := s.enforcer.GetDomainsForUser(user.Did)
474
475
if err != nil {
476
s.pages.Notice(w, "repo", "Invalid user account.")
477
return
478
}
479
480
s.pages.NewRepo(w, pages.NewRepoParams{
481
LoggedInUser: user,
482
Knots: knots,
483
})
484
case http.MethodPost:
485
user := s.auth.GetUser(r)
486
487
domain := r.FormValue("domain")
488
if domain == "" {
489
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
490
return
491
}
492
493
repoName := r.FormValue("name")
494
if repoName == "" {
495
s.pages.Notice(w, "repo", "Invalid repo name.")
496
return
497
}
498
499
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
500
if err != nil || !ok {
501
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
502
return
503
}
504
505
secret, err := s.db.GetRegistrationKey(domain)
506
if err != nil {
507
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
508
return
509
}
510
511
client, err := NewSignedClient(domain, secret)
512
if err != nil {
513
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
514
return
515
}
516
517
resp, err := client.NewRepo(user.Did, repoName)
518
if err != nil {
519
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
520
return
521
}
522
523
switch resp.StatusCode {
524
case http.StatusConflict:
525
s.pages.Notice(w, "repo", "A repository with that name already exists.")
526
return
527
case http.StatusInternalServerError:
528
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
529
case http.StatusNoContent:
530
// continue
531
}
532
533
// add to local db
534
repo := &db.Repo{
535
Did: user.Did,
536
Name: repoName,
537
Knot: domain,
538
}
539
err = s.db.AddRepo(repo)
540
if err != nil {
541
s.pages.Notice(w, "repo", "Failed to save repository information.")
542
return
543
}
544
545
// acls
546
p, _ := securejoin.SecureJoin(user.Did, repoName)
547
err = s.enforcer.AddRepo(user.Did, domain, p)
548
if err != nil {
549
log.Println(err)
550
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
551
return
552
}
553
554
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
555
return
556
}
557
}
558
559
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
560
didOrHandle := chi.URLParam(r, "user")
561
if didOrHandle == "" {
562
http.Error(w, "Bad request", http.StatusBadRequest)
563
return
564
}
565
566
ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
567
if err != nil {
568
log.Printf("resolving identity: %s", err)
569
w.WriteHeader(http.StatusNotFound)
570
return
571
}
572
573
repos, err := s.db.GetAllReposByDid(ident.DID.String())
574
if err != nil {
575
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
576
}
577
578
s.pages.ProfilePage(w, pages.ProfilePageParams{
579
LoggedInUser: s.auth.GetUser(r),
580
UserDid: ident.DID.String(),
581
UserHandle: ident.Handle.String(),
582
Repos: repos,
583
})
584
}
585
586
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
587
currentUser := s.auth.GetUser(r)
588
589
subject := r.URL.Query().Get("subject")
590
if subject == "" {
591
log.Println("invalid form")
592
return
593
}
594
595
subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
596
if err != nil {
597
log.Println("failed to follow, invalid did")
598
}
599
600
if currentUser.Did == subjectIdent.DID.String() {
601
log.Println("cant follow or unfollow yourself")
602
return
603
}
604
605
client, _ := s.auth.AuthorizedClient(r)
606
607
switch r.Method {
608
case http.MethodPost:
609
createdAt := time.Now().Format(time.RFC3339)
610
rkey := s.TID()
611
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
612
Collection: tangled.GraphFollowNSID,
613
Repo: currentUser.Did,
614
Rkey: rkey,
615
Record: &lexutil.LexiconTypeDecoder{
616
Val: &tangled.GraphFollow{
617
Subject: subjectIdent.DID.String(),
618
CreatedAt: createdAt,
619
}},
620
})
621
if err != nil {
622
log.Println("failed to create atproto record", err)
623
return
624
}
625
626
err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
627
if err != nil {
628
log.Println("failed to follow", err)
629
return
630
}
631
632
log.Println("created atproto record: ", resp.Uri)
633
634
return
635
case http.MethodDelete:
636
// find the record in the db
637
638
follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
639
if err != nil {
640
log.Println("failed to get follow relationship")
641
return
642
}
643
644
resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
645
Collection: tangled.GraphFollowNSID,
646
Repo: currentUser.Did,
647
Rkey: follow.RKey,
648
})
649
650
log.Println(resp.Commit.Cid)
651
652
if err != nil {
653
log.Println("failed to unfollow")
654
return
655
}
656
657
err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
658
if err != nil {
659
log.Println("failed to delete follow from DB")
660
// this is not an issue, the firehose event might have already done this
661
}
662
663
w.WriteHeader(http.StatusNoContent)
664
return
665
}
666
667
}
668
669
func (s *State) Router() http.Handler {
670
router := chi.NewRouter()
671
672
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
673
pat := chi.URLParam(r, "*")
674
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
675
s.UserRouter().ServeHTTP(w, r)
676
} else {
677
s.StandardRouter().ServeHTTP(w, r)
678
}
679
})
680
681
return router
682
}
683
684
func (s *State) UserRouter() http.Handler {
685
r := chi.NewRouter()
686
687
// strip @ from user
688
r.Use(StripLeadingAt)
689
690
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
691
r.Get("/", s.ProfilePage)
692
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
693
r.Get("/", s.RepoIndex)
694
r.Get("/log/{ref}", s.RepoLog)
695
r.Route("/tree/{ref}", func(r chi.Router) {
696
r.Get("/", s.RepoIndex)
697
r.Get("/*", s.RepoTree)
698
})
699
r.Get("/commit/{ref}", s.RepoCommit)
700
r.Get("/branches", s.RepoBranches)
701
r.Get("/tags", s.RepoTags)
702
r.Get("/blob/{ref}/*", s.RepoBlob)
703
704
// These routes get proxied to the knot
705
r.Get("/info/refs", s.InfoRefs)
706
r.Post("/git-upload-pack", s.UploadPack)
707
708
// settings routes, needs auth
709
r.Group(func(r chi.Router) {
710
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
711
r.Get("/", s.RepoSettings)
712
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
713
})
714
})
715
})
716
})
717
718
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
719
s.pages.Error404(w)
720
})
721
722
return r
723
}
724
725
func (s *State) StandardRouter() http.Handler {
726
r := chi.NewRouter()
727
728
r.Handle("/static/*", s.pages.Static())
729
730
r.Get("/", s.Timeline)
731
732
r.Get("/login", s.Login)
733
r.Post("/login", s.Login)
734
735
r.Route("/knots", func(r chi.Router) {
736
r.Use(AuthMiddleware(s))
737
r.Get("/", s.Knots)
738
r.Post("/key", s.RegistrationKey)
739
740
r.Route("/{domain}", func(r chi.Router) {
741
r.Post("/init", s.InitKnotServer)
742
r.Get("/", s.KnotServerInfo)
743
r.Route("/member", func(r chi.Router) {
744
r.Use(RoleMiddleware(s, "server:owner"))
745
r.Get("/", s.ListMembers)
746
r.Put("/", s.AddMember)
747
r.Delete("/", s.RemoveMember)
748
})
749
})
750
})
751
752
r.Route("/repo", func(r chi.Router) {
753
r.Route("/new", func(r chi.Router) {
754
r.Get("/", s.AddRepo)
755
r.Post("/", s.AddRepo)
756
})
757
// r.Post("/import", s.ImportRepo)
758
})
759
760
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
761
r.Post("/", s.Follow)
762
r.Delete("/", s.Follow)
763
})
764
765
r.Route("/settings", func(r chi.Router) {
766
r.Use(AuthMiddleware(s))
767
r.Get("/", s.Settings)
768
r.Put("/keys", s.SettingsKeys)
769
})
770
771
r.Get("/keys/{user}", s.Keys)
772
773
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
774
s.pages.Error404(w)
775
})
776
return r
777
}
778