use std::fmt;
use hyper::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>) -> Response<ResponseBody> {
let headers = self.build_cors_headers(req_origin);
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)]
mod tests {
use super::*;
#[test]
fn credentialed_wildcard_rejected_in_debug() {
let result = CorsConfigBuilder::default()
.allow_origin("*")
.allow_credentials(true)
.build();
assert_eq!(result, Err(CorsConfigError::CredentialedWildcard));
}
#[test]
fn credentialed_wildcard_rejected_in_release() {
let result = CorsConfigBuilder::default()
.allow_origin("*")
.allow_credentials(true)
.build();
assert_eq!(result, Err(CorsConfigError::CredentialedWildcard));
}
#[test]
fn explicit_origin_with_credentials_allowed() {
let config = CorsConfigBuilder::default()
.allow_origin("https://example.com")
.allow_credentials(true)
.build();
assert!(config.is_ok());
let cfg = config.unwrap();
assert!(!cfg.allow_all_origins);
assert!(cfg.credentials);
}
#[test]
fn wildcard_without_credentials_allowed() {
let config = CorsConfigBuilder::default()
.allow_origin("*")
.allow_credentials(false)
.build();
assert!(config.is_ok());
let cfg = config.unwrap();
assert!(cfg.allow_all_origins);
assert!(!cfg.credentials);
}
}