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}, 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}