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 s.pages.NewRepo(w, pages.NewRepoParams{
494 LoggedInUser: s.auth.GetUser(r),
495 })
496 case http.MethodPost:
497 user := s.auth.GetUser(r)
498
499 domain := r.FormValue("domain")
500 if domain == "" {
501 log.Println("invalid form")
502 return
503 }
504
505 repoName := r.FormValue("name")
506 if repoName == "" {
507 log.Println("invalid form")
508 return
509 }
510
511 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
512 if err != nil || !ok {
513 w.Write([]byte("domain inaccessible to you"))
514 return
515 }
516
517 secret, err := s.db.GetRegistrationKey(domain)
518 if err != nil {
519 log.Printf("no key found for domain %s: %s\n", domain, err)
520 return
521 }
522
523 client, err := NewSignedClient(domain, secret)
524 if err != nil {
525 log.Println("failed to create client to ", domain)
526 }
527
528 resp, err := client.NewRepo(user.Did, repoName)
529 if err != nil {
530 log.Println("failed to send create repo request", err)
531 return
532 }
533 if resp.StatusCode != http.StatusNoContent {
534 log.Println("server returned ", resp.StatusCode)
535 return
536 }
537
538 // add to local db
539 repo := &db.Repo{
540 Did: user.Did,
541 Name: repoName,
542 Knot: domain,
543 }
544 err = s.db.AddRepo(repo)
545 if err != nil {
546 log.Println("failed to add repo to db", err)
547 return
548 }
549
550 // acls
551 err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
552 if err != nil {
553 log.Println("failed to set up acls", err)
554 return
555 }
556
557 w.Write([]byte("created!"))
558 }
559}
560
561func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
562 didOrHandle := chi.URLParam(r, "user")
563 if didOrHandle == "" {
564 http.Error(w, "Bad request", http.StatusBadRequest)
565 return
566 }
567
568 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
569 if err != nil {
570 log.Printf("resolving identity: %s", err)
571 w.WriteHeader(http.StatusNotFound)
572 return
573 }
574
575 repos, err := s.db.GetAllReposByDid(ident.DID.String())
576 if err != nil {
577 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
578 }
579
580 s.pages.ProfilePage(w, pages.ProfilePageParams{
581 LoggedInUser: s.auth.GetUser(r),
582 UserDid: ident.DID.String(),
583 UserHandle: ident.Handle.String(),
584 Repos: repos,
585 })
586}
587
588func (s *State) Router() http.Handler {
589 router := chi.NewRouter()
590
591 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
592 pat := chi.URLParam(r, "*")
593 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
594 s.UserRouter().ServeHTTP(w, r)
595 } else {
596 s.StandardRouter().ServeHTTP(w, r)
597 }
598 })
599
600 return router
601}
602
603func (s *State) UserRouter() http.Handler {
604 r := chi.NewRouter()
605
606 // strip @ from user
607 r.Use(StripLeadingAt)
608
609 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
610 r.Get("/", s.ProfilePage)
611 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
612 r.Get("/", s.RepoIndex)
613 r.Get("/log/{ref}", s.RepoLog)
614 r.Route("/tree/{ref}", func(r chi.Router) {
615 r.Get("/*", s.RepoTree)
616 })
617 r.Get("/commit/{ref}", s.RepoCommit)
618 r.Get("/branches", s.RepoBranches)
619 r.Get("/tags", s.RepoTags)
620
621 // These routes get proxied to the knot
622 r.Get("/info/refs", s.InfoRefs)
623 r.Post("/git-upload-pack", s.UploadPack)
624
625 })
626 })
627
628 return r
629}
630
631func (s *State) StandardRouter() http.Handler {
632 r := chi.NewRouter()
633
634 r.Get("/", s.Timeline)
635
636 r.Get("/login", s.Login)
637 r.Post("/login", s.Login)
638
639 r.Route("/knots", func(r chi.Router) {
640 r.Use(AuthMiddleware(s))
641 r.Get("/", s.Knots)
642 r.Post("/key", s.RegistrationKey)
643
644 r.Route("/{domain}", func(r chi.Router) {
645 r.Post("/init", s.InitKnotServer)
646 r.Get("/", s.KnotServerInfo)
647 r.Route("/member", func(r chi.Router) {
648 r.Use(RoleMiddleware(s, "server:owner"))
649 r.Get("/", s.ListMembers)
650 r.Put("/", s.AddMember)
651 r.Delete("/", s.RemoveMember)
652 })
653 })
654 })
655
656 r.Route("/repo", func(r chi.Router) {
657 r.Route("/new", func(r chi.Router) {
658 r.Get("/", s.AddRepo)
659 r.Post("/", s.AddRepo)
660 })
661 // r.Post("/import", s.ImportRepo)
662 })
663
664 r.Route("/settings", func(r chi.Router) {
665 r.Use(AuthMiddleware(s))
666 r.Get("/", s.Settings)
667 r.Put("/keys", s.SettingsKeys)
668 })
669
670 r.Get("/keys/{user}", s.Keys)
671
672 return r
673}