diff --git a/Cargo.lock b/Cargo.lock index a630b93e4..27fba8cf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8778,7 +8778,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "s3s" version = "0.14.0-dev" -source = "git+https://github.com/rustfs/s3s?rev=0dd8fcaaa72eda68fafd49c38daea43bb8697558#0dd8fcaaa72eda68fafd49c38daea43bb8697558" +source = "git+https://github.com/rustfs/s3s?rev=a3b16608df35aaeed8fff08b4988d03f4ca9445b#a3b16608df35aaeed8fff08b4988d03f4ca9445b" dependencies = [ "arc-swap", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index 46907da2a..c0231f53b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -248,7 +248,7 @@ rumqttc = { package = "rumqttc-next", version = "0.30.0", features = ["websocket rustix = { version = "1.1.4", features = ["fs"] } rust-embed = { version = "8.11.0" } rustc-hash = { version = "2.1.2" } -s3s = { git = "https://github.com/rustfs/s3s", rev = "0dd8fcaaa72eda68fafd49c38daea43bb8697558", features = ["minio"] } +s3s = { git = "https://github.com/rustfs/s3s", rev = "a3b16608df35aaeed8fff08b4988d03f4ca9445b", features = ["minio"] } serial_test = "3.4.0" shadow-rs = { version = "1.7.1", default-features = false } siphasher = "1.0.2" diff --git a/crates/e2e_test/src/special_chars_test.rs b/crates/e2e_test/src/special_chars_test.rs index 60a80fdd2..e6dd33d83 100644 --- a/crates/e2e_test/src/special_chars_test.rs +++ b/crates/e2e_test/src/special_chars_test.rs @@ -26,10 +26,16 @@ #[cfg(test)] mod tests { - use crate::common::{RustFSTestEnvironment, init_logging}; + use crate::common::{RustFSTestEnvironment, init_logging, local_http_client}; use aws_sdk_s3::Client; use aws_sdk_s3::primitives::ByteStream; + use http::StatusCode; + use http::header::HOST; + use rustfs_signer::constants::UNSIGNED_PAYLOAD; + use rustfs_signer::sign_v4; + use s3s::Body; use serial_test::serial; + use std::error::Error; use tracing::{debug, info}; /// Helper function to create an S3 client for testing @@ -56,6 +62,30 @@ mod tests { } } + async fn signed_get( + url: &str, + access_key: &str, + secret_key: &str, + ) -> Result> { + let uri = url.parse::()?; + let authority = uri.authority().ok_or("request URL missing authority")?.to_string(); + let request = http::Request::builder() + .method(http::Method::GET) + .uri(uri) + .header(HOST, authority) + .header("x-amz-content-sha256", UNSIGNED_PAYLOAD) + .body(Body::empty())?; + + let signed = sign_v4(request, 0, access_key, secret_key, "", "us-east-1"); + let client = local_http_client(); + let mut request_builder = client.get(url); + for (name, value) in signed.headers() { + request_builder = request_builder.header(name, value); + } + + Ok(request_builder.send().await?) + } + /// Test PUT and GET with space character in path /// /// This reproduces Part A of the issue: @@ -274,6 +304,35 @@ mod tests { info!("Test completed successfully"); } + #[tokio::test] + #[serial] + async fn test_signed_get_missing_object_with_trailing_equals_returns_no_such_key() -> Result<(), Box> + { + init_logging(); + + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + + let client = create_s3_client(&env); + let bucket = "test-missing-equals-key"; + create_bucket(&client, bucket).await?; + + let url = format!("{}/{}/path/sitemap.xmlage=", env.url, bucket); + let response = signed_get(&url, &env.access_key, &env.secret_key).await?; + + assert_eq!( + response.status(), + StatusCode::NOT_FOUND, + "missing object key ending with '=' should pass signature validation before object lookup" + ); + + let body = response.text().await?; + assert!(body.contains("NoSuchKey"), "expected NoSuchKey XML response, got: {body}"); + + env.stop_server(); + Ok(()) + } + /// Test DELETE operation with special characters #[tokio::test] #[serial]