mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-06 18:52:15 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f125bb4ec | |||
| bda712df56 | |||
| 00102f5737 | |||
| 55246fa932 |
+113
-1
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! CORS (Cross-Origin Resource Sharing) header name constants.
|
||||
//! CORS (Cross-Origin Resource Sharing) headers and request inputs.
|
||||
//!
|
||||
//! This module provides centralized constants for CORS-related HTTP header names.
|
||||
//! The http crate doesn't provide pre-defined constants for CORS headers,
|
||||
@@ -38,3 +38,115 @@ pub mod request {
|
||||
pub mod standard {
|
||||
pub use http::header::{ORIGIN, VARY};
|
||||
}
|
||||
|
||||
/// Only the inputs used by CORS policy evaluation. Credential payloads and
|
||||
/// unrelated object metadata need not outlive the inner request.
|
||||
pub(crate) struct CorsRequest {
|
||||
pub(crate) origin: Option<http::HeaderValue>,
|
||||
pub(crate) requested_method: Option<http::HeaderValue>,
|
||||
pub(crate) requested_headers: Option<http::HeaderValue>,
|
||||
pub(crate) credentialed: bool,
|
||||
}
|
||||
|
||||
impl CorsRequest {
|
||||
pub(crate) fn from_headers(headers: &http::HeaderMap) -> Self {
|
||||
Self {
|
||||
origin: headers.get(standard::ORIGIN).cloned(),
|
||||
requested_method: headers.get(request::ACCESS_CONTROL_REQUEST_METHOD).cloned(),
|
||||
requested_headers: headers.get(request::ACCESS_CONTROL_REQUEST_HEADERS).cloned(),
|
||||
credentialed: headers.contains_key(http::header::AUTHORIZATION)
|
||||
|| headers.contains_key(http::header::COOKIE)
|
||||
|| headers.contains_key("x-amz-security-token")
|
||||
|| headers.contains_key("x-amz-content-sha256"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CorsRequest;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
|
||||
#[test]
|
||||
fn cors_request_captures_first_value_without_mutating_duplicate_headers() {
|
||||
let mut headers = HeaderMap::new();
|
||||
for (name, first, second) in [
|
||||
("origin", "https://first.example", "https://second.example"),
|
||||
("access-control-request-method", "PUT", "DELETE"),
|
||||
("access-control-request-headers", "x-first", "x-second"),
|
||||
] {
|
||||
headers.append(name, HeaderValue::from_static(first));
|
||||
headers.append(name, HeaderValue::from_static(second));
|
||||
}
|
||||
let original = headers.clone();
|
||||
|
||||
let request = CorsRequest::from_headers(&headers);
|
||||
|
||||
assert_eq!(request.origin, Some(HeaderValue::from_static("https://first.example")));
|
||||
assert_eq!(request.requested_method, Some(HeaderValue::from_static("PUT")));
|
||||
assert_eq!(request.requested_headers, Some(HeaderValue::from_static("x-first")));
|
||||
assert!(!request.credentialed);
|
||||
assert_eq!(headers, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_request_preserves_opaque_values_separately_from_absence() {
|
||||
let mut headers = HeaderMap::new();
|
||||
let opaque = HeaderValue::from_bytes(&[0x80]).expect("opaque HTTP header value");
|
||||
assert!(opaque.to_str().is_err());
|
||||
for name in ["origin", "access-control-request-method", "access-control-request-headers"] {
|
||||
headers.insert(name, opaque.clone());
|
||||
}
|
||||
let original = headers.clone();
|
||||
|
||||
let request = CorsRequest::from_headers(&headers);
|
||||
|
||||
assert_eq!(request.origin, Some(opaque.clone()));
|
||||
assert_eq!(request.requested_method, Some(opaque.clone()));
|
||||
assert_eq!(request.requested_headers, Some(opaque));
|
||||
assert!(!request.credentialed);
|
||||
assert_eq!(headers, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_request_detects_each_credential_header_by_presence() {
|
||||
for name in ["authorization", "cookie", "x-amz-security-token", "x-amz-content-sha256"] {
|
||||
for value in ["", "present"] {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(name, HeaderValue::from_static(value));
|
||||
let original = headers.clone();
|
||||
|
||||
let request = CorsRequest::from_headers(&headers);
|
||||
|
||||
assert!(request.credentialed, "{name} with value {value:?}");
|
||||
assert!(request.origin.is_none());
|
||||
assert!(request.requested_method.is_none());
|
||||
assert!(request.requested_headers.is_none());
|
||||
assert_eq!(headers, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_request_ignores_unrelated_headers_and_preserves_the_request() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-meta-large", HeaderValue::from_str(&"a".repeat(16 * 1024)).expect("large metadata"));
|
||||
for name in [
|
||||
"proxy-authorization",
|
||||
"x-amz-date",
|
||||
"x-amz-meta-authorization",
|
||||
"x-amz-security-token-extra",
|
||||
] {
|
||||
headers.insert(name, HeaderValue::from_static("present"));
|
||||
}
|
||||
let original = headers.clone();
|
||||
|
||||
let request = CorsRequest::from_headers(&headers);
|
||||
|
||||
assert!(request.origin.is_none());
|
||||
assert!(request.requested_method.is_none());
|
||||
assert!(request.requested_headers.is_none());
|
||||
assert!(!request.credentialed);
|
||||
assert_eq!(headers, original);
|
||||
}
|
||||
}
|
||||
|
||||
+1037
-177
File diff suppressed because it is too large
Load Diff
@@ -936,18 +936,12 @@ pub(crate) fn needs_cors_processing(headers: &HeaderMap) -> bool {
|
||||
///
|
||||
/// Note: This function should only be called if `needs_cors_processing()` returns true
|
||||
/// to avoid unnecessary overhead for non-CORS requests.
|
||||
pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, headers: &HeaderMap) -> Option<HeaderMap> {
|
||||
pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, request: &cors::CorsRequest) -> Option<HeaderMap> {
|
||||
use http::HeaderValue;
|
||||
|
||||
fn is_credentialed_request(headers: &HeaderMap) -> bool {
|
||||
headers.contains_key(http::header::AUTHORIZATION)
|
||||
|| headers.contains_key(http::header::COOKIE)
|
||||
|| headers.contains_key("x-amz-security-token")
|
||||
|| headers.contains_key("x-amz-content-sha256")
|
||||
}
|
||||
|
||||
// Get Origin header from request
|
||||
let origin = headers.get(cors::standard::ORIGIN)?.to_str().ok()?;
|
||||
// Invalid and absent origins do not require a bucket metadata lookup.
|
||||
let origin_header = request.origin.as_ref()?;
|
||||
let origin = origin_header.to_str().ok()?;
|
||||
|
||||
// Get CORS configuration for the bucket
|
||||
let cors_config = match get_bucket_cors_config(bucket).await {
|
||||
@@ -970,15 +964,17 @@ pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, head
|
||||
// Use Access-Control-Request-Method if present (for preflight and non-preflight requests),
|
||||
// otherwise fall back to the actual HTTP method.
|
||||
let is_preflight = method == http::Method::OPTIONS;
|
||||
let requested_method = headers
|
||||
.get(cors::request::ACCESS_CONTROL_REQUEST_METHOD)
|
||||
let requested_method = request
|
||||
.requested_method
|
||||
.as_ref()
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or(method_str);
|
||||
|
||||
// Get requested headers from preflight request
|
||||
let requested_headers = if is_preflight {
|
||||
headers
|
||||
.get(cors::request::ACCESS_CONTROL_REQUEST_HEADERS)
|
||||
request
|
||||
.requested_headers
|
||||
.as_ref()
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|h| h.split(',').map(|s| s.trim().to_lowercase()).collect::<Vec<_>>())
|
||||
} else {
|
||||
@@ -1041,27 +1037,16 @@ pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, head
|
||||
// Credentials mode + wildcard allow list requires echoing the request origin.
|
||||
// Browsers reject `Access-Control-Allow-Origin: *` with `credentials: include`.
|
||||
let has_wildcard_origin = rule.allowed_origins.iter().any(|o| o == "*");
|
||||
let credentialed_request = is_credentialed_request(headers);
|
||||
let mut origin_reflected = false;
|
||||
let credentialed_request = request.credentialed;
|
||||
|
||||
if has_wildcard_origin {
|
||||
if credentialed_request {
|
||||
if let Ok(origin_value) = HeaderValue::from_str(origin) {
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_value);
|
||||
origin_reflected = true;
|
||||
}
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_header.clone());
|
||||
} else {
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
}
|
||||
} else if let Ok(origin_value) = HeaderValue::from_str(origin) {
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_value);
|
||||
origin_reflected = true;
|
||||
}
|
||||
|
||||
// Vary: Origin whenever origin is reflected (non-"*" allow-origin).
|
||||
// This prevents proxy/browser caches from reusing CORS headers across different origins.
|
||||
if origin_reflected {
|
||||
response_headers.insert(cors::standard::VARY, HeaderValue::from_static("Origin"));
|
||||
} else {
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, origin_header.clone());
|
||||
}
|
||||
|
||||
// Credentials mode requires explicit allow-credentials.
|
||||
@@ -1086,15 +1071,6 @@ pub(crate) async fn apply_cors_headers(bucket: &str, method: &http::Method, head
|
||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_HEADERS, headers_value);
|
||||
}
|
||||
}
|
||||
if is_preflight {
|
||||
let vary = if origin_reflected {
|
||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
} else {
|
||||
"Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
};
|
||||
response_headers.insert(cors::standard::VARY, HeaderValue::from_static(vary));
|
||||
}
|
||||
|
||||
// Access-Control-Expose-Headers (for actual requests)
|
||||
if !is_preflight && let Some(ref expose_headers) = rule.expose_headers {
|
||||
let expose_headers_str = expose_headers.iter().map(|h| h.as_str()).collect::<Vec<_>>().join(", ");
|
||||
@@ -1131,7 +1107,8 @@ pub(crate) fn matches_origin_pattern(pattern: &str, origin: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap S3Response with CORS headers if needed
|
||||
/// Wrap S3Response with CORS grants if needed. Cache variation is applied by
|
||||
/// `ConditionalCorsService` to all responses, including originless requests.
|
||||
/// This function performs a lightweight check first to avoid unnecessary CORS processing
|
||||
/// for non-CORS requests (requests without Origin header)
|
||||
pub(crate) async fn wrap_response_with_cors<T>(
|
||||
@@ -1140,18 +1117,13 @@ pub(crate) async fn wrap_response_with_cors<T>(
|
||||
headers: &HeaderMap,
|
||||
output: T,
|
||||
) -> S3Response<T> {
|
||||
let mut response = S3Response::new(output);
|
||||
|
||||
// Quick check: only process CORS if Origin header is present
|
||||
if needs_cors_processing(headers)
|
||||
&& let Some(cors_headers) = apply_cors_headers(bucket, method, headers).await
|
||||
&& let Some(cors_headers) = apply_cors_headers(bucket, method, &cors::CorsRequest::from_headers(headers)).await
|
||||
{
|
||||
for (key, value) in cors_headers.iter() {
|
||||
response.headers.insert(key, value.clone());
|
||||
}
|
||||
S3Response::with_headers(output, cors_headers)
|
||||
} else {
|
||||
S3Response::new(output)
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Parse part number from Option<i32> to Option<usize> with validation
|
||||
|
||||
@@ -1927,7 +1927,7 @@ mod tests {
|
||||
let method = http::Method::GET;
|
||||
|
||||
// Should return None when no origin header
|
||||
let result = apply_cors_headers("test-bucket", &method, &headers).await;
|
||||
let result = apply_cors_headers("test-bucket", &method, &cors::CorsRequest::from_headers(&headers)).await;
|
||||
assert!(result.is_none(), "Should return None when no Origin header");
|
||||
}
|
||||
|
||||
@@ -1941,7 +1941,8 @@ mod tests {
|
||||
// Should return None when no CORS config exists
|
||||
// Note: This test may fail if test-bucket actually has CORS config
|
||||
// In a real scenario, we'd use a mock or ensure the bucket doesn't exist
|
||||
let _result = apply_cors_headers("non-existent-bucket-for-testing", &method, &headers).await;
|
||||
let _result =
|
||||
apply_cors_headers("non-existent-bucket-for-testing", &method, &cors::CorsRequest::from_headers(&headers)).await;
|
||||
// Result depends on whether bucket exists and has CORS config
|
||||
// This is expected behavior - we just verify it doesn't panic
|
||||
}
|
||||
@@ -1970,7 +1971,7 @@ mod tests {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(cors::standard::ORIGIN, "https://disallowed.example.com".parse().unwrap());
|
||||
|
||||
let result = apply_cors_headers(bucket, &http::Method::GET, &headers).await;
|
||||
let result = apply_cors_headers(bucket, &http::Method::GET, &cors::CorsRequest::from_headers(&headers)).await;
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Expected Some empty headers when bucket has CORS config but origin does not match any rule"
|
||||
@@ -2011,17 +2012,14 @@ mod tests {
|
||||
headers.insert(cors::request::ACCESS_CONTROL_REQUEST_HEADERS, "x-amz-content-sha256".parse().unwrap());
|
||||
headers.insert(http::header::AUTHORIZATION, "AWS4-HMAC-SHA256 Credential=test/20260302/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256, Signature=abc".parse().unwrap());
|
||||
|
||||
let result = apply_cors_headers(bucket, &http::Method::OPTIONS, &headers).await.unwrap();
|
||||
let result = apply_cors_headers(bucket, &http::Method::OPTIONS, &cors::CorsRequest::from_headers(&headers))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
|
||||
"https://console.localhost",
|
||||
);
|
||||
assert_eq!(result.get(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS).unwrap(), "true");
|
||||
assert_eq!(
|
||||
result.get(cors::standard::VARY).unwrap(),
|
||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||
);
|
||||
|
||||
set_bucket_metadata(bucket.to_string(), BucketMetadata::new(bucket))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2034,7 +2032,7 @@ mod tests {
|
||||
headers.insert("origin", "https://example.com".parse().unwrap());
|
||||
let method = http::Method::PATCH; // Unsupported method
|
||||
|
||||
let result = apply_cors_headers("test-bucket", &method, &headers).await;
|
||||
let result = apply_cors_headers("test-bucket", &method, &cors::CorsRequest::from_headers(&headers)).await;
|
||||
assert!(result.is_none(), "Should return None for unsupported methods");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user