use std::collections::HashSet;
use http::{HeaderValue, Method};
use lambda_http::{Body as LambdaBody, Response as LambdaResponse};
use tracing::debug;
use crate::error::{LambdaError, Result};
#[derive(Debug, Clone)]
pub struct CorsConfig {
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<Method>,
pub allowed_headers: Vec<String>,
pub allow_credentials: bool,
pub max_age: Option<u32>,
pub expose_headers: Vec<String>,
}
impl Default for CorsConfig {
fn default() -> Self {
Self {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec![Method::GET, Method::POST, Method::DELETE, Method::OPTIONS],
allowed_headers: vec![
"Content-Type".to_string(),
"Accept".to_string(),
"Authorization".to_string(),
"Mcp-Session-Id".to_string(),
"Mcp-Protocol-Version".to_string(),
"Last-Event-ID".to_string(),
],
allow_credentials: false,
max_age: Some(86400), expose_headers: vec![
"Mcp-Session-Id".to_string(),
"Mcp-Protocol-Version".to_string(),
],
}
}
}
impl CorsConfig {
pub fn allow_all() -> Self {
Self::default()
}
pub fn for_origins(origins: Vec<String>) -> Self {
Self {
allowed_origins: origins,
..Default::default()
}
}
pub fn from_env() -> Self {
let allowed_origins = std::env::var("MCP_CORS_ORIGINS")
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_else(|_| vec!["*".to_string()]);
let allow_credentials = std::env::var("MCP_CORS_CREDENTIALS")
.map(|s| s.parse().unwrap_or(false))
.unwrap_or(false);
let max_age = std::env::var("MCP_CORS_MAX_AGE")
.ok()
.and_then(|s| s.parse().ok());
Self {
allowed_origins,
allow_credentials,
max_age,
..Default::default()
}
}
}
pub fn inject_cors_headers<B>(
response: &mut lambda_http::Response<B>,
config: &CorsConfig,
request_origin: Option<&str>,
) -> Result<()> {
debug!("Injecting CORS headers for origin: {:?}", request_origin);
let allowed_origin = determine_allowed_origin(config, request_origin);
if let Some(origin) = allowed_origin {
response.headers_mut().insert(
"Access-Control-Allow-Origin",
HeaderValue::from_str(&origin)
.map_err(|e| LambdaError::Cors(format!("Invalid origin: {}", e)))?,
);
}
let methods_str = config
.allowed_methods
.iter()
.map(|m| m.as_str())
.collect::<Vec<_>>()
.join(", ");
response.headers_mut().insert(
"Access-Control-Allow-Methods",
HeaderValue::from_str(&methods_str)
.map_err(|e| LambdaError::Cors(format!("Invalid methods: {}", e)))?,
);
if !config.allowed_headers.is_empty() {
let headers_str = config.allowed_headers.join(", ");
response.headers_mut().insert(
"Access-Control-Allow-Headers",
HeaderValue::from_str(&headers_str)
.map_err(|e| LambdaError::Cors(format!("Invalid headers: {}", e)))?,
);
}
if !config.expose_headers.is_empty() {
let expose_str = config.expose_headers.join(", ");
response.headers_mut().insert(
"Access-Control-Expose-Headers",
HeaderValue::from_str(&expose_str)
.map_err(|e| LambdaError::Cors(format!("Invalid expose headers: {}", e)))?,
);
}
if config.allow_credentials {
response.headers_mut().insert(
"Access-Control-Allow-Credentials",
HeaderValue::from_static("true"),
);
}
if let Some(max_age) = config.max_age {
response.headers_mut().insert(
"Access-Control-Max-Age",
HeaderValue::from_str(&max_age.to_string())
.map_err(|e| LambdaError::Cors(format!("Invalid max age: {}", e)))?,
);
}
debug!("CORS headers injected successfully");
Ok(())
}
pub fn create_preflight_response(
config: &CorsConfig,
request_origin: Option<&str>,
) -> Result<LambdaResponse<LambdaBody>> {
debug!("Creating CORS preflight response");
let mut response = LambdaResponse::builder()
.status(200)
.body(LambdaBody::Empty)
.map_err(LambdaError::Http)?;
inject_cors_headers(&mut response, config, request_origin)?;
Ok(response)
}
fn determine_allowed_origin(config: &CorsConfig, request_origin: Option<&str>) -> Option<String> {
if config.allowed_origins.contains(&"*".to_string()) {
return Some("*".to_string());
}
let request_origin = request_origin?;
if config.allowed_origins.contains(&request_origin.to_string()) {
Some(request_origin.to_string())
} else {
None
}
}
pub fn validate_config(config: &CorsConfig) -> Result<()> {
if config.allow_credentials && config.allowed_origins.contains(&"*".to_string()) {
return Err(LambdaError::Cors(
"Cannot use wildcard origin (*) with credentials enabled".to_string(),
));
}
for origin in &config.allowed_origins {
if origin != "*" && !origin.starts_with("http://") && !origin.starts_with("https://") {
return Err(LambdaError::Cors(format!(
"Invalid origin format: {}",
origin
)));
}
}
let headers_set: HashSet<_> = config.allowed_headers.iter().collect();
if headers_set.len() != config.allowed_headers.len() {
return Err(LambdaError::Cors(
"Duplicate headers in allowed_headers".to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use lambda_http::Body;
#[test]
fn test_default_config() {
let config = CorsConfig::default();
assert!(config.allowed_origins.contains(&"*".to_string()));
assert!(config.allowed_methods.contains(&Method::GET));
assert!(config.allowed_methods.contains(&Method::POST));
assert!(config.allowed_headers.contains(&"Content-Type".to_string()));
}
#[test]
fn test_config_validation() {
let mut config = CorsConfig::default();
assert!(validate_config(&config).is_ok());
config.allow_credentials = true;
assert!(validate_config(&config).is_err());
config.allow_credentials = false;
config.allowed_origins = vec!["invalid-origin".to_string()];
assert!(validate_config(&config).is_err());
}
#[tokio::test]
async fn test_cors_headers_injection() {
let config = CorsConfig::default();
let mut response = LambdaResponse::builder()
.status(200)
.body(Body::Empty)
.unwrap();
inject_cors_headers(&mut response, &config, Some("https://example.com")).unwrap();
assert_eq!(
response.headers().get("access-control-allow-origin"),
Some(&HeaderValue::from_static("*"))
);
assert!(
response
.headers()
.contains_key("access-control-allow-methods")
);
assert!(
response
.headers()
.contains_key("access-control-allow-headers")
);
}
#[tokio::test]
async fn test_preflight_response() {
let config = CorsConfig::default();
let response = create_preflight_response(&config, Some("https://example.com")).unwrap();
assert_eq!(response.status(), 200);
assert!(
response
.headers()
.contains_key("access-control-allow-origin")
);
assert!(
response
.headers()
.contains_key("access-control-allow-methods")
);
}
}