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