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 "time" 11 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 lexutil "github.com/bluesky-social/indigo/lex/util" 14 "github.com/gliderlabs/ssh" 15 "github.com/go-chi/chi/v5" 16 "github.com/google/uuid" 17 tangled "github.com/icyphox/bild/api/tangled" 18 "github.com/icyphox/bild/appview" 19 "github.com/icyphox/bild/appview/auth" 20 "github.com/icyphox/bild/appview/db" 21) 22 23type State struct { 24 db *db.DB 25 auth *auth.Auth 26 enforcer *Enforcer 27} 28 29func Make() (*State, error) { 30 db, err := db.Make("appview.db") 31 if err != nil { 32 return nil, err 33 } 34 35 auth, err := auth.Make() 36 if err != nil { 37 return nil, err 38 } 39 40 enforcer, err := NewEnforcer() 41 if err != nil { 42 return nil, err 43 } 44 45 return &State{db, auth, enforcer}, nil 46} 47 48func (s *State) Login(w http.ResponseWriter, r *http.Request) { 49 ctx := r.Context() 50 51 switch r.Method { 52 case http.MethodGet: 53 log.Println("unimplemented") 54 return 55 case http.MethodPost: 56 handle := r.FormValue("handle") 57 appPassword := r.FormValue("app_password") 58 59 fmt.Println("handle", handle) 60 fmt.Println("app_password", appPassword) 61 62 resolved, err := auth.ResolveIdent(ctx, handle) 63 if err != nil { 64 log.Printf("resolving identity: %s", err) 65 return 66 } 67 68 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 69 if err != nil { 70 log.Printf("creating initial session: %s", err) 71 return 72 } 73 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 74 75 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 76 if err != nil { 77 log.Printf("storing session: %s", err) 78 return 79 } 80 81 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 82 http.Redirect(w, r, "/", http.StatusSeeOther) 83 return 84 } 85} 86 87// requires auth 88func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 89 switch r.Method { 90 case http.MethodGet: 91 // list open registrations under this did 92 93 return 94 case http.MethodPost: 95 session, err := s.auth.Store.Get(r, appview.SessionName) 96 if err != nil || session.IsNew { 97 log.Println("unauthorized attempt to generate registration key") 98 http.Error(w, "Forbidden", http.StatusUnauthorized) 99 return 100 } 101 102 did := session.Values[appview.SessionDid].(string) 103 104 // check if domain is valid url, and strip extra bits down to just host 105 domain := r.FormValue("domain") 106 if domain == "" { 107 http.Error(w, "Invalid form", http.StatusBadRequest) 108 return 109 } 110 111 key, err := s.db.GenerateRegistrationKey(domain, did) 112 113 if err != nil { 114 log.Println(err) 115 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 116 return 117 } 118 119 w.Write([]byte(key)) 120 } 121} 122 123func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 124 switch r.Method { 125 case http.MethodGet: 126 w.Write([]byte("unimplemented")) 127 log.Println("unimplemented") 128 return 129 case http.MethodPut: 130 did := s.auth.GetDID(r) 131 key := r.FormValue("key") 132 name := r.FormValue("name") 133 client, _ := s.auth.AuthorizedClient(r) 134 135 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 136 if err != nil { 137 log.Printf("parsing public key: %s", err) 138 return 139 } 140 141 if err := s.db.AddPublicKey(did, name, key); err != nil { 142 log.Printf("adding public key: %s", err) 143 return 144 } 145 146 // store in pds too 147 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 148 Collection: tangled.PublicKeyNSID, 149 Repo: did, 150 Rkey: uuid.New().String(), 151 Record: &lexutil.LexiconTypeDecoder{Val: &tangled.PublicKey{ 152 Created: time.Now().String(), 153 Key: key, 154 Name: name, 155 }}, 156 }) 157 158 // invalid record 159 if err != nil { 160 log.Printf("failed to create record: %s", err) 161 return 162 } 163 164 log.Println("created atproto record: ", resp.Uri) 165 166 return 167 } 168} 169 170// create a signed request and check if a node responds to that 171// 172// we should also rate limit these checks to avoid ddosing knotservers 173func (s *State) Check(w http.ResponseWriter, r *http.Request) { 174 domain := r.FormValue("domain") 175 if domain == "" { 176 http.Error(w, "Invalid form", http.StatusBadRequest) 177 return 178 } 179 180 log.Println("checking ", domain) 181 182 secret, err := s.db.GetRegistrationKey(domain) 183 if err != nil { 184 log.Printf("no key found for domain %s: %s\n", domain, err) 185 return 186 } 187 log.Println("has secret ", secret) 188 189 // make a request do the knotserver with an empty body and above signature 190 url := fmt.Sprintf("http://%s/health", domain) 191 192 pingRequest, err := buildPingRequest(url, secret) 193 if err != nil { 194 log.Println("failed to build ping request", err) 195 return 196 } 197 198 client := &http.Client{ 199 Timeout: 5 * time.Second, 200 } 201 resp, err := client.Do(pingRequest) 202 if err != nil { 203 w.Write([]byte("no dice")) 204 log.Println("domain was unreachable after 5 seconds") 205 return 206 } 207 208 if resp.StatusCode != http.StatusOK { 209 log.Println("status nok", resp.StatusCode) 210 w.Write([]byte("no dice")) 211 return 212 } 213 214 // verify response mac 215 signature := resp.Header.Get("X-Signature") 216 signatureBytes, err := hex.DecodeString(signature) 217 if err != nil { 218 return 219 } 220 221 expectedMac := hmac.New(sha256.New, []byte(secret)) 222 expectedMac.Write([]byte("ok")) 223 224 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 225 log.Printf("response body signature mismatch: %x\n", signatureBytes) 226 return 227 } 228 229 // mark as registered 230 err = s.db.Register(domain) 231 if err != nil { 232 log.Println("failed to register domain", err) 233 http.Error(w, err.Error(), http.StatusInternalServerError) 234 return 235 } 236 237 // set permissions for this did as owner 238 _, did, err := s.db.RegistrationStatus(domain) 239 if err != nil { 240 log.Println("failed to register domain", err) 241 http.Error(w, err.Error(), http.StatusInternalServerError) 242 return 243 } 244 245 // add basic acls for this domain 246 err = s.enforcer.AddDomain(domain) 247 if err != nil { 248 log.Println("failed to setup owner of domain", err) 249 http.Error(w, err.Error(), http.StatusInternalServerError) 250 return 251 } 252 253 // add this did as owner of this domain 254 err = s.enforcer.AddOwner(domain, did) 255 if err != nil { 256 log.Println("failed to setup owner of domain", err) 257 http.Error(w, err.Error(), http.StatusInternalServerError) 258 return 259 } 260 261 w.Write([]byte("check success")) 262 263 return 264} 265 266func buildPingRequest(url, secret string) (*http.Request, error) { 267 pingRequest, err := http.NewRequest("GET", url, nil) 268 if err != nil { 269 return nil, err 270 } 271 272 timestamp := time.Now().Format(time.RFC3339) 273 mac := hmac.New(sha256.New, []byte(secret)) 274 message := pingRequest.Method + pingRequest.URL.Path + timestamp 275 mac.Write([]byte(message)) 276 signature := hex.EncodeToString(mac.Sum(nil)) 277 278 pingRequest.Header.Set("X-Signature", signature) 279 pingRequest.Header.Set("X-Timestamp", timestamp) 280 281 return pingRequest, nil 282} 283 284func (s *State) Router() http.Handler { 285 r := chi.NewRouter() 286 287 r.Post("/login", s.Login) 288 289 r.Route("/node", func(r chi.Router) { 290 r.Post("/check", s.Check) 291 292 r.Group(func(r chi.Router) { 293 r.Use(AuthMiddleware(s)) 294 r.Post("/key", s.RegistrationKey) 295 }) 296 }) 297 298 r.Route("/settings", func(r chi.Router) { 299 r.Use(AuthMiddleware(s)) 300 r.Put("/keys", s.Keys) 301 }) 302 303 return r 304}