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