Skip to main content

blitz_traits/
platform.rs

1//! Abstractions of the host platform APIs a guest expects to find: fetching,
2//! and per-origin storage.
3//!
4//! These sit beside [`crate::net`] rather than inside it, and the distinction
5//! is worth stating because it is not obvious from the names.
6//!
7//! [`NetProvider`](crate::net::NetProvider) loads *resources the document
8//! needs*: a stylesheet, an image, a script. Its completion callback is
9//! [`NetHandler::bytes`](crate::net::NetHandler), which takes a resolved URL
10//! and some bytes, and that is the whole answer. There is no status code and
11//! there are no response headers, because a caller loading an image has nothing
12//! to do with either: the bytes decode or they do not.
13//!
14//! A `fetch()` caller is the opposite. A 404 is not a failure to be logged and
15//! dropped, it is the answer, and so are the headers. So this is a second trait
16//! rather than a wider `NetProvider`: an embedder that only ever loads page
17//! resources keeps implementing the small one, and the two can share a client.
18//! `blitz-net` implements both over the same `reqwest` client, the same
19//! connection pool, the same cookie jar and the same disk cache.
20//!
21//! Nothing here performs IO or names an HTTP client. The implementations live
22//! with the embedder.
23
24pub use bytes::Bytes;
25/// Re-exported so a binding can name `HeaderName` and `HeaderValue` when
26/// building a request, without taking its own dependency on `http` and pinning
27/// it to whatever version this crate happens to resolve. Same reasoning as the
28/// `keyboard_types` re-export in the crate root.
29pub use http;
30pub use http::{HeaderMap, Method, StatusCode};
31use std::sync::atomic::{AtomicU64, Ordering};
32pub use url::Url;
33
34/// A type that performs `fetch`-style requests on behalf of a guest.
35///
36/// Asynchronous by construction: [`fetch`](FetchProvider::fetch) hands the
37/// request over and returns immediately, and the answer arrives through the
38/// handler on whatever thread the implementation chooses. Callers must not
39/// assume the handler runs later, or on another thread: an implementation
40/// serving a `data:` URL may answer before `fetch` returns.
41pub trait FetchProvider: Send + Sync + 'static {
42    fn fetch(&self, request: FetchRequest, handler: Box<dyn FetchHandler>);
43}
44
45/// Receives the result of one [`FetchProvider::fetch`].
46///
47/// Takes `self: Box<Self>` so that a handler can be consumed by completing,
48/// which makes "completed twice" unrepresentable rather than merely wrong.
49/// Same shape as [`NetHandler`](crate::net::NetHandler).
50pub trait FetchHandler: Send + Sync + 'static {
51    /// Called exactly once.
52    ///
53    /// **A response with a non-success status is `Ok`, not `Err`.** A 404 has a
54    /// status, headers and usually a body, all of which the caller asked for.
55    /// [`FetchError`] is for the cases where there is no response at all.
56    fn complete(self: Box<Self>, result: Result<FetchResponse, FetchError>);
57}
58
59/// A request, loosely <https://fetch.spec.whatwg.org/#requests>.
60///
61/// Narrower than [`Request`](crate::net::Request) on purpose: no form bodies,
62/// because a guest that wants a multipart body can encode one, and no abort
63/// signal, because nothing in the current scope cancels. Both are additive
64/// later; see the README.
65#[non_exhaustive]
66#[derive(Debug, Clone)]
67pub struct FetchRequest {
68    pub url: Url,
69    pub method: Method,
70    pub headers: HeaderMap,
71    /// `None` is distinct from `Some(empty)`: the first sends no body at all,
72    /// the second sends a zero-length one with a `Content-Length: 0`.
73    pub body: Option<Bytes>,
74}
75
76impl FetchRequest {
77    /// A GET with no headers and no body.
78    pub fn get(url: Url) -> Self {
79        Self {
80            url,
81            method: Method::GET,
82            headers: HeaderMap::new(),
83            body: None,
84        }
85    }
86
87    /// The same request with a method.
88    pub fn method(mut self, method: Method) -> Self {
89        self.method = method;
90        self
91    }
92
93    /// The same request with a body.
94    pub fn body(mut self, body: Bytes) -> Self {
95        self.body = Some(body);
96        self
97    }
98}
99
100/// A response, loosely <https://fetch.spec.whatwg.org/#responses>.
101#[non_exhaustive]
102#[derive(Debug, Clone)]
103pub struct FetchResponse {
104    /// The URL the response actually came from, after any redirects.
105    ///
106    /// Not the requested URL. A guest resolving relative links out of a
107    /// response body needs the one it landed on.
108    pub url: Url,
109    pub status: StatusCode,
110    pub headers: HeaderMap,
111    pub body: Bytes,
112}
113
114impl FetchResponse {
115    /// A response with the given status and no headers or body.
116    ///
117    /// A constructor rather than a struct literal because the type is
118    /// `#[non_exhaustive]`, and it is `#[non_exhaustive]` because every
119    /// provider lives outside this crate: a field added here later would
120    /// otherwise break all of them at once. Build with this and the setters,
121    /// and a new field arrives with a default instead of a compile error.
122    pub fn new(url: Url, status: StatusCode) -> Self {
123        Self {
124            url,
125            status,
126            headers: HeaderMap::new(),
127            body: Bytes::new(),
128        }
129    }
130
131    pub fn headers(mut self, headers: HeaderMap) -> Self {
132        self.headers = headers;
133        self
134    }
135
136    pub fn body(mut self, body: Bytes) -> Self {
137        self.body = body;
138        self
139    }
140}
141
142/// Why there is no response.
143///
144/// Note what is *not* here: an HTTP status. A server that answered is a
145/// [`FetchResponse`] whatever it answered with. These are the cases where
146/// nothing answered.
147#[non_exhaustive]
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum FetchError {
150    /// The request could not be completed: DNS, TLS, connection, timeout, a
151    /// malformed response, or a local read that failed.
152    ///
153    /// One variant rather than a taxonomy because `fetch()` itself exposes one
154    /// (`TypeError`), and a guest cannot act differently on a DNS failure than
155    /// on a TLS one. The string is for a human reading a log.
156    Network(String),
157    /// The URL's scheme is not one this provider serves.
158    UnsupportedScheme(String),
159    /// The request was rejected before it was sent: an unparseable header, a
160    /// method the provider will not issue.
161    InvalidRequest(String),
162    /// Refused by policy rather than by the network. A provider that enforces
163    /// an allowlist, or refuses to let an opaque origin reach the network,
164    /// answers with this.
165    Blocked(String),
166    /// There is no provider installed. See [`DummyFetchProvider`].
167    NoProvider,
168}
169
170impl std::fmt::Display for FetchError {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            Self::Network(detail) => write!(f, "network error: {detail}"),
174            Self::UnsupportedScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"),
175            Self::InvalidRequest(detail) => write!(f, "invalid request: {detail}"),
176            Self::Blocked(reason) => write!(f, "blocked: {reason}"),
177            Self::NoProvider => write!(f, "no fetch provider is installed"),
178        }
179    }
180}
181
182impl std::error::Error for FetchError {}
183
184/// A [`FetchProvider`] that answers every request with [`FetchError::NoProvider`].
185///
186/// **It answers rather than doing nothing, which is where it differs from
187/// [`DummyNetProvider`](crate::net::DummyNetProvider), and the difference is
188/// deliberate.** A dropped resource load leaves an image unpainted, which is
189/// visible and survivable. A dropped `fetch` leaves the guest waiting on an
190/// answer that will never come, and a guest awaiting a promise that never
191/// settles is indistinguishable from a hang. So the no-op provider here fails
192/// fast instead of going quiet.
193#[derive(Default)]
194pub struct DummyFetchProvider;
195
196impl FetchProvider for DummyFetchProvider {
197    fn fetch(&self, _request: FetchRequest, handler: Box<dyn FetchHandler>) {
198        handler.complete(Err(FetchError::NoProvider));
199    }
200}
201
202/// The security principal a piece of stored state belongs to.
203///
204/// **Storage is keyed by this and never by a URL.** Two pages on one origin
205/// share storage; two pages on different origins must not see each other's,
206/// and that is a security property rather than a nicety.
207///
208/// Derived through [`Url::origin`], which is Servo's implementation of the
209/// WHATWG origin concept, so the awkward cases are already decided: `blob:`
210/// unwraps to its inner origin, and `file:` and `data:` are *opaque*.
211///
212/// # Opaque origins
213///
214/// [`Url::origin`] mints a **fresh** opaque origin on every call for a `file:`
215/// URL, so a document must derive its key once, at construction, and hold it.
216/// Deriving twice would give one document two identities and lose its own
217/// writes.
218///
219/// That is exactly the intended behaviour and not a wrinkle to route around.
220/// Collapsing every `file:` page into one shared bucket would mean any local
221/// HTML file could read any other's saved state, which is the same class of
222/// mistake as `blitz-net`'s `file:` handler reading any path it is handed.
223/// One such hole is enough.
224///
225/// [`is_persistable`](OriginKey::is_persistable) is the flag a disk-backed
226/// provider must check: an opaque origin is unique to one instance of one
227/// document, so writing it to disk stores a row that can never be read again.
228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub struct OriginKey {
230    key: String,
231    persistable: bool,
232}
233
234/// Distinguishes one opaque origin from the next within a process.
235static NEXT_OPAQUE: AtomicU64 = AtomicU64::new(0);
236
237impl OriginKey {
238    /// The origin of a document loaded from `url`.
239    ///
240    /// Call once per document and keep the result. See the type's docs for why
241    /// calling twice is a bug for `file:` and `data:` URLs.
242    pub fn for_document(url: &Url) -> Self {
243        let origin = url.origin();
244        if origin.is_tuple() {
245            Self {
246                key: origin.ascii_serialization(),
247                persistable: true,
248            }
249        } else {
250            Self::opaque()
251        }
252    }
253
254    /// A fresh opaque origin, shared with nothing.
255    ///
256    /// The serialisation of every opaque origin is the string `null`, so it
257    /// cannot be used to tell two of them apart. This appends a
258    /// process-unique counter so that a provider keying an in-memory map on
259    /// [`as_str`](OriginKey::as_str) isolates them from each other, which
260    /// `null` alone would not.
261    pub fn opaque() -> Self {
262        let nth = NEXT_OPAQUE.fetch_add(1, Ordering::Relaxed);
263        Self {
264            key: format!("null:{nth}"),
265            persistable: false,
266        }
267    }
268
269    /// A stable string for this origin, suitable as a map or table key.
270    ///
271    /// Unique per origin, including across opaque ones. Not a URL, and not to
272    /// be parsed back into one.
273    pub fn as_str(&self) -> &str {
274        &self.key
275    }
276
277    /// Whether state under this origin may be written to disk.
278    ///
279    /// False for opaque origins. A provider that persists must check this and
280    /// keep the rest in memory; see the type's docs.
281    pub fn is_persistable(&self) -> bool {
282        self.persistable
283    }
284}
285
286/// Per-origin key/value storage, the model behind `localStorage`.
287///
288/// Synchronous, because the API it backs is. Values are strings, because the
289/// API it backs stores strings: a caller with structure serialises it, exactly
290/// as it would against a browser, and no implementation here looks inside a
291/// value.
292///
293/// **Every method takes an [`OriginKey`], and an implementation must scope by
294/// it.** This is the whole security contract of the trait and it is not
295/// optional. An implementation that ignores the argument compiles, works in a
296/// single-origin test, and leaks every site's data to every other.
297pub trait StorageProvider: Send + Sync + 'static {
298    fn get(&self, origin: &OriginKey, key: &str) -> Option<String>;
299    fn set(&self, origin: &OriginKey, key: &str, value: &str) -> Result<(), StorageError>;
300    fn remove(&self, origin: &OriginKey, key: &str);
301    /// Removes everything under `origin`, and nothing under any other.
302    fn clear(&self, origin: &OriginKey);
303}
304
305/// Why a write did not happen.
306///
307/// Reads, removes and clears do not fail: an absent key is `None` and removing
308/// what is not there is a no-op, which is what the storage API specifies.
309#[non_exhaustive]
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub enum StorageError {
312    /// The origin is at its limit. `localStorage` surfaces this as a
313    /// `QuotaExceededError`, which is the one storage failure guests handle.
314    QuotaExceeded,
315    /// The backing store refused the write.
316    Backend(String),
317}
318
319impl std::fmt::Display for StorageError {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        match self {
322            Self::QuotaExceeded => write!(f, "storage quota exceeded for this origin"),
323            Self::Backend(detail) => write!(f, "storage backend error: {detail}"),
324        }
325    }
326}
327
328impl std::error::Error for StorageError {}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    fn url(text: &str) -> Url {
335        Url::parse(text).unwrap()
336    }
337
338    #[test]
339    fn same_origin_urls_share_a_key() {
340        let a = OriginKey::for_document(&url("https://example.com/one?x=1#frag"));
341        let b = OriginKey::for_document(&url("https://example.com/two"));
342        assert_eq!(a, b);
343        assert!(a.is_persistable());
344    }
345
346    #[test]
347    fn scheme_port_and_host_all_separate_origins() {
348        let https = OriginKey::for_document(&url("https://example.com/"));
349        let http = OriginKey::for_document(&url("http://example.com/"));
350        let port = OriginKey::for_document(&url("https://example.com:8443/"));
351        let host = OriginKey::for_document(&url("https://other.example.com/"));
352        assert_ne!(https, http);
353        assert_ne!(https, port);
354        assert_ne!(https, host);
355    }
356
357    /// The property that stops one local page reading another's state. Two
358    /// *identical* file URLs must still be different origins.
359    #[test]
360    fn every_file_url_gets_its_own_opaque_origin() {
361        let one = OriginKey::for_document(&url("file:///home/user/page.html"));
362        let same_again = OriginKey::for_document(&url("file:///home/user/page.html"));
363        let other = OriginKey::for_document(&url("file:///home/user/other.html"));
364
365        assert_ne!(one, same_again);
366        assert_ne!(one, other);
367        assert!(!one.is_persistable());
368        assert!(!same_again.is_persistable());
369    }
370
371    #[test]
372    fn data_urls_are_opaque_too() {
373        let key = OriginKey::for_document(&url("data:text/html,<p>hi"));
374        assert!(!key.is_persistable());
375    }
376
377    /// `ascii_serialization` answers `null` for every opaque origin, so a
378    /// provider keying on it alone would merge them all into one bucket. This
379    /// is the assertion that the counter is doing its job.
380    #[test]
381    fn opaque_keys_are_distinguishable_from_each_other() {
382        let one = OriginKey::opaque();
383        let two = OriginKey::opaque();
384        assert_ne!(one.as_str(), two.as_str());
385        assert_ne!(one.as_str(), "null");
386    }
387
388    /// A `blob:` URL carries its origin inside it, and that origin is the one
389    /// that owns any state reached through it.
390    #[test]
391    fn a_blob_url_takes_the_origin_it_was_minted_from() {
392        let blob = OriginKey::for_document(&url("blob:https://example.com/uuid-goes-here"));
393        let page = OriginKey::for_document(&url("https://example.com/index.html"));
394        assert_eq!(blob, page);
395    }
396
397    #[test]
398    fn the_dummy_provider_answers_rather_than_going_quiet() {
399        use std::sync::{Arc, Mutex};
400
401        struct Record(Arc<Mutex<Option<Result<FetchResponse, FetchError>>>>);
402        impl FetchHandler for Record {
403            fn complete(self: Box<Self>, result: Result<FetchResponse, FetchError>) {
404                *self.0.lock().unwrap() = Some(result);
405            }
406        }
407
408        let seen = Arc::new(Mutex::new(None));
409        DummyFetchProvider.fetch(
410            FetchRequest::get(url("https://example.com/")),
411            Box::new(Record(seen.clone())),
412        );
413
414        let answer = seen.lock().unwrap().take();
415        assert_eq!(
416            answer.expect("the dummy must answer").unwrap_err(),
417            FetchError::NoProvider
418        );
419    }
420}