mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 11:06:17 +00:00
fix(ilm): harden signer failures and guard remote tier delete storms (#2706)
This commit is contained in:
@@ -22,6 +22,7 @@ use super::constants::UNSIGNED_PAYLOAD;
|
||||
use super::credentials::SignatureType;
|
||||
use crate::client::{
|
||||
api_error_response::http_resp_to_error_response,
|
||||
signer_error,
|
||||
transition_api::{CreateBucketConfiguration, LocationConstraint, TransitionClient},
|
||||
};
|
||||
use http::Request;
|
||||
@@ -35,6 +36,10 @@ use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
||||
use s3s::S3ErrorCode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> std::io::Error {
|
||||
signer_error::signer_error_to_io_error(scope, error)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BucketLocationCache {
|
||||
items: HashMap<String, String>,
|
||||
@@ -179,10 +184,15 @@ impl TransitionClient {
|
||||
content_sha256 = UNSIGNED_PAYLOAD.to_string();
|
||||
}
|
||||
|
||||
if let Ok(content_sha256_value) = content_sha256.parse() {
|
||||
req.headers_mut().insert("X-Amz-Content-Sha256", content_sha256_value);
|
||||
}
|
||||
let req = rustfs_signer::sign_v4(req, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1");
|
||||
let content_sha256_value = content_sha256.parse().map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("invalid X-Amz-Content-Sha256 header value: {err}"),
|
||||
)
|
||||
})?;
|
||||
req.headers_mut().insert("X-Amz-Content-Sha256", content_sha256_value);
|
||||
let req = rustfs_signer::try_sign_v4(req, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1")
|
||||
.map_err(|err| signer_error_to_io_error("failed to sign bucket location request", err))?;
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,6 @@ pub mod constants;
|
||||
pub mod credentials;
|
||||
pub mod object_api_utils;
|
||||
pub mod object_handlers_common;
|
||||
pub mod signer_error;
|
||||
pub mod transition_api;
|
||||
pub mod utils;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
pub(crate) const SIGNER_HEADER_ERROR_MARKER: &str = "rustfs_signer_header_error";
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SignerHeaderError {
|
||||
scope: String,
|
||||
header_name: String,
|
||||
}
|
||||
|
||||
impl SignerHeaderError {
|
||||
fn new(scope: &str, header_name: &str) -> Self {
|
||||
Self {
|
||||
scope: scope.to_string(),
|
||||
header_name: header_name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SignerHeaderError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}: invalid UTF-8 header value for `{}` [{}]",
|
||||
self.scope, self.header_name, SIGNER_HEADER_ERROR_MARKER
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for SignerHeaderError {}
|
||||
|
||||
pub(crate) fn invalid_utf8_header_error(scope: &str, header_name: &str) -> Error {
|
||||
Error::new(ErrorKind::InvalidInput, SignerHeaderError::new(scope, header_name))
|
||||
}
|
||||
|
||||
pub(crate) fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> Error {
|
||||
match error {
|
||||
rustfs_signer::SignV4Error::InvalidHeaderValue { name } => invalid_utf8_header_error(scope, &name),
|
||||
other => Error::other(format!("{scope}: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn error_chain_contains_signer_header_marker(err: &(dyn StdError + 'static)) -> bool {
|
||||
let mut current = Some(err);
|
||||
while let Some(source) = current {
|
||||
if source.downcast_ref::<SignerHeaderError>().is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if source.to_string().contains(SIGNER_HEADER_ERROR_MARKER) {
|
||||
return true;
|
||||
}
|
||||
|
||||
current = source.source();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -31,6 +31,7 @@ use crate::client::{
|
||||
},
|
||||
constants::{UNSIGNED_PAYLOAD, UNSIGNED_PAYLOAD_TRAILER},
|
||||
credentials::{CredContext, Credentials, SignatureType, Static},
|
||||
signer_error,
|
||||
};
|
||||
use crate::{client::checksum::ChecksumMode, store_api::GetObjectReader};
|
||||
use futures::{Future, StreamExt};
|
||||
@@ -85,6 +86,21 @@ const C_UNKNOWN: i32 = -1;
|
||||
const C_OFFLINE: i32 = 0;
|
||||
const C_ONLINE: i32 = 1;
|
||||
|
||||
fn invalid_utf8_header_error(scope: &str, header_name: &str) -> std::io::Error {
|
||||
signer_error::invalid_utf8_header_error(scope, header_name)
|
||||
}
|
||||
|
||||
fn validate_header_values(headers: &HeaderMap, scope: &str) -> Result<(), std::io::Error> {
|
||||
for (name, value) in headers {
|
||||
value.to_str().map_err(|_| invalid_utf8_header_error(scope, name.as_str()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> std::io::Error {
|
||||
signer_error::signer_error_to_io_error(scope, error)
|
||||
}
|
||||
|
||||
//pub type ReaderImpl = Box<dyn Reader + Send + Sync + 'static>;
|
||||
pub enum ReaderImpl {
|
||||
Body(Bytes),
|
||||
@@ -560,8 +576,9 @@ impl TransitionClient {
|
||||
"extra signed headers for presign with signature v2 is not supported.",
|
||||
)));
|
||||
}
|
||||
let headers = req.headers_mut();
|
||||
if let Some(extra_headers) = metadata.extra_pre_sign_header.as_ref() {
|
||||
validate_header_values(extra_headers, "presign extra header")?;
|
||||
let headers = req.headers_mut();
|
||||
for (k, v) in extra_headers {
|
||||
headers.insert(k, v.clone());
|
||||
}
|
||||
@@ -570,7 +587,7 @@ impl TransitionClient {
|
||||
if signer_type == SignatureType::SignatureV2 {
|
||||
req = rustfs_signer::pre_sign_v2(req, &access_key_id, &secret_access_key, metadata.expires, is_virtual_host);
|
||||
} else if signer_type == SignatureType::SignatureV4 {
|
||||
req = rustfs_signer::pre_sign_v4(
|
||||
req = rustfs_signer::try_pre_sign_v4(
|
||||
req,
|
||||
&access_key_id,
|
||||
&secret_access_key,
|
||||
@@ -578,12 +595,14 @@ impl TransitionClient {
|
||||
&location,
|
||||
metadata.expires,
|
||||
OffsetDateTime::now_utc(),
|
||||
);
|
||||
)
|
||||
.map_err(|err| signer_error_to_io_error("failed to presign v4 request", err))?;
|
||||
}
|
||||
return Ok(req);
|
||||
}
|
||||
|
||||
self.set_user_agent(&mut req);
|
||||
validate_header_values(&metadata.custom_header, "request custom header")?;
|
||||
|
||||
for (k, v) in metadata.custom_header.clone() {
|
||||
if let Some(key) = k {
|
||||
@@ -593,15 +612,15 @@ impl TransitionClient {
|
||||
|
||||
//req.content_length = metadata.content_length;
|
||||
if metadata.content_length <= -1 {
|
||||
if let Ok(chunked_value) = HeaderValue::from_str(&vec!["chunked"].join(",")) {
|
||||
req.headers_mut().insert(http::header::TRANSFER_ENCODING, chunked_value);
|
||||
}
|
||||
req.headers_mut()
|
||||
.insert(http::header::TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
|
||||
}
|
||||
|
||||
if metadata.content_md5_base64.len() > 0 {
|
||||
if let Ok(md5_value) = HeaderValue::from_str(&metadata.content_md5_base64) {
|
||||
req.headers_mut().insert("Content-Md5", md5_value);
|
||||
}
|
||||
if !metadata.content_md5_base64.is_empty() {
|
||||
let md5_value = HeaderValue::from_str(&metadata.content_md5_base64).map_err(|err| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid Content-Md5 header value: {err}"))
|
||||
})?;
|
||||
req.headers_mut().insert("Content-Md5", md5_value);
|
||||
}
|
||||
|
||||
if signer_type == SignatureType::SignatureAnonymous {
|
||||
@@ -634,14 +653,15 @@ impl TransitionClient {
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
req.headers_mut().insert(header_name, header_value);
|
||||
|
||||
req = rustfs_signer::sign_v4_trailer(
|
||||
req = rustfs_signer::try_sign_v4_trailer(
|
||||
req,
|
||||
&access_key_id,
|
||||
&secret_access_key,
|
||||
&session_token,
|
||||
&location,
|
||||
metadata.trailer.clone(),
|
||||
);
|
||||
)
|
||||
.map_err(|err| signer_error_to_io_error("failed to sign v4 request", err))?;
|
||||
}
|
||||
|
||||
if metadata.content_length > 0 {
|
||||
@@ -1354,7 +1374,10 @@ pub struct CreateBucketConfiguration {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_tls_config, load_root_store_from_tls_path, with_rustls_init_guard};
|
||||
use super::{
|
||||
build_tls_config, load_root_store_from_tls_path, signer_error_to_io_error, validate_header_values, with_rustls_init_guard,
|
||||
};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
|
||||
#[test]
|
||||
fn rustls_guard_converts_panics_to_io_errors() {
|
||||
@@ -1404,4 +1427,29 @@ mod tests {
|
||||
});
|
||||
assert!(outcome.is_ok(), "provider install guard must not panic when a provider is already set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_header_values_returns_header_name_for_non_utf8_values() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-amz-meta-invalid",
|
||||
HeaderValue::from_bytes(&[0xFF]).expect("invalid utf8 bytes should be accepted by HeaderValue"),
|
||||
);
|
||||
|
||||
let err =
|
||||
validate_header_values(&headers, "request custom header").expect_err("invalid header value should fail validation");
|
||||
assert!(err.to_string().contains("x-amz-meta-invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_error_mapping_preserves_header_name() {
|
||||
let err = signer_error_to_io_error(
|
||||
"failed to sign v4 request",
|
||||
rustfs_signer::SignV4Error::InvalidHeaderValue {
|
||||
name: "x-amz-meta-invalid".to_string(),
|
||||
},
|
||||
);
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert!(err.to_string().contains("x-amz-meta-invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user