this repo has no description
1package state
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/hex"
7 "log"
8 "net/http"
9 "net/url"
10 "time"
11
12 "github.com/go-chi/chi/v5"
13 "github.com/icyphox/bild/appview"
14 "github.com/icyphox/bild/appview/auth"
15 "github.com/icyphox/bild/appview/db"
16)
17
18type State struct {
19 Db *db.DB
20 Auth *auth.Auth
21}
22
23func Make() (*State, error) {
24 db, err := db.Make("appview.db")
25 if err != nil {
26 return nil, err
27 }
28
29 auth, err := auth.Make()
30 if err != nil {
31 return nil, err
32 }
33
34 return &State{db, auth}, nil
35}
36
37func (s *State) Login(w http.ResponseWriter, r *http.Request) {
38 ctx := r.Context()
39
40 switch r.Method {
41 case http.MethodGet:
42 log.Println("unimplemented")
43 return
44 case http.MethodPost:
45 username := r.FormValue("username")
46 appPassword := r.FormValue("password")
47
48 atSession, err := s.Auth.CreateInitialSession(ctx, username, appPassword)
49 if err != nil {
50 log.Printf("creating initial session: %s", err)
51 return
52 }
53 sessionish := auth.CreateSessionWrapper{atSession}
54
55 err = s.Auth.StoreSession(r, w, &sessionish)
56 if err != nil {
57 log.Printf("storing session: %s", err)
58 return
59 }
60
61 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
62 http.Redirect(w, r, "/", http.StatusSeeOther)
63 return
64 }
65}
66
67// requires auth
68func (s *State) Key(w http.ResponseWriter, r *http.Request) {
69 switch r.Method {
70 case http.MethodGet:
71 // list open registrations under this did
72
73 return
74 case http.MethodPost:
75 session, err := s.Auth.Store.Get(r, appview.SESSION_NAME)
76 if err != nil || session.IsNew {
77 log.Println("unauthorized attempt to generate registration key")
78 http.Error(w, "Forbidden", http.StatusUnauthorized)
79 return
80 }
81
82 did := session.Values[appview.SESSION_DID].(string)
83
84 // check if domain is valid url, and strip extra bits down to just host
85 domain := r.FormValue("domain")
86 url, err := url.Parse(domain)
87 if domain == "" || err != nil {
88 http.Error(w, "Invalid form", http.StatusBadRequest)
89 return
90 }
91
92 key, err := s.Db.GenerateRegistrationKey(url.Host, did)
93
94 if err != nil {
95 log.Println(err)
96 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
97 return
98 }
99
100 w.Write([]byte(key))
101 return
102 }
103}
104
105// create a signed request and check if a node responds to that
106//
107// we should also rate limit these checks to avoid ddosing knotservers
108func (s *State) Check(w http.ResponseWriter, r *http.Request) {
109 domain := r.FormValue("domain")
110 if domain == "" {
111 http.Error(w, "Invalid form", http.StatusBadRequest)
112 return
113 }
114
115 secret, err := s.Db.GetRegistrationKey(domain)
116 if err != nil {
117 log.Printf("no key found for domain %s: %s\n", domain, err)
118 return
119 }
120
121 hmac := hmac.New(sha256.New, []byte(secret))
122 signature := hex.EncodeToString(hmac.Sum(nil))
123
124 // make a request do the knotserver with an empty body and above signature
125 url, _ := url.Parse(domain)
126 url = url.JoinPath("check")
127 pingRequest, err := http.NewRequest("GET", url.String(), nil)
128 if err != nil {
129 log.Println("failed to create ping request for ", url.String())
130 return
131 }
132 pingRequest.Header.Set("X-Signature", signature)
133
134 client := &http.Client{
135 Timeout: 5 * time.Second,
136 }
137 resp, err := client.Do(pingRequest)
138 if err != nil {
139 w.Write([]byte("no dice"))
140 log.Println("domain was unreachable after 5 seconds")
141 return
142 }
143
144 if resp.StatusCode != http.StatusOK {
145 log.Println("status nok")
146 w.Write([]byte("no dice"))
147 return
148 }
149 w.Write([]byte("check success"))
150
151 // mark as registered
152 s.Db.Register(domain)
153
154 return
155}
156
157func (s *State) Router() http.Handler {
158 r := chi.NewRouter()
159
160 r.Post("/login", s.Login)
161
162 r.Route("/node", func(r chi.Router) {
163 r.Post("/check", s.Check)
164
165 r.Group(func(r chi.Router) {
166 r.Use(AuthMiddleware(s))
167 r.Post("/key", s.Key)
168 })
169 })
170
171 return r
172}