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 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
108func (s *State) TID() string {
109 return s.tidClock.Next().String()
110}
111
112func (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
151func (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
160func (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
195func (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
228func (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
318func (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
355func (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
370func (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
389func (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
464func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
465}
466
467func (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
551func (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
578func (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
661func (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
676func (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.RepoTree)
689 })
690 r.Get("/commit/{ref}", s.RepoCommit)
691 r.Get("/branches", s.RepoBranches)
692 r.Get("/tags", s.RepoTags)
693 r.Get("/blob/{ref}/*", s.RepoBlob)
694
695 // These routes get proxied to the knot
696 r.Get("/info/refs", s.InfoRefs)
697 r.Post("/git-upload-pack", s.UploadPack)
698
699 // settings routes, needs auth
700 r.Group(func(r chi.Router) {
701 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
702 r.Get("/", s.RepoSettings)
703 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
704 })
705 })
706 })
707 })
708
709 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
710 s.pages.Error404(w)
711 })
712
713 return r
714}
715
716func (s *State) StandardRouter() http.Handler {
717 r := chi.NewRouter()
718
719 r.Handle("/static/*", s.pages.Static())
720
721 r.Get("/", s.Timeline)
722
723 r.Get("/login", s.Login)
724 r.Post("/login", s.Login)
725
726 r.Route("/knots", func(r chi.Router) {
727 r.Use(AuthMiddleware(s))
728 r.Get("/", s.Knots)
729 r.Post("/key", s.RegistrationKey)
730
731 r.Route("/{domain}", func(r chi.Router) {
732 r.Post("/init", s.InitKnotServer)
733 r.Get("/", s.KnotServerInfo)
734 r.Route("/member", func(r chi.Router) {
735 r.Use(RoleMiddleware(s, "server:owner"))
736 r.Get("/", s.ListMembers)
737 r.Put("/", s.AddMember)
738 r.Delete("/", s.RemoveMember)
739 })
740 })
741 })
742
743 r.Route("/repo", func(r chi.Router) {
744 r.Route("/new", func(r chi.Router) {
745 r.Get("/", s.AddRepo)
746 r.Post("/", s.AddRepo)
747 })
748 // r.Post("/import", s.ImportRepo)
749 })
750
751 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
752 r.Post("/", s.Follow)
753 r.Delete("/", s.Follow)
754 })
755
756 r.Route("/settings", func(r chi.Router) {
757 r.Use(AuthMiddleware(s))
758 r.Get("/", s.Settings)
759 r.Put("/keys", s.SettingsKeys)
760 })
761
762 r.Get("/keys/{user}", s.Keys)
763
764 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
765 s.pages.Error404(w)
766 })
767 return r
768}