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