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    /// Whether credentials (cookies) are allowed.
21    pub allow_credentials: bool,
22    /// Max age for preflight cache (seconds).
23    pub max_age: u32,
24}
25
26impl Default for CorsConfig {
27    fn default() -> Self {
28        Self {
29            origins: vec!["*".to_string()],
30            methods: vec![
31                "GET".to_string(),
32                "POST".to_string(),
33                "PUT".to_string(),
34                "DELETE".to_string(),
35                "PATCH".to_string(),
36                "OPTIONS".to_string(),
37            ],
38            headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
39            allow_credentials: false,
40            max_age: 86400,
41        }
42    }
43}
44
45/// Built-in CORS middleware.
46///
47/// Reads config from `appsettings.json` →`Cors` section at build time
48/// and applies CORS headers to every response. Handles preflight OPTIONS.
49pub struct CorsMiddleware {
50    config: CorsConfig,
51}
52
53impl CorsMiddleware {
54    /// Create a new CORS middleware with the given configuration.
55    pub fn new(config: CorsConfig) -> Self {
56        Self { config }
57    }
58}
59
60#[async_trait::async_trait]
61impl IMiddleware for CorsMiddleware {
62    async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
63        let origin = ctx.request().header("origin");
64
65        // Determine allowed origin
66        let allowed = self
67            .config
68            .origins
69            .iter()
70            .find(|o| *o == "*" || origin.is_some_and(|orig| *o == orig))
71            .cloned();
72
73        if let Some(ref origin_value) = allowed {
74            let header_value = if origin_value == "*" {
75                "*".to_string()
76            } else if let Some(o) = origin {
77                o.to_string()
78            } else {
79                origin_value.clone()
80            };
81
82            ctx.response_mut()
83                .set_header("access-control-allow-origin", &header_value);
84
85            if self.config.allow_credentials {
86                ctx.response_mut()
87                    .set_header("access-control-allow-credentials", "true");
88            }
89        }
90
91        // Handle preflight OPTIONS
92        if ctx.request().method().to_uppercase() == "OPTIONS" {
93            ctx.response_mut().set_status(204);
94            ctx.response_mut().set_header(
95                "access-control-allow-methods",
96                &self.config.methods.join(", "),
97            );
98            ctx.response_mut().set_header(
99                "access-control-allow-headers",
100                &self.config.headers.join(", "),
101            );
102            ctx.response_mut()
103                .set_header("access-control-max-age", &self.config.max_age.to_string());
104            // Short-circuit: preflight response is complete, no need to
105            // continue to router or final handler.
106            return Ok(ControlFlow::Break(()));
107        }
108
109        // For normal requests, expose headers
110        if let Some(ref expose) = allowed {
111            if expose != "*" {
112                ctx.response_mut().set_header(
113                    "access-control-expose-headers",
114                    &self.config.headers.join(", "),
115                );
116            }
117        }
118
119        Ok(ControlFlow::Continue(()))
120    }
121}