custom_protocol_http/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3
4//! HTTP semantics for a webview custom protocol.
5//!
6//! A webview reaches a custom protocol with a URL whose shape depends on the
7//! platform, and reads the answer with a client that is not quite an HTTP
8//! client. A server behind that protocol needs the ordinary web it was written
9//! against. This crate is the translation.
10//!
11//! # Why is this its own crate?
12//!
13//! None of it is specific to a shell, and every rule is a plain function over
14//! plain values - tested exhaustively without opening a window, on any
15//! operating system. No specification is written for a custom protocol
16//! handler, so the rules start from what the `probe` binary measured and aim at
17//! the layer where a standard already holds: a canonical origin an ordinary
18//! CSRF check can read, redirect following taken from `tower-http`.
19//!
20//! # The two decisions
21//!
22//! [`Origins`] turns a request the webview delivered into one the server may
23//! serve: refusing any URL that names somebody else's origin, and rewriting the
24//! rest into the single canonical origin the server sees on every platform.
25//! [`Platform`] is the only place in the crate that names an operating system.
26//!
27//! [`unsupported`] reads the answer back, and names the capabilities a protocol
28//! handler cannot deliver - streaming, compression, cookies, upgrades - so the
29//! shell can fail where a developer will see it rather than drop half a response
30//! in silence.
31//!
32//! # Applying them
33//!
34//! [`tower`] stacks both as middleware in front of any service, which is how a
35//! shell actually wants them, and adds the third job from the ecosystem rather
36//! than from here: no webview follows a `Location` from a custom protocol, and
37//! `tower-http` already knows how to follow one.
38//!
39//! # What it does not do
40//!
41//! It attaches no credential - no cookie jar, no token - and strips what a
42//! client attached by itself, so a server may still reason that a request with
43//! neither `Origin` nor `Sec-Fetch-Site` has no ambient authority to forge
44//! with. Supply one here and that reasoning is false; ambient authority belongs
45//! to the shell, per webview, or to nobody. `Host` is the exception, and is set
46//! because a server comparing `Origin` against it needs both.
47//!
48//! # Tracing
49//!
50//! Off by default. With the feature every refusal says why. Without it, a
51//! refusal becomes a response and the reason is gone. The origin rewrite is
52//! reported at debug, since the platform origin is the one thing invisible
53//! from inside the application. Only decisions are reported, never a body, a
54//! header value or a query string.
55//!
56//! # Why `Sec-Fetch-Site` is never there
57//!
58//! Not a webview defect, and not something a later version fixes. Fetch
59//! Metadata is appended only to a [potentially trustworthy URL][trustworthy],
60//! and `scheme://localhost` has an [opaque origin][origin] - which that
61//! algorithm answers "Not Trustworthy". So on macOS, iOS and Linux the headers
62//! cannot arrive. That opaque origin is also why [`Origin`] cannot be a thin
63//! wrapper around `url::Origin`.
64//!
65//! [`Platform::HttpSubdomain`] differs: a host ending in `.localhost` *is*
66//! potentially trustworthy, so Windows and Android should send Fetch Metadata
67//! where the others cannot. Design against the weakest case: `Origin` alone,
68//! compared against `Host`.
69//!
70//! [trustworthy]: https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-url
71//! [origin]: https://url.spec.whatwg.org/#concept-url-origin
72
73mod origin;
74#[cfg(feature = "tower")]
75pub mod tower;
76mod trace;
77mod unsupported;
78
79pub use origin::{CanonicalRequest, Denial, Origin, OriginError, Origins, Outcome, Platform};
80pub use unsupported::{Unsupported, unsupported};
81
82const _: () = {
83 const fn assert_send_sync<T: Send + Sync>() {}
84 assert_send_sync::<Origins>();
85 assert_send_sync::<Origin>();
86 assert_send_sync::<Denial>();
87 assert_send_sync::<Unsupported>();
88
89 // A layer is built once and shared, so the same promise holds for it. The
90 // failure this catches is remote: a non-`Send` field added here surfaces as
91 // an unsatisfied bound wherever somebody spawns the stack.
92 #[cfg(feature = "tower")]
93 {
94 assert_send_sync::<tower::CanonicalOriginLayer>();
95 assert_send_sync::<tower::RefuseUnsupportedLayer>();
96 }
97};