rama_http_headers/common/
cross_origin_opener_policy.rs1use std::borrow::Cow;
6use std::fmt;
7
8use rama_http_types::{HeaderName, HeaderValue};
9use rama_utils::macros::enums::enum_builder;
10
11use crate::util::{self, IterExt};
12use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
13
14use super::cross_origin_policy_util::{
15 SingleTokenWithReportTo, format_single_token_with_report_to, parse_single_token_with_report_to,
16};
17
18enum_builder! {
19 @String
34 pub enum CrossOriginOpenerPolicyValue {
35 UnsafeNone => "unsafe-none",
37 SameOriginAllowPopups => "same-origin-allow-popups",
40 SameOrigin => "same-origin",
43 NoopenerAllowPopups => "noopener-allow-popups",
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct CrossOriginOpenerPolicy {
85 pub value: CrossOriginOpenerPolicyValue,
86 pub report_to: Option<Cow<'static, str>>,
90}
91
92impl CrossOriginOpenerPolicy {
93 #[must_use]
95 pub fn unsafe_none() -> Self {
96 Self {
97 value: CrossOriginOpenerPolicyValue::UnsafeNone,
98 report_to: None,
99 }
100 }
101
102 #[must_use]
104 pub fn same_origin_allow_popups() -> Self {
105 Self {
106 value: CrossOriginOpenerPolicyValue::SameOriginAllowPopups,
107 report_to: None,
108 }
109 }
110
111 #[must_use]
113 pub fn same_origin() -> Self {
114 Self {
115 value: CrossOriginOpenerPolicyValue::SameOrigin,
116 report_to: None,
117 }
118 }
119
120 #[must_use]
122 pub fn noopener_allow_popups() -> Self {
123 Self {
124 value: CrossOriginOpenerPolicyValue::NoopenerAllowPopups,
125 report_to: None,
126 }
127 }
128
129 rama_utils::macros::generate_set_and_with! {
130 pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
131 self.report_to = Some(endpoint.into());
132 self
133 }
134 }
135}
136
137impl fmt::Display for CrossOriginOpenerPolicy {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
140 }
141}
142
143impl TypedHeader for CrossOriginOpenerPolicy {
144 fn name() -> &'static HeaderName {
145 &::rama_http_types::header::CROSS_ORIGIN_OPENER_POLICY
146 }
147}
148
149impl HeaderDecode for CrossOriginOpenerPolicy {
150 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
151 let raw = values
152 .just_one()
153 .and_then(|v| v.to_str().ok())
154 .ok_or_else(Error::invalid)?;
155 let SingleTokenWithReportTo { token, report_to } =
156 parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
157 let value = CrossOriginOpenerPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
158 Ok(Self { value, report_to })
159 }
160}
161
162impl HeaderEncode for CrossOriginOpenerPolicy {
163 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
164 values.extend(::std::iter::once(util::fmt(self)));
165 }
166}
167
168#[derive(Clone, Debug, PartialEq, Eq, Hash)]
172pub struct CrossOriginOpenerPolicyReportOnly {
173 pub value: CrossOriginOpenerPolicyValue,
174 pub report_to: Option<Cow<'static, str>>,
175}
176
177impl CrossOriginOpenerPolicyReportOnly {
178 #[must_use]
181 pub fn from_enforcing(p: CrossOriginOpenerPolicy) -> Self {
182 Self {
183 value: p.value,
184 report_to: p.report_to,
185 }
186 }
187
188 rama_utils::macros::generate_set_and_with! {
189 pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
190 self.report_to = Some(endpoint.into());
191 self
192 }
193 }
194}
195
196impl fmt::Display for CrossOriginOpenerPolicyReportOnly {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
199 }
200}
201
202impl TypedHeader for CrossOriginOpenerPolicyReportOnly {
203 fn name() -> &'static HeaderName {
204 &::rama_http_types::header::CROSS_ORIGIN_OPENER_POLICY_REPORT_ONLY
205 }
206}
207
208impl HeaderDecode for CrossOriginOpenerPolicyReportOnly {
209 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
210 let raw = values
211 .just_one()
212 .and_then(|v| v.to_str().ok())
213 .ok_or_else(Error::invalid)?;
214 let SingleTokenWithReportTo { token, report_to } =
215 parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
216 let value = CrossOriginOpenerPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
217 Ok(Self { value, report_to })
218 }
219}
220
221impl HeaderEncode for CrossOriginOpenerPolicyReportOnly {
222 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
223 values.extend(::std::iter::once(util::fmt(self)));
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::super::{test_decode, test_encode};
230 use super::*;
231
232 #[test]
233 fn round_trip_each_variant_no_report_to() {
234 for variant in [
235 CrossOriginOpenerPolicyValue::UnsafeNone,
236 CrossOriginOpenerPolicyValue::SameOriginAllowPopups,
237 CrossOriginOpenerPolicyValue::SameOrigin,
238 CrossOriginOpenerPolicyValue::NoopenerAllowPopups,
239 ] {
240 let expected_str = variant.as_str().to_owned();
241 let v = CrossOriginOpenerPolicy {
242 value: variant,
243 report_to: None,
244 };
245 let map = test_encode(v.clone());
246 let raw = map
247 .get(CrossOriginOpenerPolicy::name())
248 .expect("set")
249 .to_str()
250 .unwrap()
251 .to_owned();
252 assert_eq!(raw, expected_str);
253 let parsed = test_decode::<CrossOriginOpenerPolicy>(&[raw.as_str()]).expect("decode");
254 assert_eq!(parsed, v);
255 }
256 }
257
258 #[test]
259 fn round_trip_with_report_to() {
260 let v = CrossOriginOpenerPolicy::same_origin().with_report_to("coop-endpoint");
261 let map = test_encode(v.clone());
262 let raw = map
263 .get(CrossOriginOpenerPolicy::name())
264 .expect("set")
265 .to_str()
266 .unwrap()
267 .to_owned();
268 assert_eq!(raw, r#"same-origin; report-to="coop-endpoint""#);
269 let parsed = test_decode::<CrossOriginOpenerPolicy>(&[raw.as_str()]).expect("decode");
270 assert_eq!(parsed, v);
271 }
272
273 #[test]
274 fn parser_rejects_same_origin_plus_coep_per_spec() {
275 assert!(test_decode::<CrossOriginOpenerPolicy>(&["same-origin-plus-COEP"]).is_none());
282 assert!(test_decode::<CrossOriginOpenerPolicy>(&["same-origin-plus-coep"]).is_none());
283 }
284
285 #[test]
286 fn parser_accepts_noopener_allow_popups() {
287 let parsed =
289 test_decode::<CrossOriginOpenerPolicy>(&["noopener-allow-popups"]).expect("decode");
290 assert_eq!(parsed, CrossOriginOpenerPolicy::noopener_allow_popups());
291 }
292
293 #[test]
294 fn parser_rejects_unknown_token() {
295 assert!(test_decode::<CrossOriginOpenerPolicy>(&["nope"]).is_none());
296 }
297
298 #[test]
299 fn parser_accepts_unquoted_report_to() {
300 let parsed =
301 test_decode::<CrossOriginOpenerPolicy>(&["same-origin; report-to=coop-endpoint"])
302 .expect("decode unquoted");
303 assert_eq!(
304 parsed,
305 CrossOriginOpenerPolicy::same_origin().with_report_to("coop-endpoint"),
306 );
307 }
308
309 #[test]
310 fn parser_ignores_unknown_parameters() {
311 let parsed =
312 test_decode::<CrossOriginOpenerPolicy>(&["same-origin; foo=bar; report-to=\"ep\""])
313 .expect("decode unknown");
314 assert_eq!(
315 parsed,
316 CrossOriginOpenerPolicy::same_origin().with_report_to("ep"),
317 );
318 }
319
320 #[test]
321 fn report_only_round_trip() {
322 let v = CrossOriginOpenerPolicyReportOnly {
323 value: CrossOriginOpenerPolicyValue::SameOrigin,
324 report_to: Some("ep".into()),
325 };
326 let map = test_encode(v.clone());
327 let raw = map
328 .get(CrossOriginOpenerPolicyReportOnly::name())
329 .expect("set")
330 .to_str()
331 .unwrap()
332 .to_owned();
333 assert_eq!(raw, r#"same-origin; report-to="ep""#);
334 let parsed =
335 test_decode::<CrossOriginOpenerPolicyReportOnly>(&[raw.as_str()]).expect("decode");
336 assert_eq!(parsed, v);
337 }
338
339 #[test]
340 fn enforcing_and_report_only_use_distinct_header_names() {
341 assert_ne!(
342 CrossOriginOpenerPolicy::name(),
343 CrossOriginOpenerPolicyReportOnly::name(),
344 );
345 }
346}