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