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