Skip to main content

rama_http_headers/common/
cross_origin_embedder_policy.rs

1//! `Cross-Origin-Embedder-Policy` (COEP) and its report-only sibling.
2//!
3//! Per the [HTML Standard § COEP](https://html.spec.whatwg.org/multipage/browsers.html#coep).
4//! Both types share their payload (token + optional `report-to`
5//! parameter); only the header name differs.
6
7use std::borrow::Cow;
8use std::fmt;
9
10use rama_http_types::{HeaderName, HeaderValue};
11use rama_utils::macros::enums::enum_builder;
12
13use crate::util::{self, IterExt};
14use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
15
16use super::cross_origin_policy_util::{
17    SingleTokenWithReportTo, format_single_token_with_report_to, parse_single_token_with_report_to,
18};
19
20enum_builder! {
21    /// Embedder-policy token values per the HTML Standard.
22    ///
23    /// The auto-generated [`Unknown`](Self::Unknown) variant is
24    /// reachable only via direct construction; the COEP decoder uses
25    /// strict parsing and rejects any token outside the spec set.
26    @String
27    pub enum CrossOriginEmbedderPolicyValue {
28        /// `unsafe-none` — the spec default when the header is absent.
29        ///
30        /// Sending this explicitly is distinguishable on the wire from
31        /// absence; both produce the same browser behaviour.
32        UnsafeNone => "unsafe-none",
33        /// `require-corp` — same-origin policy, requires every loaded
34        /// resource to opt in via a `Cross-Origin-Resource-Policy`
35        /// header or CORS.
36        RequireCorp => "require-corp",
37        /// `credentialless` — like `require-corp` but allows
38        /// no-credential loads as a relaxation.
39        Credentialless => "credentialless",
40    }
41}
42
43/// `Cross-Origin-Embedder-Policy` (COEP) header.
44///
45/// Send `require-corp` or `credentialless` to opt the document into
46/// process isolation suitable for using `SharedArrayBuffer` and other
47/// cross-origin-isolated capabilities.
48///
49/// The optional `report-to` parameter names a [Reporting API endpoint]
50/// (defined in the `Reporting-Endpoints` header) where the browser
51/// posts violation reports.
52///
53/// [Reporting API endpoint]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Reporting-Endpoints
54///
55/// # Default semantics
56///
57/// When the header is absent the user agent applies `unsafe-none`
58/// (effectively: no embedder-policy enforcement). The
59/// [`UnsafeNone`](CrossOriginEmbedderPolicyValue::UnsafeNone) variant
60/// represents the header being *present* with that explicit value —
61/// both produce the same browser behaviour but the presence is
62/// distinguishable on the wire.
63///
64/// # Example
65///
66/// ```
67/// use rama_http_headers::{CrossOriginEmbedderPolicy, CrossOriginEmbedderPolicyValue};
68///
69/// let coep = CrossOriginEmbedderPolicy {
70///     value: CrossOriginEmbedderPolicyValue::RequireCorp,
71///     report_to: Some("coep-endpoint".into()),
72/// };
73/// assert_eq!(coep.to_string(), r#"require-corp; report-to="coep-endpoint""#);
74/// ```
75#[derive(Clone, Debug, PartialEq, Eq, Hash)]
76pub struct CrossOriginEmbedderPolicy {
77    pub value: CrossOriginEmbedderPolicyValue,
78    /// `; report-to="<endpoint>"` — endpoint name defined in the
79    /// `Reporting-Endpoints` header. Always emitted as a quoted
80    /// sf-string per RFC 8941.
81    pub report_to: Option<Cow<'static, str>>,
82}
83
84impl CrossOriginEmbedderPolicy {
85    /// Builder shortcut: `unsafe-none`, no `report-to`.
86    #[must_use]
87    pub fn unsafe_none() -> Self {
88        Self {
89            value: CrossOriginEmbedderPolicyValue::UnsafeNone,
90            report_to: None,
91        }
92    }
93
94    /// Builder shortcut: `require-corp`, no `report-to`.
95    #[must_use]
96    pub fn require_corp() -> Self {
97        Self {
98            value: CrossOriginEmbedderPolicyValue::RequireCorp,
99            report_to: None,
100        }
101    }
102
103    /// Builder shortcut: `credentialless`, no `report-to`.
104    #[must_use]
105    pub fn credentialless() -> Self {
106        Self {
107            value: CrossOriginEmbedderPolicyValue::Credentialless,
108            report_to: None,
109        }
110    }
111
112    rama_utils::macros::generate_set_and_with! {
113        pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
114            self.report_to = Some(endpoint.into());
115            self
116        }
117    }
118}
119
120impl fmt::Display for CrossOriginEmbedderPolicy {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
123    }
124}
125
126impl TypedHeader for CrossOriginEmbedderPolicy {
127    fn name() -> &'static HeaderName {
128        &::rama_http_types::header::CROSS_ORIGIN_EMBEDDER_POLICY
129    }
130}
131
132impl HeaderDecode for CrossOriginEmbedderPolicy {
133    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
134        let raw = values
135            .just_one()
136            .and_then(|v| v.to_str().ok())
137            .ok_or_else(Error::invalid)?;
138        let SingleTokenWithReportTo { token, report_to } =
139            parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
140        let value =
141            CrossOriginEmbedderPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
142        Ok(Self { value, report_to })
143    }
144}
145
146impl HeaderEncode for CrossOriginEmbedderPolicy {
147    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
148        values.extend(::std::iter::once(util::fmt(self)));
149    }
150}
151
152/// `Cross-Origin-Embedder-Policy-Report-Only` — same payload as
153/// [`CrossOriginEmbedderPolicy`], reports violations without enforcing
154/// the policy.
155///
156/// Use this to roll out a new embedder policy without breaking the page
157/// for users, then promote to the enforcing header once the report
158/// volume is acceptable.
159#[derive(Clone, Debug, PartialEq, Eq, Hash)]
160pub struct CrossOriginEmbedderPolicyReportOnly {
161    pub value: CrossOriginEmbedderPolicyValue,
162    pub report_to: Option<Cow<'static, str>>,
163}
164
165impl CrossOriginEmbedderPolicyReportOnly {
166    /// Convert an enforcing [`CrossOriginEmbedderPolicy`] to its
167    /// report-only sibling, mirroring the payload.
168    #[must_use]
169    pub fn from_enforcing(p: CrossOriginEmbedderPolicy) -> Self {
170        Self {
171            value: p.value,
172            report_to: p.report_to,
173        }
174    }
175
176    rama_utils::macros::generate_set_and_with! {
177        pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
178            self.report_to = Some(endpoint.into());
179            self
180        }
181    }
182}
183
184impl fmt::Display for CrossOriginEmbedderPolicyReportOnly {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
187    }
188}
189
190impl TypedHeader for CrossOriginEmbedderPolicyReportOnly {
191    fn name() -> &'static HeaderName {
192        &::rama_http_types::header::CROSS_ORIGIN_EMBEDDER_POLICY_REPORT_ONLY
193    }
194}
195
196impl HeaderDecode for CrossOriginEmbedderPolicyReportOnly {
197    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
198        let raw = values
199            .just_one()
200            .and_then(|v| v.to_str().ok())
201            .ok_or_else(Error::invalid)?;
202        let SingleTokenWithReportTo { token, report_to } =
203            parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
204        let value =
205            CrossOriginEmbedderPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
206        Ok(Self { value, report_to })
207    }
208}
209
210impl HeaderEncode for CrossOriginEmbedderPolicyReportOnly {
211    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
212        values.extend(::std::iter::once(util::fmt(self)));
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::super::{test_decode, test_encode};
219    use super::*;
220
221    #[test]
222    fn round_trip_each_variant_no_report_to() {
223        for variant in [
224            CrossOriginEmbedderPolicyValue::UnsafeNone,
225            CrossOriginEmbedderPolicyValue::RequireCorp,
226            CrossOriginEmbedderPolicyValue::Credentialless,
227        ] {
228            let expected_str = variant.as_str().to_owned();
229            let v = CrossOriginEmbedderPolicy {
230                value: variant,
231                report_to: None,
232            };
233            let map = test_encode(v.clone());
234            let raw = map
235                .get(CrossOriginEmbedderPolicy::name())
236                .expect("set")
237                .to_str()
238                .unwrap()
239                .to_owned();
240            assert_eq!(raw, expected_str);
241            let parsed = test_decode::<CrossOriginEmbedderPolicy>(&[raw.as_str()]).expect("decode");
242            assert_eq!(parsed, v);
243        }
244    }
245
246    #[test]
247    fn round_trip_with_report_to() {
248        let v = CrossOriginEmbedderPolicy::require_corp().with_report_to("coep-endpoint");
249        let map = test_encode(v.clone());
250        let raw = map
251            .get(CrossOriginEmbedderPolicy::name())
252            .expect("set")
253            .to_str()
254            .unwrap()
255            .to_owned();
256        assert_eq!(raw, r#"require-corp; report-to="coep-endpoint""#);
257        let parsed = test_decode::<CrossOriginEmbedderPolicy>(&[raw.as_str()]).expect("decode");
258        assert_eq!(parsed, v);
259    }
260
261    #[test]
262    fn parser_case_insensitive_token() {
263        assert_eq!(
264            test_decode::<CrossOriginEmbedderPolicy>(&["Require-Corp"]),
265            Some(CrossOriginEmbedderPolicy::require_corp()),
266        );
267    }
268
269    #[test]
270    fn parser_rejects_unknown_token() {
271        assert!(test_decode::<CrossOriginEmbedderPolicy>(&["nope"]).is_none());
272    }
273
274    #[test]
275    fn parser_accepts_unquoted_report_to() {
276        let parsed =
277            test_decode::<CrossOriginEmbedderPolicy>(&["require-corp; report-to=coep-endpoint"])
278                .expect("decode unquoted");
279        assert_eq!(
280            parsed,
281            CrossOriginEmbedderPolicy::require_corp().with_report_to("coep-endpoint"),
282        );
283    }
284
285    #[test]
286    fn report_only_round_trip() {
287        let v = CrossOriginEmbedderPolicyReportOnly {
288            value: CrossOriginEmbedderPolicyValue::RequireCorp,
289            report_to: Some("ep".into()),
290        };
291        let map = test_encode(v.clone());
292        let raw = map
293            .get(CrossOriginEmbedderPolicyReportOnly::name())
294            .expect("set")
295            .to_str()
296            .unwrap()
297            .to_owned();
298        assert_eq!(raw, r#"require-corp; report-to="ep""#);
299        let parsed =
300            test_decode::<CrossOriginEmbedderPolicyReportOnly>(&[raw.as_str()]).expect("decode");
301        assert_eq!(parsed, v);
302    }
303
304    #[test]
305    fn enforcing_and_report_only_use_distinct_header_names() {
306        assert_ne!(
307            CrossOriginEmbedderPolicy::name(),
308            CrossOriginEmbedderPolicyReportOnly::name(),
309        );
310    }
311}