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