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