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