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