Skip to main content

rust_web_server/cors/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use crate::header::Header;
5use crate::request::{METHOD, Request};
6use crate::response::Error;
7use crate::server_config::ServerConfig;
8
9#[derive(PartialEq, Eq, Clone, Debug)]
10pub struct Cors {
11    pub allow_all: bool,
12    pub allow_origins: Vec<String>,
13    pub allow_methods: Vec<String>,
14    pub allow_headers: Vec<String>,
15    pub allow_credentials: bool,
16    pub expose_headers: Vec<String>,
17    pub max_age: String,
18}
19
20impl Cors {
21    pub const MAX_AGE: &'static str = "86400";
22
23    /// `true` when `origin` is one of the comma-separated `allowed` origins —
24    /// the whole origin, not a substring of the list (#58): with
25    /// `https://app.example.com` allowed, `https://app.example.co` and
26    /// `https://app.example.com.evil` used to match too. ASCII case is
27    /// ignored (scheme and host are case-insensitive).
28    pub fn origin_is_allowed(allowed: &str, origin: &str) -> bool {
29        let origin = origin.trim();
30        !origin.is_empty() && allowed.split(',').any(|candidate| candidate.trim().eq_ignore_ascii_case(origin))
31    }
32
33    pub fn get_vary_header_value() -> String {
34        Header::_ORIGIN.to_string()
35    }
36
37    pub fn allow_all(request: &Request) -> Result<Vec<Header>, Error> {
38        let mut headers : Vec<Header> = vec![];
39        let origin = request.header(Header::_ORIGIN);
40        if origin.is_some() {
41            let allow_origin = Header {
42                name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
43                value: origin.unwrap().value.to_string().into()
44            };
45            headers.push(allow_origin);
46
47            let allow_credentials = Header {
48                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
49                value: "true".into()
50            };
51            headers.push(allow_credentials);
52
53            let is_options = request.method == METHOD.options;
54            if is_options {
55                let method = request.header(Header::_ACCESS_CONTROL_REQUEST_METHOD);
56                if method.is_some() {
57                    let allow_method = Header {
58                        name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
59                        value: method.unwrap().value.to_string().into()
60                    };
61                    headers.push(allow_method);
62                }
63
64                let access_control_request_headers = request.header(Header::_ACCESS_CONTROL_REQUEST_HEADERS);
65                if access_control_request_headers.is_some() {
66                    let request_headers = access_control_request_headers.unwrap();
67                    let allow_headers = Header {
68                        name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
69                        value: request_headers.value.to_lowercase().into(),
70                    };
71                    headers.push(allow_headers);
72
73                    let expose_headers = Header {
74                        name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
75                        value: request_headers.value.to_lowercase().into(),
76                    };
77                    headers.push(expose_headers);
78                }
79
80                let max_age = Header {
81                    name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
82                    value: Cors::MAX_AGE.into()
83                };
84                headers.push(max_age);
85            }
86
87        }
88
89        Ok(headers)
90    }
91
92    pub fn _process(request: &Request, cors: &Cors) -> Result<Vec<Header>, Error> {
93        let mut headers : Vec<Header> = vec![];
94
95        let allow_origins = cors.allow_origins.join(",");
96        let boxed_origin = request.header(Header::_ORIGIN);
97
98        if boxed_origin.is_none() {
99            return Ok(headers)
100        }
101
102        let origin = boxed_origin.unwrap();
103        let origin_value = format!("{}", origin.value);
104
105        let is_valid_origin = Cors::origin_is_allowed(&allow_origins, &origin_value);
106        if !is_valid_origin {
107            return Ok(headers)
108        }
109
110        let allow_origin = Header {
111            name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
112            value: origin_value.into()
113        };
114        headers.push(allow_origin);
115
116        if cors.allow_credentials {
117            let allow_credentials = Header {
118                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
119                value: cors.allow_credentials.to_string().into()
120            };
121            headers.push(allow_credentials);
122        }
123
124        // On the actual response, where the browser reads it (#58).
125        if !cors.expose_headers.is_empty() {
126            headers.push(Header {
127                name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
128                value: cors.expose_headers.join(",").to_lowercase().into(),
129            });
130        }
131
132        let is_options = request.method == METHOD.options;
133        if is_options {
134            let methods = cors.allow_methods.join(",");
135            let allow_methods = Header {
136                name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
137                value: methods.into()
138            };
139            headers.push(allow_methods);
140
141            let allow_headers_value = cors.allow_headers.join(",");
142            let allow_headers = Header {
143                name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
144                value: allow_headers_value.to_lowercase().into()
145            };
146            headers.push(allow_headers);
147
148            let max_age = Header {
149                name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
150                value: cors.max_age.to_string().into()
151            };
152            headers.push(max_age);
153        }
154
155        Ok(headers)
156    }
157
158    /// Build CORS headers using the provided [`ServerConfig`].
159    ///
160    /// This is the primary implementation. [`get_headers`] is the legacy
161    /// entry point that reads from environment variables; all new call-sites
162    /// should prefer this version.
163    pub fn get_headers_from_config(request: &Request, config: &ServerConfig) -> Vec<Header> {
164        if config.cors_allow_all {
165            return Cors::allow_all(request).unwrap_or_default();
166        }
167        Cors::process_using_config(request, config).unwrap_or_default()
168    }
169
170    fn process_using_config(request: &Request, config: &ServerConfig) -> Result<Vec<Header>, Error> {
171        let mut headers: Vec<Header> = vec![];
172
173        let boxed_origin = request.header(Header::_ORIGIN);
174        if boxed_origin.is_none() {
175            return Ok(headers);
176        }
177        let origin_value = boxed_origin.unwrap().value.clone();
178
179        if !Cors::origin_is_allowed(&config.cors_allow_origins, &origin_value) {
180            return Ok(headers);
181        }
182
183        headers.push(Header {
184            name: Header::_ACCESS_CONTROL_ALLOW_ORIGIN.into(),
185            value: origin_value,
186        });
187
188        let credentials_str = &config.cors_allow_credentials;
189        if credentials_str.eq_ignore_ascii_case("true") {
190            headers.push(Header {
191                name: Header::_ACCESS_CONTROL_ALLOW_CREDENTIALS.into(),
192                value: "true".into(),
193            });
194        }
195
196        // On the actual response, where the browser reads it — it was sent
197        // only on the preflight, which ignores it (#58).
198        if !config.cors_expose_headers.is_empty() {
199            headers.push(Header {
200                name: Header::_ACCESS_CONTROL_EXPOSE_HEADERS.into(),
201                value: config.cors_expose_headers.to_lowercase().into(),
202            });
203        }
204
205        if request.method == METHOD.options {
206            if !config.cors_allow_methods.is_empty() {
207                headers.push(Header {
208                    name: Header::_ACCESS_CONTROL_ALLOW_METHODS.into(),
209                    value: config.cors_allow_methods.clone().into(),
210                });
211            }
212            if !config.cors_allow_headers.is_empty() {
213                headers.push(Header {
214                    name: Header::_ACCESS_CONTROL_ALLOW_HEADERS.into(),
215                    value: config.cors_allow_headers.to_lowercase().into(),
216                });
217            }
218            if !config.cors_max_age.is_empty() {
219                headers.push(Header {
220                    name: Header::_ACCESS_CONTROL_MAX_AGE.into(),
221                    value: config.cors_max_age.clone().into(),
222                });
223            }
224        }
225
226        Ok(headers)
227    }
228
229    /// Legacy entry point that reads CORS settings from environment variables.
230    ///
231    /// Prefer [`get_headers_from_config`] when a [`ServerConfig`] is available
232    /// (e.g. inside [`App::execute`]). This variant is kept for call-sites that
233    /// do not yet have an `App`-level config reference.
234    pub fn process_using_default_config(request: &Request) -> Result<Vec<Header>, Error> {
235        let config = ServerConfig::current();
236        Self::process_using_config(request, &config)
237    }
238
239    /// Legacy entry point that reads CORS settings from environment variables.
240    ///
241    /// Prefer [`get_headers_from_config`] when a [`ServerConfig`] is available.
242    pub fn get_headers(request: &Request) -> Vec<Header> {
243        let config = ServerConfig::current();
244        Cors::get_headers_from_config(request, &config)
245    }
246}