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