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