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