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 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: "sh.bild.publicKey",
149 Repo: did,
150 Rkey: uuid.New().String(),
151 Record: &lexutil.LexiconTypeDecoder{Val: &shbild.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}