use std::fmt;
use axum::http::HeaderValue;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VerifiedOrigin(String);
impl VerifiedOrigin {
#[must_use]
pub fn from_trusted(s: impl Into<String>) -> Self {
Self(s.into())
}
#[must_use]
pub fn from_header(value: &HeaderValue) -> Option<Self> {
let s = value.to_str().ok()?;
if !s.bytes().all(|b| b.is_ascii()) {
return None;
}
Some(Self(s.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for VerifiedOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OriginDecision {
Allowed,
Denied,
}
#[derive(Debug, Clone, Default)]
pub enum OriginPolicy {
#[default]
DenyAll,
AllowExact { origin: VerifiedOrigin },
AllowSet { origins: Vec<VerifiedOrigin> },
}
impl OriginPolicy {
#[must_use]
pub fn deny_all() -> Self {
Self::DenyAll
}
#[must_use]
pub fn allow_exact(origin: VerifiedOrigin) -> Self {
Self::AllowExact { origin }
}
#[must_use]
pub fn allow_set(origins: Vec<VerifiedOrigin>) -> Self {
Self::AllowSet { origins }
}
#[must_use]
pub fn authorize(&self, header: Option<&HeaderValue>) -> OriginDecision {
match self {
Self::DenyAll => OriginDecision::Denied,
Self::AllowExact { origin } => {
if let Some(h) = header
&& let Some(v) = VerifiedOrigin::from_header(h)
&& &v == origin
{
OriginDecision::Allowed
} else {
OriginDecision::Denied
}
}
Self::AllowSet { origins } => {
if let Some(h) = header
&& let Some(v) = VerifiedOrigin::from_header(h)
&& origins.contains(&v)
{
OriginDecision::Allowed
} else {
OriginDecision::Denied
}
}
}
}
}