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