use crate::error::Result;
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use async_trait::async_trait;
use http::header::{HeaderValue, VARY};
use http::{Method, StatusCode};
use std::sync::Arc;
use std::time::Duration;
type OriginFn = dyn Fn(&str) -> bool + Send + Sync;
enum Origins {
List(Vec<String>),
Any,
Predicate(Arc<OriginFn>),
}
pub struct Cors {
origins: Origins,
methods: Vec<Method>,
headers: Option<Vec<String>>,
expose: Vec<String>,
credentials: bool,
max_age: Duration,
private_network: bool,
}
impl Default for Cors {
fn default() -> Self {
Self::new()
}
}
impl Cors {
pub fn new() -> Self {
Self {
origins: Origins::List(Vec::new()),
methods: vec![
Method::GET,
Method::HEAD,
Method::POST,
Method::PUT,
Method::PATCH,
Method::DELETE,
],
headers: None,
expose: Vec::new(),
credentials: false,
max_age: Duration::from_secs(600),
private_network: false,
}
}
pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
let origin = origin.into();
match &mut self.origins {
Origins::List(list) => list.push(origin),
_ => self.origins = Origins::List(vec![origin]),
}
self
}
pub fn allow_origins<I, S>(mut self, origins: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for origin in origins {
self = self.allow_origin(origin);
}
self
}
pub fn allow_any_origin(mut self) -> Self {
self.origins = Origins::Any;
self
}
pub fn allow_origin_fn<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.origins = Origins::Predicate(Arc::new(f));
self
}
pub fn allow_methods<I>(mut self, methods: I) -> Self
where
I: IntoIterator<Item = Method>,
{
self.methods = methods.into_iter().collect();
self
}
pub fn allow_headers<I, S>(mut self, headers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.headers = Some(headers.into_iter().map(Into::into).collect());
self
}
pub fn expose_headers<I, S>(mut self, headers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.expose = headers.into_iter().map(Into::into).collect();
self
}
pub fn allow_credentials(mut self) -> Self {
self.credentials = true;
self
}
pub fn max_age(mut self, max_age: Duration) -> Self {
self.max_age = max_age;
self
}
pub fn allow_private_network(mut self) -> Self {
self.private_network = true;
self
}
fn origin_allowed(&self, origin: &str) -> bool {
match &self.origins {
Origins::Any => true,
Origins::List(list) => list.iter().any(|o| o == origin),
Origins::Predicate(f) => f(origin),
}
}
fn any(&self) -> bool {
matches!(self.origins, Origins::Any)
}
fn decorate(&self, mut res: Response, origin: &str) -> Response {
if self.any() {
res = res.with_header("access-control-allow-origin", "*");
} else {
res = res.with_header("access-control-allow-origin", origin);
add_vary(&mut res, "origin");
if self.credentials {
res = res.with_header("access-control-allow-credentials", "true");
}
}
if !self.expose.is_empty() {
res = res.with_header("access-control-expose-headers", self.expose.join(", "));
}
res
}
fn preflight(&self, req: &Request, origin: &str) -> Response {
let requested = req
.headers
.get("access-control-request-method")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let method_ok = self.methods.iter().any(|m| m.as_str() == requested);
if !self.origin_allowed(origin) || !method_ok {
let mut res = Response::new(StatusCode::FORBIDDEN);
add_vary(&mut res, "origin");
return res;
}
let mut res = self.decorate(Response::new(StatusCode::NO_CONTENT), origin);
let methods: Vec<&str> = self.methods.iter().map(Method::as_str).collect();
res = res
.with_header("access-control-allow-methods", methods.join(", "))
.with_header("access-control-max-age", self.max_age.as_secs().to_string());
match &self.headers {
Some(list) if !list.is_empty() => {
res = res.with_header("access-control-allow-headers", list.join(", "));
}
Some(_) => {}
None => {
if let Some(asked) = req
.headers
.get("access-control-request-headers")
.and_then(|v| v.to_str().ok())
.filter(|v| !v.trim().is_empty())
{
res = res.with_header("access-control-allow-headers", asked);
add_vary(&mut res, "access-control-request-headers");
}
}
}
if self.private_network
&& req
.headers
.get("access-control-request-private-network")
.is_some_and(|v| v == "true")
{
res = res.with_header("access-control-allow-private-network", "true");
}
res
}
}
pub(crate) fn add_vary(res: &mut Response, token: &str) {
let existing: Vec<String> = res
.headers
.get_all(VARY)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(','))
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
if existing
.iter()
.any(|t| t == "*" || t.eq_ignore_ascii_case(token))
{
return;
}
let mut all = existing;
all.push(token.to_string());
if let Ok(value) = HeaderValue::from_str(&all.join(", ")) {
res.headers.insert(VARY, value);
}
}
#[async_trait]
impl Middleware for Cors {
async fn handle(&self, req: Request, next: Next) -> Result<Response> {
let origin = req
.headers
.get("origin")
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let Some(origin) = origin else {
let mut res = into_response(next.run(req).await);
if !self.any() {
add_vary(&mut res, "origin");
}
return Ok(res);
};
let is_preflight = req.method == Method::OPTIONS
&& req.headers.contains_key("access-control-request-method");
if is_preflight {
return Ok(self.preflight(&req, &origin));
}
let mut res = into_response(next.run(req).await);
if self.origin_allowed(&origin) {
res = self.decorate(res, &origin);
} else if !self.any() {
add_vary(&mut res, "origin");
}
Ok(res)
}
}
fn into_response(result: Result<Response>) -> Response {
result.unwrap_or_else(Response::from_error)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vary_is_merged_not_replaced() {
let mut res = Response::ok().with_header("vary", "accept-encoding");
add_vary(&mut res, "origin");
assert_eq!(res.headers[VARY], "accept-encoding, origin");
add_vary(&mut res, "Origin");
assert_eq!(res.headers[VARY], "accept-encoding, origin");
}
#[test]
fn vary_star_is_left_alone() {
let mut res = Response::ok().with_header("vary", "*");
add_vary(&mut res, "origin");
assert_eq!(res.headers[VARY], "*");
}
#[test]
fn origin_matching_is_exact() {
let cors = Cors::new().allow_origin("https://a.example");
assert!(cors.origin_allowed("https://a.example"));
assert!(!cors.origin_allowed("https://a.example.evil.com"));
assert!(!cors.origin_allowed("http://a.example"));
assert!(!Cors::new().origin_allowed("null"));
}
}