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