1use std::{
22 collections::HashMap,
23 sync::{Mutex, PoisonError},
24 time::{Duration, Instant},
25};
26
27use topcoat::{
28 context::Cx,
29 router::extensions,
30 session::{Token, TokenStore, TokenStoreFuture},
31};
32
33use crate::trace::{self, Withheld};
34
35#[derive(Debug, Clone)]
41struct RequestWebview(String);
42
43#[derive(Debug)]
46pub(crate) struct Webviews {
47 state: Mutex<HashMap<String, Webview>>,
48}
49
50#[derive(Debug, Default)]
51struct Webview {
52 showing_ours: bool,
56 held: Option<Held>,
57}
58
59#[derive(Debug)]
60struct Held {
61 token: Token,
62 expires: Option<Instant>,
65}
66
67impl Webviews {
68 pub(crate) fn new() -> Webviews {
69 Webviews {
70 state: Mutex::new(HashMap::new()),
71 }
72 }
73
74 pub(crate) fn observe(&self, label: &str, ours: bool) {
76 let mut state = self.lock();
77 let webview = state.entry(label.to_owned()).or_default();
78 webview.showing_ours = ours;
79 }
82
83 fn read(&self, label: &str, now: Instant) -> Result<Token, Withheld> {
89 let state = self.lock();
90 let webview = state.get(label).ok_or(Withheld::UnknownWebview)?;
91 if !webview.showing_ours {
92 return Err(Withheld::ShowingAnotherOrigin);
93 }
94 let held = webview.held.as_ref().ok_or(Withheld::NoToken)?;
95 if held.expires.is_some_and(|expires| now >= expires) {
96 return Err(Withheld::Expired);
97 }
98 Ok(held.token.clone())
99 }
100
101 fn write(&self, label: &str, token: Token, max_age: Duration, now: Instant) {
102 let held = Held {
103 token,
104 expires: now.checked_add(max_age),
105 };
106 self.lock().entry(label.to_owned()).or_default().held = Some(held);
107 }
108
109 fn delete(&self, label: &str) {
110 if let Some(webview) = self.lock().get_mut(label) {
111 webview.held = None;
112 }
113 }
114
115 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Webview>> {
119 self.state.lock().unwrap_or_else(PoisonError::into_inner)
120 }
121}
122
123#[derive(Debug)]
130pub(crate) struct WebviewTokenStore {
131 webviews: std::sync::Arc<Webviews>,
132}
133
134impl WebviewTokenStore {
135 pub(crate) const fn new(webviews: std::sync::Arc<Webviews>) -> WebviewTokenStore {
136 WebviewTokenStore { webviews }
137 }
138}
139
140pub(crate) fn attach<B>(mut request: http::Request<B>, label: &str) -> http::Request<B> {
147 request
148 .extensions_mut()
149 .insert(RequestWebview(label.to_owned()));
150 request
151}
152
153fn requesting_webview(cx: &Cx) -> Option<&str> {
159 extensions(cx)
160 .get::<RequestWebview>()
161 .map(|webview| webview.0.as_str())
162}
163
164impl TokenStore for WebviewTokenStore {
165 fn read<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, Option<Token>> {
166 let token =
169 requesting_webview(cx).and_then(|label| match self.webviews.read(label, now()) {
170 Ok(token) => {
171 trace::session_presented(label);
172 Some(token)
173 }
174 Err(withheld) => {
175 trace::session_withheld(label, withheld);
176 None
177 }
178 });
179 Box::pin(async move { Ok(token) })
180 }
181
182 fn write<'a>(
183 &'a self,
184 cx: &'a Cx,
185 token: Token,
186 max_age: Duration,
187 ) -> TokenStoreFuture<'a, ()> {
188 if let Some(label) = requesting_webview(cx) {
189 self.webviews.write(label, token, max_age, now());
190 trace::session_issued(label);
191 }
192 Box::pin(async move { Ok(()) })
193 }
194
195 fn delete<'a>(&'a self, cx: &'a Cx) -> TokenStoreFuture<'a, ()> {
196 if let Some(label) = requesting_webview(cx) {
197 self.webviews.delete(label);
198 trace::session_cleared(label);
199 }
200 Box::pin(async move { Ok(()) })
201 }
202}
203
204fn now() -> Instant {
207 Instant::now()
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 const LABEL: &str = "main";
215
216 fn webviews_showing_ours() -> Webviews {
217 let webviews = Webviews::new();
218 webviews.observe(LABEL, true);
219 webviews
220 }
221
222 fn bytes(token: &Token) -> [u8; 32] {
223 *token.dangerous_as_array()
224 }
225
226 #[test]
227 fn a_written_token_reads_back() {
228 let webviews = webviews_showing_ours();
229 let token = Token::random();
230 let now = Instant::now();
231 webviews.write(LABEL, token.clone(), Duration::from_secs(60), now);
232
233 let read = webviews
234 .read(LABEL, now)
235 .expect("the token was just written");
236 assert_eq!(bytes(&read), bytes(&token));
237 }
238
239 #[track_caller]
242 fn assert_withheld(result: Result<Token, Withheld>, expected: Withheld) {
243 match result {
244 Err(actual) => assert_eq!(actual, expected, "the wrong rule declined"),
245 Ok(_) => panic!("handed over the token, expected {expected:?}"),
246 }
247 }
248
249 #[test]
250 fn a_webview_with_no_token_reads_none() {
251 let webviews = webviews_showing_ours();
252 assert_withheld(webviews.read(LABEL, Instant::now()), Withheld::NoToken);
253 }
254
255 #[test]
256 fn one_webviews_token_is_not_anothers() {
257 let webviews = webviews_showing_ours();
258 webviews.observe("second", true);
259 let now = Instant::now();
260 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
261
262 assert_withheld(webviews.read("second", now), Withheld::NoToken);
263 }
264
265 #[test]
266 fn a_webview_showing_a_foreign_document_is_not_handed_the_token() {
267 let webviews = webviews_showing_ours();
268 let now = Instant::now();
269 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
270 assert!(webviews.read(LABEL, now).is_ok());
271
272 webviews.observe(LABEL, false);
273 assert_withheld(webviews.read(LABEL, now), Withheld::ShowingAnotherOrigin);
274
275 webviews.observe(LABEL, true);
276 assert!(webviews.read(LABEL, now).is_ok());
277 }
278
279 #[test]
280 fn a_webview_nobody_has_watched_is_not_handed_the_token() {
281 let webviews = Webviews::new();
282 let now = Instant::now();
283 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
284 assert_withheld(webviews.read(LABEL, now), Withheld::ShowingAnotherOrigin);
285 }
286
287 #[test]
288 fn an_expired_token_reads_none() {
289 let webviews = webviews_showing_ours();
290 let now = Instant::now();
291 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
292
293 let later = now + Duration::from_secs(61);
294 assert_withheld(webviews.read(LABEL, later), Withheld::Expired);
295 }
296
297 #[test]
298 fn a_token_expires_at_its_deadline_rather_than_after_it() {
299 let webviews = webviews_showing_ours();
300 let now = Instant::now();
301 let lifetime = Duration::from_secs(60);
302 webviews.write(LABEL, Token::random(), lifetime, now);
303
304 assert_withheld(webviews.read(LABEL, now + lifetime), Withheld::Expired);
305 assert!(
306 webviews
307 .read(LABEL, now + lifetime - Duration::from_nanos(1))
308 .is_ok()
309 );
310 }
311
312 #[test]
313 fn deleting_discards_the_token_and_keeps_the_webview() {
314 let webviews = webviews_showing_ours();
315 let now = Instant::now();
316 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
317 webviews.delete(LABEL);
318
319 assert!(webviews.read(LABEL, now).is_err());
320 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
321 assert!(webviews.read(LABEL, now).is_ok());
322 }
323
324 #[test]
325 fn a_rewrite_replaces_the_previous_token() {
326 let webviews = webviews_showing_ours();
327 let now = Instant::now();
328 webviews.write(LABEL, Token::random(), Duration::from_secs(60), now);
329 let rotated = Token::random();
330 webviews.write(LABEL, rotated.clone(), Duration::from_secs(60), now);
331
332 let read = webviews.read(LABEL, now).expect("a token is held");
333 assert_eq!(bytes(&read), bytes(&rotated));
334 }
335
336 #[test]
337 fn attach_overwrites_whatever_was_there() {
338 let mut request = http::Request::new(Vec::<u8>::new());
339 request
340 .extensions_mut()
341 .insert(RequestWebview("forged".to_owned()));
342
343 let request = attach(request, LABEL);
344
345 let attached = request
346 .extensions()
347 .get::<RequestWebview>()
348 .expect("attach inserts one");
349 assert_eq!(attached.0, LABEL);
350 }
351}