767 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(domain, repoName)
539
err = s.enforcer.AddRepo(user.Did, domain, p)
540
if err != nil {
541
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
542
return
543
}
544
545
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
546
return
547
}
548
}
549
550
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
551
didOrHandle := chi.URLParam(r, "user")
552
if didOrHandle == "" {
553
http.Error(w, "Bad request", http.StatusBadRequest)
554
return
555
}
556
557
ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
558
if err != nil {
559
log.Printf("resolving identity: %s", err)
560
w.WriteHeader(http.StatusNotFound)
561
return
562
}
563
564
repos, err := s.db.GetAllReposByDid(ident.DID.String())
565
if err != nil {
566
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
567
}
568
569
s.pages.ProfilePage(w, pages.ProfilePageParams{
570
LoggedInUser: s.auth.GetUser(r),
571
UserDid: ident.DID.String(),
572
UserHandle: ident.Handle.String(),
573
Repos: repos,
574
})
575
}
576
577
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
578
currentUser := s.auth.GetUser(r)
579
580
subject := r.URL.Query().Get("subject")
581
if subject == "" {
582
log.Println("invalid form")
583
return
584
}
585
586
subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
587
if err != nil {
588
log.Println("failed to follow, invalid did")
589
}
590
591
if currentUser.Did == subjectIdent.DID.String() {
592
log.Println("cant follow or unfollow yourself")
593
return
594
}
595
596
client, _ := s.auth.AuthorizedClient(r)
597
598
switch r.Method {
599
case http.MethodPost:
600
createdAt := time.Now().Format(time.RFC3339)
601
rkey := s.TID()
602
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
603
Collection: tangled.GraphFollowNSID,
604
Repo: currentUser.Did,
605
Rkey: rkey,
606
Record: &lexutil.LexiconTypeDecoder{
607
Val: &tangled.GraphFollow{
608
Subject: subjectIdent.DID.String(),
609
CreatedAt: createdAt,
610
}},
611
})
612
if err != nil {
613
log.Println("failed to create atproto record", err)
614
return
615
}
616
617
err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
618
if err != nil {
619
log.Println("failed to follow", err)
620
return
621
}
622
623
log.Println("created atproto record: ", resp.Uri)
624
625
return
626
case http.MethodDelete:
627
// find the record in the db
628
629
follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
630
if err != nil {
631
log.Println("failed to get follow relationship")
632
return
633
}
634
635
resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
636
Collection: tangled.GraphFollowNSID,
637
Repo: currentUser.Did,
638
Rkey: follow.RKey,
639
})
640
641
log.Println(resp.Commit.Cid)
642
643
if err != nil {
644
log.Println("failed to unfollow")
645
return
646
}
647
648
err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
649
if err != nil {
650
log.Println("failed to delete follow from DB")
651
// this is not an issue, the firehose event might have already done this
652
}
653
654
w.WriteHeader(http.StatusNoContent)
655
return
656
}
657
658
}
659
660
func (s *State) Router() http.Handler {
661
router := chi.NewRouter()
662
663
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
664
pat := chi.URLParam(r, "*")
665
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
666
s.UserRouter().ServeHTTP(w, r)
667
} else {
668
s.StandardRouter().ServeHTTP(w, r)
669
}
670
})
671
672
return router
673
}
674
675
func (s *State) UserRouter() http.Handler {
676
r := chi.NewRouter()
677
678
// strip @ from user
679
r.Use(StripLeadingAt)
680
681
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
682
r.Get("/", s.ProfilePage)
683
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
684
r.Get("/", s.RepoIndex)
685
r.Get("/log/{ref}", s.RepoLog)
686
r.Route("/tree/{ref}", func(r chi.Router) {
687
r.Get("/*", s.RepoTree)
688
})
689
r.Get("/commit/{ref}", s.RepoCommit)
690
r.Get("/branches", s.RepoBranches)
691
r.Get("/tags", s.RepoTags)
692
r.Get("/blob/{ref}/*", s.RepoBlob)
693
694
// These routes get proxied to the knot
695
r.Get("/info/refs", s.InfoRefs)
696
r.Post("/git-upload-pack", s.UploadPack)
697
698
// settings routes, needs auth
699
r.Group(func(r chi.Router) {
700
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
701
r.Get("/", s.RepoSettings)
702
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
703
})
704
})
705
})
706
})
707
708
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
709
s.pages.Error404(w)
710
})
711
712
return r
713
}
714
715
func (s *State) StandardRouter() http.Handler {
716
r := chi.NewRouter()
717
718
r.Handle("/static/*", s.pages.Static())
719
720
r.Get("/", s.Timeline)
721
722
r.Get("/login", s.Login)
723
r.Post("/login", s.Login)
724
725
r.Route("/knots", func(r chi.Router) {
726
r.Use(AuthMiddleware(s))
727
r.Get("/", s.Knots)
728
r.Post("/key", s.RegistrationKey)
729
730
r.Route("/{domain}", func(r chi.Router) {
731
r.Post("/init", s.InitKnotServer)
732
r.Get("/", s.KnotServerInfo)
733
r.Route("/member", func(r chi.Router) {
734
r.Use(RoleMiddleware(s, "server:owner"))
735
r.Get("/", s.ListMembers)
736
r.Put("/", s.AddMember)
737
r.Delete("/", s.RemoveMember)
738
})
739
})
740
})
741
742
r.Route("/repo", func(r chi.Router) {
743
r.Route("/new", func(r chi.Router) {
744
r.Get("/", s.AddRepo)
745
r.Post("/", s.AddRepo)
746
})
747
// r.Post("/import", s.ImportRepo)
748
})
749
750
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
751
r.Post("/", s.Follow)
752
r.Delete("/", s.Follow)
753
})
754
755
r.Route("/settings", func(r chi.Router) {
756
r.Use(AuthMiddleware(s))
757
r.Get("/", s.Settings)
758
r.Put("/keys", s.SettingsKeys)
759
})
760
761
r.Get("/keys/{user}", s.Keys)
762
763
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
764
s.pages.Error404(w)
765
})
766
return r
767
}
768