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