1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Cross-Origin Resource Sharing (CORS) middleware.
//!
//! Automatically configured from `appsettings.json` `Cors` section.
//! Handles preflight OPTIONS requests and sets CORS response headers.
use rust_webx_core::error::Result;
use rust_webx_core::http::IHttpContext;
use rust_webx_core::middleware::IMiddleware;
use std::ops::ControlFlow;
/// CORS configuration loaded from appsettings.json.
#[derive(Debug, Clone)]
pub struct CorsConfig {
/// Allowed origins. Use `"*"` to allow any origin.
pub origins: Vec<String>,
/// Allowed HTTP methods (default: GET, POST, PUT, DELETE, OPTIONS).
pub methods: Vec<String>,
/// Allowed request headers.
pub headers: Vec<String>,
/// Response headers exposed to the client (via access-control-expose-headers).
pub expose_headers: Vec<String>,
/// Whether credentials (cookies) are allowed.
pub allow_credentials: bool,
/// Max age for preflight cache (seconds).
pub max_age: u32,
}
impl Default for CorsConfig {
fn default() -> Self {
Self {
origins: vec!["*".to_string()],
methods: vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"OPTIONS".to_string(),
],
headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
expose_headers: Vec::new(),
allow_credentials: false,
max_age: 86400,
}
}
}
/// Built-in CORS middleware.
///
/// Reads config from `appsettings.json` →`Cors` section at build time
/// and applies CORS headers to every response. Handles preflight OPTIONS.
pub struct CorsMiddleware {
config: CorsConfig,
}
impl CorsMiddleware {
/// Create a new CORS middleware with the given configuration.
pub fn new(config: CorsConfig) -> Self {
Self { config }
}
}
#[async_trait::async_trait]
impl IMiddleware for CorsMiddleware {
async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
let origin = ctx.request().header("origin");
// Determine allowed origin
let allowed = self
.config
.origins
.iter()
.find(|o| *o == "*" || origin.is_some_and(|orig| *o == orig))
.cloned();
if let Some(ref origin_value) = allowed {
// When allow_credentials is true, "*" cannot be used per CORS spec;
// echo the request's Origin header instead.
let header_value = if origin_value == "*" && self.config.allow_credentials {
origin.unwrap_or("*").to_string()
} else if origin_value == "*" {
"*".to_string()
} else if let Some(o) = origin {
o.to_string()
} else {
origin_value.clone()
};
ctx.response_mut()
.set_header("access-control-allow-origin", &header_value);
if self.config.allow_credentials {
ctx.response_mut()
.set_header("access-control-allow-credentials", "true");
}
}
// Handle preflight OPTIONS
if ctx.request().method().to_uppercase() == "OPTIONS" {
ctx.response_mut().set_status(204);
ctx.response_mut().set_header(
"access-control-allow-methods",
&self.config.methods.join(", "),
);
ctx.response_mut().set_header(
"access-control-allow-headers",
&self.config.headers.join(", "),
);
ctx.response_mut()
.set_header("access-control-max-age", &self.config.max_age.to_string());
// Short-circuit: preflight response is complete, no need to
// continue to router or final handler.
return Ok(ControlFlow::Break(()));
}
// For normal requests, expose specified response headers to the client
if !self.config.expose_headers.is_empty() {
ctx.response_mut().set_header(
"access-control-expose-headers",
&self.config.expose_headers.join(", "),
);
}
Ok(ControlFlow::Continue(()))
}
}