//! HTML to look at, sealed: an HTML file in the viewer, whether it is the agent's (`show`), the //! server's tree's or this computer's, is a page a model may have written. It is drawn in a frame //! from an address of its own (`llembas-sealed://`), never as part of the app's page: //! //! - **No bridge.** The capability names only the app's own page; Tauri gives the IPC script to //! the main frame only, and this scheme is not the app's origin. Nothing in the frame can call //! the app. //! - **No network, no app.** It is served under the server's sealed policy (its //! `security/sealed.py`): `sandbox allow-scripts` without `allow-same-origin` makes it an opaque //! origin, nothing loads from anywhere (inline and `data:`/`blob:` only), no form is sent. //! - **No way out.** The app refuses every navigation it did not make (see `lib.rs`), so a link or //! a script in the frame goes nowhere. //! //! The page holds the HTML until it is closed (`forget`); at most `KEEP` are kept. use std::collections::VecDeque; use std::sync::Mutex; pub const SCHEME: &str = "llembas-sealed"; /// The most sealed pages held at once; the oldest goes first. const KEEP: usize = 16; /// The policy a sealed page is served under -- the server's `sealed.policy()`, minus /// `frame-ancestors`, which would have to name the app's own scheme. pub const POLICY: &str = "sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' data: blob:; style-src 'unsafe-inline' data: blob:; img-src data: blob:; font-src data: blob:; media-src data: blob:; connect-src 'none'; worker-src blob:; frame-src 'none'; object-src 'none'; form-action 'none'; base-uri 'none'"; #[derive(Default)] pub struct Pages { next: Mutex, held: Mutex>, } impl Pages { /// Hold `html`; the id it is served under. pub fn hold(&self, html: String) -> String { let id = { let mut n = self.next.lock().unwrap(); *n += 1; format!("p{}", *n) }; let mut held = self.held.lock().unwrap(); while held.len() >= KEEP { held.pop_front(); } held.push_back((id.clone(), html)); id } pub fn forget(&self, id: &str) { self.held.lock().unwrap().retain(|(i, _)| i != id); } pub fn get(&self, id: &str) -> Option { self.held .lock() .unwrap() .iter() .find(|(i, _)| i == id) .map(|(_, h)| h.clone()) } } /// Where a held page is served: the address form differs by platform, as Tauri's own does. pub fn url(id: &str) -> String { if cfg!(windows) { format!("http://{SCHEME}.localhost/{id}") } else { format!("{SCHEME}://localhost/{id}") } } /// The id in a request's path: `/p3` -> `p3`. pub fn id_of(path: &str) -> &str { path.trim_start_matches('/') .split(['?', '/']) .next() .unwrap_or("") } /// The response for a request to the scheme. pub fn respond(pages: &Pages, path: &str) -> tauri::http::Response> { let builder = tauri::http::Response::builder() .header("Content-Security-Policy", POLICY) .header("X-Content-Type-Options", "nosniff") .header("Referrer-Policy", "no-referrer") .header("Cache-Control", "no-store"); let built = match pages.get(id_of(path)) { Some(html) => builder .status(200) .header("Content-Type", "text/html; charset=utf-8") .body(html.into_bytes()), None => builder .status(404) .header("Content-Type", "text/plain; charset=utf-8") .body(Vec::new()), }; built.unwrap_or_else(|_| tauri::http::Response::new(Vec::new())) } #[cfg(test)] mod tests { use super::*; #[test] fn pages_are_held_by_id_and_the_oldest_goes() { let p = Pages::default(); let first = p.hold("

one

".into()); assert_eq!(p.get(&first).as_deref(), Some("

one

")); for i in 0..KEEP { p.hold(format!("

{i}

")); } assert_eq!(p.get(&first), None); let last = p.hold("

last

".into()); p.forget(&last); assert_eq!(p.get(&last), None); } #[test] fn a_sealed_page_is_served_sealed() { let p = Pages::default(); let id = p.hold("".into()); let res = respond(&p, &format!("/{id}")); assert_eq!(res.status(), 200); let csp = res.headers()["Content-Security-Policy"].to_str().unwrap(); assert!(csp.starts_with("sandbox allow-scripts")); assert!(!csp.contains("allow-same-origin")); assert!(csp.contains("connect-src 'none'")); assert_eq!(respond(&p, "/nope").status(), 404); assert_eq!(id_of("/p7?x=1"), "p7"); } }