#![allow(clippy::enum_variant_names)]
use axol_http::{
Method,
header::HeaderMap,
request::{RequestParts, RequestPartsRef},
response::Response,
};
mod allow_credentials;
mod allow_headers;
mod allow_methods;
mod allow_origin;
mod allow_private_network;
mod expose_headers;
mod max_age;
mod vary;
use crate::{Error, Extension, FromRequestParts, Plugin, Result, Router};
pub type CorsPredicate<T> =
std::sync::Arc<dyn for<'a> Fn(&'a str, RequestPartsRef<'a>) -> T + Send + Sync + 'static>;
pub use self::{
allow_credentials::AllowCredentials, allow_headers::AllowHeaders, allow_methods::AllowMethods,
allow_origin::AllowOrigin, allow_private_network::AllowPrivateNetwork,
expose_headers::ExposeHeaders, max_age::MaxAge, vary::Vary,
};
#[derive(Debug, Clone)]
#[must_use]
pub struct Cors {
allow_credentials: AllowCredentials,
allow_headers: AllowHeaders,
allow_methods: AllowMethods,
allow_origin: AllowOrigin,
allow_private_network: AllowPrivateNetwork,
expose_headers: ExposeHeaders,
max_age: MaxAge,
vary: Vary,
}
impl Cors {
pub fn new() -> Self {
Self {
allow_credentials: Default::default(),
allow_headers: Default::default(),
allow_methods: Default::default(),
allow_origin: Default::default(),
allow_private_network: Default::default(),
expose_headers: Default::default(),
max_age: Default::default(),
vary: Default::default(),
}
}
pub fn permissive() -> Self {
Self::new()
.allow_headers(Any)
.allow_methods(Any)
.allow_origin(Any)
.expose_headers(Any)
}
pub fn very_permissive() -> Self {
Self::new()
.allow_credentials(true)
.allow_headers(AllowHeaders::mirror_request())
.allow_methods(AllowMethods::mirror_request())
.allow_origin(AllowOrigin::mirror_request())
}
pub fn allow_credentials<T>(mut self, allow_credentials: T) -> Self
where
T: Into<AllowCredentials>,
{
self.allow_credentials = allow_credentials.into();
self
}
pub fn allow_headers<T>(mut self, headers: T) -> Self
where
T: Into<AllowHeaders>,
{
self.allow_headers = headers.into();
self
}
pub fn max_age<T>(mut self, max_age: T) -> Self
where
T: Into<MaxAge>,
{
self.max_age = max_age.into();
self
}
pub fn allow_methods<T>(mut self, methods: T) -> Self
where
T: Into<AllowMethods>,
{
self.allow_methods = methods.into();
self
}
pub fn allow_origin<T>(mut self, origin: T) -> Self
where
T: Into<AllowOrigin>,
{
self.allow_origin = origin.into();
self
}
pub fn expose_headers<T>(mut self, headers: T) -> Self
where
T: Into<ExposeHeaders>,
{
self.expose_headers = headers.into();
self
}
pub fn allow_private_network<T>(mut self, allow_private_network: T) -> Self
where
T: Into<AllowPrivateNetwork>,
{
self.allow_private_network = allow_private_network.into();
self
}
pub fn vary<T>(mut self, headers: T) -> Self
where
T: Into<Vary>,
{
self.vary = headers.into();
self
}
}
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct Any;
impl Default for Cors {
fn default() -> Self {
Self::new()
}
}
struct OptionsFilter;
#[async_trait::async_trait]
impl<'a> FromRequestParts<'a> for OptionsFilter {
async fn from_request_parts(request: RequestPartsRef<'a>) -> Result<Self> {
if request.method != Method::Options {
return Err(Error::SkipMiddleware);
}
Ok(Self)
}
}
struct NotOptionsFilter;
#[async_trait::async_trait]
impl<'a> FromRequestParts<'a> for NotOptionsFilter {
async fn from_request_parts(request: RequestPartsRef<'a>) -> Result<Self> {
if request.method == Method::Options {
return Err(Error::SkipMiddleware);
}
Ok(Self)
}
}
impl Cors {
async fn options_intercept(
_: OptionsFilter,
Extension(cors): Extension<Cors>,
parts: RequestParts,
) -> Result<Option<HeaderMap>> {
let origin = parts.headers.get("origin");
let mut headers = HeaderMap::new();
if let Some(header) = cors.allow_origin.to_header(origin, parts.as_ref()) {
headers.append_typed(&header);
}
if let Some(header) = cors.allow_credentials.to_header(origin, parts.as_ref()) {
headers.append_typed(&header);
}
if let Some((name, value)) = cors.allow_private_network.to_header(origin, parts.as_ref()) {
headers.append(name, value);
}
for value in cors.vary.values() {
headers.append("vary", value);
}
if let Some(header) = cors.allow_methods.to_header(parts.as_ref()) {
headers.append_typed(&header);
}
if let Some(header) = cors.allow_headers.to_header(parts.as_ref()) {
headers.append_typed(&header);
}
if let Some(header) = cors.max_age.to_header(origin, parts.as_ref()) {
headers.append_typed(&header);
}
Ok(Some(headers))
}
async fn response_augment(
_: NotOptionsFilter,
Extension(cors): Extension<Cors>,
parts: RequestParts,
mut response: Response,
) -> Response {
let origin = parts.headers.get("origin");
if let Some(header) = cors.allow_origin.to_header(origin, parts.as_ref()) {
response.headers.append_typed(&header);
}
if let Some(header) = cors.allow_credentials.to_header(origin, parts.as_ref()) {
response.headers.append_typed(&header);
}
if let Some((name, value)) = cors.allow_private_network.to_header(origin, parts.as_ref()) {
response.headers.append(name, value);
}
for value in cors.vary.values() {
response.headers.append("vary", value);
}
if let Some(header) = cors.expose_headers.to_header(parts.as_ref()) {
response.headers.append_typed(&header);
}
response
}
}
impl Plugin for Cors {
fn apply(self, router: Router, path: &str) -> Router {
ensure_usable_cors_rules(&self);
router
.extension(path, self)
.request_hook(path, Cors::options_intercept)
.late_response_hook(path, Cors::response_augment)
}
}
fn ensure_usable_cors_rules(layer: &Cors) {
if layer.allow_credentials.is_true() {
assert!(
!layer.allow_headers.is_wildcard(),
"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
with `Access-Control-Allow-Headers: *`"
);
assert!(
!layer.allow_methods.is_wildcard(),
"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
with `Access-Control-Allow-Methods: *`"
);
assert!(
!layer.allow_origin.is_wildcard(),
"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
with `Access-Control-Allow-Origin: *`"
);
assert!(
!layer.expose_headers.is_wildcard(),
"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \
with `Access-Control-Expose-Headers: *`"
);
}
}
pub fn preflight_request_headers() -> impl Iterator<Item = &'static str> {
[
"origin",
"access-control-request-method",
"access-control-request-headers",
]
.into_iter()
}