use crate::net::mixed_content::is_potentially_trustworthy;
use url::Url;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ReferrerPolicy {
NoReferrer,
NoReferrerWhenDowngrade,
SameOrigin,
Origin,
StrictOrigin,
OriginWhenCrossOrigin,
#[default]
StrictOriginWhenCrossOrigin,
UnsafeUrl,
}
impl ReferrerPolicy {
pub fn parse_token(token: &str) -> Option<Self> {
match token.trim().to_ascii_lowercase().as_str() {
"no-referrer" => Some(Self::NoReferrer),
"no-referrer-when-downgrade" => Some(Self::NoReferrerWhenDowngrade),
"same-origin" => Some(Self::SameOrigin),
"origin" => Some(Self::Origin),
"strict-origin" => Some(Self::StrictOrigin),
"origin-when-cross-origin" => Some(Self::OriginWhenCrossOrigin),
"strict-origin-when-cross-origin" => Some(Self::StrictOriginWhenCrossOrigin),
"unsafe-url" => Some(Self::UnsafeUrl),
_ => None,
}
}
pub fn parse_header(value: &str) -> Option<Self> {
value.split(',').filter_map(Self::parse_token).next_back()
}
}
fn strip(url: &Url) -> Option<Url> {
if !matches!(url.scheme(), "http" | "https") {
return None;
}
let mut stripped = url.clone();
stripped.set_fragment(None);
let _ = stripped.set_username("");
let _ = stripped.set_password(None);
Some(stripped)
}
fn origin_only(url: &Url) -> Option<Url> {
let mut origin = strip(url)?;
origin.set_path("/");
origin.set_query(None);
Some(origin)
}
fn is_downgrade(referrer: &Url, target: &Url) -> bool {
is_potentially_trustworthy(referrer) && !is_potentially_trustworthy(target)
}
pub(crate) fn never_sends(referrer: &Url, policy: ReferrerPolicy) -> bool {
policy == ReferrerPolicy::NoReferrer || strip(referrer).is_none()
}
const MAX_REFERRER_LEN: usize = 4096;
pub fn determine(referrer: &Url, policy: ReferrerPolicy, target: &Url) -> Option<Url> {
let stripped = strip(referrer)?;
let origin = origin_only(referrer)?;
let same_origin = referrer.origin() == target.origin();
let downgrade = is_downgrade(referrer, target);
let full = if stripped.as_str().len() > MAX_REFERRER_LEN {
origin.clone()
} else {
stripped
};
match policy {
ReferrerPolicy::NoReferrer => None,
ReferrerPolicy::UnsafeUrl => Some(full),
ReferrerPolicy::Origin => Some(origin),
ReferrerPolicy::SameOrigin => same_origin.then_some(full),
ReferrerPolicy::StrictOrigin => (!downgrade).then_some(origin),
ReferrerPolicy::OriginWhenCrossOrigin => Some(if same_origin { full } else { origin }),
ReferrerPolicy::StrictOriginWhenCrossOrigin => {
if same_origin {
Some(full)
} else if downgrade {
None
} else {
Some(origin)
}
}
ReferrerPolicy::NoReferrerWhenDowngrade => (!downgrade).then_some(full),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn u(s: &str) -> Url {
Url::parse(s).unwrap()
}
fn doc() -> Url {
u("https://example.com/page?q=1#frag")
}
fn determined(policy: ReferrerPolicy, target: &str) -> Option<String> {
determine(&doc(), policy, &u(target)).map(|r| r.to_string())
}
#[test]
fn no_referrer_sends_nothing_anywhere() {
for target in [
"https://example.com/a",
"https://other.com/a",
"http://other.com/a",
] {
assert_eq!(determined(ReferrerPolicy::NoReferrer, target), None);
}
}
#[test]
fn unsafe_url_sends_full_url_even_on_downgrade() {
for target in [
"https://example.com/a",
"https://other.com/a",
"http://other.com/a",
] {
assert_eq!(
determined(ReferrerPolicy::UnsafeUrl, target).as_deref(),
Some("https://example.com/page?q=1"),
"{target}"
);
}
}
#[test]
fn origin_sends_origin_even_on_downgrade() {
for target in [
"https://example.com/a",
"https://other.com/a",
"http://other.com/a",
] {
assert_eq!(
determined(ReferrerPolicy::Origin, target).as_deref(),
Some("https://example.com/"),
"{target}"
);
}
}
#[test]
fn same_origin_sends_only_within_the_origin() {
assert_eq!(
determined(ReferrerPolicy::SameOrigin, "https://example.com/a").as_deref(),
Some("https://example.com/page?q=1")
);
assert_eq!(
determined(ReferrerPolicy::SameOrigin, "https://other.com/a"),
None
);
}
#[test]
fn strict_origin_drops_the_referrer_on_downgrade() {
assert_eq!(
determined(ReferrerPolicy::StrictOrigin, "https://other.com/a").as_deref(),
Some("https://example.com/")
);
assert_eq!(
determined(ReferrerPolicy::StrictOrigin, "http://other.com/a"),
None
);
}
#[test]
fn origin_when_cross_origin_keeps_the_path_only_at_home() {
assert_eq!(
determined(
ReferrerPolicy::OriginWhenCrossOrigin,
"https://example.com/a"
)
.as_deref(),
Some("https://example.com/page?q=1")
);
assert_eq!(
determined(ReferrerPolicy::OriginWhenCrossOrigin, "https://other.com/a").as_deref(),
Some("https://example.com/")
);
assert_eq!(
determined(ReferrerPolicy::OriginWhenCrossOrigin, "http://other.com/a").as_deref(),
Some("https://example.com/")
);
}
#[test]
fn strict_origin_when_cross_origin_is_the_default() {
assert_eq!(
ReferrerPolicy::default(),
ReferrerPolicy::StrictOriginWhenCrossOrigin
);
assert_eq!(
determined(
ReferrerPolicy::StrictOriginWhenCrossOrigin,
"https://example.com/a"
)
.as_deref(),
Some("https://example.com/page?q=1")
);
assert_eq!(
determined(
ReferrerPolicy::StrictOriginWhenCrossOrigin,
"https://other.com/a"
)
.as_deref(),
Some("https://example.com/")
);
assert_eq!(
determined(
ReferrerPolicy::StrictOriginWhenCrossOrigin,
"http://other.com/a"
),
None
);
}
#[test]
fn no_referrer_when_downgrade_keeps_the_full_url_until_it_downgrades() {
assert_eq!(
determined(
ReferrerPolicy::NoReferrerWhenDowngrade,
"https://other.com/a"
)
.as_deref(),
Some("https://example.com/page?q=1")
);
assert_eq!(
determined(
ReferrerPolicy::NoReferrerWhenDowngrade,
"http://other.com/a"
),
None
);
}
#[test]
fn fragment_and_credentials_are_stripped() {
let referrer = u("https://user:pw@example.com/page?q=1#secret");
let sent = determine(
&referrer,
ReferrerPolicy::UnsafeUrl,
&u("https://example.com/a"),
)
.unwrap();
assert_eq!(sent.as_str(), "https://example.com/page?q=1");
assert!(sent.fragment().is_none());
assert_eq!(sent.username(), "");
assert_eq!(sent.password(), None);
}
#[test]
fn non_network_referrer_sources_send_nothing() {
for source in ["file:///home/user/secret.html", "data:text/html,hi"] {
assert_eq!(
determine(
&u(source),
ReferrerPolicy::UnsafeUrl,
&u("https://example.com/a")
),
None,
"{source}"
);
}
}
#[test]
fn insecure_to_insecure_is_not_a_downgrade() {
assert_eq!(
determine(
&u("http://example.com/page"),
ReferrerPolicy::StrictOriginWhenCrossOrigin,
&u("http://other.com/a"),
)
.map(|r| r.to_string())
.as_deref(),
Some("http://example.com/")
);
}
#[test]
fn loopback_referrer_downgrades_to_plain_http() {
assert_eq!(
determine(
&u("http://localhost:3000/page"),
ReferrerPolicy::StrictOrigin,
&u("http://other.com/a"),
),
None
);
}
#[test]
fn parses_every_policy_token_case_insensitively() {
let cases = [
("no-referrer", ReferrerPolicy::NoReferrer),
(
"no-referrer-when-downgrade",
ReferrerPolicy::NoReferrerWhenDowngrade,
),
("same-origin", ReferrerPolicy::SameOrigin),
("origin", ReferrerPolicy::Origin),
("strict-origin", ReferrerPolicy::StrictOrigin),
(
"origin-when-cross-origin",
ReferrerPolicy::OriginWhenCrossOrigin,
),
(
"strict-origin-when-cross-origin",
ReferrerPolicy::StrictOriginWhenCrossOrigin,
),
("unsafe-url", ReferrerPolicy::UnsafeUrl),
];
for (token, expected) in cases {
assert_eq!(
ReferrerPolicy::parse_token(token),
Some(expected),
"{token}"
);
assert_eq!(
ReferrerPolicy::parse_token(&token.to_uppercase()),
Some(expected),
"{token} uppercased"
);
}
assert_eq!(ReferrerPolicy::parse_token("nonsense"), None);
assert_eq!(ReferrerPolicy::parse_token(""), None);
}
#[test]
fn header_list_takes_the_last_understood_token() {
assert_eq!(
ReferrerPolicy::parse_header("no-referrer, strict-origin-when-cross-origin"),
Some(ReferrerPolicy::StrictOriginWhenCrossOrigin)
);
assert_eq!(
ReferrerPolicy::parse_header("no-referrer, some-future-policy"),
Some(ReferrerPolicy::NoReferrer)
);
assert_eq!(ReferrerPolicy::parse_header("a, b"), None);
assert_eq!(ReferrerPolicy::parse_header(""), None);
}
#[test]
fn over_long_referrer_degrades_to_origin() {
let long = u(&format!("https://example.com/{}", "a".repeat(5000)));
assert!(long.as_str().len() > MAX_REFERRER_LEN);
for policy in [
ReferrerPolicy::UnsafeUrl,
ReferrerPolicy::SameOrigin,
ReferrerPolicy::StrictOriginWhenCrossOrigin,
] {
assert_eq!(
determine(&long, policy, &u("https://example.com/a")).map(|r| r.to_string()),
Some("https://example.com/".to_string()),
"{policy:?}"
);
}
let short = u("https://example.com/page");
assert_eq!(
determine(
&short,
ReferrerPolicy::UnsafeUrl,
&u("https://example.com/a")
)
.map(|r| r.to_string()),
Some("https://example.com/page".to_string())
);
}
#[test]
fn different_port_is_cross_origin() {
assert_eq!(
determined(ReferrerPolicy::SameOrigin, "https://example.com:8443/a"),
None
);
}
}