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 http.Redirect(w, r, "/login", http.StatusSeeOther)
67 return
68 }
69
70 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
71 if err != nil {
72 log.Printf("creating initial session: %s", err)
73 return
74 }
75 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
76
77 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
78 if err != nil {
79 log.Printf("storing session: %s", err)
80 return
81 }
82
83 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
84 http.Redirect(w, r, "/", http.StatusSeeOther)
85 return
86 }
87}
88
89func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
90 user := s.auth.GetUser(r)
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}
146
147func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
148 switch r.Method {
149 case http.MethodGet:
150 w.Write([]byte("unimplemented"))
151 log.Println("unimplemented")
152 return
153 case http.MethodPut:
154 did := s.auth.GetDID(r)
155 key := r.FormValue("key")
156 name := r.FormValue("name")
157 client, _ := s.auth.AuthorizedClient(r)
158
159 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
160 if err != nil {
161 log.Printf("parsing public key: %s", err)
162 return
163 }
164
165 if err := s.db.AddPublicKey(did, name, key); err != nil {
166 log.Printf("adding public key: %s", err)
167 return
168 }
169
170 // store in pds too
171 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
172 Collection: tangled.PublicKeyNSID,
173 Repo: did,
174 Rkey: uuid.New().String(),
175 Record: &lexutil.LexiconTypeDecoder{Val: &tangled.PublicKey{
176 Created: time.Now().String(),
177 Key: key,
178 Name: name,
179 }},
180 })
181
182 // invalid record
183 if err != nil {
184 log.Printf("failed to create record: %s", err)
185 return
186 }
187
188 log.Println("created atproto record: ", resp.Uri)
189
190 return
191 }
192}
193
194// create a signed request and check if a node responds to that
195func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
196 domain := chi.URLParam(r, "domain")
197 if domain == "" {
198 http.Error(w, "malformed url", http.StatusBadRequest)
199 return
200 }
201
202 log.Println("checking ", domain)
203
204 secret, err := s.db.GetRegistrationKey(domain)
205 if err != nil {
206 log.Printf("no key found for domain %s: %s\n", domain, err)
207 return
208 }
209 log.Println("has secret ", secret)
210
211 // make a request do the knotserver with an empty body and above signature
212 url := fmt.Sprintf("http://%s/health", domain)
213
214 pingRequest, err := buildPingRequest(url, secret)
215 if err != nil {
216 log.Println("failed to build ping request", err)
217 return
218 }
219
220 client := &http.Client{
221 Timeout: 5 * time.Second,
222 }
223 resp, err := client.Do(pingRequest)
224 if err != nil {
225 w.Write([]byte("no dice"))
226 log.Println("domain was unreachable after 5 seconds")
227 return
228 }
229
230 if resp.StatusCode != http.StatusOK {
231 log.Println("status nok", resp.StatusCode)
232 w.Write([]byte("no dice"))
233 return
234 }
235
236 // verify response mac
237 signature := resp.Header.Get("X-Signature")
238 signatureBytes, err := hex.DecodeString(signature)
239 if err != nil {
240 return
241 }
242
243 expectedMac := hmac.New(sha256.New, []byte(secret))
244 expectedMac.Write([]byte("ok"))
245
246 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
247 log.Printf("response body signature mismatch: %x\n", signatureBytes)
248 return
249 }
250
251 // mark as registered
252 err = s.db.Register(domain)
253 if err != nil {
254 log.Println("failed to register domain", err)
255 http.Error(w, err.Error(), http.StatusInternalServerError)
256 return
257 }
258
259 // set permissions for this did as owner
260 reg, err := s.db.RegistrationByDomain(domain)
261 if err != nil {
262 log.Println("failed to register domain", err)
263 http.Error(w, err.Error(), http.StatusInternalServerError)
264 return
265 }
266
267 // add basic acls for this domain
268 err = s.enforcer.AddDomain(domain)
269 if err != nil {
270 log.Println("failed to setup owner of domain", err)
271 http.Error(w, err.Error(), http.StatusInternalServerError)
272 return
273 }
274
275 // add this did as owner of this domain
276 err = s.enforcer.AddOwner(domain, reg.ByDid)
277 if err != nil {
278 log.Println("failed to setup owner of domain", err)
279 http.Error(w, err.Error(), http.StatusInternalServerError)
280 return
281 }
282
283 w.Write([]byte("check success"))
284}
285
286// get knots registered by this user
287func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
288 // for now, this is just pubkeys
289 user := s.auth.GetUser(r)
290 registrations, err := s.db.RegistrationsByDid(user.Did)
291 if err != nil {
292 log.Println(err)
293 }
294
295 pages.Knots(w, pages.KnotsParams{
296 User: user,
297 Registrations: registrations,
298 })
299}
300
301func buildPingRequest(url, secret string) (*http.Request, error) {
302 pingRequest, err := http.NewRequest("GET", url, nil)
303 if err != nil {
304 return nil, err
305 }
306
307 timestamp := time.Now().Format(time.RFC3339)
308 mac := hmac.New(sha256.New, []byte(secret))
309 message := pingRequest.Method + pingRequest.URL.Path + timestamp
310 mac.Write([]byte(message))
311 signature := hex.EncodeToString(mac.Sum(nil))
312
313 pingRequest.Header.Set("X-Signature", signature)
314 pingRequest.Header.Set("X-Timestamp", timestamp)
315
316 return pingRequest, nil
317}
318
319func (s *State) Router() http.Handler {
320 r := chi.NewRouter()
321
322 r.Get("/", s.Timeline)
323
324 r.Get("/login", s.Login)
325 r.Post("/login", s.Login)
326
327 r.Route("/knots", func(r chi.Router) {
328 r.Use(AuthMiddleware(s))
329 r.Get("/", s.Knots)
330 r.Post("/init/{domain}", s.InitKnotServer)
331 r.Post("/key", s.RegistrationKey)
332 })
333
334 r.Group(func(r chi.Router) {
335 r.Use(AuthMiddleware(s))
336 r.Get("/settings", s.Settings)
337 r.Put("/settings/keys", s.Keys)
338 })
339
340 return r
341}