this repo has no description
1package state 2 3import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "log" 11 "net/http" 12 "strings" 13 "time" 14 15 comatproto "github.com/bluesky-social/indigo/api/atproto" 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 lexutil "github.com/bluesky-social/indigo/lex/util" 18 "github.com/bluesky-social/jetstream/pkg/models" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 tangled "github.com/sotangled/tangled/api/tangled" 22 "github.com/sotangled/tangled/appview" 23 "github.com/sotangled/tangled/appview/auth" 24 "github.com/sotangled/tangled/appview/db" 25 "github.com/sotangled/tangled/appview/pages" 26 "github.com/sotangled/tangled/jetstream" 27 "github.com/sotangled/tangled/rbac" 28) 29 30type State struct { 31 db *db.DB 32 auth *auth.Auth 33 enforcer *rbac.Enforcer 34 tidClock *syntax.TIDClock 35 pages *pages.Pages 36 resolver *appview.Resolver 37 jc *jetstream.JetstreamClient 38} 39 40func Make() (*State, error) { 41 db, err := db.Make(appview.SqliteDbPath) 42 if err != nil { 43 return nil, err 44 } 45 46 auth, err := auth.Make() 47 if err != nil { 48 return nil, err 49 } 50 51 enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath) 52 if err != nil { 53 return nil, err 54 } 55 56 clock := syntax.NewTIDClock(0) 57 58 pgs := pages.NewPages() 59 60 resolver := appview.NewResolver() 61 62 jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, db, false) 63 if err != nil { 64 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 65 } 66 err = jc.StartJetstream(context.Background(), func(ctx context.Context, e *models.Event) error { 67 if e.Kind != models.EventKindCommit { 68 return nil 69 } 70 71 did := e.Did 72 fmt.Println("got event", e.Commit.Collection, e.Commit.RKey, e.Commit.Record) 73 raw := json.RawMessage(e.Commit.Record) 74 75 switch e.Commit.Collection { 76 case tangled.GraphFollowNSID: 77 record := tangled.GraphFollow{} 78 err := json.Unmarshal(raw, &record) 79 if err != nil { 80 return err 81 } 82 err = db.AddFollow(did, record.Subject, e.Commit.RKey) 83 if err != nil { 84 return fmt.Errorf("failed to add follow to db: %w", err) 85 } 86 } 87 88 return nil 89 }) 90 if err != nil { 91 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 92 } 93 94 state := &State{ 95 db, 96 auth, 97 enforcer, 98 clock, 99 pgs, 100 resolver, 101 jc, 102 } 103 104 return state, nil 105} 106 107func (s *State) TID() string { 108 return s.tidClock.Next().String() 109} 110 111func (s *State) Login(w http.ResponseWriter, r *http.Request) { 112 ctx := r.Context() 113 114 switch r.Method { 115 case http.MethodGet: 116 err := s.pages.Login(w, pages.LoginParams{}) 117 if err != nil { 118 log.Printf("rendering login page: %s", err) 119 } 120 return 121 case http.MethodPost: 122 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 123 appPassword := r.FormValue("app_password") 124 125 resolved, err := s.resolver.ResolveIdent(ctx, handle) 126 if err != nil { 127 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 128 return 129 } 130 131 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 132 if err != nil { 133 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 134 return 135 } 136 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 137 138 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 139 if err != nil { 140 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 141 return 142 } 143 144 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 145 s.pages.HxRedirect(w, "/") 146 return 147 } 148} 149 150func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 151 user := s.auth.GetUser(r) 152 s.pages.Timeline(w, pages.TimelineParams{ 153 LoggedInUser: user, 154 }) 155 return 156} 157 158// requires auth 159func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 160 switch r.Method { 161 case http.MethodGet: 162 // list open registrations under this did 163 164 return 165 case http.MethodPost: 166 session, err := s.auth.Store.Get(r, appview.SessionName) 167 if err != nil || session.IsNew { 168 log.Println("unauthorized attempt to generate registration key") 169 http.Error(w, "Forbidden", http.StatusUnauthorized) 170 return 171 } 172 173 did := session.Values[appview.SessionDid].(string) 174 175 // check if domain is valid url, and strip extra bits down to just host 176 domain := r.FormValue("domain") 177 if domain == "" { 178 http.Error(w, "Invalid form", http.StatusBadRequest) 179 return 180 } 181 182 key, err := s.db.GenerateRegistrationKey(domain, did) 183 184 if err != nil { 185 log.Println(err) 186 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 187 return 188 } 189 190 w.Write([]byte(key)) 191 } 192} 193 194func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 195 user := chi.URLParam(r, "user") 196 user = strings.TrimPrefix(user, "@") 197 198 if user == "" { 199 w.WriteHeader(http.StatusBadRequest) 200 return 201 } 202 203 id, err := s.resolver.ResolveIdent(r.Context(), user) 204 if err != nil { 205 w.WriteHeader(http.StatusInternalServerError) 206 return 207 } 208 209 pubKeys, err := s.db.GetPublicKeys(id.DID.String()) 210 if err != nil { 211 w.WriteHeader(http.StatusNotFound) 212 return 213 } 214 215 if len(pubKeys) == 0 { 216 w.WriteHeader(http.StatusNotFound) 217 return 218 } 219 220 for _, k := range pubKeys { 221 key := strings.TrimRight(k.Key, "\n") 222 w.Write([]byte(fmt.Sprintln(key))) 223 } 224} 225 226// create a signed request and check if a node responds to that 227func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 228 user := s.auth.GetUser(r) 229 230 domain := chi.URLParam(r, "domain") 231 if domain == "" { 232 http.Error(w, "malformed url", http.StatusBadRequest) 233 return 234 } 235 log.Println("checking ", domain) 236 237 secret, err := s.db.GetRegistrationKey(domain) 238 if err != nil { 239 log.Printf("no key found for domain %s: %s\n", domain, err) 240 return 241 } 242 243 client, err := NewSignedClient(domain, secret) 244 if err != nil { 245 log.Println("failed to create client to ", domain) 246 } 247 248 resp, err := client.Init(user.Did) 249 if err != nil { 250 w.Write([]byte("no dice")) 251 log.Println("domain was unreachable after 5 seconds") 252 return 253 } 254 255 if resp.StatusCode == http.StatusConflict { 256 log.Println("status conflict", resp.StatusCode) 257 w.Write([]byte("already registered, sorry!")) 258 return 259 } 260 261 if resp.StatusCode != http.StatusNoContent { 262 log.Println("status nok", resp.StatusCode) 263 w.Write([]byte("no dice")) 264 return 265 } 266 267 // verify response mac 268 signature := resp.Header.Get("X-Signature") 269 signatureBytes, err := hex.DecodeString(signature) 270 if err != nil { 271 return 272 } 273 274 expectedMac := hmac.New(sha256.New, []byte(secret)) 275 expectedMac.Write([]byte("ok")) 276 277 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 278 log.Printf("response body signature mismatch: %x\n", signatureBytes) 279 return 280 } 281 282 // mark as registered 283 err = s.db.Register(domain) 284 if err != nil { 285 log.Println("failed to register domain", err) 286 http.Error(w, err.Error(), http.StatusInternalServerError) 287 return 288 } 289 290 // set permissions for this did as owner 291 reg, err := s.db.RegistrationByDomain(domain) 292 if err != nil { 293 log.Println("failed to register domain", err) 294 http.Error(w, err.Error(), http.StatusInternalServerError) 295 return 296 } 297 298 // add basic acls for this domain 299 err = s.enforcer.AddDomain(domain) 300 if err != nil { 301 log.Println("failed to setup owner of domain", err) 302 http.Error(w, err.Error(), http.StatusInternalServerError) 303 return 304 } 305 306 // add this did as owner of this domain 307 err = s.enforcer.AddOwner(domain, reg.ByDid) 308 if err != nil { 309 log.Println("failed to setup owner of domain", err) 310 http.Error(w, err.Error(), http.StatusInternalServerError) 311 return 312 } 313 314 w.Write([]byte("check success")) 315} 316 317func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 318 domain := chi.URLParam(r, "domain") 319 if domain == "" { 320 http.Error(w, "malformed url", http.StatusBadRequest) 321 return 322 } 323 324 user := s.auth.GetUser(r) 325 reg, err := s.db.RegistrationByDomain(domain) 326 if err != nil { 327 w.Write([]byte("failed to pull up registration info")) 328 return 329 } 330 331 var members []string 332 if reg.Registered != nil { 333 members, err = s.enforcer.GetUserByRole("server:member", domain) 334 if err != nil { 335 w.Write([]byte("failed to fetch member list")) 336 return 337 } 338 } 339 340 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 341 isOwner := err == nil && ok 342 343 p := pages.KnotParams{ 344 LoggedInUser: user, 345 Registration: reg, 346 Members: members, 347 IsOwner: isOwner, 348 } 349 350 s.pages.Knot(w, p) 351} 352 353// get knots registered by this user 354func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 355 // for now, this is just pubkeys 356 user := s.auth.GetUser(r) 357 registrations, err := s.db.RegistrationsByDid(user.Did) 358 if err != nil { 359 log.Println(err) 360 } 361 362 s.pages.Knots(w, pages.KnotsParams{ 363 LoggedInUser: user, 364 Registrations: registrations, 365 }) 366} 367 368// list members of domain, requires auth and requires owner status 369func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 370 domain := chi.URLParam(r, "domain") 371 if domain == "" { 372 http.Error(w, "malformed url", http.StatusBadRequest) 373 return 374 } 375 376 // list all members for this domain 377 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 378 if err != nil { 379 w.Write([]byte("failed to fetch member list")) 380 return 381 } 382 383 w.Write([]byte(strings.Join(memberDids, "\n"))) 384 return 385} 386 387// add member to domain, requires auth and requires invite access 388func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 389 domain := chi.URLParam(r, "domain") 390 if domain == "" { 391 http.Error(w, "malformed url", http.StatusBadRequest) 392 return 393 } 394 395 memberDid := r.FormValue("member") 396 if memberDid == "" { 397 http.Error(w, "malformed form", http.StatusBadRequest) 398 return 399 } 400 401 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 402 if err != nil { 403 w.Write([]byte("failed to resolve member did to a handle")) 404 return 405 } 406 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 407 408 // announce this relation into the firehose, store into owners' pds 409 client, _ := s.auth.AuthorizedClient(r) 410 currentUser := s.auth.GetUser(r) 411 addedAt := time.Now().Format(time.RFC3339) 412 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 413 Collection: tangled.KnotMemberNSID, 414 Repo: currentUser.Did, 415 Rkey: s.TID(), 416 Record: &lexutil.LexiconTypeDecoder{ 417 Val: &tangled.KnotMember{ 418 Member: memberIdent.DID.String(), 419 Domain: domain, 420 AddedAt: &addedAt, 421 }}, 422 }) 423 424 // invalid record 425 if err != nil { 426 log.Printf("failed to create record: %s", err) 427 return 428 } 429 log.Println("created atproto record: ", resp.Uri) 430 431 secret, err := s.db.GetRegistrationKey(domain) 432 if err != nil { 433 log.Printf("no key found for domain %s: %s\n", domain, err) 434 return 435 } 436 437 ksClient, err := NewSignedClient(domain, secret) 438 if err != nil { 439 log.Println("failed to create client to ", domain) 440 return 441 } 442 443 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 444 if err != nil { 445 log.Printf("failed to make request to %s: %s", domain, err) 446 return 447 } 448 449 if ksResp.StatusCode != http.StatusNoContent { 450 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 451 return 452 } 453 454 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 455 if err != nil { 456 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 457 return 458 } 459 460 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 461} 462 463func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 464} 465 466func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 467 switch r.Method { 468 case http.MethodGet: 469 user := s.auth.GetUser(r) 470 knots, err := s.enforcer.GetDomainsForUser(user.Did) 471 472 if err != nil { 473 s.pages.Notice(w, "repo", "Invalid user account.") 474 return 475 } 476 477 s.pages.NewRepo(w, pages.NewRepoParams{ 478 LoggedInUser: user, 479 Knots: knots, 480 }) 481 case http.MethodPost: 482 user := s.auth.GetUser(r) 483 484 domain := r.FormValue("domain") 485 if domain == "" { 486 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 487 return 488 } 489 490 repoName := r.FormValue("name") 491 if repoName == "" { 492 s.pages.Notice(w, "repo", "Invalid repo name.") 493 return 494 } 495 496 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 497 if err != nil || !ok { 498 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 499 return 500 } 501 502 secret, err := s.db.GetRegistrationKey(domain) 503 if err != nil { 504 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 505 return 506 } 507 508 client, err := NewSignedClient(domain, secret) 509 if err != nil { 510 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 511 return 512 } 513 514 resp, err := client.NewRepo(user.Did, repoName) 515 if err != nil { 516 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 517 return 518 } 519 if resp.StatusCode != http.StatusNoContent { 520 s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode)) 521 return 522 } 523 524 // add to local db 525 repo := &db.Repo{ 526 Did: user.Did, 527 Name: repoName, 528 Knot: domain, 529 } 530 err = s.db.AddRepo(repo) 531 if err != nil { 532 s.pages.Notice(w, "repo", "Failed to save repository information.") 533 return 534 } 535 536 // acls 537 p, _ := securejoin.SecureJoin(domain, repoName) 538 err = s.enforcer.AddRepo(user.Did, domain, p) 539 if err != nil { 540 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 541 return 542 } 543 544 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 545 return 546 } 547} 548 549func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 550 didOrHandle := chi.URLParam(r, "user") 551 if didOrHandle == "" { 552 http.Error(w, "Bad request", http.StatusBadRequest) 553 return 554 } 555 556 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 557 if err != nil { 558 log.Printf("resolving identity: %s", err) 559 w.WriteHeader(http.StatusNotFound) 560 return 561 } 562 563 repos, err := s.db.GetAllReposByDid(ident.DID.String()) 564 if err != nil { 565 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 566 } 567 568 s.pages.ProfilePage(w, pages.ProfilePageParams{ 569 LoggedInUser: s.auth.GetUser(r), 570 UserDid: ident.DID.String(), 571 UserHandle: ident.Handle.String(), 572 Repos: repos, 573 }) 574} 575 576func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 577 currentUser := s.auth.GetUser(r) 578 579 subject := r.URL.Query().Get("subject") 580 if subject == "" { 581 log.Println("invalid form") 582 return 583 } 584 585 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject) 586 if err != nil { 587 log.Println("failed to follow, invalid did") 588 } 589 590 if currentUser.Did == subjectIdent.DID.String() { 591 log.Println("cant follow or unfollow yourself") 592 return 593 } 594 595 client, _ := s.auth.AuthorizedClient(r) 596 597 switch r.Method { 598 case http.MethodPost: 599 createdAt := time.Now().Format(time.RFC3339) 600 rkey := s.TID() 601 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 602 Collection: tangled.GraphFollowNSID, 603 Repo: currentUser.Did, 604 Rkey: rkey, 605 Record: &lexutil.LexiconTypeDecoder{ 606 Val: &tangled.GraphFollow{ 607 Subject: subjectIdent.DID.String(), 608 CreatedAt: createdAt, 609 }}, 610 }) 611 if err != nil { 612 log.Println("failed to create atproto record", err) 613 return 614 } 615 616 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey) 617 if err != nil { 618 log.Println("failed to follow", err) 619 return 620 } 621 622 log.Println("created atproto record: ", resp.Uri) 623 624 return 625 case http.MethodDelete: 626 // find the record in the db 627 628 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String()) 629 if err != nil { 630 log.Println("failed to get follow relationship") 631 return 632 } 633 634 resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 635 Collection: tangled.GraphFollowNSID, 636 Repo: currentUser.Did, 637 Rkey: follow.RKey, 638 }) 639 640 log.Println(resp.Commit.Cid) 641 642 if err != nil { 643 log.Println("failed to unfollow") 644 return 645 } 646 647 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String()) 648 if err != nil { 649 log.Println("failed to delete follow from DB") 650 // this is not an issue, the firehose event might have already done this 651 } 652 653 w.WriteHeader(http.StatusNoContent) 654 return 655 } 656 657} 658 659func (s *State) Router() http.Handler { 660 router := chi.NewRouter() 661 662 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 663 pat := chi.URLParam(r, "*") 664 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 665 s.UserRouter().ServeHTTP(w, r) 666 } else { 667 s.StandardRouter().ServeHTTP(w, r) 668 } 669 }) 670 671 return router 672} 673 674func (s *State) UserRouter() http.Handler { 675 r := chi.NewRouter() 676 677 // strip @ from user 678 r.Use(StripLeadingAt) 679 680 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 681 r.Get("/", s.ProfilePage) 682 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 683 r.Get("/", s.RepoIndex) 684 r.Get("/log/{ref}", s.RepoLog) 685 r.Route("/tree/{ref}", func(r chi.Router) { 686 r.Get("/*", s.RepoTree) 687 }) 688 r.Get("/commit/{ref}", s.RepoCommit) 689 r.Get("/branches", s.RepoBranches) 690 r.Get("/tags", s.RepoTags) 691 r.Get("/blob/{ref}/*", s.RepoBlob) 692 693 // These routes get proxied to the knot 694 r.Get("/info/refs", s.InfoRefs) 695 r.Post("/git-upload-pack", s.UploadPack) 696 697 // settings routes, needs auth 698 r.Group(func(r chi.Router) { 699 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 700 r.Get("/", s.RepoSettings) 701 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 702 }) 703 }) 704 }) 705 }) 706 707 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 708 s.pages.Error404(w) 709 }) 710 711 return r 712} 713 714func (s *State) StandardRouter() http.Handler { 715 r := chi.NewRouter() 716 717 r.Handle("/static/*", s.pages.Static()) 718 719 r.Get("/", s.Timeline) 720 721 r.Get("/login", s.Login) 722 r.Post("/login", s.Login) 723 724 r.Route("/knots", func(r chi.Router) { 725 r.Use(AuthMiddleware(s)) 726 r.Get("/", s.Knots) 727 r.Post("/key", s.RegistrationKey) 728 729 r.Route("/{domain}", func(r chi.Router) { 730 r.Post("/init", s.InitKnotServer) 731 r.Get("/", s.KnotServerInfo) 732 r.Route("/member", func(r chi.Router) { 733 r.Use(RoleMiddleware(s, "server:owner")) 734 r.Get("/", s.ListMembers) 735 r.Put("/", s.AddMember) 736 r.Delete("/", s.RemoveMember) 737 }) 738 }) 739 }) 740 741 r.Route("/repo", func(r chi.Router) { 742 r.Route("/new", func(r chi.Router) { 743 r.Get("/", s.AddRepo) 744 r.Post("/", s.AddRepo) 745 }) 746 // r.Post("/import", s.ImportRepo) 747 }) 748 749 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 750 r.Post("/", s.Follow) 751 r.Delete("/", s.Follow) 752 }) 753 754 r.Route("/settings", func(r chi.Router) { 755 r.Use(AuthMiddleware(s)) 756 r.Get("/", s.Settings) 757 r.Put("/keys", s.SettingsKeys) 758 }) 759 760 r.Get("/keys/{user}", s.Keys) 761 762 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 763 s.pages.Error404(w) 764 }) 765 return r 766}