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