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