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