forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state
2
3import (
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
26type 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
35func 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
65func (s *State) TID() string {
66 return s.tidClock.Next().String()
67}
68
69func (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 s.pages.HxRedirect(w, "/")
104 return
105 }
106}
107
108func (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
117func (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
152func (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
185func (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
275func (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
312func (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
327func (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
346func (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
421func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
422}
423
424func (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
506func (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
533func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
534 currentUser := s.auth.GetUser(r)
535
536 subject := r.URL.Query().Get("subject")
537 if subject == "" {
538 log.Println("invalid form")
539 return
540 }
541
542 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
543 if err != nil {
544 log.Println("failed to follow, invalid did")
545 }
546
547 if currentUser.Did == subjectIdent.DID.String() {
548 log.Println("cant follow or unfollow yourself")
549 return
550 }
551
552 client, _ := s.auth.AuthorizedClient(r)
553
554 switch r.Method {
555 case http.MethodPost:
556 createdAt := time.Now().Format(time.RFC3339)
557 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
558 Collection: tangled.GraphFollowNSID,
559 Repo: currentUser.Did,
560 Rkey: s.TID(),
561 Record: &lexutil.LexiconTypeDecoder{
562 Val: &tangled.GraphFollow{
563 Subject: subjectIdent.DID.String(),
564 CreatedAt: createdAt,
565 }},
566 })
567 if err != nil {
568 log.Println("failed to create atproto record", err)
569 return
570 }
571
572 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), resp.Uri)
573 if err != nil {
574 log.Println("failed to follow", err)
575 return
576 }
577
578 log.Println("created atproto record: ", resp.Uri)
579
580 return
581 case http.MethodDelete:
582 // find the record in the db
583
584 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
585 if err != nil {
586 log.Println("failed to get follow relationship")
587 return
588 }
589
590 existingRecordUri, _ := syntax.ParseATURI(follow.AtUri)
591
592 resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
593 Collection: tangled.GraphFollowNSID,
594 Repo: currentUser.Did,
595 Rkey: existingRecordUri.RecordKey().String(),
596 })
597
598 log.Println(resp.Commit.Cid)
599
600 if err != nil {
601 log.Println("failed to unfollow")
602 return
603 }
604
605 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
606 if err != nil {
607 log.Println("failed to delete follow from DB")
608 // this is not an issue, the firehose event might have already done this
609 }
610
611 w.WriteHeader(http.StatusNoContent)
612 return
613 }
614
615}
616
617func (s *State) Router() http.Handler {
618 router := chi.NewRouter()
619
620 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
621 pat := chi.URLParam(r, "*")
622 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
623 s.UserRouter().ServeHTTP(w, r)
624 } else {
625 s.StandardRouter().ServeHTTP(w, r)
626 }
627 })
628
629 return router
630}
631
632func (s *State) UserRouter() http.Handler {
633 r := chi.NewRouter()
634
635 // strip @ from user
636 r.Use(StripLeadingAt)
637
638 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
639 r.Get("/", s.ProfilePage)
640 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
641 r.Get("/", s.RepoIndex)
642 r.Get("/log/{ref}", s.RepoLog)
643 r.Route("/tree/{ref}", func(r chi.Router) {
644 r.Get("/*", s.RepoTree)
645 })
646 r.Get("/commit/{ref}", s.RepoCommit)
647 r.Get("/branches", s.RepoBranches)
648 r.Get("/tags", s.RepoTags)
649 r.Get("/blob/{ref}/*", s.RepoBlob)
650
651 // These routes get proxied to the knot
652 r.Get("/info/refs", s.InfoRefs)
653 r.Post("/git-upload-pack", s.UploadPack)
654
655 // settings routes, needs auth
656 r.Group(func(r chi.Router) {
657 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
658 r.Get("/", s.RepoSettings)
659 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
660 })
661 })
662 })
663 })
664
665 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
666 s.pages.Error404(w)
667 })
668
669 return r
670}
671
672func (s *State) StandardRouter() http.Handler {
673 r := chi.NewRouter()
674
675 r.Handle("/static/*", s.pages.Static())
676
677 r.Get("/", s.Timeline)
678
679 r.Get("/login", s.Login)
680 r.Post("/login", s.Login)
681
682 r.Route("/knots", func(r chi.Router) {
683 r.Use(AuthMiddleware(s))
684 r.Get("/", s.Knots)
685 r.Post("/key", s.RegistrationKey)
686
687 r.Route("/{domain}", func(r chi.Router) {
688 r.Post("/init", s.InitKnotServer)
689 r.Get("/", s.KnotServerInfo)
690 r.Route("/member", func(r chi.Router) {
691 r.Use(RoleMiddleware(s, "server:owner"))
692 r.Get("/", s.ListMembers)
693 r.Put("/", s.AddMember)
694 r.Delete("/", s.RemoveMember)
695 })
696 })
697 })
698
699 r.Route("/repo", func(r chi.Router) {
700 r.Route("/new", func(r chi.Router) {
701 r.Get("/", s.AddRepo)
702 r.Post("/", s.AddRepo)
703 })
704 // r.Post("/import", s.ImportRepo)
705 })
706
707 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
708 r.Post("/", s.Follow)
709 r.Delete("/", s.Follow)
710 })
711
712 r.Route("/settings", func(r chi.Router) {
713 r.Use(AuthMiddleware(s))
714 r.Get("/", s.Settings)
715 r.Put("/keys", s.SettingsKeys)
716 })
717
718 r.Get("/keys/{user}", s.Keys)
719
720 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
721 s.pages.Error404(w)
722 })
723 return r
724}