forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state 2 3import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 "log" 9 "net/http" 10 "path/filepath" 11 "strings" 12 "time" 13 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 lexutil "github.com/bluesky-social/indigo/lex/util" 17 "github.com/go-chi/chi/v5" 18 tangled "github.com/sotangled/tangled/api/tangled" 19 "github.com/sotangled/tangled/appview" 20 "github.com/sotangled/tangled/appview/auth" 21 "github.com/sotangled/tangled/appview/db" 22 "github.com/sotangled/tangled/appview/pages" 23 "github.com/sotangled/tangled/rbac" 24) 25 26type State struct { 27 db *db.DB 28 auth *auth.Auth 29 enforcer *rbac.Enforcer 30 tidClock *syntax.TIDClock 31 pages *pages.Pages 32 resolver *appview.Resolver 33} 34 35func Make() (*State, error) { 36 db, err := db.Make(appview.SqliteDbPath) 37 if err != nil { 38 return nil, err 39 } 40 41 auth, err := auth.Make() 42 if err != nil { 43 return nil, err 44 } 45 46 enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath) 47 if err != nil { 48 return nil, err 49 } 50 51 clock := syntax.NewTIDClock(0) 52 53 pgs := pages.NewPages() 54 55 resolver := appview.NewResolver() 56 57 state := &State{ 58 db, 59 auth, enforcer, clock, pgs, resolver, 60 } 61 62 return state, nil 63} 64 65func (s *State) TID() string { 66 return s.tidClock.Next().String() 67} 68 69func (s *State) Login(w http.ResponseWriter, r *http.Request) { 70 ctx := r.Context() 71 72 switch r.Method { 73 case http.MethodGet: 74 err := s.pages.Login(w, pages.LoginParams{}) 75 if err != nil { 76 log.Printf("rendering login page: %s", err) 77 } 78 return 79 case http.MethodPost: 80 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 81 appPassword := r.FormValue("app_password") 82 83 resolved, err := s.resolver.ResolveIdent(ctx, handle) 84 if err != nil { 85 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 86 return 87 } 88 89 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 90 if err != nil { 91 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 92 return 93 } 94 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 95 96 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 97 if err != nil { 98 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 99 return 100 } 101 102 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 103 s.pages.HxRedirect(w, "/") 104 return 105 } 106} 107 108func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 109 user := s.auth.GetUser(r) 110 s.pages.Timeline(w, pages.TimelineParams{ 111 LoggedInUser: user, 112 }) 113 return 114} 115 116// requires auth 117func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 118 switch r.Method { 119 case http.MethodGet: 120 // list open registrations under this did 121 122 return 123 case http.MethodPost: 124 session, err := s.auth.Store.Get(r, appview.SessionName) 125 if err != nil || session.IsNew { 126 log.Println("unauthorized attempt to generate registration key") 127 http.Error(w, "Forbidden", http.StatusUnauthorized) 128 return 129 } 130 131 did := session.Values[appview.SessionDid].(string) 132 133 // check if domain is valid url, and strip extra bits down to just host 134 domain := r.FormValue("domain") 135 if domain == "" { 136 http.Error(w, "Invalid form", http.StatusBadRequest) 137 return 138 } 139 140 key, err := s.db.GenerateRegistrationKey(domain, did) 141 142 if err != nil { 143 log.Println(err) 144 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 145 return 146 } 147 148 w.Write([]byte(key)) 149 } 150} 151 152func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 153 user := chi.URLParam(r, "user") 154 user = strings.TrimPrefix(user, "@") 155 156 if user == "" { 157 w.WriteHeader(http.StatusBadRequest) 158 return 159 } 160 161 id, err := s.resolver.ResolveIdent(r.Context(), user) 162 if err != nil { 163 w.WriteHeader(http.StatusInternalServerError) 164 return 165 } 166 167 pubKeys, err := s.db.GetPublicKeys(id.DID.String()) 168 if err != nil { 169 w.WriteHeader(http.StatusNotFound) 170 return 171 } 172 173 if len(pubKeys) == 0 { 174 w.WriteHeader(http.StatusNotFound) 175 return 176 } 177 178 for _, k := range pubKeys { 179 key := strings.TrimRight(k.Key, "\n") 180 w.Write([]byte(fmt.Sprintln(key))) 181 } 182} 183 184// create a signed request and check if a node responds to that 185func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 186 user := s.auth.GetUser(r) 187 188 domain := chi.URLParam(r, "domain") 189 if domain == "" { 190 http.Error(w, "malformed url", http.StatusBadRequest) 191 return 192 } 193 log.Println("checking ", domain) 194 195 secret, err := s.db.GetRegistrationKey(domain) 196 if err != nil { 197 log.Printf("no key found for domain %s: %s\n", domain, err) 198 return 199 } 200 201 client, err := NewSignedClient(domain, secret) 202 if err != nil { 203 log.Println("failed to create client to ", domain) 204 } 205 206 resp, err := client.Init(user.Did) 207 if err != nil { 208 w.Write([]byte("no dice")) 209 log.Println("domain was unreachable after 5 seconds") 210 return 211 } 212 213 if resp.StatusCode == http.StatusConflict { 214 log.Println("status conflict", resp.StatusCode) 215 w.Write([]byte("already registered, sorry!")) 216 return 217 } 218 219 if resp.StatusCode != http.StatusNoContent { 220 log.Println("status nok", resp.StatusCode) 221 w.Write([]byte("no dice")) 222 return 223 } 224 225 // verify response mac 226 signature := resp.Header.Get("X-Signature") 227 signatureBytes, err := hex.DecodeString(signature) 228 if err != nil { 229 return 230 } 231 232 expectedMac := hmac.New(sha256.New, []byte(secret)) 233 expectedMac.Write([]byte("ok")) 234 235 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 236 log.Printf("response body signature mismatch: %x\n", signatureBytes) 237 return 238 } 239 240 // mark as registered 241 err = s.db.Register(domain) 242 if err != nil { 243 log.Println("failed to register domain", err) 244 http.Error(w, err.Error(), http.StatusInternalServerError) 245 return 246 } 247 248 // set permissions for this did as owner 249 reg, err := s.db.RegistrationByDomain(domain) 250 if err != nil { 251 log.Println("failed to register domain", err) 252 http.Error(w, err.Error(), http.StatusInternalServerError) 253 return 254 } 255 256 // add basic acls for this domain 257 err = s.enforcer.AddDomain(domain) 258 if err != nil { 259 log.Println("failed to setup owner of domain", err) 260 http.Error(w, err.Error(), http.StatusInternalServerError) 261 return 262 } 263 264 // add this did as owner of this domain 265 err = s.enforcer.AddOwner(domain, reg.ByDid) 266 if err != nil { 267 log.Println("failed to setup owner of domain", err) 268 http.Error(w, err.Error(), http.StatusInternalServerError) 269 return 270 } 271 272 w.Write([]byte("check success")) 273} 274 275func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 276 domain := chi.URLParam(r, "domain") 277 if domain == "" { 278 http.Error(w, "malformed url", http.StatusBadRequest) 279 return 280 } 281 282 user := s.auth.GetUser(r) 283 reg, err := s.db.RegistrationByDomain(domain) 284 if err != nil { 285 w.Write([]byte("failed to pull up registration info")) 286 return 287 } 288 289 var members []string 290 if reg.Registered != nil { 291 members, err = s.enforcer.GetUserByRole("server:member", domain) 292 if err != nil { 293 w.Write([]byte("failed to fetch member list")) 294 return 295 } 296 } 297 298 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 299 isOwner := err == nil && ok 300 301 p := pages.KnotParams{ 302 LoggedInUser: user, 303 Registration: reg, 304 Members: members, 305 IsOwner: isOwner, 306 } 307 308 s.pages.Knot(w, p) 309} 310 311// get knots registered by this user 312func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 313 // for now, this is just pubkeys 314 user := s.auth.GetUser(r) 315 registrations, err := s.db.RegistrationsByDid(user.Did) 316 if err != nil { 317 log.Println(err) 318 } 319 320 s.pages.Knots(w, pages.KnotsParams{ 321 LoggedInUser: user, 322 Registrations: registrations, 323 }) 324} 325 326// list members of domain, requires auth and requires owner status 327func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 328 domain := chi.URLParam(r, "domain") 329 if domain == "" { 330 http.Error(w, "malformed url", http.StatusBadRequest) 331 return 332 } 333 334 // list all members for this domain 335 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 336 if err != nil { 337 w.Write([]byte("failed to fetch member list")) 338 return 339 } 340 341 w.Write([]byte(strings.Join(memberDids, "\n"))) 342 return 343} 344 345// add member to domain, requires auth and requires invite access 346func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 347 domain := chi.URLParam(r, "domain") 348 if domain == "" { 349 http.Error(w, "malformed url", http.StatusBadRequest) 350 return 351 } 352 353 memberDid := r.FormValue("member") 354 if memberDid == "" { 355 http.Error(w, "malformed form", http.StatusBadRequest) 356 return 357 } 358 359 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 360 if err != nil { 361 w.Write([]byte("failed to resolve member did to a handle")) 362 return 363 } 364 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 365 366 // announce this relation into the firehose, store into owners' pds 367 client, _ := s.auth.AuthorizedClient(r) 368 currentUser := s.auth.GetUser(r) 369 addedAt := time.Now().Format(time.RFC3339) 370 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 371 Collection: tangled.KnotMemberNSID, 372 Repo: currentUser.Did, 373 Rkey: s.TID(), 374 Record: &lexutil.LexiconTypeDecoder{ 375 Val: &tangled.KnotMember{ 376 Member: memberIdent.DID.String(), 377 Domain: domain, 378 AddedAt: &addedAt, 379 }}, 380 }) 381 382 // invalid record 383 if err != nil { 384 log.Printf("failed to create record: %s", err) 385 return 386 } 387 log.Println("created atproto record: ", resp.Uri) 388 389 secret, err := s.db.GetRegistrationKey(domain) 390 if err != nil { 391 log.Printf("no key found for domain %s: %s\n", domain, err) 392 return 393 } 394 395 ksClient, err := NewSignedClient(domain, secret) 396 if err != nil { 397 log.Println("failed to create client to ", domain) 398 return 399 } 400 401 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 402 if err != nil { 403 log.Printf("failed to make request to %s: %s", domain, err) 404 return 405 } 406 407 if ksResp.StatusCode != http.StatusNoContent { 408 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 409 return 410 } 411 412 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 413 if err != nil { 414 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 415 return 416 } 417 418 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 419} 420 421func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 422} 423 424func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 425 switch r.Method { 426 case http.MethodGet: 427 user := s.auth.GetUser(r) 428 knots, err := s.enforcer.GetDomainsForUser(user.Did) 429 430 if err != nil { 431 s.pages.Notice(w, "repo", "Invalid user account.") 432 return 433 } 434 435 s.pages.NewRepo(w, pages.NewRepoParams{ 436 LoggedInUser: user, 437 Knots: knots, 438 }) 439 case http.MethodPost: 440 user := s.auth.GetUser(r) 441 442 domain := r.FormValue("domain") 443 if domain == "" { 444 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 445 return 446 } 447 448 repoName := r.FormValue("name") 449 if repoName == "" { 450 s.pages.Notice(w, "repo", "Invalid repo name.") 451 return 452 } 453 454 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 455 if err != nil || !ok { 456 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 457 return 458 } 459 460 secret, err := s.db.GetRegistrationKey(domain) 461 if err != nil { 462 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 463 return 464 } 465 466 client, err := NewSignedClient(domain, secret) 467 if err != nil { 468 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 469 return 470 } 471 472 resp, err := client.NewRepo(user.Did, repoName) 473 if err != nil { 474 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 475 return 476 } 477 if resp.StatusCode != http.StatusNoContent { 478 s.pages.Notice(w, "repo", fmt.Sprintf("Server returned unexpected status: %d", resp.StatusCode)) 479 return 480 } 481 482 // add to local db 483 repo := &db.Repo{ 484 Did: user.Did, 485 Name: repoName, 486 Knot: domain, 487 } 488 err = s.db.AddRepo(repo) 489 if err != nil { 490 s.pages.Notice(w, "repo", "Failed to save repository information.") 491 return 492 } 493 494 // acls 495 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName)) 496 if err != nil { 497 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 498 return 499 } 500 501 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 502 return 503 } 504} 505 506func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 507 didOrHandle := chi.URLParam(r, "user") 508 if didOrHandle == "" { 509 http.Error(w, "Bad request", http.StatusBadRequest) 510 return 511 } 512 513 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 514 if err != nil { 515 log.Printf("resolving identity: %s", err) 516 w.WriteHeader(http.StatusNotFound) 517 return 518 } 519 520 repos, err := s.db.GetAllReposByDid(ident.DID.String()) 521 if err != nil { 522 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 523 } 524 525 s.pages.ProfilePage(w, pages.ProfilePageParams{ 526 LoggedInUser: s.auth.GetUser(r), 527 UserDid: ident.DID.String(), 528 UserHandle: ident.Handle.String(), 529 Repos: repos, 530 }) 531} 532 533func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 534 currentUser := s.auth.GetUser(r) 535 536 subject := r.URL.Query().Get("subject") 537 if subject == "" { 538 log.Println("invalid form") 539 return 540 } 541 542 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject) 543 if err != nil { 544 log.Println("failed to follow, invalid did") 545 } 546 547 if currentUser.Did == subjectIdent.DID.String() { 548 log.Println("cant follow or unfollow yourself") 549 return 550 } 551 552 client, _ := s.auth.AuthorizedClient(r) 553 554 switch r.Method { 555 case http.MethodPost: 556 createdAt := time.Now().Format(time.RFC3339) 557 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 558 Collection: tangled.GraphFollowNSID, 559 Repo: currentUser.Did, 560 Rkey: s.TID(), 561 Record: &lexutil.LexiconTypeDecoder{ 562 Val: &tangled.GraphFollow{ 563 Subject: subjectIdent.DID.String(), 564 CreatedAt: createdAt, 565 }}, 566 }) 567 if err != nil { 568 log.Println("failed to create atproto record", err) 569 return 570 } 571 572 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), resp.Uri) 573 if err != nil { 574 log.Println("failed to follow", err) 575 return 576 } 577 578 log.Println("created atproto record: ", resp.Uri) 579 580 return 581 case http.MethodDelete: 582 // find the record in the db 583 584 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String()) 585 if err != nil { 586 log.Println("failed to get follow relationship") 587 return 588 } 589 590 existingRecordUri, _ := syntax.ParseATURI(follow.AtUri) 591 592 resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 593 Collection: tangled.GraphFollowNSID, 594 Repo: currentUser.Did, 595 Rkey: existingRecordUri.RecordKey().String(), 596 }) 597 598 log.Println(resp.Commit.Cid) 599 600 if err != nil { 601 log.Println("failed to unfollow") 602 return 603 } 604 605 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String()) 606 if err != nil { 607 log.Println("failed to delete follow from DB") 608 // this is not an issue, the firehose event might have already done this 609 } 610 611 w.WriteHeader(http.StatusNoContent) 612 return 613 } 614 615} 616 617func (s *State) Router() http.Handler { 618 router := chi.NewRouter() 619 620 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 621 pat := chi.URLParam(r, "*") 622 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 623 s.UserRouter().ServeHTTP(w, r) 624 } else { 625 s.StandardRouter().ServeHTTP(w, r) 626 } 627 }) 628 629 return router 630} 631 632func (s *State) UserRouter() http.Handler { 633 r := chi.NewRouter() 634 635 // strip @ from user 636 r.Use(StripLeadingAt) 637 638 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 639 r.Get("/", s.ProfilePage) 640 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 641 r.Get("/", s.RepoIndex) 642 r.Get("/log/{ref}", s.RepoLog) 643 r.Route("/tree/{ref}", func(r chi.Router) { 644 r.Get("/*", s.RepoTree) 645 }) 646 r.Get("/commit/{ref}", s.RepoCommit) 647 r.Get("/branches", s.RepoBranches) 648 r.Get("/tags", s.RepoTags) 649 r.Get("/blob/{ref}/*", s.RepoBlob) 650 651 // These routes get proxied to the knot 652 r.Get("/info/refs", s.InfoRefs) 653 r.Post("/git-upload-pack", s.UploadPack) 654 655 // settings routes, needs auth 656 r.Group(func(r chi.Router) { 657 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 658 r.Get("/", s.RepoSettings) 659 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 660 }) 661 }) 662 }) 663 }) 664 665 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 666 s.pages.Error404(w) 667 }) 668 669 return r 670} 671 672func (s *State) StandardRouter() http.Handler { 673 r := chi.NewRouter() 674 675 r.Handle("/static/*", s.pages.Static()) 676 677 r.Get("/", s.Timeline) 678 679 r.Get("/login", s.Login) 680 r.Post("/login", s.Login) 681 682 r.Route("/knots", func(r chi.Router) { 683 r.Use(AuthMiddleware(s)) 684 r.Get("/", s.Knots) 685 r.Post("/key", s.RegistrationKey) 686 687 r.Route("/{domain}", func(r chi.Router) { 688 r.Post("/init", s.InitKnotServer) 689 r.Get("/", s.KnotServerInfo) 690 r.Route("/member", func(r chi.Router) { 691 r.Use(RoleMiddleware(s, "server:owner")) 692 r.Get("/", s.ListMembers) 693 r.Put("/", s.AddMember) 694 r.Delete("/", s.RemoveMember) 695 }) 696 }) 697 }) 698 699 r.Route("/repo", func(r chi.Router) { 700 r.Route("/new", func(r chi.Router) { 701 r.Get("/", s.AddRepo) 702 r.Post("/", s.AddRepo) 703 }) 704 // r.Post("/import", s.ImportRepo) 705 }) 706 707 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 708 r.Post("/", s.Follow) 709 r.Delete("/", s.Follow) 710 }) 711 712 r.Route("/settings", func(r chi.Router) { 713 r.Use(AuthMiddleware(s)) 714 r.Get("/", s.Settings) 715 r.Put("/keys", s.SettingsKeys) 716 }) 717 718 r.Get("/keys/{user}", s.Keys) 719 720 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 721 s.pages.Error404(w) 722 }) 723 return r 724}