this repo has no description
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 := r.FormValue("handle")
81 appPassword := r.FormValue("app_password")
82
83 fmt.Println("handle", handle)
84 fmt.Println("app_password", appPassword)
85
86 resolved, err := s.resolver.ResolveIdent(ctx, handle)
87 if err != nil {
88 log.Printf("resolving identity: %s", err)
89 http.Redirect(w, r, "/login", http.StatusSeeOther)
90 return
91 }
92
93 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
94 if err != nil {
95 log.Printf("creating initial session: %s", err)
96 return
97 }
98 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
99
100 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
101 if err != nil {
102 log.Printf("storing session: %s", err)
103 return
104 }
105
106 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
107 http.Redirect(w, r, "/", http.StatusSeeOther)
108 return
109 }
110}
111
112func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
113 user := s.auth.GetUser(r)
114 s.pages.Timeline(w, pages.TimelineParams{
115 LoggedInUser: user,
116 })
117 return
118}
119
120// requires auth
121func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
122 switch r.Method {
123 case http.MethodGet:
124 // list open registrations under this did
125
126 return
127 case http.MethodPost:
128 session, err := s.auth.Store.Get(r, appview.SessionName)
129 if err != nil || session.IsNew {
130 log.Println("unauthorized attempt to generate registration key")
131 http.Error(w, "Forbidden", http.StatusUnauthorized)
132 return
133 }
134
135 did := session.Values[appview.SessionDid].(string)
136
137 // check if domain is valid url, and strip extra bits down to just host
138 domain := r.FormValue("domain")
139 if domain == "" {
140 http.Error(w, "Invalid form", http.StatusBadRequest)
141 return
142 }
143
144 key, err := s.db.GenerateRegistrationKey(domain, did)
145
146 if err != nil {
147 log.Println(err)
148 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
149 return
150 }
151
152 w.Write([]byte(key))
153 }
154}
155
156func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
157 user := chi.URLParam(r, "user")
158 user = strings.TrimPrefix(user, "@")
159
160 if user == "" {
161 w.WriteHeader(http.StatusBadRequest)
162 return
163 }
164
165 id, err := s.resolver.ResolveIdent(r.Context(), user)
166 if err != nil {
167 w.WriteHeader(http.StatusInternalServerError)
168 return
169 }
170
171 pubKeys, err := s.db.GetPublicKeys(id.DID.String())
172 if err != nil {
173 w.WriteHeader(http.StatusNotFound)
174 return
175 }
176
177 if len(pubKeys) == 0 {
178 w.WriteHeader(http.StatusNotFound)
179 return
180 }
181
182 for _, k := range pubKeys {
183 key := strings.TrimRight(k.Key, "\n")
184 w.Write([]byte(fmt.Sprintln(key)))
185 }
186}
187
188// create a signed request and check if a node responds to that
189func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
190 user := s.auth.GetUser(r)
191
192 domain := chi.URLParam(r, "domain")
193 if domain == "" {
194 http.Error(w, "malformed url", http.StatusBadRequest)
195 return
196 }
197 log.Println("checking ", domain)
198
199 secret, err := s.db.GetRegistrationKey(domain)
200 if err != nil {
201 log.Printf("no key found for domain %s: %s\n", domain, err)
202 return
203 }
204
205 client, err := NewSignedClient(domain, secret)
206 if err != nil {
207 log.Println("failed to create client to ", domain)
208 }
209
210 resp, err := client.Init(user.Did)
211 if err != nil {
212 w.Write([]byte("no dice"))
213 log.Println("domain was unreachable after 5 seconds")
214 return
215 }
216
217 if resp.StatusCode == http.StatusConflict {
218 log.Println("status conflict", resp.StatusCode)
219 w.Write([]byte("already registered, sorry!"))
220 return
221 }
222
223 if resp.StatusCode != http.StatusNoContent {
224 log.Println("status nok", resp.StatusCode)
225 w.Write([]byte("no dice"))
226 return
227 }
228
229 // verify response mac
230 signature := resp.Header.Get("X-Signature")
231 signatureBytes, err := hex.DecodeString(signature)
232 if err != nil {
233 return
234 }
235
236 expectedMac := hmac.New(sha256.New, []byte(secret))
237 expectedMac.Write([]byte("ok"))
238
239 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
240 log.Printf("response body signature mismatch: %x\n", signatureBytes)
241 return
242 }
243
244 // mark as registered
245 err = s.db.Register(domain)
246 if err != nil {
247 log.Println("failed to register domain", err)
248 http.Error(w, err.Error(), http.StatusInternalServerError)
249 return
250 }
251
252 // set permissions for this did as owner
253 reg, err := s.db.RegistrationByDomain(domain)
254 if err != nil {
255 log.Println("failed to register domain", err)
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
259
260 // add basic acls for this domain
261 err = s.enforcer.AddDomain(domain)
262 if err != nil {
263 log.Println("failed to setup owner of domain", err)
264 http.Error(w, err.Error(), http.StatusInternalServerError)
265 return
266 }
267
268 // add this did as owner of this domain
269 err = s.enforcer.AddOwner(domain, reg.ByDid)
270 if err != nil {
271 log.Println("failed to setup owner of domain", err)
272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
274 }
275
276 w.Write([]byte("check success"))
277}
278
279func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
280 domain := chi.URLParam(r, "domain")
281 if domain == "" {
282 http.Error(w, "malformed url", http.StatusBadRequest)
283 return
284 }
285
286 user := s.auth.GetUser(r)
287 reg, err := s.db.RegistrationByDomain(domain)
288 if err != nil {
289 w.Write([]byte("failed to pull up registration info"))
290 return
291 }
292
293 var members []string
294 if reg.Registered != nil {
295 members, err = s.enforcer.GetUserByRole("server:member", domain)
296 if err != nil {
297 w.Write([]byte("failed to fetch member list"))
298 return
299 }
300 }
301
302 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
303 isOwner := err == nil && ok
304
305 p := pages.KnotParams{
306 LoggedInUser: user,
307 Registration: reg,
308 Members: members,
309 IsOwner: isOwner,
310 }
311
312 s.pages.Knot(w, p)
313}
314
315// get knots registered by this user
316func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
317 // for now, this is just pubkeys
318 user := s.auth.GetUser(r)
319 registrations, err := s.db.RegistrationsByDid(user.Did)
320 if err != nil {
321 log.Println(err)
322 }
323
324 s.pages.Knots(w, pages.KnotsParams{
325 LoggedInUser: user,
326 Registrations: registrations,
327 })
328}
329
330// list members of domain, requires auth and requires owner status
331func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
332 domain := chi.URLParam(r, "domain")
333 if domain == "" {
334 http.Error(w, "malformed url", http.StatusBadRequest)
335 return
336 }
337
338 // list all members for this domain
339 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
340 if err != nil {
341 w.Write([]byte("failed to fetch member list"))
342 return
343 }
344
345 w.Write([]byte(strings.Join(memberDids, "\n")))
346 return
347}
348
349// add member to domain, requires auth and requires invite access
350func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
351 domain := chi.URLParam(r, "domain")
352 if domain == "" {
353 http.Error(w, "malformed url", http.StatusBadRequest)
354 return
355 }
356
357 memberDid := r.FormValue("member")
358 if memberDid == "" {
359 http.Error(w, "malformed form", http.StatusBadRequest)
360 return
361 }
362
363 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid)
364 if err != nil {
365 w.Write([]byte("failed to resolve member did to a handle"))
366 return
367 }
368 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
369
370 // announce this relation into the firehose, store into owners' pds
371 client, _ := s.auth.AuthorizedClient(r)
372 currentUser := s.auth.GetUser(r)
373 addedAt := time.Now().Format(time.RFC3339)
374 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
375 Collection: tangled.KnotMemberNSID,
376 Repo: currentUser.Did,
377 Rkey: s.TID(),
378 Record: &lexutil.LexiconTypeDecoder{
379 Val: &tangled.KnotMember{
380 Member: memberIdent.DID.String(),
381 Domain: domain,
382 AddedAt: &addedAt,
383 }},
384 })
385 // invalid record
386 if err != nil {
387 log.Printf("failed to create record: %s", err)
388 return
389 }
390 log.Println("created atproto record: ", resp.Uri)
391
392 secret, err := s.db.GetRegistrationKey(domain)
393 if err != nil {
394 log.Printf("no key found for domain %s: %s\n", domain, err)
395 return
396 }
397
398 ksClient, err := NewSignedClient(domain, secret)
399 if err != nil {
400 log.Println("failed to create client to ", domain)
401 return
402 }
403
404 ksResp, err := ksClient.AddMember(memberIdent.DID.String())
405 if err != nil {
406 log.Printf("failed to make request to %s: %s", domain, err)
407 return
408 }
409
410 if ksResp.StatusCode != http.StatusNoContent {
411 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
412 return
413 }
414
415 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
416 if err != nil {
417 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
418 return
419 }
420
421 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
422}
423
424func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
425}
426
427func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
428 switch r.Method {
429 case http.MethodGet:
430 user := s.auth.GetUser(r)
431 knots, err := s.enforcer.GetDomainsForUser(user.Did)
432
433 if err != nil {
434 s.pages.Notice(w, "repo", "Invalid user account.")
435 return
436 }
437
438 s.pages.NewRepo(w, pages.NewRepoParams{
439 LoggedInUser: user,
440 Knots: knots,
441 })
442 case http.MethodPost:
443 user := s.auth.GetUser(r)
444
445 domain := r.FormValue("domain")
446 if domain == "" {
447 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
448 return
449 }
450
451 repoName := r.FormValue("name")
452 if repoName == "" {
453 s.pages.Notice(w, "repo", "Invalid repo name.")
454 return
455 }
456
457 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
458 if err != nil || !ok {
459 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
460 return
461 }
462
463 secret, err := s.db.GetRegistrationKey(domain)
464 if err != nil {
465 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
466 return
467 }
468
469 client, err := NewSignedClient(domain, secret)
470 if err != nil {
471 s.pages.Notice(w, "repo", "Failed to connect to knot server.")
472 return
473 }
474
475 resp, err := client.NewRepo(user.Did, repoName)
476 if err != nil {
477 s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
478 return
479 }
480 if resp.StatusCode != http.StatusNoContent {
481 s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode))
482 return
483 }
484
485 // add to local db
486 repo := &db.Repo{
487 Did: user.Did,
488 Name: repoName,
489 Knot: domain,
490 }
491 err = s.db.AddRepo(repo)
492 if err != nil {
493 s.pages.Notice(w, "repo", "Failed to save repository information.")
494 return
495 }
496
497 // acls
498 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
499 if err != nil {
500 s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
501 return
502 }
503
504 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
505 return
506 }
507}
508
509func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
510 didOrHandle := chi.URLParam(r, "user")
511 if didOrHandle == "" {
512 http.Error(w, "Bad request", http.StatusBadRequest)
513 return
514 }
515
516 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
517 if err != nil {
518 log.Printf("resolving identity: %s", err)
519 w.WriteHeader(http.StatusNotFound)
520 return
521 }
522
523 repos, err := s.db.GetAllReposByDid(ident.DID.String())
524 if err != nil {
525 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
526 }
527
528 s.pages.ProfilePage(w, pages.ProfilePageParams{
529 LoggedInUser: s.auth.GetUser(r),
530 UserDid: ident.DID.String(),
531 UserHandle: ident.Handle.String(),
532 Repos: repos,
533 })
534}
535
536func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
537 subject := r.FormValue("subject")
538
539 if subject == "" {
540 log.Println("invalid form")
541 return
542 }
543
544 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
545 currentUser := s.auth.GetUser(r)
546
547 client, _ := s.auth.AuthorizedClient(r)
548 createdAt := time.Now().Format(time.RFC3339)
549 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
550 Collection: tangled.GraphFollowNSID,
551 Repo: currentUser.Did,
552 Rkey: s.TID(),
553 Record: &lexutil.LexiconTypeDecoder{
554 Val: &tangled.GraphFollow{
555 Subject: subjectIdent.DID.String(),
556 CreatedAt: createdAt,
557 }},
558 })
559
560 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String())
561 if err != nil {
562 log.Println("failed to follow", err)
563 return
564 }
565
566 // invalid record
567 if err != nil {
568 log.Printf("failed to create record: %s", err)
569 return
570 }
571 log.Println("created atproto record: ", resp.Uri)
572
573 return
574}
575
576func (s *State) Router() http.Handler {
577 router := chi.NewRouter()
578
579 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
580 pat := chi.URLParam(r, "*")
581 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
582 s.UserRouter().ServeHTTP(w, r)
583 } else {
584 s.StandardRouter().ServeHTTP(w, r)
585 }
586 })
587
588 return router
589}
590
591func (s *State) UserRouter() http.Handler {
592 r := chi.NewRouter()
593
594 // strip @ from user
595 r.Use(StripLeadingAt)
596
597 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
598 r.Get("/", s.ProfilePage)
599 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
600 r.Get("/", s.RepoIndex)
601 r.Get("/log/{ref}", s.RepoLog)
602 r.Route("/tree/{ref}", func(r chi.Router) {
603 r.Get("/*", s.RepoTree)
604 })
605 r.Get("/commit/{ref}", s.RepoCommit)
606 r.Get("/branches", s.RepoBranches)
607 r.Get("/tags", s.RepoTags)
608 r.Get("/blob/{ref}/*", s.RepoBlob)
609
610 // These routes get proxied to the knot
611 r.Get("/info/refs", s.InfoRefs)
612 r.Post("/git-upload-pack", s.UploadPack)
613
614 })
615 })
616
617 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
618 s.pages.Error404(w)
619 })
620
621 return r
622}
623
624func (s *State) StandardRouter() http.Handler {
625 r := chi.NewRouter()
626
627 r.Handle("/static/*", s.pages.Static())
628
629 r.Get("/", s.Timeline)
630
631 r.Get("/login", s.Login)
632 r.Post("/login", s.Login)
633
634 r.Route("/knots", func(r chi.Router) {
635 r.Use(AuthMiddleware(s))
636 r.Get("/", s.Knots)
637 r.Post("/key", s.RegistrationKey)
638
639 r.Route("/{domain}", func(r chi.Router) {
640 r.Post("/init", s.InitKnotServer)
641 r.Get("/", s.KnotServerInfo)
642 r.Route("/member", func(r chi.Router) {
643 r.Use(RoleMiddleware(s, "server:owner"))
644 r.Get("/", s.ListMembers)
645 r.Put("/", s.AddMember)
646 r.Delete("/", s.RemoveMember)
647 })
648 })
649 })
650
651 r.Route("/repo", func(r chi.Router) {
652 r.Route("/new", func(r chi.Router) {
653 r.Get("/", s.AddRepo)
654 r.Post("/", s.AddRepo)
655 })
656 // r.Post("/import", s.ImportRepo)
657 })
658
659 r.With(AuthMiddleware(s)).Put("/follow", s.Follow)
660
661 r.Route("/settings", func(r chi.Router) {
662 r.Use(AuthMiddleware(s))
663 r.Get("/", s.Settings)
664 r.Put("/keys", s.SettingsKeys)
665 })
666
667 r.Get("/keys/{user}", s.Keys)
668
669 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
670 s.pages.Error404(w)
671 })
672 return r
673}