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
386 // invalid record
387 if err != nil {
388 log.Printf("failed to create record: %s", err)
389 return
390 }
391 log.Println("created atproto record: ", resp.Uri)
392
393 secret, err := s.db.GetRegistrationKey(domain)
394 if err != nil {
395 log.Printf("no key found for domain %s: %s\n", domain, err)
396 return
397 }
398
399 ksClient, err := NewSignedClient(domain, secret)
400 if err != nil {
401 log.Println("failed to create client to ", domain)
402 return
403 }
404
405 ksResp, err := ksClient.AddMember(memberIdent.DID.String())
406 if err != nil {
407 log.Printf("failed to make request to %s: %s", domain, err)
408 return
409 }
410
411 if ksResp.StatusCode != http.StatusNoContent {
412 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
413 return
414 }
415
416 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
417 if err != nil {
418 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
419 return
420 }
421
422 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
423}
424
425func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
426}
427
428func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
429 switch r.Method {
430 case http.MethodGet:
431 user := s.auth.GetUser(r)
432 knots, err := s.enforcer.GetDomainsForUser(user.Did)
433
434 if err != nil {
435 s.pages.Notice(w, "repo", "Invalid user account.")
436 return
437 }
438
439 s.pages.NewRepo(w, pages.NewRepoParams{
440 LoggedInUser: user,
441 Knots: knots,
442 })
443 case http.MethodPost:
444 user := s.auth.GetUser(r)
445
446 domain := r.FormValue("domain")
447 if domain == "" {
448 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
449 return
450 }
451
452 repoName := r.FormValue("name")
453 if repoName == "" {
454 s.pages.Notice(w, "repo", "Invalid repo name.")
455 return
456 }
457
458 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
459 if err != nil || !ok {
460 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
461 return
462 }
463
464 secret, err := s.db.GetRegistrationKey(domain)
465 if err != nil {
466 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
467 return
468 }
469
470 client, err := NewSignedClient(domain, secret)
471 if err != nil {
472 s.pages.Notice(w, "repo", "Failed to connect to knot server.")
473 return
474 }
475
476 resp, err := client.NewRepo(user.Did, repoName)
477 if err != nil {
478 s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
479 return
480 }
481 if resp.StatusCode != http.StatusNoContent {
482 s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode))
483 return
484 }
485
486 // add to local db
487 repo := &db.Repo{
488 Did: user.Did,
489 Name: repoName,
490 Knot: domain,
491 }
492 err = s.db.AddRepo(repo)
493 if err != nil {
494 s.pages.Notice(w, "repo", "Failed to save repository information.")
495 return
496 }
497
498 // acls
499 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
500 if err != nil {
501 s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
502 return
503 }
504
505 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
506 return
507 }
508}
509
510func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
511 didOrHandle := chi.URLParam(r, "user")
512 if didOrHandle == "" {
513 http.Error(w, "Bad request", http.StatusBadRequest)
514 return
515 }
516
517 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
518 if err != nil {
519 log.Printf("resolving identity: %s", err)
520 w.WriteHeader(http.StatusNotFound)
521 return
522 }
523
524 repos, err := s.db.GetAllReposByDid(ident.DID.String())
525 if err != nil {
526 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
527 }
528
529 s.pages.ProfilePage(w, pages.ProfilePageParams{
530 LoggedInUser: s.auth.GetUser(r),
531 UserDid: ident.DID.String(),
532 UserHandle: ident.Handle.String(),
533 Repos: repos,
534 })
535}
536
537func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
538 subject := r.FormValue("subject")
539
540 if subject == "" {
541 log.Println("invalid form")
542 return
543 }
544
545 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
546 currentUser := s.auth.GetUser(r)
547
548 client, _ := s.auth.AuthorizedClient(r)
549 createdAt := time.Now().Format(time.RFC3339)
550 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
551 Collection: tangled.GraphFollowNSID,
552 Repo: currentUser.Did,
553 Rkey: s.TID(),
554 Record: &lexutil.LexiconTypeDecoder{
555 Val: &tangled.GraphFollow{
556 Subject: subjectIdent.DID.String(),
557 CreatedAt: createdAt,
558 }},
559 })
560
561 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String())
562 if err != nil {
563 log.Println("failed to follow", err)
564 return
565 }
566
567 log.Println("created atproto record: ", resp.Uri)
568
569 return
570}
571
572func (s *State) Router() http.Handler {
573 router := chi.NewRouter()
574
575 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
576 pat := chi.URLParam(r, "*")
577 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
578 s.UserRouter().ServeHTTP(w, r)
579 } else {
580 s.StandardRouter().ServeHTTP(w, r)
581 }
582 })
583
584 return router
585}
586
587func (s *State) UserRouter() http.Handler {
588 r := chi.NewRouter()
589
590 // strip @ from user
591 r.Use(StripLeadingAt)
592
593 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
594 r.Get("/", s.ProfilePage)
595 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
596 r.Get("/", s.RepoIndex)
597 r.Get("/log/{ref}", s.RepoLog)
598 r.Route("/tree/{ref}", func(r chi.Router) {
599 r.Get("/*", s.RepoTree)
600 })
601 r.Get("/commit/{ref}", s.RepoCommit)
602 r.Get("/branches", s.RepoBranches)
603 r.Get("/tags", s.RepoTags)
604 r.Get("/blob/{ref}/*", s.RepoBlob)
605
606 // These routes get proxied to the knot
607 r.Get("/info/refs", s.InfoRefs)
608 r.Post("/git-upload-pack", s.UploadPack)
609
610 // settings routes, needs auth
611 r.Group(func(r chi.Router) {
612 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
613 r.Get("/", s.RepoSettings)
614 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
615 })
616 })
617 })
618 })
619
620 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
621 s.pages.Error404(w)
622 })
623
624 return r
625}
626
627func (s *State) StandardRouter() http.Handler {
628 r := chi.NewRouter()
629
630 r.Handle("/static/*", s.pages.Static())
631
632 r.Get("/", s.Timeline)
633
634 r.Get("/login", s.Login)
635 r.Post("/login", s.Login)
636
637 r.Route("/knots", func(r chi.Router) {
638 r.Use(AuthMiddleware(s))
639 r.Get("/", s.Knots)
640 r.Post("/key", s.RegistrationKey)
641
642 r.Route("/{domain}", func(r chi.Router) {
643 r.Post("/init", s.InitKnotServer)
644 r.Get("/", s.KnotServerInfo)
645 r.Route("/member", func(r chi.Router) {
646 r.Use(RoleMiddleware(s, "server:owner"))
647 r.Get("/", s.ListMembers)
648 r.Put("/", s.AddMember)
649 r.Delete("/", s.RemoveMember)
650 })
651 })
652 })
653
654 r.Route("/repo", func(r chi.Router) {
655 r.Route("/new", func(r chi.Router) {
656 r.Get("/", s.AddRepo)
657 r.Post("/", s.AddRepo)
658 })
659 // r.Post("/import", s.ImportRepo)
660 })
661
662 r.With(AuthMiddleware(s)).Put("/follow", s.Follow)
663
664 r.Route("/settings", func(r chi.Router) {
665 r.Use(AuthMiddleware(s))
666 r.Get("/", s.Settings)
667 r.Put("/keys", s.SettingsKeys)
668 })
669
670 r.Get("/keys/{user}", s.Keys)
671
672 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
673 s.pages.Error404(w)
674 })
675 return r
676}