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