Skip to main content

orinium_browser/engine/
origin.rs

1//! Origin model for rendered documents.
2//!
3//! See <https://html.spec.whatwg.org/multipage/origin.html>. The browser layer
4//! uses origins to:
5//! - expose a consistent `origin` through the `window` / `location` /
6//!   `document` JavaScript APIs,
7//! - gate access to internal schemes (`resource:`, ...) from web pages,
8//! - compute `Origin` / `Referer` request headers without leaking bundled
9//!   resource URLs to external servers,
10//! - enforce same-origin / cross-origin visibility for `fetch()` and
11//!   `XMLHttpRequest`.
12
13use url::{Origin as UrlOrigin, Url};
14
15/// The origin of a document or a fetch initiator.
16///
17/// Network schemes (`http`, `https`, `ftp`, `ws`, `wss`) become tuple origins
18/// whose equality ignores default ports. Everything else (`resource:`, `data:`,
19/// `about:`, `file:`, custom schemes) is opaque: it serializes as `"null"` in
20/// JavaScript and never compares equal to another origin.
21/// An origin may also carry internal/privileged status. Only browser-created
22/// origins ([`Origin::internal`]) hold internal privileges. No URL scheme grants
23/// it: loading `resource://` content does not make a page internal.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct Origin {
26    inner: UrlOrigin,
27    is_internal: bool,
28}
29
30impl Origin {
31    /// Computes the origin of a parsed URL.
32    ///
33    /// No URL grants internal privilege: a `resource://` page is an ordinary
34    /// opaque origin. Internal status is only ever conferred explicitly by the
35    /// browser through [`Origin::internal`] for chrome-created origins.
36    pub fn from_url(url: &Url) -> Self {
37        Self {
38            inner: url.origin(),
39            is_internal: false,
40        }
41    }
42
43    /// Computes the origin of a URL string, falling back to a fresh opaque
44    /// origin when the string does not parse.
45    pub fn from_url_string(url: &str) -> Self {
46        match Url::parse(url) {
47            Ok(url) => Self::from_url(&url),
48            Err(_) => Self::opaque(),
49        }
50    }
51
52    /// A fresh internal privileged origin (for browser-internal components).
53    pub fn internal() -> Self {
54        Self {
55            inner: UrlOrigin::new_opaque(),
56            is_internal: true,
57        }
58    }
59
60    /// A fresh opaque origin (sandboxed contexts, parse failures, unprivileged opaque origins).
61    pub fn opaque() -> Self {
62        Self {
63            inner: UrlOrigin::new_opaque(),
64            is_internal: false,
65        }
66    }
67
68    /// Serialization exposed to JavaScript: `scheme://host[:port]` or `"null"`.
69    pub fn ascii_serialization(&self) -> String {
70        self.inner.ascii_serialization()
71    }
72
73    /// Whether this is an `http(s)://` tuple origin that proxies a real web page.
74    pub fn is_network(&self) -> bool {
75        self.inner.is_tuple()
76    }
77
78    /// Whether this is an opaque origin (not a network tuple origin).
79    pub fn is_opaque(&self) -> bool {
80        !self.inner.is_tuple()
81    }
82
83    /// Whether this origin is internal and holds browser privileges (access to `resource:`, `file:`).
84    pub fn is_internal(&self) -> bool {
85        self.is_internal
86    }
87
88    /// Same-origin check. Opaque origins are never equal to any other origin.
89    pub fn same_origin(&self, other: &Self) -> bool {
90        self.inner == other.inner
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::test_support::parse_url as url;
98
99    #[test]
100    fn network_origins_equal_with_default_port_normalization() {
101        let a = Origin::from_url(&url("https://example.test/path"));
102        let b = Origin::from_url(&url("https://example.test:443/other"));
103        assert_eq!(a, b);
104        assert!(a.same_origin(&b));
105        assert_eq!(a.ascii_serialization(), "https://example.test");
106    }
107
108    #[test]
109    fn non_default_ports_make_distinct_origins() {
110        let a = Origin::from_url(&url("https://example.test:443/"));
111        let b = Origin::from_url(&url("https://example.test:8443/"));
112        assert!(a.is_network());
113        assert!(b.is_network());
114        assert!(!a.same_origin(&b));
115        assert_eq!(b.ascii_serialization(), "https://example.test:8443");
116    }
117
118    #[test]
119    fn scheme_and_host_differences_break_origin_equality() {
120        let https = Origin::from_url(&url("https://example.test/"));
121        let http = Origin::from_url(&url("http://example.test/"));
122        let other_host = Origin::from_url(&url("https://other.test/"));
123        assert!(!https.same_origin(&http));
124        assert!(!https.same_origin(&other_host));
125    }
126
127    #[test]
128    fn internal_schemes_are_opaque_and_serialize_as_null() {
129        for raw in [
130            "resource:///devtools/index.html",
131            "data:text/plain,hello",
132            "about:blank",
133        ] {
134            let origin = Origin::from_url(&url(raw));
135            assert!(origin.is_opaque(), "{} should be opaque", raw);
136            assert_eq!(origin.ascii_serialization(), "null");
137        }
138    }
139
140    #[test]
141    fn opaque_origins_never_compare_equal() {
142        let a = Origin::from_url_string("resource:///devtools/index.html");
143        let b = Origin::from_url_string("resource:///devtools/index.html");
144        assert!(!a.same_origin(&b));
145        assert_ne!(a, b);
146    }
147
148    #[test]
149    fn unparsable_url_strings_fall_back_to_opaque() {
150        let origin = Origin::from_url_string("::not a url::");
151        assert!(origin.is_opaque());
152        assert!(!origin.is_internal());
153        assert_eq!(origin.ascii_serialization(), "null");
154    }
155
156    #[test]
157    fn resource_url_is_opaque_but_not_internal() {
158        let origin = Origin::from_url(&url("resource:///test/index.html"));
159        assert!(!origin.is_internal());
160        assert!(origin.is_opaque());
161        assert!(!origin.is_network());
162        assert_eq!(origin.ascii_serialization(), "null");
163    }
164
165    #[test]
166    fn data_and_about_urls_are_opaque_but_not_internal() {
167        for raw in ["data:text/plain,hello", "about:blank"] {
168            let origin = Origin::from_url(&url(raw));
169            assert!(origin.is_opaque(), "{} should be opaque", raw);
170            assert!(!origin.is_internal(), "{} should not be internal", raw);
171            assert!(!origin.is_network(), "{} should not be network", raw);
172        }
173    }
174
175    #[test]
176    fn internal_and_opaque_constructors() {
177        let internal = Origin::internal();
178        assert!(internal.is_internal());
179        assert!(internal.is_opaque());
180
181        let opaque = Origin::opaque();
182        assert!(!opaque.is_internal());
183        assert!(opaque.is_opaque());
184    }
185}