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}