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