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