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 "path/filepath"
11 "strings"
12 "time"
13
14 comatproto "github.com/bluesky-social/indigo/api/atproto"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 lexutil "github.com/bluesky-social/indigo/lex/util"
17 "github.com/gliderlabs/ssh"
18 "github.com/go-chi/chi/v5"
19 tangled "github.com/sotangled/tangled/api/tangled"
20 "github.com/sotangled/tangled/appview"
21 "github.com/sotangled/tangled/appview/auth"
22 "github.com/sotangled/tangled/appview/db"
23 "github.com/sotangled/tangled/appview/pages"
24 "github.com/sotangled/tangled/rbac"
25)
26
27type State struct {
28 db *db.DB
29 auth *auth.Auth
30 enforcer *rbac.Enforcer
31 tidClock *syntax.TIDClock
32 pages *pages.Pages
33}
34
35func Make() (*State, error) {
36 db, err := db.Make(appview.SqliteDbPath)
37 if err != nil {
38 return nil, err
39 }
40
41 auth, err := auth.Make()
42 if err != nil {
43 return nil, err
44 }
45
46 enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath)
47 if err != nil {
48 return nil, err
49 }
50
51 clock := syntax.NewTIDClock(0)
52
53 pgs := pages.NewPages()
54
55 return &State{db, auth, enforcer, clock, pgs}, nil
56}
57
58func (s *State) TID() string {
59 return s.tidClock.Next().String()
60}
61
62func (s *State) Login(w http.ResponseWriter, r *http.Request) {
63 ctx := r.Context()
64
65 switch r.Method {
66 case http.MethodGet:
67 err := s.pages.Login(w, pages.LoginParams{})
68 if err != nil {
69 log.Printf("rendering login page: %s", err)
70 }
71 return
72 case http.MethodPost:
73 handle := r.FormValue("handle")
74 appPassword := r.FormValue("app_password")
75
76 fmt.Println("handle", handle)
77 fmt.Println("app_password", appPassword)
78
79 resolved, err := auth.ResolveIdent(ctx, handle)
80 if err != nil {
81 log.Printf("resolving identity: %s", err)
82 http.Redirect(w, r, "/login", http.StatusSeeOther)
83 return
84 }
85
86 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
87 if err != nil {
88 log.Printf("creating initial session: %s", err)
89 return
90 }
91 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
92
93 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
94 if err != nil {
95 log.Printf("storing session: %s", err)
96 return
97 }
98
99 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
100 http.Redirect(w, r, "/", http.StatusSeeOther)
101 return
102 }
103}
104
105func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
106 user := s.auth.GetUser(r)
107 s.pages.Timeline(w, pages.TimelineParams{
108 User: user,
109 })
110 return
111}
112
113// requires auth
114func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
115 switch r.Method {
116 case http.MethodGet:
117 // list open registrations under this did
118
119 return
120 case http.MethodPost:
121 session, err := s.auth.Store.Get(r, appview.SessionName)
122 if err != nil || session.IsNew {
123 log.Println("unauthorized attempt to generate registration key")
124 http.Error(w, "Forbidden", http.StatusUnauthorized)
125 return
126 }
127
128 did := session.Values[appview.SessionDid].(string)
129
130 // check if domain is valid url, and strip extra bits down to just host
131 domain := r.FormValue("domain")
132 if domain == "" {
133 http.Error(w, "Invalid form", http.StatusBadRequest)
134 return
135 }
136
137 key, err := s.db.GenerateRegistrationKey(domain, did)
138
139 if err != nil {
140 log.Println(err)
141 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
142 return
143 }
144
145 w.Write([]byte(key))
146 }
147}
148
149func (s *State) Settings(w http.ResponseWriter, r *http.Request) {
150 // for now, this is just pubkeys
151 user := s.auth.GetUser(r)
152 pubKeys, err := s.db.GetPublicKeys(user.Did)
153 if err != nil {
154 log.Println(err)
155 }
156
157 s.pages.Settings(w, pages.SettingsParams{
158 User: user,
159 PubKeys: pubKeys,
160 })
161}
162
163func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
164 user := chi.URLParam(r, "user")
165 user = strings.TrimPrefix(user, "@")
166
167 if user == "" {
168 w.WriteHeader(http.StatusBadRequest)
169 return
170 }
171
172 id, err := auth.ResolveIdent(r.Context(), user)
173 if err != nil {
174 w.WriteHeader(http.StatusInternalServerError)
175 return
176 }
177
178 pubKeys, err := s.db.GetPublicKeys(id.DID.String())
179 if err != nil {
180 w.WriteHeader(http.StatusNotFound)
181 return
182 }
183
184 if len(pubKeys) == 0 {
185 w.WriteHeader(http.StatusNotFound)
186 return
187 }
188
189 for _, k := range pubKeys {
190 key := strings.TrimRight(k.Key, "\n")
191 w.Write([]byte(fmt.Sprintln(key)))
192 }
193}
194
195func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) {
196 switch r.Method {
197 case http.MethodGet:
198 w.Write([]byte("unimplemented"))
199 log.Println("unimplemented")
200 return
201 case http.MethodPut:
202 did := s.auth.GetDid(r)
203 key := r.FormValue("key")
204 name := r.FormValue("name")
205 client, _ := s.auth.AuthorizedClient(r)
206
207 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
208 if err != nil {
209 log.Printf("parsing public key: %s", err)
210 return
211 }
212
213 if err := s.db.AddPublicKey(did, name, key); err != nil {
214 log.Printf("adding public key: %s", err)
215 return
216 }
217
218 // store in pds too
219 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
220 Collection: tangled.PublicKeyNSID,
221 Repo: did,
222 Rkey: s.TID(),
223 Record: &lexutil.LexiconTypeDecoder{
224 Val: &tangled.PublicKey{
225 Created: time.Now().Format(time.RFC3339),
226 Key: key,
227 Name: name,
228 }},
229 })
230 // invalid record
231 if err != nil {
232 log.Printf("failed to create record: %s", err)
233 return
234 }
235
236 log.Println("created atproto record: ", resp.Uri)
237
238 return
239 }
240}
241
242// create a signed request and check if a node responds to that
243func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
244 user := s.auth.GetUser(r)
245
246 domain := chi.URLParam(r, "domain")
247 if domain == "" {
248 http.Error(w, "malformed url", http.StatusBadRequest)
249 return
250 }
251 log.Println("checking ", domain)
252
253 secret, err := s.db.GetRegistrationKey(domain)
254 if err != nil {
255 log.Printf("no key found for domain %s: %s\n", domain, err)
256 return
257 }
258
259 client, err := NewSignedClient(domain, secret)
260 if err != nil {
261 log.Println("failed to create client to ", domain)
262 }
263
264 resp, err := client.Init(user.Did, []string{})
265 if err != nil {
266 w.Write([]byte("no dice"))
267 log.Println("domain was unreachable after 5 seconds")
268 return
269 }
270
271 if resp.StatusCode == http.StatusConflict {
272 log.Println("status conflict", resp.StatusCode)
273 w.Write([]byte("already registered, sorry!"))
274 return
275 }
276
277 if resp.StatusCode != http.StatusNoContent {
278 log.Println("status nok", resp.StatusCode)
279 w.Write([]byte("no dice"))
280 return
281 }
282
283 // verify response mac
284 signature := resp.Header.Get("X-Signature")
285 signatureBytes, err := hex.DecodeString(signature)
286 if err != nil {
287 return
288 }
289
290 expectedMac := hmac.New(sha256.New, []byte(secret))
291 expectedMac.Write([]byte("ok"))
292
293 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
294 log.Printf("response body signature mismatch: %x\n", signatureBytes)
295 return
296 }
297
298 // mark as registered
299 err = s.db.Register(domain)
300 if err != nil {
301 log.Println("failed to register domain", err)
302 http.Error(w, err.Error(), http.StatusInternalServerError)
303 return
304 }
305
306 // set permissions for this did as owner
307 reg, err := s.db.RegistrationByDomain(domain)
308 if err != nil {
309 log.Println("failed to register domain", err)
310 http.Error(w, err.Error(), http.StatusInternalServerError)
311 return
312 }
313
314 // add basic acls for this domain
315 err = s.enforcer.AddDomain(domain)
316 if err != nil {
317 log.Println("failed to setup owner of domain", err)
318 http.Error(w, err.Error(), http.StatusInternalServerError)
319 return
320 }
321
322 // add this did as owner of this domain
323 err = s.enforcer.AddOwner(domain, reg.ByDid)
324 if err != nil {
325 log.Println("failed to setup owner of domain", err)
326 http.Error(w, err.Error(), http.StatusInternalServerError)
327 return
328 }
329
330 w.Write([]byte("check success"))
331}
332
333func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
334 domain := chi.URLParam(r, "domain")
335 if domain == "" {
336 http.Error(w, "malformed url", http.StatusBadRequest)
337 return
338 }
339
340 user := s.auth.GetUser(r)
341 reg, err := s.db.RegistrationByDomain(domain)
342 if err != nil {
343 w.Write([]byte("failed to pull up registration info"))
344 return
345 }
346
347 var members []string
348 if reg.Registered != nil {
349 members, err = s.enforcer.GetUserByRole("server:member", domain)
350 if err != nil {
351 w.Write([]byte("failed to fetch member list"))
352 return
353 }
354 }
355
356 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
357 isOwner := err == nil && ok
358
359 p := pages.KnotParams{
360 User: user,
361 Registration: reg,
362 Members: members,
363 IsOwner: isOwner,
364 }
365
366 s.pages.Knot(w, p)
367}
368
369// get knots registered by this user
370func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
371 // for now, this is just pubkeys
372 user := s.auth.GetUser(r)
373 registrations, err := s.db.RegistrationsByDid(user.Did)
374 if err != nil {
375 log.Println(err)
376 }
377
378 s.pages.Knots(w, pages.KnotsParams{
379 User: user,
380 Registrations: registrations,
381 })
382}
383
384// list members of domain, requires auth and requires owner status
385func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
386 domain := chi.URLParam(r, "domain")
387 if domain == "" {
388 http.Error(w, "malformed url", http.StatusBadRequest)
389 return
390 }
391
392 // list all members for this domain
393 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
394 if err != nil {
395 w.Write([]byte("failed to fetch member list"))
396 return
397 }
398
399 w.Write([]byte(strings.Join(memberDids, "\n")))
400 return
401}
402
403// add member to domain, requires auth and requires invite access
404func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
405 domain := chi.URLParam(r, "domain")
406 if domain == "" {
407 http.Error(w, "malformed url", http.StatusBadRequest)
408 return
409 }
410
411 memberDid := r.FormValue("member")
412 if memberDid == "" {
413 http.Error(w, "malformed form", http.StatusBadRequest)
414 return
415 }
416
417 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid)
418 if err != nil {
419 w.Write([]byte("failed to resolve member did to a handle"))
420 return
421 }
422 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
423
424 // announce this relation into the firehose, store into owners' pds
425 client, _ := s.auth.AuthorizedClient(r)
426 currentUser := s.auth.GetUser(r)
427 addedAt := time.Now().Format(time.RFC3339)
428 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
429 Collection: tangled.KnotMemberNSID,
430 Repo: currentUser.Did,
431 Rkey: s.TID(),
432 Record: &lexutil.LexiconTypeDecoder{
433 Val: &tangled.KnotMember{
434 Member: memberIdent.DID.String(),
435 Domain: domain,
436 AddedAt: &addedAt,
437 }},
438 })
439 // invalid record
440 if err != nil {
441 log.Printf("failed to create record: %s", err)
442 return
443 }
444 log.Println("created atproto record: ", resp.Uri)
445
446 secret, err := s.db.GetRegistrationKey(domain)
447 if err != nil {
448 log.Printf("no key found for domain %s: %s\n", domain, err)
449 return
450 }
451
452 ksClient, err := NewSignedClient(domain, secret)
453 if err != nil {
454 log.Println("failed to create client to ", domain)
455 return
456 }
457
458 ksResp, err := ksClient.AddMember(memberIdent.DID.String(), []string{})
459 if err != nil {
460 log.Printf("failet to make request to %s: %s", domain, err)
461 }
462
463 if ksResp.StatusCode != http.StatusNoContent {
464 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
465 return
466 }
467
468 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
469 if err != nil {
470 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
471 return
472 }
473
474 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
475}
476
477func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
478}
479
480func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
481 switch r.Method {
482 case http.MethodGet:
483 s.pages.NewRepo(w, pages.NewRepoParams{
484 User: s.auth.GetUser(r),
485 })
486 case http.MethodPost:
487 user := s.auth.GetUser(r)
488
489 domain := r.FormValue("domain")
490 if domain == "" {
491 log.Println("invalid form")
492 return
493 }
494
495 repoName := r.FormValue("name")
496 if repoName == "" {
497 log.Println("invalid form")
498 return
499 }
500
501 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
502 if err != nil || !ok {
503 w.Write([]byte("domain inaccessible to you"))
504 return
505 }
506
507 secret, err := s.db.GetRegistrationKey(domain)
508 if err != nil {
509 log.Printf("no key found for domain %s: %s\n", domain, err)
510 return
511 }
512
513 client, err := NewSignedClient(domain, secret)
514 if err != nil {
515 log.Println("failed to create client to ", domain)
516 }
517
518 resp, err := client.NewRepo(user.Did, repoName)
519 if err != nil {
520 log.Println("failed to send create repo request", err)
521 return
522 }
523 if resp.StatusCode != http.StatusNoContent {
524 log.Println("server returned ", resp.StatusCode)
525 return
526 }
527
528 // add to local db
529 repo := &db.Repo{
530 Did: user.Did,
531 Name: repoName,
532 Knot: domain,
533 }
534 err = s.db.AddRepo(repo)
535 if err != nil {
536 log.Println("failed to add repo to db", err)
537 return
538 }
539
540 // acls
541 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
542 if err != nil {
543 log.Println("failed to set up acls", err)
544 return
545 }
546
547 w.Write([]byte("created!"))
548 }
549}
550
551func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
552 didOrHandle := chi.URLParam(r, "user")
553 if didOrHandle == "" {
554 http.Error(w, "Bad request", http.StatusBadRequest)
555 return
556 }
557
558 ident, err := auth.ResolveIdent(r.Context(), didOrHandle)
559 if err != nil {
560 log.Printf("resolving identity: %s", err)
561 w.WriteHeader(http.StatusNotFound)
562 return
563 }
564
565 repos, err := s.db.GetAllReposByDid(ident.DID.String())
566 if err != nil {
567 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
568 }
569
570 s.pages.ProfilePage(w, pages.ProfilePageParams{
571 LoggedInUser: s.auth.GetUser(r),
572 UserDid: ident.DID.String(),
573 UserHandle: ident.Handle.String(),
574 Repos: repos,
575 })
576}
577
578func (s *State) Router() http.Handler {
579 router := chi.NewRouter()
580
581 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
582 pat := chi.URLParam(r, "*")
583 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
584 s.UserRouter().ServeHTTP(w, r)
585 } else {
586 s.StandardRouter().ServeHTTP(w, r)
587 }
588 })
589
590 return router
591}
592
593func (s *State) UserRouter() http.Handler {
594 r := chi.NewRouter()
595
596 // strip @ from user
597 r.Use(StripLeadingAt)
598
599 r.Route("/{user}", func(r chi.Router) {
600 r.Get("/", s.ProfilePage)
601 })
602
603 return r
604}
605
606func (s *State) StandardRouter() http.Handler {
607 r := chi.NewRouter()
608
609 r.Get("/", s.Timeline)
610
611 r.Get("/login", s.Login)
612 r.Post("/login", s.Login)
613
614 r.Route("/knots", func(r chi.Router) {
615 r.Use(AuthMiddleware(s))
616 r.Get("/", s.Knots)
617 r.Post("/key", s.RegistrationKey)
618
619 r.Route("/{domain}", func(r chi.Router) {
620 r.Post("/init", s.InitKnotServer)
621 r.Get("/", s.KnotServerInfo)
622 r.Route("/member", func(r chi.Router) {
623 r.Use(RoleMiddleware(s, "server:owner"))
624 r.Get("/", s.ListMembers)
625 r.Put("/", s.AddMember)
626 r.Delete("/", s.RemoveMember)
627 })
628 })
629 })
630
631 r.Route("/repo", func(r chi.Router) {
632 r.Route("/new", func(r chi.Router) {
633 r.Get("/", s.AddRepo)
634 r.Post("/", s.AddRepo)
635 })
636 // r.Post("/import", s.ImportRepo)
637 })
638
639 r.Route("/settings", func(r chi.Router) {
640 r.Use(AuthMiddleware(s))
641 r.Get("/", s.Settings)
642 r.Put("/keys", s.SettingsKeys)
643 })
644
645 r.Get("/keys/{user}", s.Keys)
646
647 return r
648}