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