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