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