Skip to main content

rws_http/cors/
mod.rs

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