this repo has no description
1package state
2
3import (
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
30type 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
40func 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
107func (s *State) TID() string {
108 return s.tidClock.Next().String()
109}
110
111func (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
150func (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
159func (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
194func (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
227func (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
317func (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
354func (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
369func (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
388func (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
463func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
464}
465
466func (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
549func (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
576func (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
659func (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
674func (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
714func (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}