mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 16:59:52 +00:00
fix(storage): align archive content-encoding with S3 semantics (#2496)
This commit is contained in:
@@ -15,6 +15,8 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client, rustfs_binary_path};
|
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client, rustfs_binary_path};
|
||||||
|
use aws_sdk_s3::Client as S3Client;
|
||||||
|
use aws_sdk_s3::error::ProvideErrorMetadata;
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||||
use http::header::{CONTENT_TYPE, HOST};
|
use http::header::{CONTENT_TYPE, HOST};
|
||||||
@@ -216,12 +218,129 @@ mod tests {
|
|||||||
Ok(builder.send().await?)
|
Ok(builder.send().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn assert_archive_object_content_encoding(
|
||||||
|
client: &S3Client,
|
||||||
|
bucket: &str,
|
||||||
|
key: &str,
|
||||||
|
expected_content_encoding: Option<&str>,
|
||||||
|
expected_body: &[u8],
|
||||||
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let head_resp = client.head_object().bucket(bucket).key(key).send().await?;
|
||||||
|
assert_eq!(head_resp.content_encoding(), expected_content_encoding);
|
||||||
|
|
||||||
|
let get_resp = client.get_object().bucket(bucket).key(key).send().await?;
|
||||||
|
assert_eq!(get_resp.content_encoding(), expected_content_encoding);
|
||||||
|
let body = get_resp.body.collect().await?.into_bytes();
|
||||||
|
assert_eq!(body.as_ref(), expected_body);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn complete_archive_multipart_upload_with_content_encoding(
|
||||||
|
client: &S3Client,
|
||||||
|
bucket: &str,
|
||||||
|
key: &str,
|
||||||
|
content_encoding: Option<&str>,
|
||||||
|
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
|
||||||
|
let payload = random_bytes(MULTIPART_PART_SIZE + 256 * 1024);
|
||||||
|
let zip_bytes = build_zip_bytes(&[("payload.bin", payload.as_slice())])?;
|
||||||
|
assert!(zip_bytes.len() > MULTIPART_PART_SIZE, "zip payload must exceed multipart threshold");
|
||||||
|
|
||||||
|
let mut create_builder = client
|
||||||
|
.create_multipart_upload()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.content_type("application/zip");
|
||||||
|
if let Some(content_encoding) = content_encoding {
|
||||||
|
create_builder = create_builder.content_encoding(content_encoding);
|
||||||
|
}
|
||||||
|
let create_output = create_builder.send().await?;
|
||||||
|
let upload_id = create_output.upload_id().expect("multipart upload id");
|
||||||
|
|
||||||
|
let first_part = zip_bytes[..MULTIPART_PART_SIZE].to_vec();
|
||||||
|
let second_part = zip_bytes[MULTIPART_PART_SIZE..].to_vec();
|
||||||
|
|
||||||
|
let upload_part_1 = client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.upload_id(upload_id)
|
||||||
|
.part_number(1)
|
||||||
|
.body(ByteStream::from(first_part))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let upload_part_2 = client
|
||||||
|
.upload_part()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.upload_id(upload_id)
|
||||||
|
.part_number(2)
|
||||||
|
.body(ByteStream::from(second_part))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let completed_upload = CompletedMultipartUpload::builder()
|
||||||
|
.parts(
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(1)
|
||||||
|
.e_tag(upload_part_1.e_tag().unwrap_or_default())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.parts(
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(2)
|
||||||
|
.e_tag(upload_part_2.e_tag().unwrap_or_default())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
client
|
||||||
|
.complete_multipart_upload()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.upload_id(upload_id)
|
||||||
|
.multipart_upload(completed_upload)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(zip_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_archive_put_rejects_content_encoding_by_default() -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn test_archive_put_allows_content_encoding_by_default() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
init_logging();
|
init_logging();
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
env.start_rustfs_server_without_cleanup(vec![]).await?;
|
start_rustfs_server_with_env(&mut env, &[]).await?;
|
||||||
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle.zip");
|
||||||
|
let response =
|
||||||
|
signed_put_request_with_headers(&object_url, &env.access_key, &env.secret_key, zip_bytes, "application/zip", "gzip")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let head_resp = client
|
||||||
|
.head_object()
|
||||||
|
.bucket(ARCHIVE_TEST_BUCKET)
|
||||||
|
.key("bundle.zip")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(head_resp.content_encoding(), Some("gzip"));
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_put_rejects_content_encoding_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING", "true")]).await?;
|
||||||
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle.zip");
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle.zip");
|
||||||
@@ -232,7 +351,145 @@ mod tests {
|
|||||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
let body = response.text().await?;
|
let body = response.text().await?;
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("InvalidArgument") || body.contains("Content-Encoding"),
|
body.contains("InvalidArgument") || body.contains("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING"),
|
||||||
|
"unexpected error body: {body}"
|
||||||
|
);
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_put_with_aws_chunked_does_not_persist_content_encoding_by_default()
|
||||||
|
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[]).await?;
|
||||||
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle-aws-chunked.zip");
|
||||||
|
let response = signed_put_request_with_headers(
|
||||||
|
&object_url,
|
||||||
|
&env.access_key,
|
||||||
|
&env.secret_key,
|
||||||
|
zip_bytes.clone(),
|
||||||
|
"application/zip",
|
||||||
|
"aws-chunked",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
assert_archive_object_content_encoding(
|
||||||
|
&client,
|
||||||
|
ARCHIVE_TEST_BUCKET,
|
||||||
|
"bundle-aws-chunked.zip",
|
||||||
|
None,
|
||||||
|
zip_bytes.as_slice(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_put_with_aws_chunked_and_effective_encoding_roundtrips_by_default()
|
||||||
|
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[]).await?;
|
||||||
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle-aws-chunked-gzip.zip");
|
||||||
|
let response = signed_put_request_with_headers(
|
||||||
|
&object_url,
|
||||||
|
&env.access_key,
|
||||||
|
&env.secret_key,
|
||||||
|
zip_bytes.clone(),
|
||||||
|
"application/zip",
|
||||||
|
"aws-chunked,gzip",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
assert_archive_object_content_encoding(
|
||||||
|
&client,
|
||||||
|
ARCHIVE_TEST_BUCKET,
|
||||||
|
"bundle-aws-chunked-gzip.zip",
|
||||||
|
Some("gzip"),
|
||||||
|
zip_bytes.as_slice(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_put_with_aws_chunked_allowed_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING", "true")]).await?;
|
||||||
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle-strict-aws-chunked.zip");
|
||||||
|
let response = signed_put_request_with_headers(
|
||||||
|
&object_url,
|
||||||
|
&env.access_key,
|
||||||
|
&env.secret_key,
|
||||||
|
zip_bytes.clone(),
|
||||||
|
"application/zip",
|
||||||
|
"aws-chunked",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
assert_archive_object_content_encoding(
|
||||||
|
&client,
|
||||||
|
ARCHIVE_TEST_BUCKET,
|
||||||
|
"bundle-strict-aws-chunked.zip",
|
||||||
|
None,
|
||||||
|
zip_bytes.as_slice(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_put_with_aws_chunked_and_effective_encoding_rejects_when_strict_mode_enabled()
|
||||||
|
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING", "true")]).await?;
|
||||||
|
env.create_test_bucket(ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let zip_bytes = build_zip_bytes(&[("alpha.txt", b"archive-body")])?;
|
||||||
|
let object_url = format!("{}/{}/{}", env.url, ARCHIVE_TEST_BUCKET, "bundle-strict-aws-chunked-gzip.zip");
|
||||||
|
let response = signed_put_request_with_headers(
|
||||||
|
&object_url,
|
||||||
|
&env.access_key,
|
||||||
|
&env.secret_key,
|
||||||
|
zip_bytes,
|
||||||
|
"application/zip",
|
||||||
|
"aws-chunked,gzip",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = response.text().await?;
|
||||||
|
assert!(
|
||||||
|
body.contains("InvalidArgument") || body.contains("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING"),
|
||||||
"unexpected error body: {body}"
|
"unexpected error body: {body}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -398,6 +655,99 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_multipart_with_aws_chunked_and_effective_encoding_roundtrips_by_default()
|
||||||
|
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[]).await?;
|
||||||
|
env.create_test_bucket(MULTIPART_ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let zip_bytes = complete_archive_multipart_upload_with_content_encoding(
|
||||||
|
&client,
|
||||||
|
MULTIPART_ARCHIVE_TEST_BUCKET,
|
||||||
|
"multipart-aws-chunked-gzip.zip",
|
||||||
|
Some("aws-chunked,gzip"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_archive_object_content_encoding(
|
||||||
|
&client,
|
||||||
|
MULTIPART_ARCHIVE_TEST_BUCKET,
|
||||||
|
"multipart-aws-chunked-gzip.zip",
|
||||||
|
Some("gzip"),
|
||||||
|
zip_bytes.as_slice(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_multipart_with_aws_chunked_allowed_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>>
|
||||||
|
{
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING", "true")]).await?;
|
||||||
|
env.create_test_bucket(MULTIPART_ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let zip_bytes = complete_archive_multipart_upload_with_content_encoding(
|
||||||
|
&client,
|
||||||
|
MULTIPART_ARCHIVE_TEST_BUCKET,
|
||||||
|
"multipart-strict-aws-chunked.zip",
|
||||||
|
Some("aws-chunked"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_archive_object_content_encoding(
|
||||||
|
&client,
|
||||||
|
MULTIPART_ARCHIVE_TEST_BUCKET,
|
||||||
|
"multipart-strict-aws-chunked.zip",
|
||||||
|
None,
|
||||||
|
zip_bytes.as_slice(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_archive_multipart_with_aws_chunked_and_effective_encoding_rejects_when_strict_mode_enabled()
|
||||||
|
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
start_rustfs_server_with_env(&mut env, &[("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING", "true")]).await?;
|
||||||
|
env.create_test_bucket(MULTIPART_ARCHIVE_TEST_BUCKET).await?;
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let create_result = client
|
||||||
|
.create_multipart_upload()
|
||||||
|
.bucket(MULTIPART_ARCHIVE_TEST_BUCKET)
|
||||||
|
.key("multipart-strict-aws-chunked-gzip.zip")
|
||||||
|
.content_type("application/zip")
|
||||||
|
.content_encoding("aws-chunked,gzip")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
let err = create_result.expect_err("strict mode should reject effective archive content encoding");
|
||||||
|
assert_eq!(err.code(), Some("InvalidArgument"));
|
||||||
|
assert!(
|
||||||
|
err.message().is_some_and(|message| {
|
||||||
|
message.contains("Content-Encoding") && message.contains("RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING")
|
||||||
|
}),
|
||||||
|
"unexpected error metadata: code={:?}, message={:?}",
|
||||||
|
err.code(),
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_presigned_get_and_reverse_proxy_preserve_multipart_bytes_with_fast_path()
|
async fn test_presigned_get_and_reverse_proxy_preserve_multipart_bytes_with_fast_path()
|
||||||
|
|||||||
@@ -138,4 +138,63 @@ mod tests {
|
|||||||
|
|
||||||
env.stop_server();
|
env.stop_server();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue #2475 / Route A: when aws-chunked is combined with an effective object encoding,
|
||||||
|
/// only the effective encoding should roundtrip through GET/HEAD.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_content_encoding_aws_chunked_with_effective_encoding_roundtrip() {
|
||||||
|
init_logging();
|
||||||
|
info!("aws-chunked,gzip should persist only gzip");
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
||||||
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let bucket = "content-encoding-aws-chunked-gzip-test";
|
||||||
|
let key = "streamed/object.txt";
|
||||||
|
let content = b"streaming upload body with effective gzip encoding";
|
||||||
|
|
||||||
|
client
|
||||||
|
.create_bucket()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bucket");
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.content_type("text/plain")
|
||||||
|
.content_encoding("aws-chunked,gzip")
|
||||||
|
.body(ByteStream::from_static(content))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("PUT failed");
|
||||||
|
|
||||||
|
let get_resp = client.get_object().bucket(bucket).key(key).send().await.expect("GET failed");
|
||||||
|
assert_eq!(
|
||||||
|
get_resp.content_encoding(),
|
||||||
|
Some("gzip"),
|
||||||
|
"GET must return only the effective content encoding after aws-chunked is stripped"
|
||||||
|
);
|
||||||
|
let body = get_resp.body.collect().await.unwrap().into_bytes();
|
||||||
|
assert_eq!(body.as_ref(), content, "Body content mismatch");
|
||||||
|
|
||||||
|
let head_resp = client
|
||||||
|
.head_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("HEAD failed");
|
||||||
|
assert_eq!(
|
||||||
|
head_resp.content_encoding(),
|
||||||
|
Some("gzip"),
|
||||||
|
"HEAD must return only the effective content encoding after aws-chunked is stripped"
|
||||||
|
);
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ pub(crate) fn normalize_content_encoding_for_storage(value: &str) -> Option<Stri
|
|||||||
if normalized.is_empty() { None } else { Some(normalized) }
|
if normalized.is_empty() { None } else { Some(normalized) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENV_ALLOW_ARCHIVE_CONTENT_ENCODING: &str = "RUSTFS_ALLOW_ARCHIVE_CONTENT_ENCODING";
|
const ENV_REJECT_ARCHIVE_CONTENT_ENCODING: &str = "RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING";
|
||||||
|
|
||||||
const ARCHIVE_CONTENT_ENCODING_BLOCKED_SUFFIXES: &[&str] = &[
|
const ARCHIVE_CONTENT_ENCODING_BLOCKED_SUFFIXES: &[&str] = &[
|
||||||
".zip",
|
".zip",
|
||||||
@@ -394,11 +394,11 @@ pub(crate) fn validate_archive_content_encoding(
|
|||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
content_encoding: Option<&str>,
|
content_encoding: Option<&str>,
|
||||||
) -> S3Result<()> {
|
) -> S3Result<()> {
|
||||||
if rustfs_utils::get_env_bool(ENV_ALLOW_ARCHIVE_CONTENT_ENCODING, false) {
|
if !archive_content_encoding_strict_mode() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(content_encoding) = content_encoding.map(str::trim).filter(|value| !value.is_empty()) else {
|
let Some(content_encoding) = content_encoding.and_then(normalize_content_encoding_for_storage) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -411,11 +411,15 @@ pub(crate) fn validate_archive_content_encoding(
|
|||||||
Err(S3Error::with_message(
|
Err(S3Error::with_message(
|
||||||
S3ErrorCode::InvalidArgument,
|
S3ErrorCode::InvalidArgument,
|
||||||
format!(
|
format!(
|
||||||
"Content-Encoding '{content_encoding}' is not allowed for archive objects by default; set {ENV_ALLOW_ARCHIVE_CONTENT_ENCODING}=true to allow legacy behavior"
|
"Content-Encoding '{content_encoding}' is not allowed for archive objects when {ENV_REJECT_ARCHIVE_CONTENT_ENCODING}=true; unset {ENV_REJECT_ARCHIVE_CONTENT_ENCODING} or set it to false to restore compatibility-first behavior"
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn archive_content_encoding_strict_mode() -> bool {
|
||||||
|
rustfs_utils::get_env_bool(ENV_REJECT_ARCHIVE_CONTENT_ENCODING, false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extracts metadata from headers and returns it as a HashMap with object name for MIME type detection.
|
/// Extracts metadata from headers and returns it as a HashMap with object name for MIME type detection.
|
||||||
pub fn extract_metadata_from_mime_with_object_name(
|
pub fn extract_metadata_from_mime_with_object_name(
|
||||||
headers: &HeaderMap<HeaderValue>,
|
headers: &HeaderMap<HeaderValue>,
|
||||||
@@ -1428,15 +1432,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_archive_content_encoding_rejects_archive_suffix_by_default() {
|
fn test_validate_archive_content_encoding_allows_archive_suffix_by_default() {
|
||||||
let err = validate_archive_content_encoding("bundle.tar.gz", Some("application/gzip"), Some("gzip")).unwrap_err();
|
validate_archive_content_encoding("bundle.tar.gz", Some("application/gzip"), Some("gzip")).expect("default allow");
|
||||||
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_archive_content_encoding_rejects_archive_mime_by_default() {
|
fn test_validate_archive_content_encoding_allows_archive_mime_by_default() {
|
||||||
let err = validate_archive_content_encoding("bundle", Some("application/zip"), Some("gzip")).unwrap_err();
|
validate_archive_content_encoding("bundle", Some("application/zip"), Some("gzip")).expect("default allow");
|
||||||
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1445,9 +1447,51 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_archive_content_encoding_allows_legacy_opt_in() {
|
fn test_validate_archive_content_encoding_allows_archive_sigv4_streaming_encoding_by_default() {
|
||||||
temp_env::with_var(ENV_ALLOW_ARCHIVE_CONTENT_ENCODING, Some("true"), || {
|
validate_archive_content_encoding("bundle.tar.gz", Some("application/gzip"), Some("aws-chunked"))
|
||||||
validate_archive_content_encoding("bundle.zip", Some("application/zip"), Some("gzip")).expect("legacy opt-in");
|
.expect("aws-chunked is request-side only");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_archive_content_encoding_allows_archive_sigv4_streaming_encoding_case_insensitive() {
|
||||||
|
validate_archive_content_encoding("bundle.zip", Some("application/zip"), Some("AWS-CHUNKED"))
|
||||||
|
.expect("aws-chunked stripping should be case-insensitive");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_archive_content_encoding_allows_effective_archive_encoding_after_aws_chunked_stripped_by_default() {
|
||||||
|
validate_archive_content_encoding("bundle.zip", Some("application/zip"), Some("aws-chunked, gzip"))
|
||||||
|
.expect("default allow after stripping aws-chunked");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_archive_content_encoding_rejects_archive_suffix_in_strict_mode() {
|
||||||
|
temp_env::with_var(ENV_REJECT_ARCHIVE_CONTENT_ENCODING, Some("true"), || {
|
||||||
|
let err = validate_archive_content_encoding("bundle.tar.gz", Some("application/gzip"), Some("gzip")).unwrap_err();
|
||||||
|
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_archive_content_encoding_rejects_archive_mime_in_strict_mode() {
|
||||||
|
temp_env::with_var(ENV_REJECT_ARCHIVE_CONTENT_ENCODING, Some("true"), || {
|
||||||
|
let err = validate_archive_content_encoding("bundle", Some("application/zip"), Some("gzip")).unwrap_err();
|
||||||
|
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_archive_content_encoding_rejects_effective_archive_encoding_after_aws_chunked_stripped_in_strict_mode() {
|
||||||
|
temp_env::with_var(ENV_REJECT_ARCHIVE_CONTENT_ENCODING, Some("true"), || {
|
||||||
|
let err =
|
||||||
|
validate_archive_content_encoding("bundle.zip", Some("application/zip"), Some("aws-chunked, gzip")).unwrap_err();
|
||||||
|
assert_eq!(err.code(), &S3ErrorCode::InvalidArgument);
|
||||||
|
assert_eq!(
|
||||||
|
err.message(),
|
||||||
|
Some(
|
||||||
|
"Content-Encoding 'gzip' is not allowed for archive objects when RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING=true; unset RUSTFS_REJECT_ARCHIVE_CONTENT_ENCODING or set it to false to restore compatibility-first behavior"
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user