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 "github.com/go-chi/chi/v5" 13 "github.com/icyphox/bild/appview" 14 "github.com/icyphox/bild/appview/auth" 15 "github.com/icyphox/bild/appview/db" 16) 17 18type State struct { 19 Db *db.DB 20 Auth *auth.Auth 21} 22 23func Make() (*State, error) { 24 db, err := db.Make("appview.db") 25 if err != nil { 26 return nil, err 27 } 28 29 auth, err := auth.Make() 30 if err != nil { 31 return nil, err 32 } 33 34 return &State{db, auth}, nil 35} 36 37func (s *State) Login(w http.ResponseWriter, r *http.Request) { 38 ctx := r.Context() 39 40 switch r.Method { 41 case http.MethodGet: 42 log.Println("unimplemented") 43 return 44 case http.MethodPost: 45 username := r.FormValue("username") 46 appPassword := r.FormValue("password") 47 48 atSession, err := s.Auth.CreateInitialSession(ctx, username, appPassword) 49 if err != nil { 50 log.Printf("creating initial session: %s", err) 51 return 52 } 53 sessionish := auth.CreateSessionWrapper{atSession} 54 55 err = s.Auth.StoreSession(r, w, &sessionish) 56 if err != nil { 57 log.Printf("storing session: %s", err) 58 return 59 } 60 61 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 62 http.Redirect(w, r, "/", http.StatusSeeOther) 63 return 64 } 65} 66 67// requires auth 68func (s *State) Key(w http.ResponseWriter, r *http.Request) { 69 switch r.Method { 70 case http.MethodGet: 71 // list open registrations under this did 72 73 return 74 case http.MethodPost: 75 session, err := s.Auth.Store.Get(r, appview.SESSION_NAME) 76 if err != nil || session.IsNew { 77 log.Println("unauthorized attempt to generate registration key") 78 http.Error(w, "Forbidden", http.StatusUnauthorized) 79 return 80 } 81 82 did := session.Values[appview.SESSION_DID].(string) 83 84 // check if domain is valid url, and strip extra bits down to just host 85 domain := r.FormValue("domain") 86 if domain == "" || err != nil { 87 log.Println(err) 88 http.Error(w, "Invalid form", http.StatusBadRequest) 89 return 90 } 91 92 key, err := s.Db.GenerateRegistrationKey(domain, did) 93 94 if err != nil { 95 log.Println(err) 96 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 97 return 98 } 99 100 w.Write([]byte(key)) 101 return 102 } 103} 104 105// create a signed request and check if a node responds to that 106// 107// we should also rate limit these checks to avoid ddosing knotservers 108func (s *State) Check(w http.ResponseWriter, r *http.Request) { 109 domain := r.FormValue("domain") 110 if domain == "" { 111 http.Error(w, "Invalid form", http.StatusBadRequest) 112 return 113 } 114 115 log.Println("checking ", domain) 116 117 secret, err := s.Db.GetRegistrationKey(domain) 118 if err != nil { 119 log.Printf("no key found for domain %s: %s\n", domain, err) 120 return 121 } 122 log.Println("has secret ", secret) 123 124 // make a request do the knotserver with an empty body and above signature 125 url := fmt.Sprintf("http://%s/internal/health", domain) 126 127 pingRequest, err := buildPingRequest(url, secret) 128 if err != nil { 129 log.Println("failed to build ping request", err) 130 return 131 } 132 133 client := &http.Client{ 134 Timeout: 5 * time.Second, 135 } 136 resp, err := client.Do(pingRequest) 137 if err != nil { 138 w.Write([]byte("no dice")) 139 log.Println("domain was unreachable after 5 seconds") 140 return 141 } 142 143 if resp.StatusCode != http.StatusOK { 144 log.Println("status nok", resp.StatusCode) 145 w.Write([]byte("no dice")) 146 return 147 } 148 149 // verify response mac 150 signature := resp.Header.Get("X-Signature") 151 signatureBytes, err := hex.DecodeString(signature) 152 if err != nil { 153 return 154 } 155 156 expectedMac := hmac.New(sha256.New, []byte(secret)) 157 expectedMac.Write([]byte("ok")) 158 159 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 160 log.Printf("response body signature mismatch: %x\n", signatureBytes) 161 return 162 } 163 164 w.Write([]byte("check success")) 165 166 // mark as registered 167 s.Db.Register(domain) 168 169 return 170} 171 172func buildPingRequest(url, secret string) (*http.Request, error) { 173 pingRequest, err := http.NewRequest("GET", url, nil) 174 if err != nil { 175 return nil, err 176 } 177 178 timestamp := time.Now().Format(time.RFC3339) 179 mac := hmac.New(sha256.New, []byte(secret)) 180 message := pingRequest.Method + pingRequest.URL.Path + timestamp 181 mac.Write([]byte(message)) 182 signature := hex.EncodeToString(mac.Sum(nil)) 183 184 pingRequest.Header.Set("X-Signature", signature) 185 pingRequest.Header.Set("X-Timestamp", timestamp) 186 187 return pingRequest, nil 188} 189 190func (s *State) Router() http.Handler { 191 r := chi.NewRouter() 192 193 r.Post("/login", s.Login) 194 195 r.Route("/node", func(r chi.Router) { 196 r.Post("/check", s.Check) 197 198 r.Group(func(r chi.Router) { 199 r.Use(AuthMiddleware(s)) 200 r.Post("/key", s.Key) 201 }) 202 }) 203 204 return r 205}