use crate::service::{Outcome, Service, WatchRef};
use crate::{Reply, Request};
#[derive(Clone, Debug)]
enum Origins {
Any,
List(Vec<String>),
}
#[derive(Clone, Debug)]
pub struct Cors {
origins: Origins,
methods: Vec<String>,
headers: Option<Vec<String>>,
expose: Vec<String>,
credentials: bool,
max_age: Option<u32>,
private_network: bool,
}
impl Default for Cors {
fn default() -> Self {
Cors {
origins: Origins::List(Vec::new()),
methods: ["GET", "HEAD", "POST"].map(String::from).to_vec(),
headers: Some(Vec::new()),
expose: Vec::new(),
credentials: false,
max_age: None,
private_network: false,
}
}
}
impl Cors {
pub fn new() -> Cors {
Cors::default()
}
pub fn allow_origin(mut self, origin: &str) -> Self {
if let Origins::List(l) = &mut self.origins {
l.push(origin.to_ascii_lowercase());
}
self
}
pub fn allow_any_origin(mut self) -> Self {
self.origins = Origins::Any;
self
}
pub fn allow_methods(mut self, methods: &[&str]) -> Self {
self.methods = methods.iter().map(|m| m.to_ascii_uppercase()).collect();
self
}
pub fn allow_headers(mut self, headers: &[&str]) -> Self {
self.headers = Some(headers.iter().map(|h| h.to_ascii_lowercase()).collect());
self
}
pub fn allow_any_header(mut self) -> Self {
self.headers = None;
self
}
pub fn expose_headers(mut self, headers: &[&str]) -> Self {
self.expose = headers.iter().map(|h| h.to_ascii_lowercase()).collect();
self
}
pub fn allow_credentials(mut self, on: bool) -> Self {
self.credentials = on;
self
}
pub fn max_age(mut self, secs: u32) -> Self {
self.max_age = Some(secs);
self
}
pub fn allow_private_network(mut self, on: bool) -> Self {
self.private_network = on;
self
}
pub fn wrap<S: Service>(self, inner: S) -> CorsService<S> {
CorsService { cors: self, inner }
}
fn allowed(&self, origin: &[u8]) -> bool {
match &self.origins {
Origins::Any => true,
Origins::List(l) => l.iter().any(|o| o.as_bytes().eq_ignore_ascii_case(origin)),
}
}
fn allow_origin_value<'o>(&self, origin: &'o [u8]) -> &'o [u8] {
if matches!(self.origins, Origins::Any) && !self.credentials {
b"*"
} else {
origin
}
}
}
#[derive(Debug)]
pub struct CorsService<S> {
cors: Cors,
inner: S,
}
impl<S: Service> Service for CorsService<S> {
fn call(&self, req: &mut Request<'_>, reply: Reply<'_>) -> Outcome {
let c = &self.cors;
let Some(origin) = req.header("origin") else { return self.inner.call(req, reply) };
let vary = !(matches!(c.origins, Origins::Any) && !c.credentials);
let reply = if vary { reply.header("vary", "origin") } else { reply };
if !c.allowed(origin) {
return self.inner.call(req, reply);
}
let reply = reply.header("access-control-allow-origin", c.allow_origin_value(origin));
let reply = if c.credentials { reply.header("access-control-allow-credentials", "true") } else { reply };
if req.method() == b"OPTIONS" {
if let Some(m) = req.header("access-control-request-method") {
let method_ok = c.methods.iter().any(|x| x.as_bytes() == m) || matches!(m, b"GET" | b"HEAD" | b"POST");
let asked = req.header("access-control-request-headers").unwrap_or(b"");
let asked: Vec<String> = String::from_utf8_lossy(asked).split(',').map(|h| h.trim().to_ascii_lowercase()).filter(|h| !h.is_empty()).collect();
let headers_ok = match &c.headers {
None => true,
Some(l) => asked.iter().all(|h| l.contains(h) || matches!(h.as_str(), "accept" | "accept-language" | "content-language" | "content-type")),
};
if !method_ok || !headers_ok {
return reply.status(204).header("vary", "access-control-request-method, access-control-request-headers").empty();
}
let mut r = reply.status(204).header("access-control-allow-methods", c.methods.join(", ")).header("vary", "access-control-request-method, access-control-request-headers");
if !asked.is_empty() {
r = r.header("access-control-allow-headers", asked.join(", "));
}
if let Some(a) = c.max_age {
r = r.header("access-control-max-age", a.to_string());
}
if c.private_network && req.header("access-control-request-private-network") == Some(b"true") {
r = r.header("access-control-allow-private-network", "true");
}
return r.empty();
}
}
let reply = if c.expose.is_empty() { reply } else { reply.header("access-control-expose-headers", c.expose.join(", ")) };
self.inner.call(req, reply)
}
fn watch(&self, req: &Request<'_>) -> Option<WatchRef<'_>> {
self.inner.watch(req)
}
}