From 78cb142c91b850c2b9c6f0e5c1ba54f2b81b80f7 Mon Sep 17 00:00:00 2001 From: houseme Date: Sat, 29 Aug 2026 19:31:33 +0800 Subject: [PATCH] fix(s3): accept empty put without content length (#6849) Co-authored-by: heihutu --- Cargo.lock | 143 +++++++++------- Cargo.toml | 10 +- crates/e2e_test/src/lib.rs | 4 + .../src/put_object_no_content_length_test.rs | 152 ++++++++++++++++++ rustfs/src/app/object/extract.rs | 2 +- rustfs/src/server/http.rs | 1 + 6 files changed, 250 insertions(+), 62 deletions(-) create mode 100644 crates/e2e_test/src/put_object_no_content_length_test.rs diff --git a/Cargo.lock b/Cargo.lock index 6c36f4ee7..1f6087f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher 0.5.2", "cpubits", @@ -73,7 +73,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" dependencies = [ "aead", - "aes 0.9.2", + "aes 0.9.3", "cipher 0.5.2", "ctr", "ctutils", @@ -491,7 +491,7 @@ dependencies = [ "arrow-select", "chrono", "half", - "indexmap 2.14.0", + "indexmap 2.14.1", "itoa", "lexical-core", "memchr", @@ -626,9 +626,9 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463" +checksum = "6f2e989b33246fe9240d39accf4dd9a01e0b6c1f3ce9dd095e0a47fa02505523" dependencies = [ "futures-core", "libc", @@ -638,6 +638,7 @@ dependencies = [ "tokio", "tokio-stream", "xattr", + "zerocopy", ] [[package]] @@ -1220,7 +1221,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap 2.14.0", + "indexmap 2.14.1", "pin-project-lite", "rustls", "rustls-native-certs", @@ -2831,7 +2832,7 @@ dependencies = [ "datafusion-session", "datafusion-sql", "futures", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "log", "object_store", @@ -2906,7 +2907,7 @@ dependencies = [ "foldhash 0.2.0", "half", "hashbrown 0.17.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "libc", "log", @@ -3111,7 +3112,7 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "recursive", "serde_json", @@ -3126,7 +3127,7 @@ checksum = "2604994999d5aeca1d1df645ffc98bc787447aaff05dde27aad0342b48fc1fe0" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", ] @@ -3282,7 +3283,7 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-physical-expr", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "log", "recursive", @@ -3304,7 +3305,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.17.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "parking_lot", "petgraph 0.8.3", @@ -3338,7 +3339,7 @@ dependencies = [ "datafusion-common", "datafusion-expr-common", "hashbrown 0.17.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "parking_lot", "pin-project", @@ -3389,7 +3390,7 @@ dependencies = [ "futures", "half", "hashbrown 0.17.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "itertools 0.15.0", "log", "num-traits", @@ -3442,7 +3443,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-functions-nested", - "indexmap 2.14.0", + "indexmap 2.14.1", "log", "recursive", "regex", @@ -4207,7 +4208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4342,12 +4343,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "zlib-rs", ] @@ -4617,7 +4618,7 @@ checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ "fnv", "hashbrown 0.16.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "stable_deref_trait", ] @@ -4895,7 +4896,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.5.0", - "indexmap 2.14.0", + "indexmap 2.14.1", "slab", "tokio", "tokio-util", @@ -5338,9 +5339,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -5560,9 +5561,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -5665,7 +5666,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5855,7 +5856,7 @@ dependencies = [ "base64 0.22.1", "getrandom 0.2.17", "js-sys", - "pem", + "pem 3.0.6", "serde", "serde_json", "signature 2.2.0", @@ -5873,7 +5874,7 @@ dependencies = [ "bytes", "crc", "crc32c", - "indexmap 2.14.0", + "indexmap 2.14.1", "paste", "uuid", ] @@ -5904,7 +5905,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", ] [[package]] @@ -6384,7 +6385,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.16.1", - "indexmap 2.14.0", + "indexmap 2.14.1", "metrics", "ordered-float 5.5.0", "quanta", @@ -6528,7 +6529,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -6538,6 +6538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -7630,6 +7631,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -7682,7 +7693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.14.0", + "indexmap 2.14.1", ] [[package]] @@ -7693,7 +7704,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap 2.14.1", "serde", ] @@ -7727,6 +7738,16 @@ dependencies = [ "serde", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_shared 0.14.0", + "serde", +] + [[package]] name = "phf_shared" version = "0.12.1" @@ -7745,6 +7766,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -7845,7 +7875,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" dependencies = [ - "aes 0.9.2", + "aes 0.9.3", "aes-gcm", "cbc 0.2.1", "der 0.8.1", @@ -8343,7 +8373,7 @@ dependencies = [ "log", "murmur3", "nom 7.1.3", - "pem", + "pem 3.0.6", "prost 0.13.5", "prost-build 0.13.5", "prost-derive 0.13.5", @@ -8578,7 +8608,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8774,12 +8804,12 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.9" +version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" dependencies = [ "aws-lc-rs", - "pem", + "pem 4.0.0", "rustls-pki-types", "time", "x509-parser", @@ -9185,7 +9215,7 @@ version = "0.63.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bab1b87d915817d5d9cc352637cd40d5f0b298a48c6309af9156a4addc3031" dependencies = [ - "aes 0.9.2", + "aes 0.9.3", "aws-lc-rs", "bitflags 2.13.1", "block-padding 0.4.2", @@ -9916,7 +9946,7 @@ dependencies = [ "bytes", "fnv", "hmac 0.13.0", - "indexmap 2.14.0", + "indexmap 2.14.1", "kafka-protocol", "metrics", "pbkdf2 0.13.0", @@ -10904,7 +10934,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -10977,7 +11007,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -11034,7 +11064,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "s3s" version = "0.15.0" -source = "git+https://github.com/rustfs/s3s.git?rev=6e7b41252c7ba218a90886f58d297716ddf68acf#6e7b41252c7ba218a90886f58d297716ddf68acf" +source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" dependencies = [ "arc-swap", "arrayvec", @@ -11061,6 +11091,7 @@ dependencies = [ "mime", "nom 8.0.0", "numeric_cast", + "phf 0.14.0", "pin-project-lite", "quick-xml", "regex", @@ -11090,7 +11121,7 @@ dependencies = [ [[package]] name = "s3s-sigv2" version = "0.16.0-alpha.1" -source = "git+https://github.com/rustfs/s3s.git?rev=6e7b41252c7ba218a90886f58d297716ddf68acf#6e7b41252c7ba218a90886f58d297716ddf68acf" +source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" dependencies = [ "jiff", "thiserror 2.0.20", @@ -11099,7 +11130,7 @@ dependencies = [ [[package]] name = "s3s-sigv4" version = "0.16.0-alpha.1" -source = "git+https://github.com/rustfs/s3s.git?rev=6e7b41252c7ba218a90886f58d297716ddf68acf#6e7b41252c7ba218a90886f58d297716ddf68acf" +source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" dependencies = [ "arrayvec", "base64-simd", @@ -11346,7 +11377,7 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "itoa", "memchr", "serde", @@ -11417,7 +11448,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.0", + "indexmap 2.14.1", "jiff", "schemars 0.9.0", "schemars 1.2.2", @@ -11841,7 +11872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" dependencies = [ "aead", - "aes 0.9.2", + "aes 0.9.3", "aes-gcm", "chacha20", "cipher 0.5.2", @@ -12196,7 +12227,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -12564,7 +12595,7 @@ version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ - "indexmap 2.14.0", + "indexmap 2.14.1", "toml_datetime", "toml_parser", "winnow", @@ -12658,7 +12689,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", + "indexmap 2.14.1", "pin-project-lite", "slab", "sync_wrapper", @@ -13315,7 +13346,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -13786,7 +13817,7 @@ version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes 0.9.2", + "aes 0.9.3", "bzip2", "constant_time_eq", "crc32fast", @@ -13794,7 +13825,7 @@ dependencies = [ "flate2", "getrandom 0.4.3", "hmac 0.13.0", - "indexmap 2.14.0", + "indexmap 2.14.1", "lzma-rust2", "memchr", "pbkdf2 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index 421f2753e..52032c0ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ futures-util = "0.3.34" pollster = "1.0.1" pulsar = { default-features = false, version = "6.9.0" } lapin = { default-features = false, version = "4.10.0" } -hyper = { version = "1.11.0" } +hyper = { version = "1.11.1" } hyper-rustls = { default-features = false, version = "0.27.9" } hyper-util = { version = "0.1.20" } http = "1.5.0" @@ -232,7 +232,7 @@ tokio-postgres-rustls = "0.14.0" # Utilities and Tools anyhow = "1.0.104" arc-swap = "1.9.2" -astral-tokio-tar = "0.6.4" +astral-tokio-tar = "0.7.0" atoi = "3.1.0" atomic_enum = "0.3.0" aws-config = { version = "1.11.0" } @@ -257,7 +257,7 @@ datafusion = { default-features = false, version = "55.0.0" } derive_builder = "0.20.2" enumset = "1.1.14" faster-hex = "0.10.0" -flate2 = "1.1.9" +flate2 = "1.1.10" glob = "0.3.4" google-cloud-storage = "1.18.0" google-cloud-auth = "1.16.0" @@ -304,7 +304,7 @@ rustify = { version = "0.7", default-features = false } rustix = { version = "1.1.4" } rust-embed = { version = "8.12.0" } rustc-hash = { version = "2.1.3" } -s3s = { git = "https://github.com/rustfs/s3s.git", rev = "6e7b41252c7ba218a90886f58d297716ddf68acf", version = "0.15.0", features = ["minio"] } +s3s = { git = "https://github.com/rustfs/s3s.git", rev = "9c4690d8e73fc8d184031a19b2c4539ebc77d180", version = "0.15.0", features = ["minio"] } serial_test = "4.0.1" shadow-rs = { default-features = false, version = "2.0.0" } siphasher = "1.0.3" @@ -355,7 +355,7 @@ pyroscope = { version = "2.1.1" } libunftp = { version = "0.23.0" } unftp-core = "0.1.0" suppaftp = { version = "10.0.2" } -rcgen = { version = "0.14.9", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] } +rcgen = { version = "0.14.10", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] } russh = { version = "0.63.1" } russh-sftp = "2.4.0" diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index ede4268ad..85c5f9e23 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -164,6 +164,10 @@ mod delete_objects_versioning_test; #[cfg(test)] mod delete_object_no_content_length_test; +// Regression test for signed empty PutObject requests without Content-Length. +#[cfg(test)] +mod put_object_no_content_length_test; + // Delete-marker visibility baseline for data-movement migration proof. #[cfg(test)] mod delete_marker_migration_semantics_test; diff --git a/crates/e2e_test/src/put_object_no_content_length_test.rs b/crates/e2e_test/src/put_object_no_content_length_test.rs new file mode 100644 index 000000000..9f4a35ba1 --- /dev/null +++ b/crates/e2e_test/src/put_object_no_content_length_test.rs @@ -0,0 +1,152 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Regression coverage for rustfs#6830: a signed empty `PutObject` request +//! without `Content-Length` and without `Transfer-Encoding` is still a +//! zero-length object upload. + +#[cfg(test)] +mod tests { + use crate::common::{RustFSTestEnvironment, init_logging}; + use http::header::{CONTENT_LENGTH, HOST, TRANSFER_ENCODING}; + use rustfs_signer::sign_v4; + use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH; + use s3s::Body; + use std::error::Error; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpStream; + use tokio::time::{Duration, timeout}; + use tracing::info; + + const RAW_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10); + + fn parse_status(raw_response: &str) -> Option { + raw_response.lines().next()?.split_whitespace().nth(1)?.parse().ok() + } + + async fn send_raw_signed_put( + url: &str, + access_key: &str, + secret_key: &str, + transfer_encoding: Option<&str>, + raw_body: &[u8], + ) -> Result> { + let uri = url.parse::()?; + let authority = uri.authority().ok_or("request URL missing authority")?.to_string(); + let path_and_query = uri.path_and_query().ok_or("request URL missing path")?.as_str().to_string(); + + let mut request = http::Request::builder() + .method(http::Method::PUT) + .uri(uri) + .header(HOST, authority.clone()) + .header("x-amz-content-sha256", EMPTY_STRING_SHA256_HASH); + if let Some(value) = transfer_encoding { + request = request.header(TRANSFER_ENCODING, value); + } + + let signed = sign_v4(request.body(Body::empty())?, 0, access_key, secret_key, "", "us-east-1"); + + let mut raw_request = format!("PUT {path_and_query} HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n"); + for (name, value) in signed.headers() { + if name == HOST || name == CONTENT_LENGTH { + continue; + } + raw_request.push_str(name.as_str()); + raw_request.push_str(": "); + raw_request.push_str(value.to_str()?); + raw_request.push_str("\r\n"); + } + raw_request.push_str("\r\n"); + + assert!( + !raw_request.to_ascii_lowercase().contains("\r\ncontent-length:"), + "raw regression request must omit Content-Length; request was:\n{raw_request}" + ); + + let mut stream = TcpStream::connect(&authority).await?; + stream.write_all(raw_request.as_bytes()).await?; + stream.write_all(raw_body).await?; + stream.flush().await?; + + let mut response = Vec::new(); + timeout(RAW_RESPONSE_TIMEOUT, stream.read_to_end(&mut response)) + .await + .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out reading raw PUT response"))??; + Ok(String::from_utf8_lossy(&response).into_owned()) + } + + #[tokio::test] + async fn test_put_object_without_content_length_boundaries() -> Result<(), Box> { + init_logging(); + info!("TEST: PutObject without Content-Length boundaries"); + + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + + let client = env.create_s3_client(); + let empty_bucket = "put-no-content-length"; + let empty_key = "empty.bin"; + let chunked_bucket = "put-chunked-no-length"; + let chunked_key = "chunked.bin"; + + client.create_bucket().bucket(empty_bucket).send().await?; + client.create_bucket().bucket(chunked_bucket).send().await?; + + let url = format!("{}/{}/{}", env.url, empty_bucket, empty_key); + let raw_response = send_raw_signed_put(&url, &env.access_key, &env.secret_key, None, b"").await?; + info!("raw empty PUT response:\n{}", raw_response); + + assert_eq!( + parse_status(&raw_response), + Some(200), + "empty PutObject without Content-Length should succeed, got:\n{raw_response}" + ); + assert!( + raw_response.to_ascii_lowercase().contains("\r\netag:"), + "successful PutObject should return an ETag header: {raw_response}" + ); + + let head = client.head_object().bucket(empty_bucket).key(empty_key).send().await?; + assert_eq!(head.content_length(), Some(0), "stored object must be zero length"); + + let url = format!("{}/{}/{}", env.url, chunked_bucket, chunked_key); + let raw_response = send_raw_signed_put(&url, &env.access_key, &env.secret_key, Some("chunked"), b"0\r\n\r\n").await?; + info!("raw chunked PUT response:\n{}", raw_response); + + assert_eq!( + parse_status(&raw_response), + Some(411), + "unknown-length chunked PutObject must stay rejected, got:\n{raw_response}" + ); + assert!( + raw_response.contains("MissingContentLength"), + "expected MissingContentLength, got:\n{raw_response}" + ); + + let missing = client + .head_object() + .bucket(chunked_bucket) + .key(chunked_key) + .send() + .await + .expect_err("rejected unknown-length PUT must not create an object"); + assert_eq!( + missing.raw_response().map(|response| response.status().as_u16()), + Some(404), + "rejected unknown-length PUT absence probe must return HTTP 404, got {missing:?}" + ); + + Ok(()) + } +} diff --git a/rustfs/src/app/object/extract.rs b/rustfs/src/app/object/extract.rs index 1a4cbcc4c..0ec1dd160 100644 --- a/rustfs/src/app/object/extract.rs +++ b/rustfs/src/app/object/extract.rs @@ -656,7 +656,7 @@ impl DefaultObjectUsecase { req_info.object = Some(fpath.clone()); req_info.version_id = None; } - let entry_size = f.header().size().unwrap_or_default(); + let entry_size = f.effective_size(); validate_put_object_extract_entry_size(&fpath, entry_size, extract_limits)?; total_unpacked_size = total_unpacked_size .checked_add(entry_size) diff --git a/rustfs/src/server/http.rs b/rustfs/src/server/http.rs index 400120b3c..a9786bd3f 100644 --- a/rustfs/src/server/http.rs +++ b/rustfs/src/server/http.rs @@ -2273,6 +2273,7 @@ mod tests { let s3_config = rustfs_s3_config(); assert!(s3_config.normalize_forward_slash_path); + assert!(s3_config.normalize_content_length); assert!(s3_config.enable_sig_v2); assert!(s3_config.sig_v4_allowed_services.iter().any(|service| service == "s3")); assert!(s3_config.sig_v4_allowed_services.iter().any(|service| service == "sts"));