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