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 switch r.Method { 154 case http.MethodGet: 155 w.Write([]byte("unimplemented")) 156 log.Println("unimplemented") 157 return 158 case http.MethodPut: 159 did := s.auth.GetDid(r) 160 key := r.FormValue("key") 161 name := r.FormValue("name") 162 client, _ := s.auth.AuthorizedClient(r) 163 164 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 165 if err != nil { 166 log.Printf("parsing public key: %s", err) 167 return 168 } 169 170 if err := s.db.AddPublicKey(did, name, key); err != nil { 171 log.Printf("adding public key: %s", err) 172 return 173 } 174 175 // store in pds too 176 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 177 Collection: tangled.PublicKeyNSID, 178 Repo: did, 179 Rkey: uuid.New().String(), 180 Record: &lexutil.LexiconTypeDecoder{ 181 Val: &tangled.PublicKey{ 182 Created: time.Now().Format(time.RFC3339), 183 Key: key, 184 Name: name, 185 }}, 186 }) 187 // invalid record 188 if err != nil { 189 log.Printf("failed to create record: %s", err) 190 return 191 } 192 193 log.Println("created atproto record: ", resp.Uri) 194 195 return 196 } 197} 198 199// create a signed request and check if a node responds to that 200func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 201 user := s.auth.GetUser(r) 202 203 domain := chi.URLParam(r, "domain") 204 if domain == "" { 205 http.Error(w, "malformed url", http.StatusBadRequest) 206 return 207 } 208 log.Println("checking ", domain) 209 210 url := fmt.Sprintf("http://%s/init", domain) 211 212 body, _ := json.Marshal(map[string]interface{}{ 213 "did": user.Did, 214 "keys": []string{}, 215 }) 216 pingRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) 217 if err != nil { 218 log.Println("failed to build ping request", err) 219 return 220 } 221 222 secret, err := s.db.GetRegistrationKey(domain) 223 if err != nil { 224 log.Printf("no key found for domain %s: %s\n", domain, err) 225 return 226 } 227 client := SignedClient(secret) 228 229 resp, err := client.Do(pingRequest) 230 if err != nil { 231 w.Write([]byte("no dice")) 232 log.Println("domain was unreachable after 5 seconds") 233 return 234 } 235 236 if resp.StatusCode == http.StatusConflict { 237 log.Println("status conflict", resp.StatusCode) 238 w.Write([]byte("already registered, sorry!")) 239 return 240 } 241 242 if resp.StatusCode != http.StatusNoContent { 243 log.Println("status nok", resp.StatusCode) 244 w.Write([]byte("no dice")) 245 return 246 } 247 248 // verify response mac 249 signature := resp.Header.Get("X-Signature") 250 signatureBytes, err := hex.DecodeString(signature) 251 if err != nil { 252 return 253 } 254 255 expectedMac := hmac.New(sha256.New, []byte(secret)) 256 expectedMac.Write([]byte("ok")) 257 258 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 259 log.Printf("response body signature mismatch: %x\n", signatureBytes) 260 return 261 } 262 263 // mark as registered 264 err = s.db.Register(domain) 265 if err != nil { 266 log.Println("failed to register domain", err) 267 http.Error(w, err.Error(), http.StatusInternalServerError) 268 return 269 } 270 271 // set permissions for this did as owner 272 reg, err := s.db.RegistrationByDomain(domain) 273 if err != nil { 274 log.Println("failed to register domain", err) 275 http.Error(w, err.Error(), http.StatusInternalServerError) 276 return 277 } 278 279 // add basic acls for this domain 280 err = s.enforcer.AddDomain(domain) 281 if err != nil { 282 log.Println("failed to setup owner of domain", err) 283 http.Error(w, err.Error(), http.StatusInternalServerError) 284 return 285 } 286 287 // add this did as owner of this domain 288 err = s.enforcer.AddOwner(domain, reg.ByDid) 289 if err != nil { 290 log.Println("failed to setup owner of domain", err) 291 http.Error(w, err.Error(), http.StatusInternalServerError) 292 return 293 } 294 295 w.Write([]byte("check success")) 296} 297 298func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 299 domain := chi.URLParam(r, "domain") 300 if domain == "" { 301 http.Error(w, "malformed url", http.StatusBadRequest) 302 return 303 } 304 305 user := s.auth.GetUser(r) 306 reg, err := s.db.RegistrationByDomain(domain) 307 if err != nil { 308 w.Write([]byte("failed to pull up registration info")) 309 return 310 } 311 312 var members []string 313 if reg.Registered != nil { 314 members, err = s.enforcer.E.GetUsersForRole("server:member", domain) 315 if err != nil { 316 w.Write([]byte("failed to fetch member list")) 317 return 318 } 319 } 320 321 ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain) 322 isOwner := err == nil && ok 323 324 p := pages.KnotParams{ 325 User: user, 326 Registration: reg, 327 Members: members, 328 IsOwner: isOwner, 329 } 330 331 pages.Knot(w, p) 332} 333 334// get knots registered by this user 335func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 336 // for now, this is just pubkeys 337 user := s.auth.GetUser(r) 338 registrations, err := s.db.RegistrationsByDid(user.Did) 339 if err != nil { 340 log.Println(err) 341 } 342 343 pages.Knots(w, pages.KnotsParams{ 344 User: user, 345 Registrations: registrations, 346 }) 347} 348 349// list members of domain, requires auth and requires owner status 350func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 351 domain := chi.URLParam(r, "domain") 352 if domain == "" { 353 http.Error(w, "malformed url", http.StatusBadRequest) 354 return 355 } 356 357 // list all members for this domain 358 memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain) 359 if err != nil { 360 w.Write([]byte("failed to fetch member list")) 361 return 362 } 363 364 w.Write([]byte(strings.Join(memberDids, "\n"))) 365 return 366} 367 368// add member to domain, requires auth and requires invite access 369func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 370 domain := chi.URLParam(r, "domain") 371 if domain == "" { 372 http.Error(w, "malformed url", http.StatusBadRequest) 373 return 374 } 375 376 memberDid := r.FormValue("member") 377 if memberDid == "" { 378 http.Error(w, "malformed form", http.StatusBadRequest) 379 return 380 } 381 382 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid) 383 if err != nil { 384 w.Write([]byte("failed to resolve member did to a handle")) 385 return 386 } 387 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 388 389 // announce this relation into the firehose, store into owners' pds 390 client, _ := s.auth.AuthorizedClient(r) 391 currentUser := s.auth.GetUser(r) 392 addedAt := time.Now().Format(time.RFC3339) 393 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 394 Collection: tangled.KnotMemberNSID, 395 Repo: currentUser.Did, 396 Rkey: uuid.New().String(), 397 Record: &lexutil.LexiconTypeDecoder{ 398 Val: &tangled.KnotMember{ 399 Member: memberIdent.DID.String(), 400 Domain: domain, 401 AddedAt: &addedAt, 402 }}, 403 }) 404 // invalid record 405 if err != nil { 406 log.Printf("failed to create record: %s", err) 407 return 408 } 409 410 log.Println("created atproto record: ", resp.Uri) 411 412 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 413 if err != nil { 414 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 415 return 416 } 417 418 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 419} 420 421func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 422} 423 424func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) { 425 switch r.Method { 426 case http.MethodGet: 427 pages.NewRepo(w, pages.NewRepoParams{ 428 User: s.auth.GetUser(r), 429 }) 430 case http.MethodPost: 431 user := s.auth.GetUser(r) 432 433 domain := r.FormValue("domain") 434 if domain == "" { 435 log.Println("invalid form") 436 return 437 } 438 439 repoName := r.FormValue("name") 440 if repoName == "" { 441 log.Println("invalid form") 442 return 443 } 444 445 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 446 if err != nil || !ok { 447 w.Write([]byte("domain inaccessible to you")) 448 return 449 } 450 451 secret, err := s.db.GetRegistrationKey(domain) 452 if err != nil { 453 log.Printf("no key found for domain %s: %s\n", domain, err) 454 return 455 } 456 457 client := SignedClient(secret) 458 url := fmt.Sprintf("http://%s/repo/new", domain) 459 body, _ := json.Marshal(map[string]interface{}{ 460 "did": user.Did, 461 "name": repoName, 462 }) 463 createRepoRequest, err := http.NewRequest("PUT", url, bytes.NewReader(body)) 464 465 resp, err := client.Do(createRepoRequest) 466 467 if err != nil { 468 log.Println("failed to send create repo request", err) 469 return 470 } 471 472 if resp.StatusCode != http.StatusNoContent { 473 log.Println("server returned ", resp.StatusCode) 474 return 475 } 476 477 // add to local db 478 repo := &db.Repo{ 479 Did: user.Did, 480 Name: repoName, 481 Knot: domain, 482 } 483 484 err = s.db.AddRepo(repo) 485 if err != nil { 486 log.Println("failed to add repo to db", err) 487 return 488 } 489 490 w.Write([]byte("created!")) 491 } 492} 493 494func (s *State) Router() http.Handler { 495 r := chi.NewRouter() 496 497 r.Get("/", s.Timeline) 498 499 r.Get("/login", s.Login) 500 r.Post("/login", s.Login) 501 502 r.Route("/knots", func(r chi.Router) { 503 r.Use(AuthMiddleware(s)) 504 r.Get("/", s.Knots) 505 r.Post("/key", s.RegistrationKey) 506 507 r.Route("/{domain}", func(r chi.Router) { 508 r.Post("/init", s.InitKnotServer) 509 r.Get("/", s.KnotServerInfo) 510 r.Route("/member", func(r chi.Router) { 511 r.Use(RoleMiddleware(s, "server:owner")) 512 r.Get("/", s.ListMembers) 513 r.Put("/", s.AddMember) 514 r.Delete("/", s.RemoveMember) 515 }) 516 }) 517 }) 518 519 r.Route("/repo", func(r chi.Router) { 520 r.Route("/new", func(r chi.Router) { 521 r.Get("/", s.AddRepo) 522 r.Post("/", s.AddRepo) 523 }) 524 // r.Post("/import", s.ImportRepo) 525 }) 526 527 r.Group(func(r chi.Router) { 528 r.Use(AuthMiddleware(s)) 529 r.Get("/settings", s.Settings) 530 r.Put("/settings/keys", s.Keys) 531 }) 532 533 return r 534}