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