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