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