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.WriteHeader(http.StatusBadRequest) 164 return 165 } 166 167 id, err := auth.ResolveIdent(r.Context(), user) 168 if err != nil { 169 w.WriteHeader(http.StatusInternalServerError) 170 return 171 } 172 173 pubKeys, err := s.db.GetPublicKeys(id.DID.String()) 174 if err != nil { 175 w.WriteHeader(http.StatusNotFound) 176 return 177 } 178 179 if len(pubKeys) == 0 { 180 w.WriteHeader(http.StatusNotFound) 181 return 182 } 183 184 for _, k := range pubKeys { 185 key := strings.TrimRight(k.Key, "\n") 186 w.Write([]byte(fmt.Sprintln(key))) 187 } 188} 189 190func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { 191 switch r.Method { 192 case http.MethodGet: 193 w.Write([]byte("unimplemented")) 194 log.Println("unimplemented") 195 return 196 case http.MethodPut: 197 did := s.auth.GetDid(r) 198 key := r.FormValue("key") 199 name := r.FormValue("name") 200 client, _ := s.auth.AuthorizedClient(r) 201 202 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 203 if err != nil { 204 log.Printf("parsing public key: %s", err) 205 return 206 } 207 208 if err := s.db.AddPublicKey(did, name, key); err != nil { 209 log.Printf("adding public key: %s", err) 210 return 211 } 212 213 // store in pds too 214 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 215 Collection: tangled.PublicKeyNSID, 216 Repo: did, 217 Rkey: s.TID(), 218 Record: &lexutil.LexiconTypeDecoder{ 219 Val: &tangled.PublicKey{ 220 Created: time.Now().Format(time.RFC3339), 221 Key: key, 222 Name: name, 223 }}, 224 }) 225 // invalid record 226 if err != nil { 227 log.Printf("failed to create record: %s", err) 228 return 229 } 230 231 log.Println("created atproto record: ", resp.Uri) 232 233 return 234 } 235} 236 237// create a signed request and check if a node responds to that 238func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 239 user := s.auth.GetUser(r) 240 241 domain := chi.URLParam(r, "domain") 242 if domain == "" { 243 http.Error(w, "malformed url", http.StatusBadRequest) 244 return 245 } 246 log.Println("checking ", domain) 247 248 secret, err := s.db.GetRegistrationKey(domain) 249 if err != nil { 250 log.Printf("no key found for domain %s: %s\n", domain, err) 251 return 252 } 253 254 client, err := NewSignedClient(domain, secret) 255 if err != nil { 256 log.Println("failed to create client to ", domain) 257 } 258 259 resp, err := client.Init(user.Did, []string{}) 260 if err != nil { 261 w.Write([]byte("no dice")) 262 log.Println("domain was unreachable after 5 seconds") 263 return 264 } 265 266 if resp.StatusCode == http.StatusConflict { 267 log.Println("status conflict", resp.StatusCode) 268 w.Write([]byte("already registered, sorry!")) 269 return 270 } 271 272 if resp.StatusCode != http.StatusNoContent { 273 log.Println("status nok", resp.StatusCode) 274 w.Write([]byte("no dice")) 275 return 276 } 277 278 // verify response mac 279 signature := resp.Header.Get("X-Signature") 280 signatureBytes, err := hex.DecodeString(signature) 281 if err != nil { 282 return 283 } 284 285 expectedMac := hmac.New(sha256.New, []byte(secret)) 286 expectedMac.Write([]byte("ok")) 287 288 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 289 log.Printf("response body signature mismatch: %x\n", signatureBytes) 290 return 291 } 292 293 // mark as registered 294 err = s.db.Register(domain) 295 if err != nil { 296 log.Println("failed to register domain", err) 297 http.Error(w, err.Error(), http.StatusInternalServerError) 298 return 299 } 300 301 // set permissions for this did as owner 302 reg, err := s.db.RegistrationByDomain(domain) 303 if err != nil { 304 log.Println("failed to register domain", err) 305 http.Error(w, err.Error(), http.StatusInternalServerError) 306 return 307 } 308 309 // add basic acls for this domain 310 err = s.enforcer.AddDomain(domain) 311 if err != nil { 312 log.Println("failed to setup owner of domain", err) 313 http.Error(w, err.Error(), http.StatusInternalServerError) 314 return 315 } 316 317 // add this did as owner of this domain 318 err = s.enforcer.AddOwner(domain, reg.ByDid) 319 if err != nil { 320 log.Println("failed to setup owner of domain", err) 321 http.Error(w, err.Error(), http.StatusInternalServerError) 322 return 323 } 324 325 w.Write([]byte("check success")) 326} 327 328func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 329 domain := chi.URLParam(r, "domain") 330 if domain == "" { 331 http.Error(w, "malformed url", http.StatusBadRequest) 332 return 333 } 334 335 user := s.auth.GetUser(r) 336 reg, err := s.db.RegistrationByDomain(domain) 337 if err != nil { 338 w.Write([]byte("failed to pull up registration info")) 339 return 340 } 341 342 var members []string 343 if reg.Registered != nil { 344 members, err = s.enforcer.GetUserByRole("server:member", domain) 345 if err != nil { 346 w.Write([]byte("failed to fetch member list")) 347 return 348 } 349 } 350 351 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 352 isOwner := err == nil && ok 353 354 p := pages.KnotParams{ 355 User: user, 356 Registration: reg, 357 Members: members, 358 IsOwner: isOwner, 359 } 360 361 pages.Knot(w, p) 362} 363 364// get knots registered by this user 365func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 366 // for now, this is just pubkeys 367 user := s.auth.GetUser(r) 368 registrations, err := s.db.RegistrationsByDid(user.Did) 369 if err != nil { 370 log.Println(err) 371 } 372 373 pages.Knots(w, pages.KnotsParams{ 374 User: user, 375 Registrations: registrations, 376 }) 377} 378 379// list members of domain, requires auth and requires owner status 380func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 381 domain := chi.URLParam(r, "domain") 382 if domain == "" { 383 http.Error(w, "malformed url", http.StatusBadRequest) 384 return 385 } 386 387 // list all members for this domain 388 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 389 if err != nil { 390 w.Write([]byte("failed to fetch member list")) 391 return 392 } 393 394 w.Write([]byte(strings.Join(memberDids, "\n"))) 395 return 396} 397 398// add member to domain, requires auth and requires invite access 399func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 400 domain := chi.URLParam(r, "domain") 401 if domain == "" { 402 http.Error(w, "malformed url", http.StatusBadRequest) 403 return 404 } 405 406 memberDid := r.FormValue("member") 407 if memberDid == "" { 408 http.Error(w, "malformed form", http.StatusBadRequest) 409 return 410 } 411 412 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid) 413 if err != nil { 414 w.Write([]byte("failed to resolve member did to a handle")) 415 return 416 } 417 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 418 419 // announce this relation into the firehose, store into owners' pds 420 client, _ := s.auth.AuthorizedClient(r) 421 currentUser := s.auth.GetUser(r) 422 addedAt := time.Now().Format(time.RFC3339) 423 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 424 Collection: tangled.KnotMemberNSID, 425 Repo: currentUser.Did, 426 Rkey: s.TID(), 427 Record: &lexutil.LexiconTypeDecoder{ 428 Val: &tangled.KnotMember{ 429 Member: memberIdent.DID.String(), 430 Domain: domain, 431 AddedAt: &addedAt, 432 }}, 433 }) 434 // invalid record 435 if err != nil { 436 log.Printf("failed to create record: %s", err) 437 return 438 } 439 log.Println("created atproto record: ", resp.Uri) 440 441 secret, err := s.db.GetRegistrationKey(domain) 442 if err != nil { 443 log.Printf("no key found for domain %s: %s\n", domain, err) 444 return 445 } 446 447 ksClient, err := NewSignedClient(domain, secret) 448 if err != nil { 449 log.Println("failed to create client to ", domain) 450 return 451 } 452 453 ksResp, err := ksClient.AddMember(memberIdent.DID.String(), []string{}) 454 if err != nil { 455 log.Printf("failet to make request to %s: %s", domain, err) 456 } 457 458 if ksResp.StatusCode != http.StatusNoContent { 459 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 460 return 461 } 462 463 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 464 if err != nil { 465 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 466 return 467 } 468 469 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 470} 471 472func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 473} 474 475func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 476 switch r.Method { 477 case http.MethodGet: 478 pages.NewRepo(w, pages.NewRepoParams{ 479 User: s.auth.GetUser(r), 480 }) 481 case http.MethodPost: 482 user := s.auth.GetUser(r) 483 484 domain := r.FormValue("domain") 485 if domain == "" { 486 log.Println("invalid form") 487 return 488 } 489 490 repoName := r.FormValue("name") 491 if repoName == "" { 492 log.Println("invalid form") 493 return 494 } 495 496 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 497 if err != nil || !ok { 498 w.Write([]byte("domain inaccessible to you")) 499 return 500 } 501 502 secret, err := s.db.GetRegistrationKey(domain) 503 if err != nil { 504 log.Printf("no key found for domain %s: %s\n", domain, err) 505 return 506 } 507 508 client, err := NewSignedClient(domain, secret) 509 if err != nil { 510 log.Println("failed to create client to ", domain) 511 } 512 513 resp, err := client.NewRepo(user.Did, repoName) 514 if err != nil { 515 log.Println("failed to send create repo request", err) 516 return 517 } 518 if resp.StatusCode != http.StatusNoContent { 519 log.Println("server returned ", resp.StatusCode) 520 return 521 } 522 523 // add to local db 524 repo := &db.Repo{ 525 Did: user.Did, 526 Name: repoName, 527 Knot: domain, 528 } 529 err = s.db.AddRepo(repo) 530 if err != nil { 531 log.Println("failed to add repo to db", err) 532 return 533 } 534 535 // acls 536 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName)) 537 if err != nil { 538 log.Println("failed to set up acls", err) 539 return 540 } 541 542 w.Write([]byte("created!")) 543 } 544} 545 546func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 547 didOrHandle := chi.URLParam(r, "user") 548 if didOrHandle == "" { 549 http.Error(w, "Bad request", http.StatusBadRequest) 550 return 551 } 552 553 ident, err := auth.ResolveIdent(r.Context(), didOrHandle) 554 if err != nil { 555 log.Printf("resolving identity: %s", err) 556 w.WriteHeader(http.StatusNotFound) 557 return 558 } 559 560 repos, err := s.db.GetAllReposByDid(ident.DID.String()) 561 if err != nil { 562 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 563 } 564 565 pages.ProfilePage(w, pages.ProfilePageParams{ 566 LoggedInUser: s.auth.GetUser(r), 567 UserDid: ident.DID.String(), 568 UserHandle: ident.Handle.String(), 569 Repos: repos, 570 }) 571} 572 573func (s *State) Router() http.Handler { 574 router := chi.NewRouter() 575 576 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 577 pat := chi.URLParam(r, "*") 578 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 579 s.UserRouter().ServeHTTP(w, r) 580 } else { 581 s.StandardRouter().ServeHTTP(w, r) 582 } 583 }) 584 585 return router 586} 587 588func (s *State) UserRouter() http.Handler { 589 r := chi.NewRouter() 590 591 // strip @ from user 592 r.Use(StripLeadingAt) 593 594 r.Route("/{user}", func(r chi.Router) { 595 r.Get("/", s.ProfilePage) 596 }) 597 598 return r 599} 600 601func (s *State) StandardRouter() http.Handler { 602 r := chi.NewRouter() 603 604 r.Get("/", s.Timeline) 605 606 r.Get("/login", s.Login) 607 r.Post("/login", s.Login) 608 609 r.Route("/knots", func(r chi.Router) { 610 r.Use(AuthMiddleware(s)) 611 r.Get("/", s.Knots) 612 r.Post("/key", s.RegistrationKey) 613 614 r.Route("/{domain}", func(r chi.Router) { 615 r.Post("/init", s.InitKnotServer) 616 r.Get("/", s.KnotServerInfo) 617 r.Route("/member", func(r chi.Router) { 618 r.Use(RoleMiddleware(s, "server:owner")) 619 r.Get("/", s.ListMembers) 620 r.Put("/", s.AddMember) 621 r.Delete("/", s.RemoveMember) 622 }) 623 }) 624 }) 625 626 r.Route("/repo", func(r chi.Router) { 627 r.Route("/new", func(r chi.Router) { 628 r.Get("/", s.AddRepo) 629 r.Post("/", s.AddRepo) 630 }) 631 // r.Post("/import", s.ImportRepo) 632 }) 633 634 r.Route("/settings", func(r chi.Router) { 635 r.Use(AuthMiddleware(s)) 636 r.Get("/", s.Settings) 637 r.Put("/keys", s.SettingsKeys) 638 }) 639 640 r.Get("/keys/{user}", s.Keys) 641 642 return r 643}