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