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 "strings"
11 "time"
12
13 comatproto "github.com/bluesky-social/indigo/api/atproto"
14 lexutil "github.com/bluesky-social/indigo/lex/util"
15 "github.com/gliderlabs/ssh"
16 "github.com/go-chi/chi/v5"
17 "github.com/google/uuid"
18 tangled "github.com/sotangled/tangled/api/tangled"
19 "github.com/sotangled/tangled/appview"
20 "github.com/sotangled/tangled/appview/auth"
21 "github.com/sotangled/tangled/appview/db"
22 "github.com/sotangled/tangled/appview/pages"
23)
24
25type State struct {
26 db *db.DB
27 auth *auth.Auth
28 enforcer *Enforcer
29}
30
31func Make() (*State, error) {
32 db, err := db.Make("appview.db")
33 if err != nil {
34 return nil, err
35 }
36
37 auth, err := auth.Make()
38 if err != nil {
39 return nil, err
40 }
41
42 enforcer, err := NewEnforcer()
43 if err != nil {
44 return nil, err
45 }
46
47 return &State{db, auth, enforcer}, nil
48}
49
50func (s *State) Login(w http.ResponseWriter, r *http.Request) {
51 ctx := r.Context()
52
53 switch r.Method {
54 case http.MethodGet:
55 pages.Login(w, pages.LoginParams{})
56 return
57 case http.MethodPost:
58 handle := r.FormValue("handle")
59 appPassword := r.FormValue("app_password")
60
61 fmt.Println("handle", handle)
62 fmt.Println("app_password", appPassword)
63
64 resolved, err := auth.ResolveIdent(ctx, handle)
65 if err != nil {
66 log.Printf("resolving identity: %s", err)
67 http.Redirect(w, r, "/login", http.StatusSeeOther)
68 return
69 }
70
71 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
72 if err != nil {
73 log.Printf("creating initial session: %s", err)
74 return
75 }
76 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
77
78 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
79 if err != nil {
80 log.Printf("storing session: %s", err)
81 return
82 }
83
84 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
85 http.Redirect(w, r, "/", http.StatusSeeOther)
86 return
87 }
88}
89
90func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
91 user := s.auth.GetUser(r)
92 pages.Timeline(w, pages.TimelineParams{
93 User: user,
94 })
95 return
96}
97
98// requires auth
99func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
100 switch r.Method {
101 case http.MethodGet:
102 // list open registrations under this did
103
104 return
105 case http.MethodPost:
106 session, err := s.auth.Store.Get(r, appview.SessionName)
107 if err != nil || session.IsNew {
108 log.Println("unauthorized attempt to generate registration key")
109 http.Error(w, "Forbidden", http.StatusUnauthorized)
110 return
111 }
112
113 did := session.Values[appview.SessionDid].(string)
114
115 // check if domain is valid url, and strip extra bits down to just host
116 domain := r.FormValue("domain")
117 if domain == "" {
118 http.Error(w, "Invalid form", http.StatusBadRequest)
119 return
120 }
121
122 key, err := s.db.GenerateRegistrationKey(domain, did)
123
124 if err != nil {
125 log.Println(err)
126 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
127 return
128 }
129
130 w.Write([]byte(key))
131 }
132}
133
134func (s *State) Settings(w http.ResponseWriter, r *http.Request) {
135 // for now, this is just pubkeys
136 user := s.auth.GetUser(r)
137 pubKeys, err := s.db.GetPublicKeys(user.Did)
138 if err != nil {
139 log.Println(err)
140 }
141
142 pages.Settings(w, pages.SettingsParams{
143 User: user,
144 PubKeys: pubKeys,
145 })
146}
147
148func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
149 switch r.Method {
150 case http.MethodGet:
151 w.Write([]byte("unimplemented"))
152 log.Println("unimplemented")
153 return
154 case http.MethodPut:
155 did := s.auth.GetDID(r)
156 key := r.FormValue("key")
157 name := r.FormValue("name")
158 client, _ := s.auth.AuthorizedClient(r)
159
160 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
161 if err != nil {
162 log.Printf("parsing public key: %s", err)
163 return
164 }
165
166 if err := s.db.AddPublicKey(did, name, key); err != nil {
167 log.Printf("adding public key: %s", err)
168 return
169 }
170
171 // store in pds too
172 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
173 Collection: tangled.PublicKeyNSID,
174 Repo: did,
175 Rkey: uuid.New().String(),
176 Record: &lexutil.LexiconTypeDecoder{Val: &tangled.PublicKey{
177 Created: time.Now().String(),
178 Key: key,
179 Name: name,
180 }},
181 })
182
183 // invalid record
184 if err != nil {
185 log.Printf("failed to create record: %s", err)
186 return
187 }
188
189 log.Println("created atproto record: ", resp.Uri)
190
191 return
192 }
193}
194
195// create a signed request and check if a node responds to that
196func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
197 domain := chi.URLParam(r, "domain")
198 if domain == "" {
199 http.Error(w, "malformed url", http.StatusBadRequest)
200 return
201 }
202
203 log.Println("checking ", domain)
204
205 secret, err := s.db.GetRegistrationKey(domain)
206 if err != nil {
207 log.Printf("no key found for domain %s: %s\n", domain, err)
208 return
209 }
210 log.Println("has secret ", secret)
211
212 // make a request do the knotserver with an empty body and above signature
213 url := fmt.Sprintf("http://%s/health", domain)
214
215 pingRequest, err := buildPingRequest(url, secret)
216 if err != nil {
217 log.Println("failed to build ping request", err)
218 return
219 }
220
221 client := &http.Client{
222 Timeout: 5 * time.Second,
223 }
224 resp, err := client.Do(pingRequest)
225 if err != nil {
226 w.Write([]byte("no dice"))
227 log.Println("domain was unreachable after 5 seconds")
228 return
229 }
230
231 if resp.StatusCode != http.StatusOK {
232 log.Println("status nok", resp.StatusCode)
233 w.Write([]byte("no dice"))
234 return
235 }
236
237 // verify response mac
238 signature := resp.Header.Get("X-Signature")
239 signatureBytes, err := hex.DecodeString(signature)
240 if err != nil {
241 return
242 }
243
244 expectedMac := hmac.New(sha256.New, []byte(secret))
245 expectedMac.Write([]byte("ok"))
246
247 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
248 log.Printf("response body signature mismatch: %x\n", signatureBytes)
249 return
250 }
251
252 // mark as registered
253 err = s.db.Register(domain)
254 if err != nil {
255 log.Println("failed to register domain", err)
256 http.Error(w, err.Error(), http.StatusInternalServerError)
257 return
258 }
259
260 // set permissions for this did as owner
261 reg, err := s.db.RegistrationByDomain(domain)
262 if err != nil {
263 log.Println("failed to register domain", err)
264 http.Error(w, err.Error(), http.StatusInternalServerError)
265 return
266 }
267
268 // add basic acls for this domain
269 err = s.enforcer.AddDomain(domain)
270 if err != nil {
271 log.Println("failed to setup owner of domain", err)
272 http.Error(w, err.Error(), http.StatusInternalServerError)
273 return
274 }
275
276 // add this did as owner of this domain
277 err = s.enforcer.AddOwner(domain, reg.ByDid)
278 if err != nil {
279 log.Println("failed to setup owner of domain", err)
280 http.Error(w, err.Error(), http.StatusInternalServerError)
281 return
282 }
283
284 w.Write([]byte("check success"))
285}
286
287func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
288 domain := chi.URLParam(r, "domain")
289 if domain == "" {
290 http.Error(w, "malformed url", http.StatusBadRequest)
291 return
292 }
293
294 user := s.auth.GetUser(r)
295 reg, err := s.db.RegistrationByDomain(domain)
296 if err != nil {
297 w.Write([]byte("failed to pull up registration info"))
298 return
299 }
300
301 var members []string
302 if reg.Registered != nil {
303 members, err = s.enforcer.E.GetUsersForRole("server:member", domain)
304 if err != nil {
305 w.Write([]byte("failed to fetch member list"))
306 return
307 }
308 }
309
310 ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain)
311 isOwner := err == nil && ok
312
313 p := pages.KnotParams{
314 User: user,
315 Registration: reg,
316 Members: members,
317 IsOwner: isOwner,
318 }
319
320 pages.Knot(w, p)
321}
322
323// get knots registered by this user
324func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
325 // for now, this is just pubkeys
326 user := s.auth.GetUser(r)
327 registrations, err := s.db.RegistrationsByDid(user.Did)
328 if err != nil {
329 log.Println(err)
330 }
331
332 pages.Knots(w, pages.KnotsParams{
333 User: user,
334 Registrations: registrations,
335 })
336}
337
338// list members of domain, requires auth and requires owner status
339func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
340 domain := chi.URLParam(r, "domain")
341 if domain == "" {
342 http.Error(w, "malformed url", http.StatusBadRequest)
343 return
344 }
345
346 // list all members for this domain
347 memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain)
348 if err != nil {
349 w.Write([]byte("failed to fetch member list"))
350 return
351 }
352
353 w.Write([]byte(strings.Join(memberDids, "\n")))
354 return
355}
356
357// add member to domain, requires auth and requires invite access
358func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
359 domain := chi.URLParam(r, "domain")
360 if domain == "" {
361 http.Error(w, "malformed url", http.StatusBadRequest)
362 return
363 }
364
365 memberDid := r.FormValue("member")
366 if memberDid == "" {
367 http.Error(w, "malformed form", http.StatusBadRequest)
368 return
369 }
370
371 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid)
372 if err != nil {
373 w.Write([]byte("failed to resolve member did to a handle"))
374 return
375 }
376
377 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
378
379 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
380 if err != nil {
381 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
382 return
383 }
384
385 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
386}
387
388// list members of domain, requires auth and requires owner status
389func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
390}
391
392func buildPingRequest(url, secret string) (*http.Request, error) {
393 pingRequest, err := http.NewRequest("GET", url, nil)
394 if err != nil {
395 return nil, err
396 }
397
398 timestamp := time.Now().Format(time.RFC3339)
399 mac := hmac.New(sha256.New, []byte(secret))
400 message := pingRequest.Method + pingRequest.URL.Path + timestamp
401 mac.Write([]byte(message))
402 signature := hex.EncodeToString(mac.Sum(nil))
403
404 pingRequest.Header.Set("X-Signature", signature)
405 pingRequest.Header.Set("X-Timestamp", timestamp)
406
407 return pingRequest, nil
408}
409
410func (s *State) Router() http.Handler {
411 r := chi.NewRouter()
412
413 r.Get("/", s.Timeline)
414
415 r.Get("/login", s.Login)
416 r.Post("/login", s.Login)
417
418 r.Route("/knots", func(r chi.Router) {
419 r.Use(AuthMiddleware(s))
420 r.Get("/", s.Knots)
421 r.Post("/key", s.RegistrationKey)
422
423 r.Route("/{domain}", func(r chi.Router) {
424 r.Get("/", s.KnotServerInfo)
425 r.Post("/init", s.InitKnotServer)
426 r.Route("/member", func(r chi.Router) {
427 r.Use(RoleMiddleware(s, "server:owner"))
428 r.Get("/", s.ListMembers)
429 r.Put("/", s.AddMember)
430 r.Delete("/", s.RemoveMember)
431 })
432 })
433 })
434
435 r.Group(func(r chi.Router) {
436 r.Use(AuthMiddleware(s))
437 r.Get("/settings", s.Settings)
438 r.Put("/settings/keys", s.Keys)
439 })
440
441 return r
442}