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