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 "path/filepath"
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 "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)
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
102func (s *State) TID() string {
103 return s.tidClock.Next().String()
104}
105
106func (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
145func (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
154func (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
189func (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
222func (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
312func (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
349func (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
364func (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
383func (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
458func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
459}
460
461func (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 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
533 if err != nil {
534 s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
535 return
536 }
537
538 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
539 return
540 }
541}
542
543func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
544 didOrHandle := chi.URLParam(r, "user")
545 if didOrHandle == "" {
546 http.Error(w, "Bad request", http.StatusBadRequest)
547 return
548 }
549
550 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
551 if err != nil {
552 log.Printf("resolving identity: %s", err)
553 w.WriteHeader(http.StatusNotFound)
554 return
555 }
556
557 repos, err := s.db.GetAllReposByDid(ident.DID.String())
558 if err != nil {
559 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
560 }
561
562 s.pages.ProfilePage(w, pages.ProfilePageParams{
563 LoggedInUser: s.auth.GetUser(r),
564 UserDid: ident.DID.String(),
565 UserHandle: ident.Handle.String(),
566 Repos: repos,
567 })
568}
569
570func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
571 currentUser := s.auth.GetUser(r)
572
573 subject := r.URL.Query().Get("subject")
574 if subject == "" {
575 log.Println("invalid form")
576 return
577 }
578
579 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
580 if err != nil {
581 log.Println("failed to follow, invalid did")
582 }
583
584 if currentUser.Did == subjectIdent.DID.String() {
585 log.Println("cant follow or unfollow yourself")
586 return
587 }
588
589 client, _ := s.auth.AuthorizedClient(r)
590
591 switch r.Method {
592 case http.MethodPost:
593 createdAt := time.Now().Format(time.RFC3339)
594 rkey := s.TID()
595 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
596 Collection: tangled.GraphFollowNSID,
597 Repo: currentUser.Did,
598 Rkey: rkey,
599 Record: &lexutil.LexiconTypeDecoder{
600 Val: &tangled.GraphFollow{
601 Subject: subjectIdent.DID.String(),
602 CreatedAt: createdAt,
603 }},
604 })
605 if err != nil {
606 log.Println("failed to create atproto record", err)
607 return
608 }
609
610 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
611 if err != nil {
612 log.Println("failed to follow", err)
613 return
614 }
615
616 log.Println("created atproto record: ", resp.Uri)
617
618 return
619 case http.MethodDelete:
620 // find the record in the db
621
622 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
623 if err != nil {
624 log.Println("failed to get follow relationship")
625 return
626 }
627
628 resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
629 Collection: tangled.GraphFollowNSID,
630 Repo: currentUser.Did,
631 Rkey: follow.RKey,
632 })
633
634 log.Println(resp.Commit.Cid)
635
636 if err != nil {
637 log.Println("failed to unfollow")
638 return
639 }
640
641 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
642 if err != nil {
643 log.Println("failed to delete follow from DB")
644 // this is not an issue, the firehose event might have already done this
645 }
646
647 w.WriteHeader(http.StatusNoContent)
648 return
649 }
650
651}
652
653func (s *State) Router() http.Handler {
654 router := chi.NewRouter()
655
656 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
657 pat := chi.URLParam(r, "*")
658 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
659 s.UserRouter().ServeHTTP(w, r)
660 } else {
661 s.StandardRouter().ServeHTTP(w, r)
662 }
663 })
664
665 return router
666}
667
668func (s *State) UserRouter() http.Handler {
669 r := chi.NewRouter()
670
671 // strip @ from user
672 r.Use(StripLeadingAt)
673
674 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
675 r.Get("/", s.ProfilePage)
676 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
677 r.Get("/", s.RepoIndex)
678 r.Get("/log/{ref}", s.RepoLog)
679 r.Route("/tree/{ref}", func(r chi.Router) {
680 r.Get("/*", s.RepoTree)
681 })
682 r.Get("/commit/{ref}", s.RepoCommit)
683 r.Get("/branches", s.RepoBranches)
684 r.Get("/tags", s.RepoTags)
685 r.Get("/blob/{ref}/*", s.RepoBlob)
686
687 // These routes get proxied to the knot
688 r.Get("/info/refs", s.InfoRefs)
689 r.Post("/git-upload-pack", s.UploadPack)
690
691 // settings routes, needs auth
692 r.Group(func(r chi.Router) {
693 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
694 r.Get("/", s.RepoSettings)
695 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
696 })
697 })
698 })
699 })
700
701 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
702 s.pages.Error404(w)
703 })
704
705 return r
706}
707
708func (s *State) StandardRouter() http.Handler {
709 r := chi.NewRouter()
710
711 r.Handle("/static/*", s.pages.Static())
712
713 r.Get("/", s.Timeline)
714
715 r.Get("/login", s.Login)
716 r.Post("/login", s.Login)
717
718 r.Route("/knots", func(r chi.Router) {
719 r.Use(AuthMiddleware(s))
720 r.Get("/", s.Knots)
721 r.Post("/key", s.RegistrationKey)
722
723 r.Route("/{domain}", func(r chi.Router) {
724 r.Post("/init", s.InitKnotServer)
725 r.Get("/", s.KnotServerInfo)
726 r.Route("/member", func(r chi.Router) {
727 r.Use(RoleMiddleware(s, "server:owner"))
728 r.Get("/", s.ListMembers)
729 r.Put("/", s.AddMember)
730 r.Delete("/", s.RemoveMember)
731 })
732 })
733 })
734
735 r.Route("/repo", func(r chi.Router) {
736 r.Route("/new", func(r chi.Router) {
737 r.Get("/", s.AddRepo)
738 r.Post("/", s.AddRepo)
739 })
740 // r.Post("/import", s.ImportRepo)
741 })
742
743 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
744 r.Post("/", s.Follow)
745 r.Delete("/", s.Follow)
746 })
747
748 r.Route("/settings", func(r chi.Router) {
749 r.Use(AuthMiddleware(s))
750 r.Get("/", s.Settings)
751 r.Put("/keys", s.SettingsKeys)
752 })
753
754 r.Get("/keys/{user}", s.Keys)
755
756 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
757 s.pages.Error404(w)
758 })
759 return r
760}