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 w.Write([]byte(fmt.Sprintln(k.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 url := fmt.Sprintf("http://%s/init", domain) 237 238 body, _ := json.Marshal(map[string]interface{}{ 239 "did": user.Did, 240 "keys": []string{}, 241 }) 242 pingRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) 243 if err != nil { 244 log.Println("failed to build ping request", err) 245 return 246 } 247 248 secret, err := s.db.GetRegistrationKey(domain) 249 if err != nil { 250 log.Printf("no key found for domain %s: %s\n", domain, err) 251 return 252 } 253 client := SignedClient(secret) 254 255 resp, err := client.Do(pingRequest) 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.E.GetUsersForRole("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.E.HasGroupingPolicy(user.Did, "server:owner", 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.E.GetUsersForRole("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: uuid.New().String(), 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 436 log.Println("created atproto record: ", resp.Uri) 437 438 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 439 if err != nil { 440 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 441 return 442 } 443 444 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 445} 446 447func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 448} 449 450func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 451 switch r.Method { 452 case http.MethodGet: 453 pages.NewRepo(w, pages.NewRepoParams{ 454 User: s.auth.GetUser(r), 455 }) 456 case http.MethodPost: 457 user := s.auth.GetUser(r) 458 459 domain := r.FormValue("domain") 460 if domain == "" { 461 log.Println("invalid form") 462 return 463 } 464 465 repoName := r.FormValue("name") 466 if repoName == "" { 467 log.Println("invalid form") 468 return 469 } 470 471 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 472 if err != nil || !ok { 473 w.Write([]byte("domain inaccessible to you")) 474 return 475 } 476 477 secret, err := s.db.GetRegistrationKey(domain) 478 if err != nil { 479 log.Printf("no key found for domain %s: %s\n", domain, err) 480 return 481 } 482 483 client := SignedClient(secret) 484 url := fmt.Sprintf("http://%s/repo/new", domain) 485 body, _ := json.Marshal(map[string]interface{}{ 486 "did": user.Did, 487 "name": repoName, 488 }) 489 createRepoRequest, err := http.NewRequest("PUT", url, bytes.NewReader(body)) 490 491 resp, err := client.Do(createRepoRequest) 492 493 if err != nil { 494 log.Println("failed to send create repo request", err) 495 return 496 } 497 498 if resp.StatusCode != http.StatusNoContent { 499 log.Println("server returned ", resp.StatusCode) 500 return 501 } 502 503 // add to local db 504 repo := &db.Repo{ 505 Did: user.Did, 506 Name: repoName, 507 Knot: domain, 508 } 509 510 err = s.db.AddRepo(repo) 511 if err != nil { 512 log.Println("failed to add repo to db", err) 513 return 514 } 515 516 w.Write([]byte("created!")) 517 } 518} 519 520func (s *State) Router() http.Handler { 521 r := chi.NewRouter() 522 523 r.Get("/", s.Timeline) 524 525 r.Get("/login", s.Login) 526 r.Post("/login", s.Login) 527 528 r.Route("/knots", func(r chi.Router) { 529 r.Use(AuthMiddleware(s)) 530 r.Get("/", s.Knots) 531 r.Post("/key", s.RegistrationKey) 532 533 r.Route("/{domain}", func(r chi.Router) { 534 r.Post("/init", s.InitKnotServer) 535 r.Get("/", s.KnotServerInfo) 536 r.Route("/member", func(r chi.Router) { 537 r.Use(RoleMiddleware(s, "server:owner")) 538 r.Get("/", s.ListMembers) 539 r.Put("/", s.AddMember) 540 r.Delete("/", s.RemoveMember) 541 }) 542 }) 543 }) 544 545 r.Route("/repo", func(r chi.Router) { 546 r.Route("/new", func(r chi.Router) { 547 r.Get("/", s.AddRepo) 548 r.Post("/", s.AddRepo) 549 }) 550 // r.Post("/import", s.ImportRepo) 551 }) 552 553 r.Route("/settings", func(r chi.Router) { 554 r.Use(AuthMiddleware(s)) 555 r.Get("/", s.Settings) 556 r.Put("/keys", s.SettingsKeys) 557 }) 558 559 r.Get("/keys/{user}", s.Keys) 560 561 return r 562}