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 pages.Timeline(w, pages.TimelineParams{
91 User: user,
92 })
93 return
94}
95
96// requires auth
97func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
98 switch r.Method {
99 case http.MethodGet:
100 // list open registrations under this did
101
102 return
103 case http.MethodPost:
104 session, err := s.auth.Store.Get(r, appview.SessionName)
105 if err != nil || session.IsNew {
106 log.Println("unauthorized attempt to generate registration key")
107 http.Error(w, "Forbidden", http.StatusUnauthorized)
108 return
109 }
110
111 did := session.Values[appview.SessionDid].(string)
112
113 // check if domain is valid url, and strip extra bits down to just host
114 domain := r.FormValue("domain")
115 if domain == "" {
116 http.Error(w, "Invalid form", http.StatusBadRequest)
117 return
118 }
119
120 key, err := s.db.GenerateRegistrationKey(domain, did)
121
122 if err != nil {
123 log.Println(err)
124 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
125 return
126 }
127
128 w.Write([]byte(key))
129 }
130}
131
132func (s *State) Settings(w http.ResponseWriter, r *http.Request) {
133 // for now, this is just pubkeys
134 user := s.auth.GetUser(r)
135 pubKeys, err := s.db.GetPublicKeys(user.Did)
136 if err != nil {
137 log.Println(err)
138 }
139
140 pages.Settings(w, pages.SettingsParams{
141 User: user,
142 PubKeys: pubKeys,
143 })
144}
145
146func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
147 switch r.Method {
148 case http.MethodGet:
149 w.Write([]byte("unimplemented"))
150 log.Println("unimplemented")
151 return
152 case http.MethodPut:
153 did := s.auth.GetDID(r)
154 key := r.FormValue("key")
155 name := r.FormValue("name")
156 client, _ := s.auth.AuthorizedClient(r)
157
158 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
159 if err != nil {
160 log.Printf("parsing public key: %s", err)
161 return
162 }
163
164 if err := s.db.AddPublicKey(did, name, key); err != nil {
165 log.Printf("adding public key: %s", err)
166 return
167 }
168
169 // store in pds too
170 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
171 Collection: tangled.PublicKeyNSID,
172 Repo: did,
173 Rkey: uuid.New().String(),
174 Record: &lexutil.LexiconTypeDecoder{Val: &tangled.PublicKey{
175 Created: time.Now().String(),
176 Key: key,
177 Name: name,
178 }},
179 })
180
181 // invalid record
182 if err != nil {
183 log.Printf("failed to create record: %s", err)
184 return
185 }
186
187 log.Println("created atproto record: ", resp.Uri)
188
189 return
190 }
191}
192
193// create a signed request and check if a node responds to that
194func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
195 domain := chi.URLParam(r, "domain")
196 if domain == "" {
197 http.Error(w, "malformed url", http.StatusBadRequest)
198 return
199 }
200
201 log.Println("checking ", domain)
202
203 secret, err := s.db.GetRegistrationKey(domain)
204 if err != nil {
205 log.Printf("no key found for domain %s: %s\n", domain, err)
206 return
207 }
208 log.Println("has secret ", secret)
209
210 // make a request do the knotserver with an empty body and above signature
211 url := fmt.Sprintf("http://%s/health", domain)
212
213 pingRequest, err := buildPingRequest(url, secret)
214 if err != nil {
215 log.Println("failed to build ping request", err)
216 return
217 }
218
219 client := &http.Client{
220 Timeout: 5 * time.Second,
221 }
222 resp, err := client.Do(pingRequest)
223 if err != nil {
224 w.Write([]byte("no dice"))
225 log.Println("domain was unreachable after 5 seconds")
226 return
227 }
228
229 if resp.StatusCode != http.StatusOK {
230 log.Println("status nok", resp.StatusCode)
231 w.Write([]byte("no dice"))
232 return
233 }
234
235 // verify response mac
236 signature := resp.Header.Get("X-Signature")
237 signatureBytes, err := hex.DecodeString(signature)
238 if err != nil {
239 return
240 }
241
242 expectedMac := hmac.New(sha256.New, []byte(secret))
243 expectedMac.Write([]byte("ok"))
244
245 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
246 log.Printf("response body signature mismatch: %x\n", signatureBytes)
247 return
248 }
249
250 // mark as registered
251 err = s.db.Register(domain)
252 if err != nil {
253 log.Println("failed to register domain", err)
254 http.Error(w, err.Error(), http.StatusInternalServerError)
255 return
256 }
257
258 // set permissions for this did as owner
259 reg, err := s.db.RegistrationByDomain(domain)
260 if err != nil {
261 log.Println("failed to register domain", err)
262 http.Error(w, err.Error(), http.StatusInternalServerError)
263 return
264 }
265
266 // add basic acls for this domain
267 err = s.enforcer.AddDomain(domain)
268 if err != nil {
269 log.Println("failed to setup owner of domain", err)
270 http.Error(w, err.Error(), http.StatusInternalServerError)
271 return
272 }
273
274 // add this did as owner of this domain
275 err = s.enforcer.AddOwner(domain, reg.ByDid)
276 if err != nil {
277 log.Println("failed to setup owner of domain", err)
278 http.Error(w, err.Error(), http.StatusInternalServerError)
279 return
280 }
281
282 w.Write([]byte("check success"))
283}
284
285// get knots registered by this user
286func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
287 // for now, this is just pubkeys
288 user := s.auth.GetUser(r)
289 registrations, err := s.db.RegistrationsByDid(user.Did)
290 if err != nil {
291 log.Println(err)
292 }
293
294 pages.Knots(w, pages.KnotsParams{
295 User: user,
296 Registrations: registrations,
297 })
298}
299
300func buildPingRequest(url, secret string) (*http.Request, error) {
301 pingRequest, err := http.NewRequest("GET", url, nil)
302 if err != nil {
303 return nil, err
304 }
305
306 timestamp := time.Now().Format(time.RFC3339)
307 mac := hmac.New(sha256.New, []byte(secret))
308 message := pingRequest.Method + pingRequest.URL.Path + timestamp
309 mac.Write([]byte(message))
310 signature := hex.EncodeToString(mac.Sum(nil))
311
312 pingRequest.Header.Set("X-Signature", signature)
313 pingRequest.Header.Set("X-Timestamp", timestamp)
314
315 return pingRequest, nil
316}
317
318func (s *State) Router() http.Handler {
319 r := chi.NewRouter()
320
321 r.Get("/", s.Timeline)
322
323 r.Get("/login", s.Login)
324 r.Post("/login", s.Login)
325
326 r.Route("/knots", func(r chi.Router) {
327 r.Use(AuthMiddleware(s))
328 r.Get("/", s.Knots)
329 r.Post("/init/{domain}", s.InitKnotServer)
330 r.Post("/key", s.RegistrationKey)
331 })
332
333 r.Group(func(r chi.Router) {
334 r.Use(AuthMiddleware(s))
335 r.Get("/settings", s.Settings)
336 r.Put("/settings/keys", s.Keys)
337 })
338
339 return r
340}