use std::fmt;
use hyper::{Method, Response};
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Empty};
use crate::handler::ResponseBody;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CorsConfigError {
CredentialedWildcard,
}
impl fmt::Display for CorsConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CorsConfigError::CredentialedWildcard => write!(
f,
"CORS misconfiguration: allow_origin(\"*\") with allow_credentials(true) \
is not allowed — it grants credentialed access to every origin. \
Use an explicit origin list instead."
),
}
}
}
impl std::error::Error for CorsConfigError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CorsConfig {
pub allow_origins: Vec<String>,
pub allow_all_origins: bool,
pub credentials: bool,
}
impl CorsConfig {
fn build_cors_headers(&self, req_origin: Option<&str>) -> Vec<(String, String)> {
let mut headers = Vec::new();
let origin = if self.allow_all_origins {
"*"
} else if let Some(origin) = req_origin {
if self.allow_origins.iter().any(|o| o == origin) {
origin
} else {
return headers;
}
} else {
return headers;
};
headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
if origin != "*" {
headers.push(("vary".to_string(), "origin".to_string()));
}
if self.credentials {
headers.push(("access-control-allow-credentials".to_string(), "true".to_string()));
}
headers
}
pub fn preflight_response(
&self,
req_origin: Option<&str>,
requested_headers: Option<&str>,
allowed_methods: &[Method],
) -> Response<ResponseBody> {
let mut headers = self.build_cors_headers(req_origin);
if !headers.is_empty() {
if let Some(requested) = requested_headers {
headers.push(("access-control-allow-headers".to_string(), requested.to_string()));
}
if !allowed_methods.is_empty() {
let mut method_strs: Vec<&str> = allowed_methods.iter().map(|m| m.as_str()).collect();
method_strs.sort();
method_strs.dedup();
headers.push(("access-control-allow-methods".to_string(), method_strs.join(", ")));
}
}
let mut resp = Response::builder()
.status(hyper::StatusCode::NO_CONTENT);
for (name, value) in headers {
if let Ok(val) = value.parse::<hyper::header::HeaderValue>() {
if let Ok(header_name) = name.parse::<hyper::header::HeaderName>() {
resp = resp.header(header_name, val);
}
}
}
resp
.body(BoxBody::new(Empty::new().map_err(|never: std::convert::Infallible| match never {})))
.expect("status is valid and headers are static ASCII")
}
pub fn apply_to_response(&self, resp: &mut Response<ResponseBody>, req_origin: Option<&str>) {
let headers = self.build_cors_headers(req_origin);
for (name, value) in headers {
if let Ok(val) = value.parse::<hyper::header::HeaderValue>() {
if let Ok(header_name) = name.parse::<hyper::header::HeaderName>() {
resp.headers_mut().insert(header_name, val);
}
}
}
}
}
#[derive(Default, Debug)]
pub struct CorsConfigBuilder {
allow_origins: Vec<String>,
credentials: bool,
}
impl CorsConfigBuilder {
pub fn allow_origin(mut self, origin: &str) -> Self {
self.allow_origins.push(origin.to_string());
self
}
pub fn allow_credentials(mut self, yes: bool) -> Self {
self.credentials = yes;
self
}
pub fn build(self) -> Result<CorsConfig, CorsConfigError> {
let allow_all_origins = self.allow_origins.len() == 1 && self.allow_origins[0] == "*";
if self.credentials && allow_all_origins {
return Err(CorsConfigError::CredentialedWildcard);
}
Ok(CorsConfig {
allow_origins: self.allow_origins,
allow_all_origins,
credentials: self.credentials,
})
}
}
#[cfg(test)]
#[path = "../tests/unit/cors.rs"]
mod tests;