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