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 := buildPingRequest(url, secret) 216 if err != nil { 217 log.Println("failed to build ping request", err) 218 return 219 } 220 221 client := &http.Client{ 222 Timeout: 5 * time.Second, 223 } 224 resp, err := client.Do(pingRequest) 225 if err != nil { 226 w.Write([]byte("no dice")) 227 log.Println("domain was unreachable after 5 seconds") 228 return 229 } 230 231 if resp.StatusCode != http.StatusOK { 232 log.Println("status nok", resp.StatusCode) 233 w.Write([]byte("no dice")) 234 return 235 } 236 237 // verify response mac 238 signature := resp.Header.Get("X-Signature") 239 signatureBytes, err := hex.DecodeString(signature) 240 if err != nil { 241 return 242 } 243 244 expectedMac := hmac.New(sha256.New, []byte(secret)) 245 expectedMac.Write([]byte("ok")) 246 247 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 248 log.Printf("response body signature mismatch: %x\n", signatureBytes) 249 return 250 } 251 252 // mark as registered 253 err = s.db.Register(domain) 254 if err != nil { 255 log.Println("failed to register domain", err) 256 http.Error(w, err.Error(), http.StatusInternalServerError) 257 return 258 } 259 260 // set permissions for this did as owner 261 reg, err := s.db.RegistrationByDomain(domain) 262 if err != nil { 263 log.Println("failed to register domain", err) 264 http.Error(w, err.Error(), http.StatusInternalServerError) 265 return 266 } 267 268 // add basic acls for this domain 269 err = s.enforcer.AddDomain(domain) 270 if err != nil { 271 log.Println("failed to setup owner of domain", err) 272 http.Error(w, err.Error(), http.StatusInternalServerError) 273 return 274 } 275 276 // add this did as owner of this domain 277 err = s.enforcer.AddOwner(domain, reg.ByDid) 278 if err != nil { 279 log.Println("failed to setup owner of domain", err) 280 http.Error(w, err.Error(), http.StatusInternalServerError) 281 return 282 } 283 284 w.Write([]byte("check success")) 285} 286 287func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 288 domain := chi.URLParam(r, "domain") 289 if domain == "" { 290 http.Error(w, "malformed url", http.StatusBadRequest) 291 return 292 } 293 294 user := s.auth.GetUser(r) 295 reg, err := s.db.RegistrationByDomain(domain) 296 if err != nil { 297 w.Write([]byte("failed to pull up registration info")) 298 return 299 } 300 301 var members []string 302 if reg.Registered != nil { 303 members, err = s.enforcer.E.GetUsersForRole("server:member", domain) 304 if err != nil { 305 w.Write([]byte("failed to fetch member list")) 306 return 307 } 308 } 309 310 ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain) 311 isOwner := err == nil && ok 312 313 p := pages.KnotParams{ 314 User: user, 315 Registration: reg, 316 Members: members, 317 IsOwner: isOwner, 318 } 319 320 pages.Knot(w, p) 321} 322 323// get knots registered by this user 324func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 325 // for now, this is just pubkeys 326 user := s.auth.GetUser(r) 327 registrations, err := s.db.RegistrationsByDid(user.Did) 328 if err != nil { 329 log.Println(err) 330 } 331 332 pages.Knots(w, pages.KnotsParams{ 333 User: user, 334 Registrations: registrations, 335 }) 336} 337 338// list members of domain, requires auth and requires owner status 339func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 340 domain := chi.URLParam(r, "domain") 341 if domain == "" { 342 http.Error(w, "malformed url", http.StatusBadRequest) 343 return 344 } 345 346 // list all members for this domain 347 memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain) 348 if err != nil { 349 w.Write([]byte("failed to fetch member list")) 350 return 351 } 352 353 w.Write([]byte(strings.Join(memberDids, "\n"))) 354 return 355} 356 357// add member to domain, requires auth and requires invite access 358func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 359 domain := chi.URLParam(r, "domain") 360 if domain == "" { 361 http.Error(w, "malformed url", http.StatusBadRequest) 362 return 363 } 364 365 memberDid := r.FormValue("member") 366 if memberDid == "" { 367 http.Error(w, "malformed form", http.StatusBadRequest) 368 return 369 } 370 371 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid) 372 if err != nil { 373 w.Write([]byte("failed to resolve member did to a handle")) 374 return 375 } 376 377 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 378 379 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 380 if err != nil { 381 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 382 return 383 } 384 385 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 386} 387 388// list members of domain, requires auth and requires owner status 389func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 390} 391 392func buildPingRequest(url, secret string) (*http.Request, error) { 393 pingRequest, err := http.NewRequest("GET", url, nil) 394 if err != nil { 395 return nil, err 396 } 397 398 timestamp := time.Now().Format(time.RFC3339) 399 mac := hmac.New(sha256.New, []byte(secret)) 400 message := pingRequest.Method + pingRequest.URL.Path + timestamp 401 mac.Write([]byte(message)) 402 signature := hex.EncodeToString(mac.Sum(nil)) 403 404 pingRequest.Header.Set("X-Signature", signature) 405 pingRequest.Header.Set("X-Timestamp", timestamp) 406 407 return pingRequest, nil 408} 409 410func (s *State) Router() http.Handler { 411 r := chi.NewRouter() 412 413 r.Get("/", s.Timeline) 414 415 r.Get("/login", s.Login) 416 r.Post("/login", s.Login) 417 418 r.Route("/knots", func(r chi.Router) { 419 r.Use(AuthMiddleware(s)) 420 r.Get("/", s.Knots) 421 r.Post("/key", s.RegistrationKey) 422 423 r.Route("/{domain}", func(r chi.Router) { 424 r.Get("/", s.KnotServerInfo) 425 r.Post("/init", s.InitKnotServer) 426 r.Route("/member", func(r chi.Router) { 427 r.Use(RoleMiddleware(s, "server:owner")) 428 r.Get("/", s.ListMembers) 429 r.Put("/", s.AddMember) 430 r.Delete("/", s.RemoveMember) 431 }) 432 }) 433 }) 434 435 r.Group(func(r chi.Router) { 436 r.Use(AuthMiddleware(s)) 437 r.Get("/settings", s.Settings) 438 r.Put("/settings/keys", s.Keys) 439 }) 440 441 return r 442}