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