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 switch r.Method {
154 case http.MethodGet:
155 w.Write([]byte("unimplemented"))
156 log.Println("unimplemented")
157 return
158 case http.MethodPut:
159 did := s.auth.GetDid(r)
160 key := r.FormValue("key")
161 name := r.FormValue("name")
162 client, _ := s.auth.AuthorizedClient(r)
163
164 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
165 if err != nil {
166 log.Printf("parsing public key: %s", err)
167 return
168 }
169
170 if err := s.db.AddPublicKey(did, name, key); err != nil {
171 log.Printf("adding public key: %s", err)
172 return
173 }
174
175 // store in pds too
176 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
177 Collection: tangled.PublicKeyNSID,
178 Repo: did,
179 Rkey: uuid.New().String(),
180 Record: &lexutil.LexiconTypeDecoder{
181 Val: &tangled.PublicKey{
182 Created: time.Now().Format(time.RFC3339),
183 Key: key,
184 Name: name,
185 }},
186 })
187 // invalid record
188 if err != nil {
189 log.Printf("failed to create record: %s", err)
190 return
191 }
192
193 log.Println("created atproto record: ", resp.Uri)
194
195 return
196 }
197}
198
199// create a signed request and check if a node responds to that
200func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
201 user := s.auth.GetUser(r)
202
203 domain := chi.URLParam(r, "domain")
204 if domain == "" {
205 http.Error(w, "malformed url", http.StatusBadRequest)
206 return
207 }
208 log.Println("checking ", domain)
209
210 url := fmt.Sprintf("http://%s/init", domain)
211
212 body, _ := json.Marshal(map[string]interface{}{
213 "did": user.Did,
214 "keys": []string{},
215 })
216 pingRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
217 if err != nil {
218 log.Println("failed to build ping request", err)
219 return
220 }
221
222 secret, err := s.db.GetRegistrationKey(domain)
223 if err != nil {
224 log.Printf("no key found for domain %s: %s\n", domain, err)
225 return
226 }
227 client := SignedClient(secret)
228
229 resp, err := client.Do(pingRequest)
230 if err != nil {
231 w.Write([]byte("no dice"))
232 log.Println("domain was unreachable after 5 seconds")
233 return
234 }
235
236 if resp.StatusCode == http.StatusConflict {
237 log.Println("status conflict", resp.StatusCode)
238 w.Write([]byte("already registered, sorry!"))
239 return
240 }
241
242 if resp.StatusCode != http.StatusNoContent {
243 log.Println("status nok", resp.StatusCode)
244 w.Write([]byte("no dice"))
245 return
246 }
247
248 // verify response mac
249 signature := resp.Header.Get("X-Signature")
250 signatureBytes, err := hex.DecodeString(signature)
251 if err != nil {
252 return
253 }
254
255 expectedMac := hmac.New(sha256.New, []byte(secret))
256 expectedMac.Write([]byte("ok"))
257
258 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
259 log.Printf("response body signature mismatch: %x\n", signatureBytes)
260 return
261 }
262
263 // mark as registered
264 err = s.db.Register(domain)
265 if err != nil {
266 log.Println("failed to register domain", err)
267 http.Error(w, err.Error(), http.StatusInternalServerError)
268 return
269 }
270
271 // set permissions for this did as owner
272 reg, err := s.db.RegistrationByDomain(domain)
273 if err != nil {
274 log.Println("failed to register domain", err)
275 http.Error(w, err.Error(), http.StatusInternalServerError)
276 return
277 }
278
279 // add basic acls for this domain
280 err = s.enforcer.AddDomain(domain)
281 if err != nil {
282 log.Println("failed to setup owner of domain", err)
283 http.Error(w, err.Error(), http.StatusInternalServerError)
284 return
285 }
286
287 // add this did as owner of this domain
288 err = s.enforcer.AddOwner(domain, reg.ByDid)
289 if err != nil {
290 log.Println("failed to setup owner of domain", err)
291 http.Error(w, err.Error(), http.StatusInternalServerError)
292 return
293 }
294
295 w.Write([]byte("check success"))
296}
297
298func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
299 domain := chi.URLParam(r, "domain")
300 if domain == "" {
301 http.Error(w, "malformed url", http.StatusBadRequest)
302 return
303 }
304
305 user := s.auth.GetUser(r)
306 reg, err := s.db.RegistrationByDomain(domain)
307 if err != nil {
308 w.Write([]byte("failed to pull up registration info"))
309 return
310 }
311
312 var members []string
313 if reg.Registered != nil {
314 members, err = s.enforcer.E.GetUsersForRole("server:member", domain)
315 if err != nil {
316 w.Write([]byte("failed to fetch member list"))
317 return
318 }
319 }
320
321 ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain)
322 isOwner := err == nil && ok
323
324 p := pages.KnotParams{
325 User: user,
326 Registration: reg,
327 Members: members,
328 IsOwner: isOwner,
329 }
330
331 pages.Knot(w, p)
332}
333
334// get knots registered by this user
335func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
336 // for now, this is just pubkeys
337 user := s.auth.GetUser(r)
338 registrations, err := s.db.RegistrationsByDid(user.Did)
339 if err != nil {
340 log.Println(err)
341 }
342
343 pages.Knots(w, pages.KnotsParams{
344 User: user,
345 Registrations: registrations,
346 })
347}
348
349// list members of domain, requires auth and requires owner status
350func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
351 domain := chi.URLParam(r, "domain")
352 if domain == "" {
353 http.Error(w, "malformed url", http.StatusBadRequest)
354 return
355 }
356
357 // list all members for this domain
358 memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain)
359 if err != nil {
360 w.Write([]byte("failed to fetch member list"))
361 return
362 }
363
364 w.Write([]byte(strings.Join(memberDids, "\n")))
365 return
366}
367
368// add member to domain, requires auth and requires invite access
369func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
370 domain := chi.URLParam(r, "domain")
371 if domain == "" {
372 http.Error(w, "malformed url", http.StatusBadRequest)
373 return
374 }
375
376 memberDid := r.FormValue("member")
377 if memberDid == "" {
378 http.Error(w, "malformed form", http.StatusBadRequest)
379 return
380 }
381
382 memberIdent, err := auth.ResolveIdent(r.Context(), memberDid)
383 if err != nil {
384 w.Write([]byte("failed to resolve member did to a handle"))
385 return
386 }
387 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
388
389 // announce this relation into the firehose, store into owners' pds
390 client, _ := s.auth.AuthorizedClient(r)
391 currentUser := s.auth.GetUser(r)
392 addedAt := time.Now().Format(time.RFC3339)
393 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
394 Collection: tangled.KnotMemberNSID,
395 Repo: currentUser.Did,
396 Rkey: uuid.New().String(),
397 Record: &lexutil.LexiconTypeDecoder{
398 Val: &tangled.KnotMember{
399 Member: memberIdent.DID.String(),
400 Domain: domain,
401 AddedAt: &addedAt,
402 }},
403 })
404 // invalid record
405 if err != nil {
406 log.Printf("failed to create record: %s", err)
407 return
408 }
409
410 log.Println("created atproto record: ", resp.Uri)
411
412 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
413 if err != nil {
414 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
415 return
416 }
417
418 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
419}
420
421func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
422}
423
424func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
425 switch r.Method {
426 case http.MethodGet:
427 pages.NewRepo(w, pages.NewRepoParams{
428 User: s.auth.GetUser(r),
429 })
430 case http.MethodPost:
431 user := s.auth.GetUser(r)
432
433 domain := r.FormValue("domain")
434 if domain == "" {
435 log.Println("invalid form")
436 return
437 }
438
439 repoName := r.FormValue("name")
440 if repoName == "" {
441 log.Println("invalid form")
442 return
443 }
444
445 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
446 if err != nil || !ok {
447 w.Write([]byte("domain inaccessible to you"))
448 return
449 }
450
451 secret, err := s.db.GetRegistrationKey(domain)
452 if err != nil {
453 log.Printf("no key found for domain %s: %s\n", domain, err)
454 return
455 }
456
457 client := SignedClient(secret)
458 url := fmt.Sprintf("http://%s/repo/new", domain)
459 body, _ := json.Marshal(map[string]interface{}{
460 "did": user.Did,
461 "name": repoName,
462 })
463 createRepoRequest, err := http.NewRequest("PUT", url, bytes.NewReader(body))
464
465 resp, err := client.Do(createRepoRequest)
466
467 if err != nil {
468 log.Println("failed to send create repo request", err)
469 return
470 }
471
472 if resp.StatusCode != http.StatusNoContent {
473 log.Println("server returned ", resp.StatusCode)
474 return
475 }
476
477 // add to local db
478 repo := &db.Repo{
479 Did: user.Did,
480 Name: repoName,
481 Knot: domain,
482 }
483
484 err = s.db.AddRepo(repo)
485 if err != nil {
486 log.Println("failed to add repo to db", err)
487 return
488 }
489
490 w.Write([]byte("created!"))
491 }
492}
493
494func (s *State) Router() http.Handler {
495 r := chi.NewRouter()
496
497 r.Get("/", s.Timeline)
498
499 r.Get("/login", s.Login)
500 r.Post("/login", s.Login)
501
502 r.Route("/knots", func(r chi.Router) {
503 r.Use(AuthMiddleware(s))
504 r.Get("/", s.Knots)
505 r.Post("/key", s.RegistrationKey)
506
507 r.Route("/{domain}", func(r chi.Router) {
508 r.Post("/init", s.InitKnotServer)
509 r.Get("/", s.KnotServerInfo)
510 r.Route("/member", func(r chi.Router) {
511 r.Use(RoleMiddleware(s, "server:owner"))
512 r.Get("/", s.ListMembers)
513 r.Put("/", s.AddMember)
514 r.Delete("/", s.RemoveMember)
515 })
516 })
517 })
518
519 r.Route("/repo", func(r chi.Router) {
520 r.Route("/new", func(r chi.Router) {
521 r.Get("/", s.AddRepo)
522 r.Post("/", s.AddRepo)
523 })
524 // r.Post("/import", s.ImportRepo)
525 })
526
527 r.Group(func(r chi.Router) {
528 r.Use(AuthMiddleware(s))
529 r.Get("/settings", s.Settings)
530 r.Put("/settings/keys", s.Keys)
531 })
532
533 return r
534}