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