Skip to content

Commit 6d310e8

Browse files
committed
refactor: encapsulate CORS preflight and response injection logic into reusable functions; improve test coverage
1 parent 19b69e1 commit 6d310e8

2 files changed

Lines changed: 113 additions & 28 deletions

File tree

api/src/s3/cors.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,6 @@ fn matched_allowed_headers(rule: &BucketCorsRule, requested_headers: &[String])
196196
return rule.allowed_headers.clone();
197197
}
198198

199-
if rule.allowed_headers.iter().any(|header| header == "*") {
200-
return requested_headers.to_vec();
201-
}
202-
203199
requested_headers.to_vec()
204200
}
205201

@@ -438,4 +434,73 @@ mod tests {
438434

439435
assert!(matched.is_none());
440436
}
437+
438+
fn matched_rule() -> MatchedCorsRule {
439+
MatchedCorsRule {
440+
allow_origin: "https://example.org".to_string(),
441+
allow_methods: vec!["GET".to_string(), "PUT".to_string()],
442+
allow_headers: vec!["content-type".to_string(), "x-test".to_string()],
443+
expose_headers: vec!["etag".to_string()],
444+
max_age_seconds: Some(60),
445+
}
446+
}
447+
448+
#[test]
449+
fn builds_preflight_success_response_with_expected_headers() {
450+
let response = build_preflight_response(matched_rule());
451+
452+
assert_eq!(response.status(), StatusCode::NO_CONTENT);
453+
assert_eq!(
454+
response.headers()["access-control-allow-origin"],
455+
"https://example.org"
456+
);
457+
assert_eq!(
458+
response.headers()["access-control-allow-methods"],
459+
"GET, PUT"
460+
);
461+
assert_eq!(
462+
response.headers()["access-control-allow-headers"],
463+
"content-type, x-test"
464+
);
465+
assert_eq!(response.headers()["access-control-max-age"], "60");
466+
assert_eq!(
467+
response.headers()[VARY],
468+
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
469+
);
470+
}
471+
472+
#[test]
473+
fn builds_preflight_failure_response_without_allow_headers() {
474+
let response = build_preflight_forbidden_response();
475+
476+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
477+
assert!(
478+
response
479+
.headers()
480+
.get("access-control-allow-origin")
481+
.is_none()
482+
);
483+
assert_eq!(
484+
response.headers()[VARY],
485+
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
486+
);
487+
}
488+
489+
#[test]
490+
fn injects_actual_cors_headers_and_preserves_existing_vary_entries() {
491+
let mut response = HttpResponse::new(s3s::Body::default());
492+
*response.status_mut() = StatusCode::OK;
493+
response
494+
.headers_mut()
495+
.insert(VARY, HeaderValue::from_static("Accept-Encoding"));
496+
497+
inject_actual_cors_headers(&mut response, matched_rule());
498+
499+
assert_eq!(
500+
response.headers()["access-control-allow-origin"],
501+
"https://example.org"
502+
);
503+
assert_eq!(response.headers()["access-control-expose-headers"], "etag");
504+
assert_eq!(response.headers()[VARY], "Accept-Encoding, Origin");
505+
}
441506
}

api/src/s3/s3_server.rs

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -187,28 +187,24 @@ impl Service<Request<Incoming>> for WrappingService {
187187
}
188188
};
189189

190-
if let (Some(origin), Some(requested_method), Some(config)) = (
191-
origin.as_deref(),
192-
preflight_requested_method.as_deref(),
193-
cors_configuration.as_ref(),
194-
)
195-
&& method == Method::OPTIONS
190+
if let Some(config) = cors_configuration.as_ref()
191+
&& let Some(response) = handle_cors_preflight(
192+
config,
193+
&method,
194+
origin.as_deref(),
195+
preflight_requested_method.as_deref(),
196+
&requested_headers,
197+
)
196198
{
197-
let response = match match_preflight_rule(config, origin, requested_method, &requested_headers) {
198-
Some(matched_rule) => build_preflight_response(matched_rule),
199-
None => build_preflight_forbidden_response()
200-
};
201199
emit_request_completed(&span, "s3", response.status().as_u16(), started);
202200
return Ok(response);
203201
}
204202

205203
let result = shared.call(s3s_request).instrument(span.clone()).await;
206204
match result {
207205
Ok(mut response) => {
208-
if let (Some(origin), Some(config)) = (origin.as_deref(), cors_configuration.as_ref()) {
209-
if let Some(matched_rule) = match_actual_rule(config, origin, &method) {
210-
inject_actual_cors_headers(&mut response, matched_rule);
211-
}
206+
if let Some(config) = cors_configuration.as_ref() {
207+
apply_cors_to_response(&mut response, config, origin.as_deref(), &method);
212208
}
213209
emit_request_completed(&span, "s3", response.status().as_u16(), started);
214210
Ok(response)
@@ -250,6 +246,40 @@ fn virtual_hosted_bucket(host: &str, domain: &str) -> Option<String> {
250246
(!prefix.is_empty()).then(|| prefix.to_owned())
251247
}
252248

249+
fn handle_cors_preflight(
250+
config: &BucketCorsConfiguration,
251+
method: &Method,
252+
origin: Option<&str>,
253+
requested_method: Option<&str>,
254+
requested_headers: &[String],
255+
) -> Option<HttpResponse> {
256+
if *method != Method::OPTIONS {
257+
return None;
258+
}
259+
let origin = origin?;
260+
let requested_method = requested_method?;
261+
262+
Some(
263+
match match_preflight_rule(config, origin, requested_method, requested_headers) {
264+
Some(matched_rule) => build_preflight_response(matched_rule),
265+
None => build_preflight_forbidden_response(),
266+
},
267+
)
268+
}
269+
270+
fn apply_cors_to_response(
271+
response: &mut HttpResponse,
272+
config: &BucketCorsConfiguration,
273+
origin: Option<&str>,
274+
method: &Method,
275+
) {
276+
if let Some(origin) = origin
277+
&& let Some(matched_rule) = match_actual_rule(config, origin, method)
278+
{
279+
inject_actual_cors_headers(response, matched_rule);
280+
}
281+
}
282+
253283
async fn load_bucket_cors_config(
254284
driver_ctx: Arc<DriverContext>,
255285
bucket: Option<String>,
@@ -272,16 +302,6 @@ async fn load_bucket_cors_config(
272302
mod tests {
273303
use super::*;
274304

275-
fn matched_rule() -> super::super::cors::MatchedCorsRule {
276-
super::super::cors::MatchedCorsRule {
277-
allow_origin: "https://example.org".to_string(),
278-
allow_methods: vec!["GET".to_string(), "PUT".to_string()],
279-
allow_headers: vec!["content-type".to_string(), "x-test".to_string()],
280-
expose_headers: vec!["etag".to_string()],
281-
max_age_seconds: Some(60),
282-
}
283-
}
284-
285305
#[test]
286306
fn extracts_bucket_name_from_path_style_request() {
287307
assert_eq!(

0 commit comments

Comments
 (0)