use std::borrow::Cow;
use std::fmt;
use rama_http_types::{HeaderName, HeaderValue};
use rama_utils::macros::enums::enum_builder;
use crate::util::{self, IterExt};
use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
use super::cross_origin_policy_util::{
SingleTokenWithReportTo, format_single_token_with_report_to, parse_single_token_with_report_to,
};
enum_builder! {
@String
pub enum CrossOriginOpenerPolicyValue {
UnsafeNone => "unsafe-none",
SameOriginAllowPopups => "same-origin-allow-popups",
SameOrigin => "same-origin",
NoopenerAllowPopups => "noopener-allow-popups",
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CrossOriginOpenerPolicy {
pub value: CrossOriginOpenerPolicyValue,
pub report_to: Option<Cow<'static, str>>,
}
impl CrossOriginOpenerPolicy {
#[must_use]
pub fn unsafe_none() -> Self {
Self {
value: CrossOriginOpenerPolicyValue::UnsafeNone,
report_to: None,
}
}
#[must_use]
pub fn same_origin_allow_popups() -> Self {
Self {
value: CrossOriginOpenerPolicyValue::SameOriginAllowPopups,
report_to: None,
}
}
#[must_use]
pub fn same_origin() -> Self {
Self {
value: CrossOriginOpenerPolicyValue::SameOrigin,
report_to: None,
}
}
#[must_use]
pub fn noopener_allow_popups() -> Self {
Self {
value: CrossOriginOpenerPolicyValue::NoopenerAllowPopups,
report_to: None,
}
}
rama_utils::macros::generate_set_and_with! {
pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
self.report_to = Some(endpoint.into());
self
}
}
}
impl fmt::Display for CrossOriginOpenerPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
}
}
impl TypedHeader for CrossOriginOpenerPolicy {
fn name() -> &'static HeaderName {
&::rama_http_types::header::CROSS_ORIGIN_OPENER_POLICY
}
}
impl HeaderDecode for CrossOriginOpenerPolicy {
fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
let raw = values
.just_one()
.and_then(|v| v.to_str().ok())
.ok_or_else(Error::invalid)?;
let SingleTokenWithReportTo { token, report_to } =
parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
let value = CrossOriginOpenerPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
Ok(Self { value, report_to })
}
}
impl HeaderEncode for CrossOriginOpenerPolicy {
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
values.extend(::std::iter::once(util::fmt(self)));
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CrossOriginOpenerPolicyReportOnly {
pub value: CrossOriginOpenerPolicyValue,
pub report_to: Option<Cow<'static, str>>,
}
impl CrossOriginOpenerPolicyReportOnly {
#[must_use]
pub fn from_enforcing(p: CrossOriginOpenerPolicy) -> Self {
Self {
value: p.value,
report_to: p.report_to,
}
}
rama_utils::macros::generate_set_and_with! {
pub fn report_to(mut self, endpoint: impl Into<Cow<'static, str>>) -> Self {
self.report_to = Some(endpoint.into());
self
}
}
}
impl fmt::Display for CrossOriginOpenerPolicyReportOnly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_single_token_with_report_to(f, self.value.as_str(), self.report_to.as_deref())
}
}
impl TypedHeader for CrossOriginOpenerPolicyReportOnly {
fn name() -> &'static HeaderName {
&::rama_http_types::header::CROSS_ORIGIN_OPENER_POLICY_REPORT_ONLY
}
}
impl HeaderDecode for CrossOriginOpenerPolicyReportOnly {
fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
let raw = values
.just_one()
.and_then(|v| v.to_str().ok())
.ok_or_else(Error::invalid)?;
let SingleTokenWithReportTo { token, report_to } =
parse_single_token_with_report_to(raw).ok_or_else(Error::invalid)?;
let value = CrossOriginOpenerPolicyValue::strict_parse(token).ok_or_else(Error::invalid)?;
Ok(Self { value, report_to })
}
}
impl HeaderEncode for CrossOriginOpenerPolicyReportOnly {
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
values.extend(::std::iter::once(util::fmt(self)));
}
}
#[cfg(test)]
mod tests {
use super::super::{test_decode, test_encode};
use super::*;
#[test]
fn round_trip_each_variant_no_report_to() {
for variant in [
CrossOriginOpenerPolicyValue::UnsafeNone,
CrossOriginOpenerPolicyValue::SameOriginAllowPopups,
CrossOriginOpenerPolicyValue::SameOrigin,
CrossOriginOpenerPolicyValue::NoopenerAllowPopups,
] {
let expected_str = variant.as_str().to_owned();
let v = CrossOriginOpenerPolicy {
value: variant,
report_to: None,
};
let map = test_encode(v.clone());
let raw = map
.get(CrossOriginOpenerPolicy::name())
.expect("set")
.to_str()
.unwrap()
.to_owned();
assert_eq!(raw, expected_str);
let parsed = test_decode::<CrossOriginOpenerPolicy>(&[raw.as_str()]).expect("decode");
assert_eq!(parsed, v);
}
}
#[test]
fn round_trip_with_report_to() {
let v = CrossOriginOpenerPolicy::same_origin().with_report_to("coop-endpoint");
let map = test_encode(v.clone());
let raw = map
.get(CrossOriginOpenerPolicy::name())
.expect("set")
.to_str()
.unwrap()
.to_owned();
assert_eq!(raw, r#"same-origin; report-to="coop-endpoint""#);
let parsed = test_decode::<CrossOriginOpenerPolicy>(&[raw.as_str()]).expect("decode");
assert_eq!(parsed, v);
}
#[test]
fn parser_rejects_same_origin_plus_coep_per_spec() {
assert!(test_decode::<CrossOriginOpenerPolicy>(&["same-origin-plus-COEP"]).is_none());
assert!(test_decode::<CrossOriginOpenerPolicy>(&["same-origin-plus-coep"]).is_none());
}
#[test]
fn parser_accepts_noopener_allow_popups() {
let parsed =
test_decode::<CrossOriginOpenerPolicy>(&["noopener-allow-popups"]).expect("decode");
assert_eq!(parsed, CrossOriginOpenerPolicy::noopener_allow_popups());
}
#[test]
fn parser_rejects_unknown_token() {
assert!(test_decode::<CrossOriginOpenerPolicy>(&["nope"]).is_none());
}
#[test]
fn parser_accepts_unquoted_report_to() {
let parsed =
test_decode::<CrossOriginOpenerPolicy>(&["same-origin; report-to=coop-endpoint"])
.expect("decode unquoted");
assert_eq!(
parsed,
CrossOriginOpenerPolicy::same_origin().with_report_to("coop-endpoint"),
);
}
#[test]
fn parser_ignores_unknown_parameters() {
let parsed =
test_decode::<CrossOriginOpenerPolicy>(&["same-origin; foo=bar; report-to=\"ep\""])
.expect("decode unknown");
assert_eq!(
parsed,
CrossOriginOpenerPolicy::same_origin().with_report_to("ep"),
);
}
#[test]
fn report_only_round_trip() {
let v = CrossOriginOpenerPolicyReportOnly {
value: CrossOriginOpenerPolicyValue::SameOrigin,
report_to: Some("ep".into()),
};
let map = test_encode(v.clone());
let raw = map
.get(CrossOriginOpenerPolicyReportOnly::name())
.expect("set")
.to_str()
.unwrap()
.to_owned();
assert_eq!(raw, r#"same-origin; report-to="ep""#);
let parsed =
test_decode::<CrossOriginOpenerPolicyReportOnly>(&[raw.as_str()]).expect("decode");
assert_eq!(parsed, v);
}
#[test]
fn enforcing_and_report_only_use_distinct_header_names() {
assert_ne!(
CrossOriginOpenerPolicy::name(),
CrossOriginOpenerPolicyReportOnly::name(),
);
}
}