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