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 log.Println("invalid user?", err)
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 log.Println("invalid form")
448 return
449 }
450
451 repoName := r.FormValue("name")
452 if repoName == "" {
453 log.Println("invalid form")
454 return
455 }
456
457 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
458 if err != nil || !ok {
459 w.Write([]byte("domain inaccessible to you"))
460 return
461 }
462
463 secret, err := s.db.GetRegistrationKey(domain)
464 if err != nil {
465 log.Printf("no key found for domain %s: %s\n", domain, err)
466 return
467 }
468
469 client, err := NewSignedClient(domain, secret)
470 if err != nil {
471 log.Println("failed to create client to ", domain)
472 }
473
474 resp, err := client.NewRepo(user.Did, repoName)
475 if err != nil {
476 log.Println("failed to send create repo request", err)
477 return
478 }
479 if resp.StatusCode != http.StatusNoContent {
480 log.Println("server returned ", resp.StatusCode)
481 return
482 }
483
484 // add to local db
485 repo := &db.Repo{
486 Did: user.Did,
487 Name: repoName,
488 Knot: domain,
489 }
490 err = s.db.AddRepo(repo)
491 if err != nil {
492 log.Println("failed to add repo to db", err)
493 return
494 }
495
496 // acls
497 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
498 if err != nil {
499 log.Println("failed to set up acls", err)
500 return
501 }
502
503 w.Write([]byte("created!"))
504 }
505}
506
507func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
508 didOrHandle := chi.URLParam(r, "user")
509 if didOrHandle == "" {
510 http.Error(w, "Bad request", http.StatusBadRequest)
511 return
512 }
513
514 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
515 if err != nil {
516 log.Printf("resolving identity: %s", err)
517 w.WriteHeader(http.StatusNotFound)
518 return
519 }
520
521 repos, err := s.db.GetAllReposByDid(ident.DID.String())
522 if err != nil {
523 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
524 }
525
526 s.pages.ProfilePage(w, pages.ProfilePageParams{
527 LoggedInUser: s.auth.GetUser(r),
528 UserDid: ident.DID.String(),
529 UserHandle: ident.Handle.String(),
530 Repos: repos,
531 })
532}
533
534func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
535 subject := r.FormValue("subject")
536
537 if subject == "" {
538 log.Println("invalid form")
539 return
540 }
541
542 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
543 currentUser := s.auth.GetUser(r)
544
545 client, _ := s.auth.AuthorizedClient(r)
546 createdAt := time.Now().Format(time.RFC3339)
547 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
548 Collection: tangled.GraphFollowNSID,
549 Repo: currentUser.Did,
550 Rkey: s.TID(),
551 Record: &lexutil.LexiconTypeDecoder{
552 Val: &tangled.GraphFollow{
553 Subject: subjectIdent.DID.String(),
554 CreatedAt: createdAt,
555 }},
556 })
557
558 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String())
559 if err != nil {
560 log.Println("failed to follow", err)
561 return
562 }
563
564 // invalid record
565 if err != nil {
566 log.Printf("failed to create record: %s", err)
567 return
568 }
569 log.Println("created atproto record: ", resp.Uri)
570
571 return
572}
573
574func (s *State) Router() http.Handler {
575 router := chi.NewRouter()
576
577 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
578 pat := chi.URLParam(r, "*")
579 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
580 s.UserRouter().ServeHTTP(w, r)
581 } else {
582 s.StandardRouter().ServeHTTP(w, r)
583 }
584 })
585
586 return router
587}
588
589func (s *State) UserRouter() http.Handler {
590 r := chi.NewRouter()
591
592 // strip @ from user
593 r.Use(StripLeadingAt)
594
595 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
596 r.Get("/", s.ProfilePage)
597 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
598 r.Get("/", s.RepoIndex)
599 r.Get("/log/{ref}", s.RepoLog)
600 r.Route("/tree/{ref}", func(r chi.Router) {
601 r.Get("/*", s.RepoTree)
602 })
603 r.Get("/commit/{ref}", s.RepoCommit)
604 r.Get("/branches", s.RepoBranches)
605 r.Get("/tags", s.RepoTags)
606 r.Get("/blob/{ref}/*", s.RepoBlob)
607
608 // These routes get proxied to the knot
609 r.Get("/info/refs", s.InfoRefs)
610 r.Post("/git-upload-pack", s.UploadPack)
611
612 })
613 })
614
615 return r
616}
617
618func (s *State) StandardRouter() http.Handler {
619 r := chi.NewRouter()
620
621 r.Handle("/static/*", s.pages.Static())
622
623 r.Get("/", s.Timeline)
624
625 r.Get("/login", s.Login)
626 r.Post("/login", s.Login)
627
628 r.Route("/knots", func(r chi.Router) {
629 r.Use(AuthMiddleware(s))
630 r.Get("/", s.Knots)
631 r.Post("/key", s.RegistrationKey)
632
633 r.Route("/{domain}", func(r chi.Router) {
634 r.Post("/init", s.InitKnotServer)
635 r.Get("/", s.KnotServerInfo)
636 r.Route("/member", func(r chi.Router) {
637 r.Use(RoleMiddleware(s, "server:owner"))
638 r.Get("/", s.ListMembers)
639 r.Put("/", s.AddMember)
640 r.Delete("/", s.RemoveMember)
641 })
642 })
643 })
644
645 r.Route("/repo", func(r chi.Router) {
646 r.Route("/new", func(r chi.Router) {
647 r.Get("/", s.AddRepo)
648 r.Post("/", s.AddRepo)
649 })
650 // r.Post("/import", s.ImportRepo)
651 })
652
653 r.With(AuthMiddleware(s)).Put("/follow", s.Follow)
654
655 r.Route("/settings", func(r chi.Router) {
656 r.Use(AuthMiddleware(s))
657 r.Get("/", s.Settings)
658 r.Put("/keys", s.SettingsKeys)
659 })
660
661 r.Get("/keys/{user}", s.Keys)
662
663 return r
664}