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