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
use super::method::Method;
use super::middleware::{Next, ResponseFuture};
use super::request::Request;
use super::response::Response;
use std::borrow::Cow;
use std::sync::Arc;
/// Default methods for CORS preflight responses.
const DEFAULT_METHODS: &str = "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS";
/// Default max-age for preflight cache (1 hour), written as the header value it
/// becomes. The number was only ever rendered to build this text, and a default
/// held as text is a constant every response borrows instead of copies.
const DEFAULT_MAX_AGE: &str = "3600";
/// Default allowed headers for CORS preflight responses.
const DEFAULT_HEADERS: &str = "Content-Type, Authorization, Accept";
impl std::fmt::Debug for CorsBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CorsBuilder")
.field("origin_count", &self.origins.len())
.field("methods", &self.methods)
.field("headers", &self.headers)
.field("max_age", &self.max_age)
.field("allow_credentials", &self.allow_credentials)
.finish()
}
}
/// Builder for customizing CORS middleware configuration.
///
/// Construct via [`builder()`] for fine-grained control, or use
/// [`allow_origins()`] for the common case with sensible defaults.
///
/// The three header values are `Cow` because a default configuration never
/// changes them: the builder stores the constants borrowed, so a CORS response
/// hands them on without copying. Only a caller that sets its own values pays
/// for them, and only once, at build time.
pub struct CorsBuilder {
origins: Box<[Box<str>]>,
methods: Cow<'static, str>,
headers: Cow<'static, str>,
max_age: Cow<'static, str>,
allow_credentials: bool,
}
impl CorsBuilder {
/// Set allowed origins.
pub fn origins(mut self, origins: &[&str]) -> Self {
self.origins = origins.iter().map(|o| Box::from(*o)).collect();
self
}
/// Set allowed HTTP methods.
pub fn methods(mut self, methods: &[&str]) -> Self {
self.methods = Cow::Owned(methods.join(", "));
self
}
/// Set allowed request headers.
pub fn headers(mut self, headers: &[&str]) -> Self {
self.headers = Cow::Owned(headers.join(", "));
self
}
/// Set the preflight cache duration in seconds.
pub fn max_age(mut self, seconds: u32) -> Self {
self.max_age = Cow::Owned(seconds.to_string());
self
}
/// Enable `Access-Control-Allow-Credentials: true`.
pub fn credentials(mut self) -> Self {
self.allow_credentials = true;
self
}
/// Build the CORS middleware closure.
pub fn build(self) -> impl Fn(&Request, Next) -> ResponseFuture + Send + Sync + 'static {
if self.origins.is_empty() {
tracing::warn!(
"CORS middleware built with no allowed origins — all requests will pass through without CORS headers"
);
}
let shared = Arc::new(self);
move |req, next| cors_middleware(Arc::clone(&shared), req, next)
}
/// Check whether `origin` matches any allowed origin.
/// Returns the resolved Allow-Origin header value:
/// - `"*"` when a wildcard origin is present and credentials are disabled
/// - the request origin when matched by wildcard with credentials, or by exact match
/// - `None` when no origin matches
///
/// The wildcard answer is a constant, so it is borrowed. Returning a borrow
/// of the request instead made the caller copy `"*"` onto the heap on every
/// response — and `allow_origins(&["*"])` without credentials is the common
/// configuration.
fn resolve_origin(&self, origin: &str) -> Option<Cow<'static, str>> {
let has_wildcard = self.origins.iter().any(|allowed| allowed.as_ref() == "*");
let exact_match = self
.origins
.iter()
.any(|allowed| allowed.as_ref() == origin);
match (has_wildcard, exact_match) {
(false, false) => None,
(true, _) if !self.allow_credentials => Some(Cow::Borrowed("*")),
_ => Some(Cow::Owned(origin.to_owned())),
}
}
/// Add this configuration's CORS headers to one response.
///
/// Every name here is a literal, and the `Vary` value is one of two fixed
/// sentences, so they are borrowed rather than copied: these headers ride on
/// every CORS response, and a copy per response is the allocation
/// `HeaderPair` exists to avoid. The three configured values are cloned, and
/// cloning a borrowed `Cow` copies nothing — a default configuration reaches
/// the wire without a single copy.
///
/// The origin arrives owned and is moved into the header. Taking it by
/// reference copied it a second time, on top of the copy the caller already
/// made to carry it into the response future.
fn apply_cors_headers(
&self,
resp: Response,
origin: Cow<'static, str>,
preflight: bool,
) -> Response {
let vary = match preflight {
true => "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
false => "Origin",
};
let resp = resp
.with_static_header("Access-Control-Allow-Origin", origin)
.with_static_header("Access-Control-Allow-Methods", self.methods.clone())
.with_static_header("Access-Control-Allow-Headers", self.headers.clone())
.with_static_header("Access-Control-Max-Age", self.max_age.clone())
.with_static_header("Vary", Cow::Borrowed(vary));
match self.allow_credentials {
true => {
resp.with_static_header("Access-Control-Allow-Credentials", Cow::Borrowed("true"))
}
false => resp,
}
}
}
fn is_preflight(req: &Request) -> bool {
matches!(req.method_enum(), Some(Method::Options))
&& req.header("access-control-request-method").is_some()
}
fn cors_middleware(config: Arc<CorsBuilder>, req: &Request, next: Next) -> ResponseFuture {
let origin_ref = match req.header("origin") {
Some(o) => o,
None => return next.call(req),
};
// `resolve_origin` decides whether the value it answers with is a constant
// or a copy of the request header, so the copy is made there rather than
// forced on every answer here. Either way what comes back is owned for
// `'static`, which is what lets it outlive the borrow of the request.
let header_origin = match config.resolve_origin(origin_ref) {
Some(resolved) => resolved,
None => return next.call(req),
};
let preflight = is_preflight(req);
match preflight {
true => {
let resp = config.apply_cors_headers(Response::empty_raw(204), header_origin, true);
Box::pin(async move { resp })
}
false => {
let resp_fut = next.call(req);
Box::pin(async move {
let resp = resp_fut.await;
config.apply_cors_headers(resp, header_origin, false)
})
}
}
}
/// Create CORS middleware that allows the specified origins.
///
/// Handles preflight OPTIONS requests automatically and adds
/// CORS headers to responses for matching origins.
pub fn allow_origins(
origins: &[&str],
) -> impl Fn(&Request, Next) -> ResponseFuture + Send + Sync + 'static {
builder().origins(origins).build()
}
/// Create a CORS builder for customizing allowed origins, methods, headers, and max-age.
pub fn builder() -> CorsBuilder {
CorsBuilder {
origins: Box::default(),
methods: Cow::Borrowed(DEFAULT_METHODS),
headers: Cow::Borrowed(DEFAULT_HEADERS),
max_age: Cow::Borrowed(DEFAULT_MAX_AGE),
allow_credentials: false,
}
}