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