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 key = strings.TrimSpace(key)
205 name := r.FormValue("name")
206 client, _ := s.auth.AuthorizedClient(r)
207
208 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
209 if err != nil {
210 log.Printf("parsing public key: %s", err)
211 return
212 }
213
214 if err := s.db.AddPublicKey(did, name, key); err != nil {
215 log.Printf("adding public key: %s", err)
216 return
217 }
218
219 // store in pds too
220 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
221 Collection: tangled.PublicKeyNSID,
222 Repo: did,
223 Rkey: s.TID(),
224 Record: &lexutil.LexiconTypeDecoder{
225 Val: &tangled.PublicKey{
226 Created: time.Now().Format(time.RFC3339),
227 Key: key,
228 Name: name,
229 }},
230 })
231 // invalid record
232 if err != nil {
233 log.Printf("failed to create record: %s", err)
234 return
235 }
236
237 log.Println("created atproto record: ", resp.Uri)
238
239 return
240 }
241}
242
243// create a signed request and check if a node responds to that
244func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
245 user := s.auth.GetUser(r)
246
247 domain := chi.URLParam(r, "domain")
248 if domain == "" {
249 http.Error(w, "malformed url", http.StatusBadRequest)
250 return
251 }
252 log.Println("checking ", domain)
253
254 secret, err := s.db.GetRegistrationKey(domain)
255 if err != nil {
256 log.Printf("no key found for domain %s: %s\n", domain, err)
257 return
258 }
259
260 client, err := NewSignedClient(domain, secret)
261 if err != nil {
262 log.Println("failed to create client to ", domain)
263 }
264
265 resp, err := client.Init(user.Did)
266 if err != nil {
267 w.Write([]byte("no dice"))
268 log.Println("domain was unreachable after 5 seconds")
269 return
270 }
271
272 if resp.StatusCode == http.StatusConflict {
273 log.Println("status conflict", resp.StatusCode)
274 w.Write([]byte("already registered, sorry!"))
275 return
276 }
277
278 if resp.StatusCode != http.StatusNoContent {
279 log.Println("status nok", resp.StatusCode)
280 w.Write([]byte("no dice"))
281 return
282 }
283
284 // verify response mac
285 signature := resp.Header.Get("X-Signature")
286 signatureBytes, err := hex.DecodeString(signature)
287 if err != nil {
288 return
289 }
290
291 expectedMac := hmac.New(sha256.New, []byte(secret))
292 expectedMac.Write([]byte("ok"))
293
294 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
295 log.Printf("response body signature mismatch: %x\n", signatureBytes)
296 return
297 }
298
299 // mark as registered
300 err = s.db.Register(domain)
301 if err != nil {
302 log.Println("failed to register domain", err)
303 http.Error(w, err.Error(), http.StatusInternalServerError)
304 return
305 }
306
307 // set permissions for this did as owner
308 reg, err := s.db.RegistrationByDomain(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 // add basic acls for this domain
316 err = s.enforcer.AddDomain(domain)
317 if err != nil {
318 log.Println("failed to setup owner of domain", err)
319 http.Error(w, err.Error(), http.StatusInternalServerError)
320 return
321 }
322
323 // add this did as owner of this domain
324 err = s.enforcer.AddOwner(domain, reg.ByDid)
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 w.Write([]byte("check success"))
332}
333
334func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
335 domain := chi.URLParam(r, "domain")
336 if domain == "" {
337 http.Error(w, "malformed url", http.StatusBadRequest)
338 return
339 }
340
341 user := s.auth.GetUser(r)
342 reg, err := s.db.RegistrationByDomain(domain)
343 if err != nil {
344 w.Write([]byte("failed to pull up registration info"))
345 return
346 }
347
348 var members []string
349 if reg.Registered != nil {
350 members, err = s.enforcer.GetUserByRole("server:member", domain)
351 if err != nil {
352 w.Write([]byte("failed to fetch member list"))
353 return
354 }
355 }
356
357 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
358 isOwner := err == nil && ok
359
360 p := pages.KnotParams{
361 User: user,
362 Registration: reg,
363 Members: members,
364 IsOwner: isOwner,
365 }
366
367 s.pages.Knot(w, p)
368}
369
370// get knots registered by this user
371func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
372 // for now, this is just pubkeys
373 user := s.auth.GetUser(r)
374 registrations, err := s.db.RegistrationsByDid(user.Did)
375 if err != nil {
376 log.Println(err)
377 }
378
379 s.pages.Knots(w, pages.KnotsParams{
380 User: user,
381 Registrations: registrations,
382 })
383}
384
385// list members of domain, requires auth and requires owner status
386func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
387 domain := chi.URLParam(r, "domain")
388 if domain == "" {
389 http.Error(w, "malformed url", http.StatusBadRequest)
390 return
391 }
392
393 // list all members for this domain
394 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
395 if err != nil {
396 w.Write([]byte("failed to fetch member list"))
397 return
398 }
399
400 w.Write([]byte(strings.Join(memberDids, "\n")))
401 return
402}
403
404// add member to domain, requires auth and requires invite access
405func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
406 domain := chi.URLParam(r, "domain")
407 if domain == "" {
408 http.Error(w, "malformed url", http.StatusBadRequest)
409 return
410 }
411
412 memberDid := r.FormValue("member")
413 if memberDid == "" {
414 http.Error(w, "malformed form", http.StatusBadRequest)
415 return
416 }
417
418 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid)
419 if err != nil {
420 w.Write([]byte("failed to resolve member did to a handle"))
421 return
422 }
423 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
424
425 // announce this relation into the firehose, store into owners' pds
426 client, _ := s.auth.AuthorizedClient(r)
427 currentUser := s.auth.GetUser(r)
428 addedAt := time.Now().Format(time.RFC3339)
429 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
430 Collection: tangled.KnotMemberNSID,
431 Repo: currentUser.Did,
432 Rkey: s.TID(),
433 Record: &lexutil.LexiconTypeDecoder{
434 Val: &tangled.KnotMember{
435 Member: memberIdent.DID.String(),
436 Domain: domain,
437 AddedAt: &addedAt,
438 }},
439 })
440 // invalid record
441 if err != nil {
442 log.Printf("failed to create record: %s", err)
443 return
444 }
445 log.Println("created atproto record: ", resp.Uri)
446
447 secret, err := s.db.GetRegistrationKey(domain)
448 if err != nil {
449 log.Printf("no key found for domain %s: %s\n", domain, err)
450 return
451 }
452
453 ksClient, err := NewSignedClient(domain, secret)
454 if err != nil {
455 log.Println("failed to create client to ", domain)
456 return
457 }
458
459 ksResp, err := ksClient.AddMember(memberIdent.DID.String())
460 if err != nil {
461 log.Printf("failed to make request to %s: %s", domain, err)
462 }
463
464 if ksResp.StatusCode != http.StatusNoContent {
465 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
466 return
467 }
468
469 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
470 if err != nil {
471 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
472 return
473 }
474
475 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
476}
477
478func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
479}
480
481func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
482 switch r.Method {
483 case http.MethodGet:
484 s.pages.NewRepo(w, pages.NewRepoParams{
485 User: s.auth.GetUser(r),
486 })
487 case http.MethodPost:
488 user := s.auth.GetUser(r)
489
490 domain := r.FormValue("domain")
491 if domain == "" {
492 log.Println("invalid form")
493 return
494 }
495
496 repoName := r.FormValue("name")
497 if repoName == "" {
498 log.Println("invalid form")
499 return
500 }
501
502 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
503 if err != nil || !ok {
504 w.Write([]byte("domain inaccessible to you"))
505 return
506 }
507
508 secret, err := s.db.GetRegistrationKey(domain)
509 if err != nil {
510 log.Printf("no key found for domain %s: %s\n", domain, err)
511 return
512 }
513
514 client, err := NewSignedClient(domain, secret)
515 if err != nil {
516 log.Println("failed to create client to ", domain)
517 }
518
519 resp, err := client.NewRepo(user.Did, repoName)
520 if err != nil {
521 log.Println("failed to send create repo request", err)
522 return
523 }
524 if resp.StatusCode != http.StatusNoContent {
525 log.Println("server returned ", resp.StatusCode)
526 return
527 }
528
529 // add to local db
530 repo := &db.Repo{
531 Did: user.Did,
532 Name: repoName,
533 Knot: domain,
534 }
535 err = s.db.AddRepo(repo)
536 if err != nil {
537 log.Println("failed to add repo to db", err)
538 return
539 }
540
541 // acls
542 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
543 if err != nil {
544 log.Println("failed to set up acls", err)
545 return
546 }
547
548 w.Write([]byte("created!"))
549 }
550}
551
552func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
553 didOrHandle := chi.URLParam(r, "user")
554 if didOrHandle == "" {
555 http.Error(w, "Bad request", http.StatusBadRequest)
556 return
557 }
558
559 ident, err := auth.ResolveIdent(r.Context(), didOrHandle)
560 if err != nil {
561 log.Printf("resolving identity: %s", err)
562 w.WriteHeader(http.StatusNotFound)
563 return
564 }
565
566 repos, err := s.db.GetAllReposByDid(ident.DID.String())
567 if err != nil {
568 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
569 }
570
571 s.pages.ProfilePage(w, pages.ProfilePageParams{
572 LoggedInUser: s.auth.GetUser(r),
573 UserDid: ident.DID.String(),
574 UserHandle: ident.Handle.String(),
575 Repos: repos,
576 })
577}
578
579func (s *State) Router() http.Handler {
580 router := chi.NewRouter()
581
582 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
583 pat := chi.URLParam(r, "*")
584 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
585 s.UserRouter().ServeHTTP(w, r)
586 } else {
587 s.StandardRouter().ServeHTTP(w, r)
588 }
589 })
590
591 return router
592}
593
594func (s *State) UserRouter() http.Handler {
595 r := chi.NewRouter()
596
597 // strip @ from user
598 r.Use(StripLeadingAt)
599
600 r.With(ResolveIdent).Route("/{user}", func(r chi.Router) {
601 r.Get("/", s.ProfilePage)
602 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
603 r.Get("/", s.RepoIndex)
604 r.Get("/log/{ref}", s.RepoLog)
605 r.Route("/tree/{ref}", func(r chi.Router) {
606 r.Get("/*", s.RepoTree)
607 })
608 r.Get("/commit/{ref}", s.RepoCommit)
609
610 // These routes get proxied to the knot
611 r.Get("/info/refs", s.InfoRefs)
612 r.Post("/git-upload-pack", s.UploadPack)
613
614 })
615 })
616
617 return r
618}
619
620func (s *State) StandardRouter() http.Handler {
621 r := chi.NewRouter()
622
623 r.Get("/", s.Timeline)
624
625 r.Get("/login", s.Login)
626 r.Post("/login", s.Login)
627
628 r.Route("/knots", func(r chi.Router) {
629 r.Use(AuthMiddleware(s))
630 r.Get("/", s.Knots)
631 r.Post("/key", s.RegistrationKey)
632
633 r.Route("/{domain}", func(r chi.Router) {
634 r.Post("/init", s.InitKnotServer)
635 r.Get("/", s.KnotServerInfo)
636 r.Route("/member", func(r chi.Router) {
637 r.Use(RoleMiddleware(s, "server:owner"))
638 r.Get("/", s.ListMembers)
639 r.Put("/", s.AddMember)
640 r.Delete("/", s.RemoveMember)
641 })
642 })
643 })
644
645 r.Route("/repo", func(r chi.Router) {
646 r.Route("/new", func(r chi.Router) {
647 r.Get("/", s.AddRepo)
648 r.Post("/", s.AddRepo)
649 })
650 // r.Post("/import", s.ImportRepo)
651 })
652
653 r.Route("/settings", func(r chi.Router) {
654 r.Use(AuthMiddleware(s))
655 r.Get("/", s.Settings)
656 r.Put("/keys", s.SettingsKeys)
657 })
658
659 r.Get("/keys/{user}", s.Keys)
660
661 return r
662}