1use std::fmt;
2use hyper::Response;
3use http_body_util::combinators::BoxBody;
4use http_body_util::{BodyExt, Empty};
5
6use crate::handler::ResponseBody;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum CorsConfigError {
11 CredentialedWildcard,
17}
18
19impl fmt::Display for CorsConfigError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 CorsConfigError::CredentialedWildcard => write!(
23 f,
24 "CORS misconfiguration: allow_origin(\"*\") with allow_credentials(true) \
25 is not allowed — it grants credentialed access to every origin. \
26 Use an explicit origin list instead."
27 ),
28 }
29 }
30}
31
32impl std::error::Error for CorsConfigError {}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct CorsConfig {
43 pub allow_origins: Vec<String>,
45 pub allow_all_origins: bool,
47 pub credentials: bool,
49}
50
51impl CorsConfig {
52 fn build_cors_headers(&self, req_origin: Option<&str>) -> Vec<(String, String)> {
53 let mut headers = Vec::new();
54
55 let origin = if self.allow_all_origins {
56 "*"
57 } else if let Some(origin) = req_origin {
58 if self.allow_origins.iter().any(|o| o == origin) {
59 origin
60 } else {
61 return headers;
62 }
63 } else {
64 return headers;
65 };
66
67 headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
68
69 if origin != "*" {
70 headers.push(("vary".to_string(), "origin".to_string()));
71 }
72
73 if self.credentials {
74 headers.push(("access-control-allow-credentials".to_string(), "true".to_string()));
75 }
76
77 headers
78 }
79
80 pub fn preflight_response(&self, req_origin: Option<&str>) -> Response<ResponseBody> {
85 let headers = self.build_cors_headers(req_origin);
86 let mut resp = Response::builder()
87 .status(hyper::StatusCode::NO_CONTENT);
88
89 for (name, value) in headers {
90 if let Ok(val) = value.parse::<hyper::header::HeaderValue>() {
91 if let Ok(header_name) = name.parse::<hyper::header::HeaderName>() {
92 resp = resp.header(header_name, val);
93 }
94 }
95 }
96
97 resp
98 .body(BoxBody::new(Empty::new().map_err(|never: std::convert::Infallible| match never {})))
99 .expect("status is valid and headers are static ASCII")
100 }
101
102 pub fn apply_to_response(&self, resp: &mut Response<ResponseBody>, req_origin: Option<&str>) {
107 let headers = self.build_cors_headers(req_origin);
108 for (name, value) in headers {
109 if let Ok(val) = value.parse::<hyper::header::HeaderValue>() {
110 if let Ok(header_name) = name.parse::<hyper::header::HeaderName>() {
111 resp.headers_mut().insert(header_name, val);
112 }
113 }
114 }
115 }
116}
117
118#[derive(Default, Debug)]
123pub struct CorsConfigBuilder {
124 allow_origins: Vec<String>,
125 credentials: bool,
126}
127
128impl CorsConfigBuilder {
129 pub fn allow_origin(mut self, origin: &str) -> Self {
133 self.allow_origins.push(origin.to_string());
134 self
135 }
136
137 pub fn allow_credentials(mut self, yes: bool) -> Self {
142 self.credentials = yes;
143 self
144 }
145
146 pub fn build(self) -> Result<CorsConfig, CorsConfigError> {
148 let allow_all_origins = self.allow_origins.len() == 1 && self.allow_origins[0] == "*";
149
150 if self.credentials && allow_all_origins {
151 return Err(CorsConfigError::CredentialedWildcard);
152 }
153
154 Ok(CorsConfig {
155 allow_origins: self.allow_origins,
156 allow_all_origins,
157 credentials: self.credentials,
158 })
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn credentialed_wildcard_rejected_in_debug() {
168 let result = CorsConfigBuilder::default()
169 .allow_origin("*")
170 .allow_credentials(true)
171 .build();
172
173 assert_eq!(result, Err(CorsConfigError::CredentialedWildcard));
174 }
175
176 #[test]
177 fn credentialed_wildcard_rejected_in_release() {
178 let result = CorsConfigBuilder::default()
179 .allow_origin("*")
180 .allow_credentials(true)
181 .build();
182
183 assert_eq!(result, Err(CorsConfigError::CredentialedWildcard));
184 }
185
186 #[test]
187 fn explicit_origin_with_credentials_allowed() {
188 let config = CorsConfigBuilder::default()
189 .allow_origin("https://example.com")
190 .allow_credentials(true)
191 .build();
192
193 assert!(config.is_ok());
194 let cfg = config.unwrap();
195 assert!(!cfg.allow_all_origins);
196 assert!(cfg.credentials);
197 }
198
199 #[test]
200 fn wildcard_without_credentials_allowed() {
201 let config = CorsConfigBuilder::default()
202 .allow_origin("*")
203 .allow_credentials(false)
204 .build();
205
206 assert!(config.is_ok());
207 let cfg = config.unwrap();
208 assert!(cfg.allow_all_origins);
209 assert!(!cfg.credentials);
210 }
211}