1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
use std::collections::HashMap;
use http::HeaderMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Configuration for CORS (Cross-Origin Resource Sharing).
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)]
#[schemars(example = cors_example_1())]
#[schemars(example = cors_example_2())]
pub struct CORSConfig {
#[serde(default = "default_cors_enabled")]
pub enabled: bool,
/// Set to true to allow any origin. If true, the `origins` and `match_origin` fields are ignored.
#[serde(default = "default_allow_any_origin")]
pub allow_any_origin: bool,
/// List of CORS policies. The first policy that matches the request origin will be applied.
/// If no policies match, the request will be rejected.
/// If `allow_any_origin` is true, this field is ignored.
/// This allows you to define different CORS settings for different origins.
/// For example, you might want to allow credentials for some origins but not others.
/// If multiple policies match, the first one in the list will be applied.
///
/// Example:
/// ```yaml
/// allow_credentials: false
/// policies:
/// - match_origin: ["^https://.*\.credentials-example\.com$"]
/// allow_credentials: true
/// - match_origin: ["^https://.*\.example\.com$"]
/// ```
///
/// In this example, requests from any subdomain of `credentials-example.com` will be allowed to include credentials,
/// while requests from any subdomain of `example.com` will not be allowed to include credentials.
/// Requests from origins not matching either pattern will be rejected.
///
/// ## Policy Inheritance Rules
///
/// Each policy defined in the `policies` array can provide its own CORS settings.
/// If a setting is not specified within a policy, the corresponding global CORS setting is used as a fallback.
///
/// Here's a breakdown of how inheritance works for each field:
///
/// - `allow_credentials` and `max_age`: If a policy omits a value for these settings,
/// it automatically uses the value from the global configuration.
/// - `allow_headers` and `expose_headers`: A policy's behavior for these header lists depends on the value provided:
/// - If a list with specific headers is provided (e.g., `["Content-Type"]`), it completely overrides the global list.
/// - If an empty list (`[]`) is provided, the policy will inherit the headers from the global configuration.
/// - `methods`: A policy's behavior for this header list depends on the value provided:
/// - If `methods` is not specified at all (`null`) or set to an empty list (`[]`),
/// the policy inherits the methods from the global configuration.
/// - If the list contains specific methods (e.g., `["GET", "POST"]`), only those methods are used, overriding the global list.
/// - `preflight_response_headers`: Per-policy entries are merged on top of the global map.
/// Keys defined in the policy override the global ones, while keys defined only globally are still applied.
pub policies: Vec<CORSPolicyConfig>,
/// Set to true to allow credentials (cookies, authorization headers, or TLS client certificates) in cross-origin requests.
/// This will set the `Access-Control-Allow-Credentials` header to `true`.
#[serde(default = "default_allow_credentials")]
pub allow_credentials: bool,
/// List of headers that the server allows the client to send in a cross-origin request.
/// This will set the `Access-Control-Allow-Headers` header.
/// If not set, the server will reflect the headers specified in the `Access-Control-Request-Headers` request header.
/// Example: ["Content-Type", "Authorization"]
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_headers: Option<Vec<String>>,
/// List of methods that the server allows for cross-origin requests.
/// This will set the `Access-Control-Allow-Methods` header.
/// If not set, the server will reflect the method specified in the `Access-Control-Request-Method` request header.
/// Example: ["GET", "POST", "OPTIONS"]
#[serde(skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<String>>,
/// List of headers that the client is allowed to access from the response.
/// This will set the `Access-Control-Expose-Headers` header.
/// If not set, no additional headers are exposed to the client.
/// Example: ["X-Custom-Header", "X-Another-Header"]
#[serde(skip_serializing_if = "Option::is_none")]
pub expose_headers: Option<Vec<String>>,
/// The maximum time (in seconds) that the results of a preflight request can be cached by the client.
/// This will set the `Access-Control-Max-Age` header.
/// If not set, the browser will not cache the preflight response.
/// Example: 86400 (24 hours)
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<u64>,
/// Additional headers to set on CORS preflight (OPTIONS) responses.
///
/// The `headers` configuration block does not affect preflight responses
/// because they are returned early by the CORS layer. This map provides a
/// first-class way to attach arbitrary headers (e.g. `Cache-Control`,
/// `Server-Timing`, `X-*` custom headers) to those preflight responses.
///
/// Keys must be valid HTTP header names (RFC 7230) and values must be
/// valid HTTP header values.
///
/// The headers provided here are applied after the CORS engine's managed headers
/// (`Access-Control-*`, `Vary`) and therefore override them when keys collide.
///
/// Example:
/// ```yaml
/// preflight_response_headers:
/// Cache-Control: "public, max-age=86400"
/// ```
#[serde(
default,
with = "http_serde::header_map",
skip_serializing_if = "HeaderMap::is_empty"
)]
#[schemars(with = "HashMap<String, String>")]
pub preflight_response_headers: HeaderMap,
}
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, Clone)]
pub struct CORSPolicyConfig {
/// List of allowed origins. If `allow_any_origin` is true, this field is ignored.
/// If both `origins` and `match_origin` are set, the request origin must match one of the values in either list to be allowed.
/// An origin is a combination of scheme, host, and port (if specified).
/// Example: "https://example.com", "http://localhost:3000"
#[serde(skip_serializing_if = "Option::is_none")]
pub origins: Option<Vec<String>>,
/// List of regex patterns to match allowed origins. If `allow_any_origin` is true, this field is ignored.
/// If both `origins` and `match_origin` are set, the request origin must match one of the values in either list to be allowed.
/// Each pattern should be a valid regex.
/// Example: "^https://.*\.example\.com$", "^http://localhost:\d+$"
#[serde(skip_serializing_if = "Option::is_none")]
pub match_origin: Option<Vec<String>>,
/// Set to true to allow credentials (cookies, authorization headers, or TLS client certificates) in cross-origin requests.
/// This will set the `Access-Control-Allow-Credentials` header to `true`.
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_credentials: Option<bool>,
/// List of headers that the server allows the client to send in a cross-origin request.
/// This will set the `Access-Control-Allow-Headers` header.
/// If not set, the server will reflect the headers specified in the `Access-Control-Request-Headers` request header.
/// Example: ["Content-Type", "Authorization"]
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_headers: Option<Vec<String>>,
/// List of methods that the server allows for cross-origin requests.
/// This will set the `Access-Control-Allow-Methods` header.
/// If not set, the server will reflect the method specified in the `Access-Control-Request-Method` request header.
/// Example: ["GET", "POST", "OPTIONS"]
#[serde(skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<String>>,
/// List of headers that the client is allowed to access from the response.
/// This will set the `Access-Control-Expose-Headers` header.
/// If not set, no additional headers are exposed to the client.
/// Example: ["X-Custom-Header", "X-Another-Header"]
#[serde(skip_serializing_if = "Option::is_none")]
pub expose_headers: Option<Vec<String>>,
/// The maximum time (in seconds) that the results of a preflight request can be cached by the client.
/// This will set the `Access-Control-Max-Age` header.
/// If not set, the browser will not cache the preflight response.
/// Example: 86400 (24 hours)
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<u64>,
/// Additional headers to set on CORS preflight (OPTIONS) responses for this policy.
///
/// Entries are merged on top of the global `cors.preflight_response_headers` map.
/// Keys defined here override the global value for the same key, while keys defined only
/// globally still apply.
///
/// See `cors.preflight_response_headers` for details and caveats.
#[serde(
default,
with = "http_serde::header_map",
skip_serializing_if = "HeaderMap::is_empty"
)]
#[schemars(with = "HashMap<String, String>")]
pub preflight_response_headers: HeaderMap,
}
fn default_cors_enabled() -> bool {
false
}
fn default_allow_any_origin() -> bool {
false
}
fn default_allow_credentials() -> bool {
false
}
fn cors_example_1() -> CORSConfig {
CORSConfig {
enabled: true,
allow_any_origin: false,
policies: vec![CORSPolicyConfig {
origins: Some(vec![
"https://example.com".to_string(),
"https://another.com".to_string(),
]),
..Default::default()
}],
allow_credentials: false,
allow_headers: None,
methods: Some(vec![
"GET".to_string(),
"POST".to_string(),
"OPTIONS".to_string(),
]),
expose_headers: None,
max_age: Some(120),
preflight_response_headers: {
let mut headers = HeaderMap::new();
headers.insert(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("public, max-age=86400"),
);
headers
},
}
}
fn cors_example_2() -> CORSConfig {
CORSConfig {
enabled: true,
allow_any_origin: true,
..Default::default()
}
}