use std::fmt::{self, Debug, Formatter};
use trillium::{Conn, KnownHeaderName};
use url::{Origin, Url};
#[derive(Default)]
pub(crate) enum OriginPolicy {
#[default]
SameOrigin,
Any,
List(Vec<Origin>),
Predicate(OriginPredicate),
}
impl Debug for OriginPolicy {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::SameOrigin => f.write_str("SameOrigin"),
Self::Any => f.write_str("Any"),
Self::List(origins) => f.debug_tuple("List").field(origins).finish(),
Self::Predicate(_) => f
.debug_tuple("Predicate")
.field(&format_args!(".."))
.finish(),
}
}
}
type PredicateFn = Box<dyn Fn(Option<&str>) -> bool + Send + Sync + 'static>;
pub(crate) struct OriginPredicate(PredicateFn);
impl<F> From<F> for OriginPredicate
where
F: Fn(Option<&str>) -> bool + Send + Sync + 'static,
{
fn from(f: F) -> Self {
Self(Box::new(f))
}
}
impl OriginPolicy {
pub(crate) fn list<'a>(origins: impl IntoIterator<Item = &'a str>) -> Self {
Self::List(origins.into_iter().map(parse_allowed_origin).collect())
}
pub(crate) fn allows(&self, conn: &Conn) -> bool {
let origin = conn.request_headers().get_str(KnownHeaderName::Origin);
match self {
Self::Any => true,
Self::Predicate(OriginPredicate(predicate)) => predicate(origin),
Self::List(allowed) => origin.is_none_or(|origin| {
Url::parse(origin).is_ok_and(|url| allowed.contains(&url.origin()))
}),
Self::SameOrigin => origin.is_none_or(|origin| same_origin(origin, conn.host())),
}
}
}
fn same_origin(origin: &str, authority: Option<&str>) -> bool {
let (Some(authority), Ok(origin)) = (authority, Url::parse(origin)) else {
return false;
};
let Ok(authority) = Url::parse(&format!("http://{authority}")) else {
return false;
};
let (Some(origin_host), Some(authority_host)) = (origin.host(), authority.host()) else {
return false;
};
origin_host == authority_host
&& match (origin.port(), authority.port()) {
(Some(origin_port), Some(authority_port)) => origin_port == authority_port,
_ => true,
}
}
fn parse_allowed_origin(origin: &str) -> Origin {
let url = Url::parse(origin)
.unwrap_or_else(|error| panic!("could not parse allowed origin `{origin}`: {error}"));
assert!(
url.path() == "/"
&& url.query().is_none()
&& url.fragment().is_none()
&& url.username().is_empty()
&& url.password().is_none(),
"allowed origin `{origin}` must contain only a scheme, host, and optional port"
);
let origin_tuple = url.origin();
assert!(
origin_tuple.is_tuple(),
"allowed origin `{origin}` does not have a host"
);
origin_tuple
}