mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-02 02:08:41 +00:00
feat(s3): enforce multipart presigned size limits (#6732)
This commit is contained in:
@@ -67,6 +67,7 @@ use super::storage_api::multipart_usecase::sse::{
|
||||
use super::storage_api::multipart_usecase::{
|
||||
StorageObjectInfo as ObjectInfo, StorageObjectOptions as ObjectOptions, StoragePutObjReader as PutObjReader,
|
||||
};
|
||||
use crate::app::object::{guard_put_object_body_read_timeout, put_object_body_read_timeout};
|
||||
use crate::app::object_data_cache::{
|
||||
ObjectDataCacheAdapter, invalidate_object_data_cache_after_complete_multipart_success,
|
||||
invalidate_object_data_cache_before_mutation,
|
||||
@@ -78,7 +79,11 @@ use crate::app::object_usecase::{
|
||||
use crate::app::runtime_sources::{
|
||||
AppContext, current_app_context, current_object_data_cache_for_context, current_object_store_handle_for_context,
|
||||
};
|
||||
use crate::auth::{VerifiedPresignedRequest, reject_presigned_put_max_content_length_for_other_operation};
|
||||
use crate::auth::{
|
||||
VerifiedPresignedRequest, VerifiedSigV4Request, parse_presigned_multipart_max_total_object_size,
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation,
|
||||
reject_presigned_put_max_content_length_for_other_operation,
|
||||
};
|
||||
use crate::capacity::record_capacity_write;
|
||||
use crate::error::ApiError;
|
||||
use crate::table_catalog;
|
||||
@@ -92,8 +97,9 @@ use rustfs_utils::CompressionAlgorithm;
|
||||
#[cfg(test)]
|
||||
use rustfs_utils::http::insert_header;
|
||||
use rustfs_utils::http::{
|
||||
SUFFIX_REPLICATION_PRESERVE_CIPHERTEXT, SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TIMESTAMP,
|
||||
SUFFIX_SOURCE_REPLICATION_REQUEST, contains_key_str, get_header, get_source_scheme,
|
||||
SUFFIX_MAX_TOTAL_OBJECT_SIZE, SUFFIX_REPLICATION_PRESERVE_CIPHERTEXT, SUFFIX_REPLICATION_STATUS,
|
||||
SUFFIX_REPLICATION_TIMESTAMP, SUFFIX_SOURCE_REPLICATION_REQUEST, contains_key_str, get_consistent_str, get_header,
|
||||
get_source_scheme,
|
||||
headers::{AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS},
|
||||
insert_str,
|
||||
};
|
||||
@@ -108,6 +114,7 @@ use s3s::{S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::io::StreamReader;
|
||||
use tracing::{instrument, warn};
|
||||
@@ -226,6 +233,22 @@ fn create_multipart_upload_metadata(
|
||||
metadata
|
||||
}
|
||||
|
||||
fn multipart_max_total_object_size(metadata: &HashMap<String, String>) -> S3Result<Option<u64>> {
|
||||
if !contains_key_str(metadata, SUFFIX_MAX_TOTAL_OBJECT_SIZE) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let value = get_consistent_str(metadata, SUFFIX_MAX_TOTAL_OBJECT_SIZE).ok_or_else(|| {
|
||||
S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
"multipart size capability metadata is missing or inconsistent".to_string(),
|
||||
)
|
||||
})?;
|
||||
value.parse::<u64>().map(Some).map_err(|_| {
|
||||
S3Error::with_message(S3ErrorCode::InvalidRequest, "multipart size capability metadata is invalid".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// A multipart session advertises disk compression only when the staged-rollout
|
||||
/// switch (`RUSTFS_COMPRESSION_MULTIPART_ENABLED`) is on, the object key/headers
|
||||
/// qualify, AND the session is not an SSE-C ciphertext-passthrough replication
|
||||
@@ -398,6 +421,11 @@ impl DefaultMultipartUsecase {
|
||||
&self,
|
||||
req: S3Request<AbortMultipartUploadInput>,
|
||||
) -> S3Result<S3Response<AbortMultipartUploadOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -444,6 +472,11 @@ impl DefaultMultipartUsecase {
|
||||
&self,
|
||||
req: S3Request<CompleteMultipartUploadInput>,
|
||||
) -> S3Result<S3Response<CompleteMultipartUploadOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -752,6 +785,11 @@ impl DefaultMultipartUsecase {
|
||||
&self,
|
||||
req: S3Request<CreateMultipartUploadInput>,
|
||||
) -> S3Result<S3Response<CreateMultipartUploadOutput>> {
|
||||
let multipart_max_total_object_size = parse_presigned_multipart_max_total_object_size(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -807,6 +845,9 @@ impl DefaultMultipartUsecase {
|
||||
)?;
|
||||
|
||||
let mut metadata = create_multipart_upload_metadata(input_metadata, &req.headers, tagging, storage_class.as_ref());
|
||||
if let Some(limit) = multipart_max_total_object_size {
|
||||
insert_str(&mut metadata, SUFFIX_MAX_TOTAL_OBJECT_SIZE, limit.to_string());
|
||||
}
|
||||
|
||||
let has_explicit_object_lock_retention = object_lock_mode.is_some()
|
||||
|| object_lock_retain_until_date.is_some()
|
||||
@@ -978,6 +1019,11 @@ impl DefaultMultipartUsecase {
|
||||
#[instrument(level = "debug", skip(self, req))]
|
||||
#[hotpath::measure(impl_type = "MultipartUsecase")]
|
||||
pub async fn execute_upload_part(&self, req: S3Request<UploadPartInput>) -> S3Result<S3Response<UploadPartOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -1006,6 +1052,40 @@ impl DefaultMultipartUsecase {
|
||||
|
||||
let mut size = resolve_upload_part_size(&req.headers, content_length)?;
|
||||
let mut body_stream = body.ok_or_else(|| s3_error!(IncompleteBody))?;
|
||||
let Some(store) = self.object_store() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
let fi = store
|
||||
.get_multipart_info(&bucket, &key, &upload_id, &opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let max_total_object_size = multipart_max_total_object_size(&fi.user_defined)?;
|
||||
if max_total_object_size.is_some() && size.is_some_and(|size| size < 0) {
|
||||
return Err(S3Error::new(S3ErrorCode::UnexpectedContent));
|
||||
}
|
||||
if max_total_object_size.is_some() && size.is_none() {
|
||||
return Err(S3Error::new(S3ErrorCode::UnexpectedContent));
|
||||
}
|
||||
if let (Some(limit), Some(size)) = (max_total_object_size, size)
|
||||
&& u64::try_from(size).is_ok_and(|size| size > limit)
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::EntityTooLarge));
|
||||
}
|
||||
if max_total_object_size.is_some() {
|
||||
let request_id = req
|
||||
.extensions
|
||||
.get::<super::storage_api::multipart_usecase::request_context::RequestContext>()
|
||||
.map(|ctx| ctx.request_id.clone())
|
||||
.unwrap_or_default();
|
||||
body_stream = guard_put_object_body_read_timeout(
|
||||
body_stream,
|
||||
&bucket,
|
||||
&key,
|
||||
&request_id,
|
||||
content_length,
|
||||
put_object_body_read_timeout().max(Duration::from_secs(rustfs_config::DEFAULT_HTTP_REQUEST_BODY_READ_TIMEOUT)),
|
||||
);
|
||||
}
|
||||
|
||||
if size.is_none() {
|
||||
let mut total = 0i64;
|
||||
@@ -1026,16 +1106,6 @@ impl DefaultMultipartUsecase {
|
||||
body_stream = StreamingBlob::wrap(stream);
|
||||
}
|
||||
|
||||
// Get multipart info early to check if managed encryption will be applied
|
||||
let Some(store) = self.object_store() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
let fi = store
|
||||
.get_multipart_info(&bucket, &key, &upload_id, &opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let mut size = size.ok_or_else(|| s3_error!(UnexpectedContent))?;
|
||||
let ingress_stage_start = rustfs_io_metrics::put_stage_metrics_enabled().then(std::time::Instant::now);
|
||||
|
||||
@@ -1250,6 +1320,11 @@ impl DefaultMultipartUsecase {
|
||||
&self,
|
||||
req: S3Request<ListMultipartUploadsInput>,
|
||||
) -> S3Result<S3Response<ListMultipartUploadsOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -1302,6 +1377,11 @@ impl DefaultMultipartUsecase {
|
||||
}
|
||||
|
||||
pub async fn execute_list_parts(&self, req: S3Request<ListPartsInput>) -> S3Result<S3Response<ListPartsOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -1338,6 +1418,11 @@ impl DefaultMultipartUsecase {
|
||||
&self,
|
||||
req: S3Request<UploadPartCopyInput>,
|
||||
) -> S3Result<S3Response<UploadPartCopyOutput>> {
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
req.extensions.get::<VerifiedSigV4Request>().is_some(),
|
||||
)?;
|
||||
reject_presigned_put_max_content_length_for_other_operation(
|
||||
&req.headers,
|
||||
req.uri.query(),
|
||||
@@ -1441,6 +1526,7 @@ impl DefaultMultipartUsecase {
|
||||
.get_multipart_info(&bucket, &key, &upload_id, &dst_opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let destination_size_limit = multipart_max_total_object_size(&mp_info.user_defined)?;
|
||||
EncryptionRequest {
|
||||
bucket: &bucket,
|
||||
key: &key,
|
||||
@@ -1523,19 +1609,25 @@ impl DefaultMultipartUsecase {
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
|
||||
let source_logical_size = match src_info.get_actual_size() {
|
||||
Ok(size) if size >= 0 => size,
|
||||
Ok(_) | Err(_) if destination_size_limit.is_some() => {
|
||||
return Err(S3Error::new(S3ErrorCode::UnexpectedContent));
|
||||
}
|
||||
Ok(_) | Err(_) => src_info.size,
|
||||
};
|
||||
|
||||
let (_start_offset, length) = if let Some(ref range_spec) = rs {
|
||||
// Copy-source ranges are expressed over the logical plaintext object.
|
||||
// Encrypted (and compressed) objects have a larger or smaller physical
|
||||
// representation, so validating against `size` rejects valid later parts.
|
||||
let validation_size = src_info.get_actual_size().unwrap_or(src_info.size);
|
||||
|
||||
validate_copy_source_range_not_exceeds(range_spec, validation_size)?;
|
||||
validate_copy_source_range_not_exceeds(range_spec, source_logical_size)?;
|
||||
|
||||
range_spec
|
||||
.get_offset_length(validation_size)
|
||||
.get_offset_length(source_logical_size)
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRange, e.to_string()))?
|
||||
} else {
|
||||
(0, src_info.size)
|
||||
(0, source_logical_size)
|
||||
};
|
||||
|
||||
let is_disk_compressed =
|
||||
@@ -2137,6 +2229,16 @@ mod tests {
|
||||
assert_eq!(metadata.get(AMZ_OBJECT_TAGGING), Some(&"project=rustfs".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_max_total_object_size_reads_compatible_internal_metadata() {
|
||||
let mut metadata = HashMap::new();
|
||||
insert_str(&mut metadata, SUFFIX_MAX_TOTAL_OBJECT_SIZE, "104857600".to_string());
|
||||
assert_eq!(multipart_max_total_object_size(&metadata).unwrap(), Some(104_857_600));
|
||||
|
||||
metadata.insert("x-minio-internal-max-total-object-size".to_string(), "1".to_string());
|
||||
assert!(multipart_max_total_object_size(&metadata).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_complete_multipart_upload_rejects_missing_parts_payload() {
|
||||
let input = CompleteMultipartUploadInput::builder()
|
||||
|
||||
@@ -195,6 +195,7 @@ pub(crate) use self::delete::*;
|
||||
pub(crate) use self::extract::*;
|
||||
pub(crate) use self::get::*;
|
||||
use self::put::*;
|
||||
pub(crate) use self::put::{guard_put_object_body_read_timeout, put_object_body_read_timeout};
|
||||
pub(crate) use self::shared::*;
|
||||
#[cfg(test)]
|
||||
use self::test_support::*;
|
||||
|
||||
@@ -90,7 +90,7 @@ fn resolve_put_object_authoritative_size(headers: &HeaderMap, content_length: Op
|
||||
/// Returns `Duration::ZERO` when disabled (`RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT=0`),
|
||||
/// in which case [`guard_put_object_body_read_timeout`] passes the body through
|
||||
/// untouched.
|
||||
fn put_object_body_read_timeout() -> Duration {
|
||||
pub(crate) fn put_object_body_read_timeout() -> Duration {
|
||||
Duration::from_secs(rustfs_utils::get_env_u64(
|
||||
rustfs_config::ENV_HTTP_REQUEST_BODY_READ_TIMEOUT,
|
||||
rustfs_config::DEFAULT_HTTP_REQUEST_BODY_READ_TIMEOUT,
|
||||
@@ -260,7 +260,7 @@ impl ByteStream for RequestBodyReadTimeout {
|
||||
/// Wrap an incoming request body with [`RequestBodyReadTimeout`] unless the
|
||||
/// feature is disabled (`timeout == 0`), in which case the body is returned
|
||||
/// untouched. `remaining_length` is preserved via [`StreamingBlob::new`].
|
||||
fn guard_put_object_body_read_timeout(
|
||||
pub(crate) fn guard_put_object_body_read_timeout(
|
||||
body: StreamingBlob,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
|
||||
@@ -53,6 +53,7 @@ const EVENT_SESSION_TOKEN_EXTRACTION: &str = "session_token_extraction";
|
||||
|
||||
/// RustFS-specific query capability for a single presigned PutObject request.
|
||||
pub(crate) const RUSTFS_MAX_CONTENT_LENGTH_QUERY: &str = "x-rustfs-max-content-length";
|
||||
pub(crate) const RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY: &str = "x-rustfs-max-total-object-size";
|
||||
|
||||
/// Inserted by the S3 access boundary after the upstream verifier accepts a
|
||||
/// request as SigV4 presigned. Downstream capability parsing must require this
|
||||
@@ -60,6 +61,9 @@ pub(crate) const RUSTFS_MAX_CONTENT_LENGTH_QUERY: &str = "x-rustfs-max-content-l
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct VerifiedPresignedRequest;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct VerifiedSigV4Request;
|
||||
|
||||
/// Performs constant-time string comparison to prevent timing attacks.
|
||||
///
|
||||
/// This function should be used when comparing sensitive values like passwords,
|
||||
@@ -1111,6 +1115,92 @@ pub(crate) fn reject_presigned_put_max_content_length_for_other_operation(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the V2 multipart total-size capability after SigV4 authentication.
|
||||
/// Header-authenticated CreateMultipartUpload requests are accepted because the
|
||||
/// custom query is covered by the SigV4 canonical request; later multipart
|
||||
/// operations read the immutable value from the upload session metadata.
|
||||
pub(crate) fn parse_presigned_multipart_max_total_object_size(
|
||||
header: &HeaderMap,
|
||||
query: Option<&str>,
|
||||
verified_sigv4: bool,
|
||||
) -> S3Result<Option<u64>> {
|
||||
let Some(query) = query else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut value = None;
|
||||
let mut decoded_query = Vec::new();
|
||||
for (name, candidate) in form_urlencoded::parse(query.as_bytes()) {
|
||||
decoded_query.push((name.to_string(), candidate.to_string()));
|
||||
if name == RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY {
|
||||
if value.is_some() {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("{RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY} must appear exactly once"),
|
||||
));
|
||||
}
|
||||
value = Some(candidate.into_owned());
|
||||
} else if name.eq_ignore_ascii_case(RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY) {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("query parameter name must be exactly {RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let auth_type = get_request_auth_type_with_query(header, Some(query));
|
||||
let is_presigned = matches!(auth_type, AuthType::Presigned);
|
||||
let is_header_signed = matches!(auth_type, AuthType::Signed);
|
||||
let complete_presigned_query = [
|
||||
("x-amz-algorithm", "AWS4-HMAC-SHA256"),
|
||||
("x-amz-date", ""),
|
||||
("x-amz-expires", ""),
|
||||
("x-amz-signedheaders", ""),
|
||||
("x-amz-credential", ""),
|
||||
("x-amz-signature", ""),
|
||||
]
|
||||
.into_iter()
|
||||
.all(|(name, expected)| {
|
||||
decoded_query
|
||||
.iter()
|
||||
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
|
||||
.is_some_and(|(_, candidate)| !candidate.is_empty() && (expected.is_empty() || candidate == expected))
|
||||
});
|
||||
|
||||
let authenticated = verified_sigv4 && (is_header_signed || (is_presigned && complete_presigned_query));
|
||||
if !authenticated {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("{RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY} requires a verified SigV4 request"),
|
||||
));
|
||||
}
|
||||
|
||||
value.parse::<u64>().map(Some).map_err(|_| {
|
||||
S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("{RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY} must be a non-negative 64-bit integer"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn reject_presigned_multipart_max_total_object_size_for_other_operation(
|
||||
header: &HeaderMap,
|
||||
query: Option<&str>,
|
||||
verified_sigv4: bool,
|
||||
) -> S3Result<()> {
|
||||
if parse_presigned_multipart_max_total_object_size(header, query, verified_sigv4)?.is_some() {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("{RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY} is only supported for CreateMultipartUpload"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1816,6 +1906,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_max_total_object_size_requires_signed_create_request() {
|
||||
let headers = HeaderMap::new();
|
||||
let signed_prefix = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260827T000000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Credential=test/20260827/us-east-1/s3/aws4_request&X-Amz-Signature=signature";
|
||||
let query = format!("{signed_prefix}&x-rustfs-max-total-object-size=104857600");
|
||||
|
||||
assert_eq!(
|
||||
parse_presigned_multipart_max_total_object_size(&headers, Some(&query), true).unwrap(),
|
||||
Some(104_857_600)
|
||||
);
|
||||
assert_eq!(
|
||||
reject_presigned_multipart_max_total_object_size_for_other_operation(&headers, Some(&query), true)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
&S3ErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_max_total_object_size_rejects_tampering_and_invalid_values() {
|
||||
let headers = HeaderMap::new();
|
||||
let signed_prefix = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260827T000000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Credential=test/20260827/us-east-1/s3/aws4_request&X-Amz-Signature=signature";
|
||||
for query in [
|
||||
"x-rustfs-max-total-object-size=1",
|
||||
"X-RustFS-Max-Total-Object-Size=1",
|
||||
"X-Amz-Algorithm=AWS4-HMAC-SHA256&x-rustfs-max-total-object-size=1&x-rustfs-max-total-object-size=2",
|
||||
"X-Amz-Algorithm=AWS4-HMAC-SHA256&x-rustfs-max-total-object-size=-1",
|
||||
"X-Amz-Algorithm=AWS4-HMAC-SHA256&x-rustfs-max-total-object-size=18446744073709551616",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse_presigned_multipart_max_total_object_size(&headers, Some(query), true)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
&S3ErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
|
||||
let forged = format!("{signed_prefix}&x-rustfs-max-total-object-size=1");
|
||||
assert_eq!(
|
||||
parse_presigned_multipart_max_total_object_size(&headers, Some(&forged), false)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
&S3ErrorCode::InvalidRequest
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_is_expired() {
|
||||
let mut cred = create_test_credentials();
|
||||
|
||||
@@ -372,6 +372,7 @@ impl From<StorageError> for ApiError {
|
||||
StorageError::ObjectExistsAsDirectory(_, _) => S3ErrorCode::InvalidArgument,
|
||||
StorageError::InvalidPart(_, _, _) => S3ErrorCode::InvalidPart,
|
||||
StorageError::EntityTooSmall(_, _, _) => S3ErrorCode::EntityTooSmall,
|
||||
StorageError::EntityTooLarge(_, _) => S3ErrorCode::EntityTooLarge,
|
||||
StorageError::PreconditionFailed => S3ErrorCode::PreconditionFailed,
|
||||
StorageError::NotModified => S3ErrorCode::NotModified,
|
||||
StorageError::InvalidRangeSpec(_) => S3ErrorCode::InvalidRange,
|
||||
|
||||
@@ -16,9 +16,10 @@ use super::ObjectOptions;
|
||||
use super::ecfs::FS;
|
||||
use super::{ECStore, PolicySys, ReplicationStatusType, StorageError, get_lock_acquire_timeout, is_err_bucket_not_found};
|
||||
use crate::auth::{
|
||||
AuthType, RUSTFS_MAX_CONTENT_LENGTH_QUERY, VerifiedPresignedRequest, check_key_valid_with_context,
|
||||
get_condition_values_with_client_info, get_condition_values_with_query_and_client_info, get_request_auth_type_with_query,
|
||||
get_session_token, parse_presigned_put_max_content_length,
|
||||
AuthType, RUSTFS_MAX_CONTENT_LENGTH_QUERY, RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY, VerifiedPresignedRequest,
|
||||
VerifiedSigV4Request, check_key_valid_with_context, get_condition_values_with_client_info,
|
||||
get_condition_values_with_query_and_client_info, get_request_auth_type_with_query, get_session_token,
|
||||
parse_presigned_multipart_max_total_object_size, parse_presigned_put_max_content_length,
|
||||
};
|
||||
use crate::error::ApiError;
|
||||
use crate::license::license_check;
|
||||
@@ -1771,7 +1772,9 @@ impl S3Access for FS {
|
||||
|
||||
// Publish this server's context slot so downstream data-plane handlers
|
||||
// resolve the same store (backlog#1052 S6).
|
||||
let verified_presigned = matches!(get_request_auth_type_with_query(cx.headers(), cx.uri().query()), AuthType::Presigned);
|
||||
let auth_type = get_request_auth_type_with_query(cx.headers(), cx.uri().query());
|
||||
let verified_presigned = matches!(auth_type, AuthType::Presigned);
|
||||
let verified_sigv4 = matches!(auth_type, AuthType::Presigned | AuthType::Signed);
|
||||
{
|
||||
let ext = cx.extensions_mut();
|
||||
ext.insert(self.server_ctx().clone());
|
||||
@@ -1779,6 +1782,9 @@ impl S3Access for FS {
|
||||
if verified_presigned {
|
||||
ext.insert(VerifiedPresignedRequest);
|
||||
}
|
||||
if verified_sigv4 {
|
||||
ext.insert(VerifiedSigV4Request);
|
||||
}
|
||||
}
|
||||
|
||||
// The size capability is intentionally scoped to the single-object
|
||||
@@ -1793,6 +1799,14 @@ impl S3Access for FS {
|
||||
format!("{RUSTFS_MAX_CONTENT_LENGTH_QUERY} is only supported for presigned PutObject"),
|
||||
));
|
||||
}
|
||||
if parse_presigned_multipart_max_total_object_size(cx.headers(), cx.uri().query(), verified_sigv4)?.is_some()
|
||||
&& cx.s3_op().name() != "CreateMultipartUpload"
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidRequest,
|
||||
format!("{RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY} is only supported for CreateMultipartUpload"),
|
||||
));
|
||||
}
|
||||
license_check().map_err(|er| match er.kind() {
|
||||
std::io::ErrorKind::PermissionDenied => s3_error!(AccessDenied, "{er}"),
|
||||
_ => {
|
||||
|
||||
Reference in New Issue
Block a user