Skip to main content

rust_webx_host/
cors.rs

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