mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 22:59:59 +00:00
chore: upgrade dependencies and migrate to aws-lc-rs (#1333)
This commit is contained in:
+22
-22
@@ -367,35 +367,35 @@ async fn _setup_console_tls_config(tls_path: Option<&String>) -> Result<Option<R
|
||||
debug!("Found TLS directory for console, checking for certificates");
|
||||
|
||||
// Make sure to use a modern encryption suite
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
|
||||
if !cert_key_pairs.is_empty() {
|
||||
debug!(
|
||||
"Found {} certificates for console, creating SNI-aware multi-cert resolver",
|
||||
cert_key_pairs.len()
|
||||
);
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path)
|
||||
&& !cert_key_pairs.is_empty()
|
||||
{
|
||||
debug!(
|
||||
"Found {} certificates for console, creating SNI-aware multi-cert resolver",
|
||||
cert_key_pairs.len()
|
||||
);
|
||||
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with multi-certificate SNI support");
|
||||
return Ok(Some(RustlsConfig::from_config(Arc::new(server_config))));
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
info!(target: "rustfs::console::tls", "Console TLS enabled with multi-certificate SNI support");
|
||||
return Ok(Some(RustlsConfig::from_config(Arc::new(server_config))));
|
||||
}
|
||||
|
||||
// 2. Revert to the traditional single-certificate mode
|
||||
|
||||
@@ -636,50 +636,50 @@ fn extract_metrics_init_params(uri: &Uri) -> MetricsParams {
|
||||
for param in params {
|
||||
let mut parts = param.split('=');
|
||||
if let Some(key) = parts.next() {
|
||||
if key == "disks" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.disks = value.to_string();
|
||||
}
|
||||
if key == "disks"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.disks = value.to_string();
|
||||
}
|
||||
if key == "hosts" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.hosts = value.to_string();
|
||||
}
|
||||
if key == "hosts"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.hosts = value.to_string();
|
||||
}
|
||||
if key == "interval" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.tick = value.to_string();
|
||||
}
|
||||
if key == "interval"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.tick = value.to_string();
|
||||
}
|
||||
if key == "n" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.n = value.parse::<u64>().unwrap_or(u64::MAX);
|
||||
}
|
||||
if key == "n"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.n = value.parse::<u64>().unwrap_or(u64::MAX);
|
||||
}
|
||||
if key == "types" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.types = value.parse::<u32>().unwrap_or_default();
|
||||
}
|
||||
if key == "types"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.types = value.parse::<u32>().unwrap_or_default();
|
||||
}
|
||||
if key == "by-disk" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.by_disk = value.to_string();
|
||||
}
|
||||
if key == "by-disk"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.by_disk = value.to_string();
|
||||
}
|
||||
if key == "by-host" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.by_host = value.to_string();
|
||||
}
|
||||
if key == "by-host"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.by_host = value.to_string();
|
||||
}
|
||||
if key == "by-jobID" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.by_job_id = value.to_string();
|
||||
}
|
||||
if key == "by-jobID"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.by_job_id = value.to_string();
|
||||
}
|
||||
if key == "by-depID" {
|
||||
if let Some(value) = parts.next() {
|
||||
mp.by_dep_id = value.to_string();
|
||||
}
|
||||
if key == "by-depID"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
mp.by_dep_id = value.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,10 +830,10 @@ fn extract_heal_init_params(body: &Bytes, uri: &Uri, params: Params<'_, '_>) ->
|
||||
for param in params {
|
||||
let mut parts = param.split('=');
|
||||
if let Some(key) = parts.next() {
|
||||
if key == "clientToken" {
|
||||
if let Some(value) = parts.next() {
|
||||
hip.client_token = value.to_string();
|
||||
}
|
||||
if key == "clientToken"
|
||||
&& let Some(value) = parts.next()
|
||||
{
|
||||
hip.client_token = value.to_string();
|
||||
}
|
||||
if key == "forceStart" && parts.next().is_some() {
|
||||
hip.force_start = true;
|
||||
|
||||
@@ -277,10 +277,11 @@ impl Operation for UpdateGroupMembers {
|
||||
} else {
|
||||
warn!("add group members");
|
||||
|
||||
if let Err(err) = iam_store.get_group_description(&args.group).await {
|
||||
if is_err_no_such_group(&err) && has_space_be(&args.group) {
|
||||
return Err(s3_error!(InvalidArgument, "not such group"));
|
||||
}
|
||||
if let Err(err) = iam_store.get_group_description(&args.group).await
|
||||
&& is_err_no_such_group(&err)
|
||||
&& has_space_be(&args.group)
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "not such group"));
|
||||
}
|
||||
|
||||
iam_store.add_users_to_group(&args.group, args.members).await.map_err(|e| {
|
||||
|
||||
@@ -96,10 +96,10 @@ impl Operation for AddUser {
|
||||
return Err(s3_error!(InvalidArgument, "access key is empty"));
|
||||
}
|
||||
|
||||
if let Some(sys_cred) = get_global_action_cred() {
|
||||
if constant_time_eq(&sys_cred.access_key, ak) {
|
||||
return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
|
||||
}
|
||||
if let Some(sys_cred) = get_global_action_cred()
|
||||
&& constant_time_eq(&sys_cred.access_key, ak)
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "can't create user with system access key"));
|
||||
}
|
||||
|
||||
let Ok(iam_store) = rustfs_iam::get() else {
|
||||
@@ -777,10 +777,10 @@ impl Operation for ImportIam {
|
||||
let groups: HashMap<String, GroupInfo> = serde_json::from_slice(&file_content)
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, e.to_string()))?;
|
||||
for (group_name, group_info) in groups {
|
||||
if let Err(e) = iam_store.get_group_description(&group_name).await {
|
||||
if matches!(e, rustfs_iam::error::Error::NoSuchGroup(_)) || has_space_be(&group_name) {
|
||||
return Err(s3_error!(InvalidArgument, "group not found or has space be"));
|
||||
}
|
||||
if let Err(e) = iam_store.get_group_description(&group_name).await
|
||||
&& (matches!(e, rustfs_iam::error::Error::NoSuchGroup(_)) || has_space_be(&group_name))
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "group not found or has space be"));
|
||||
}
|
||||
|
||||
if let Err(e) = iam_store.add_users_to_group(&group_name, group_info.members.clone()).await {
|
||||
|
||||
+57
-57
@@ -175,10 +175,10 @@ pub async fn check_key_valid(session_token: &str, access_key: &str) -> S3Result<
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("check claims failed1 {e}")))?;
|
||||
|
||||
if !ok {
|
||||
if let Some(u) = u {
|
||||
if u.credentials.status == "off" {
|
||||
return Err(s3_error!(InvalidRequest, "ErrAccessKeyDisabled"));
|
||||
}
|
||||
if let Some(u) = u
|
||||
&& u.credentials.status == "off"
|
||||
{
|
||||
return Err(s3_error!(InvalidRequest, "ErrAccessKeyDisabled"));
|
||||
}
|
||||
|
||||
return Err(s3_error!(InvalidRequest, "ErrAccessKeyDisabled"));
|
||||
@@ -200,10 +200,10 @@ pub async fn check_key_valid(session_token: &str, access_key: &str) -> S3Result<
|
||||
constant_time_eq(&sys_cred.access_key, &cred.access_key) || constant_time_eq(&cred.parent_user, &sys_cred.access_key);
|
||||
|
||||
// permitRootAccess
|
||||
if let Some(claims) = &cred.claims {
|
||||
if claims.contains_key(SESSION_POLICY_NAME) {
|
||||
owner = false
|
||||
}
|
||||
if let Some(claims) = &cred.claims
|
||||
&& claims.contains_key(SESSION_POLICY_NAME)
|
||||
{
|
||||
owner = false
|
||||
}
|
||||
|
||||
Ok((cred, owner))
|
||||
@@ -358,10 +358,10 @@ pub fn get_condition_values(
|
||||
args.insert("authType".to_owned(), vec![auth_type]);
|
||||
}
|
||||
|
||||
if let Some(lc) = region {
|
||||
if !lc.is_empty() {
|
||||
args.insert("LocationConstraint".to_owned(), vec![lc.to_string()]);
|
||||
}
|
||||
if let Some(lc) = region
|
||||
&& !lc.is_empty()
|
||||
{
|
||||
args.insert("LocationConstraint".to_owned(), vec![lc.to_string()]);
|
||||
}
|
||||
|
||||
let mut clone_header = header.clone();
|
||||
@@ -411,23 +411,23 @@ pub fn get_condition_values(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(grps_val) = claims.get("groups") {
|
||||
if let Some(grps_is) = grps_val.as_array() {
|
||||
let grps = grps_is
|
||||
.iter()
|
||||
.filter_map(|g| g.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<String>>();
|
||||
if !grps.is_empty() {
|
||||
args.insert("groups".to_string(), grps);
|
||||
}
|
||||
if let Some(grps_val) = claims.get("groups")
|
||||
&& let Some(grps_is) = grps_val.as_array()
|
||||
{
|
||||
let grps = grps_is
|
||||
.iter()
|
||||
.filter_map(|g| g.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<String>>();
|
||||
if !grps.is_empty() {
|
||||
args.insert("groups".to_string(), grps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(groups) = &cred.groups {
|
||||
if !args.contains_key("groups") {
|
||||
args.insert("groups".to_string(), groups.clone());
|
||||
}
|
||||
if let Some(groups) = &cred.groups
|
||||
&& !args.contains_key("groups")
|
||||
{
|
||||
args.insert("groups".to_string(), groups.clone());
|
||||
}
|
||||
|
||||
args
|
||||
@@ -502,10 +502,10 @@ fn determine_auth_type_and_version(header: &HeaderMap) -> (String, String) {
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has JWT, false otherwise
|
||||
fn is_request_jwt(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return auth_str.starts_with(JWT_ALGORITHM);
|
||||
}
|
||||
if let Some(auth) = header.get("authorization")
|
||||
&& let Ok(auth_str) = auth.to_str()
|
||||
{
|
||||
return auth_str.starts_with(JWT_ALGORITHM);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -518,10 +518,10 @@ fn is_request_jwt(header: &HeaderMap) -> bool {
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has AWS Signature Version '4', false otherwise
|
||||
fn is_request_signature_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return auth_str.starts_with(SIGN_V4_ALGORITHM);
|
||||
}
|
||||
if let Some(auth) = header.get("authorization")
|
||||
&& let Ok(auth_str) = auth.to_str()
|
||||
{
|
||||
return auth_str.starts_with(SIGN_V4_ALGORITHM);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -534,10 +534,10 @@ fn is_request_signature_v4(header: &HeaderMap) -> bool {
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has AWS Signature Version '2', false otherwise
|
||||
fn is_request_signature_v2(header: &HeaderMap) -> bool {
|
||||
if let Some(auth) = header.get("authorization") {
|
||||
if let Ok(auth_str) = auth.to_str() {
|
||||
return !auth_str.starts_with(SIGN_V4_ALGORITHM) && auth_str.starts_with(SIGN_V2_ALGORITHM);
|
||||
}
|
||||
if let Some(auth) = header.get("authorization")
|
||||
&& let Ok(auth_str) = auth.to_str()
|
||||
{
|
||||
return !auth_str.starts_with(SIGN_V4_ALGORITHM) && auth_str.starts_with(SIGN_V2_ALGORITHM);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -578,40 +578,40 @@ fn is_request_presigned_signature_v2(header: &HeaderMap) -> bool {
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has AWS Post policy Signature Version '4', false otherwise
|
||||
fn is_request_post_policy_signature_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_type) = header.get("content-type") {
|
||||
if let Ok(ct) = content_type.to_str() {
|
||||
return ct.contains("multipart/form-data");
|
||||
}
|
||||
if let Some(content_type) = header.get("content-type")
|
||||
&& let Ok(ct) = content_type.to_str()
|
||||
{
|
||||
return ct.contains("multipart/form-data");
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Verify if the request has AWS Streaming Signature Version '4'
|
||||
fn is_request_sign_streaming_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == STREAMING_CONTENT_SHA256;
|
||||
}
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256")
|
||||
&& let Ok(sha256_str) = content_sha256.to_str()
|
||||
{
|
||||
return sha256_str == STREAMING_CONTENT_SHA256;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4' with trailer
|
||||
fn is_request_sign_streaming_trailer_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == STREAMING_CONTENT_SHA256_TRAILER;
|
||||
}
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256")
|
||||
&& let Ok(sha256_str) = content_sha256.to_str()
|
||||
{
|
||||
return sha256_str == STREAMING_CONTENT_SHA256_TRAILER;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4' with unsigned content and trailer
|
||||
fn is_request_unsigned_trailer_v4(header: &HeaderMap) -> bool {
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256") {
|
||||
if let Ok(sha256_str) = content_sha256.to_str() {
|
||||
return sha256_str == UNSIGNED_PAYLOAD_TRAILER;
|
||||
}
|
||||
if let Some(content_sha256) = header.get("x-amz-content-sha256")
|
||||
&& let Ok(sha256_str) = content_sha256.to_str()
|
||||
{
|
||||
return sha256_str == UNSIGNED_PAYLOAD_TRAILER;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -634,10 +634,10 @@ pub fn get_query_param<'a>(query: &'a str, param_name: &str) -> Option<&'a str>
|
||||
|
||||
for pair in query.split('&') {
|
||||
let mut parts = pair.splitn(2, '=');
|
||||
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
|
||||
if key.to_lowercase() == param_name {
|
||||
return Some(value);
|
||||
}
|
||||
if let (Some(key), Some(value)) = (parts.next(), parts.next())
|
||||
&& key.to_lowercase() == param_name
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
+10
-12
@@ -193,18 +193,16 @@ impl From<ApiError> for S3Error {
|
||||
impl From<StorageError> for ApiError {
|
||||
fn from(err: StorageError) -> Self {
|
||||
// Special handling for Io errors that may contain ChecksumMismatch
|
||||
if let StorageError::Io(ref io_err) = err {
|
||||
if let Some(inner) = io_err.get_ref() {
|
||||
if inner.downcast_ref::<rustfs_rio::ChecksumMismatch>().is_some()
|
||||
|| inner.downcast_ref::<rustfs_rio::BadDigest>().is_some()
|
||||
{
|
||||
return ApiError {
|
||||
code: S3ErrorCode::BadDigest,
|
||||
message: ApiError::error_code_to_message(&S3ErrorCode::BadDigest),
|
||||
source: Some(Box::new(err)),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let StorageError::Io(ref io_err) = err
|
||||
&& let Some(inner) = io_err.get_ref()
|
||||
&& (inner.downcast_ref::<rustfs_rio::ChecksumMismatch>().is_some()
|
||||
|| inner.downcast_ref::<rustfs_rio::BadDigest>().is_some())
|
||||
{
|
||||
return ApiError {
|
||||
code: S3ErrorCode::BadDigest,
|
||||
message: ApiError::error_code_to_message(&S3ErrorCode::BadDigest),
|
||||
source: Some(Box::new(err)),
|
||||
};
|
||||
}
|
||||
|
||||
let code = match &err {
|
||||
|
||||
@@ -735,14 +735,14 @@ impl StorageBackend<super::server::FtpsUser> for FtpsDriver {
|
||||
|
||||
match s3_client.list_objects_v2(list_input).await {
|
||||
Ok(output) => {
|
||||
if let Some(objects) = output.contents {
|
||||
if !objects.is_empty() {
|
||||
debug!("FTPS RMD - bucket '{}' is not empty, cannot delete", bucket);
|
||||
return Err(Error::new(
|
||||
ErrorKind::PermanentFileNotAvailable,
|
||||
format!("Bucket '{}' is not empty", bucket),
|
||||
));
|
||||
}
|
||||
if let Some(objects) = output.contents
|
||||
&& !objects.is_empty()
|
||||
{
|
||||
debug!("FTPS RMD - bucket '{}' is not empty, cannot delete", bucket);
|
||||
return Err(Error::new(
|
||||
ErrorKind::PermanentFileNotAvailable,
|
||||
format!("Bucket '{}' is not empty", bucket),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -98,16 +98,16 @@ impl FtpsConfig {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(path) = &self.cert_file {
|
||||
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
|
||||
return Err(FtpsInitError::InvalidConfig(format!("Certificate file not found: {}", path)));
|
||||
}
|
||||
if let Some(path) = &self.cert_file
|
||||
&& !tokio::fs::try_exists(path).await.unwrap_or(false)
|
||||
{
|
||||
return Err(FtpsInitError::InvalidConfig(format!("Certificate file not found: {}", path)));
|
||||
}
|
||||
|
||||
if let Some(path) = &self.key_file {
|
||||
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
|
||||
return Err(FtpsInitError::InvalidConfig(format!("Key file not found: {}", path)));
|
||||
}
|
||||
if let Some(path) = &self.key_file
|
||||
&& !tokio::fs::try_exists(path).await.unwrap_or(false)
|
||||
{
|
||||
return Err(FtpsInitError::InvalidConfig(format!("Key file not found: {}", path)));
|
||||
}
|
||||
|
||||
// Validate passive ports format
|
||||
|
||||
@@ -753,16 +753,16 @@ impl Handler for SftpHandler {
|
||||
|
||||
match s3_client.list_objects_v2(list_input).await {
|
||||
Ok(output) => {
|
||||
if let Some(objects) = output.contents {
|
||||
if !objects.is_empty() {
|
||||
debug!("SFTP REMOVE - bucket '{}' is not empty, cannot delete", bucket);
|
||||
return Ok(Status {
|
||||
id,
|
||||
status_code: StatusCode::Failure,
|
||||
error_message: format!("Bucket '{}' is not empty", bucket),
|
||||
language_tag: "en".into(),
|
||||
});
|
||||
}
|
||||
if let Some(objects) = output.contents
|
||||
&& !objects.is_empty()
|
||||
{
|
||||
debug!("SFTP REMOVE - bucket '{}' is not empty, cannot delete", bucket);
|
||||
return Ok(Status {
|
||||
id,
|
||||
status_code: StatusCode::Failure,
|
||||
error_message: format!("Bucket '{}' is not empty", bucket),
|
||||
language_tag: "en".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -696,10 +696,10 @@ fn compare_keys(stored_key: &str, client_key_base64: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Ok(stored_bytes) = BASE64.decode(stored_key_data) {
|
||||
if let Ok(client_bytes) = BASE64.decode(client_key_base64) {
|
||||
return stored_bytes == client_bytes;
|
||||
}
|
||||
if let Ok(stored_bytes) = BASE64.decode(stored_key_data)
|
||||
&& let Ok(client_bytes) = BASE64.decode(client_key_base64)
|
||||
{
|
||||
return stored_bytes == client_bytes;
|
||||
}
|
||||
|
||||
false
|
||||
|
||||
+10
-10
@@ -260,12 +260,12 @@ async fn walk_dir(path: PathBuf, cert_name: &str, cert_data: &mut Vec<u8>) {
|
||||
// Only check direct subdirectories, no deeper recursion
|
||||
if let Ok(mut sub_rd) = tokio::fs::read_dir(&entry.path()).await {
|
||||
while let Ok(Some(sub_entry)) = sub_rd.next_entry().await {
|
||||
if let Ok(sub_ft) = sub_entry.file_type().await {
|
||||
if sub_ft.is_file() {
|
||||
load_if_matches(&sub_entry, cert_name, cert_data).await;
|
||||
}
|
||||
// Ignore subdirectories and symlinks in subdirs to limit to one level
|
||||
if let Ok(sub_ft) = sub_entry.file_type().await
|
||||
&& sub_ft.is_file()
|
||||
{
|
||||
load_if_matches(&sub_entry, cert_name, cert_data).await;
|
||||
}
|
||||
// Ignore subdirectories and symlinks in subdirs to limit to one level
|
||||
}
|
||||
}
|
||||
} else if ft.is_symlink() {
|
||||
@@ -277,12 +277,12 @@ async fn walk_dir(path: PathBuf, cert_name: &str, cert_data: &mut Vec<u8>) {
|
||||
// Treat as directory but only check its direct contents
|
||||
if let Ok(mut sub_rd) = tokio::fs::read_dir(&entry.path()).await {
|
||||
while let Ok(Some(sub_entry)) = sub_rd.next_entry().await {
|
||||
if let Ok(sub_ft) = sub_entry.file_type().await {
|
||||
if sub_ft.is_file() {
|
||||
load_if_matches(&sub_entry, cert_name, cert_data).await;
|
||||
}
|
||||
// Ignore deeper levels
|
||||
if let Ok(sub_ft) = sub_entry.file_type().await
|
||||
&& sub_ft.is_file()
|
||||
{
|
||||
load_if_matches(&sub_entry, cert_name, cert_data).await;
|
||||
}
|
||||
// Ignore deeper levels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,40 +282,35 @@ impl Predicate for CompressionPredicate {
|
||||
// CompressionLayer before calling this predicate, so we don't need to check them here.
|
||||
|
||||
// Check Content-Length header for minimum size threshold
|
||||
if let Some(content_length) = response.headers().get(http::header::CONTENT_LENGTH) {
|
||||
if let Ok(length_str) = content_length.to_str() {
|
||||
if let Ok(length) = length_str.parse::<u64>() {
|
||||
if length < self.config.min_size {
|
||||
debug!(
|
||||
"Skipping compression for small response: size={} bytes, min_size={}",
|
||||
length, self.config.min_size
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(content_length) = response.headers().get(http::header::CONTENT_LENGTH)
|
||||
&& let Ok(length_str) = content_length.to_str()
|
||||
&& let Ok(length) = length_str.parse::<u64>()
|
||||
&& length < self.config.min_size
|
||||
{
|
||||
debug!(
|
||||
"Skipping compression for small response: size={} bytes, min_size={}",
|
||||
length, self.config.min_size
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the response matches configured extension via Content-Disposition
|
||||
if let Some(content_disposition) = response.headers().get(http::header::CONTENT_DISPOSITION) {
|
||||
if let Ok(cd) = content_disposition.to_str() {
|
||||
if let Some(filename) = CompressionConfig::extract_filename_from_content_disposition(cd) {
|
||||
if self.config.matches_extension(&filename) {
|
||||
debug!("Compressing response: filename '{}' matches configured extension", filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(content_disposition) = response.headers().get(http::header::CONTENT_DISPOSITION)
|
||||
&& let Ok(cd) = content_disposition.to_str()
|
||||
&& let Some(filename) = CompressionConfig::extract_filename_from_content_disposition(cd)
|
||||
&& self.config.matches_extension(&filename)
|
||||
{
|
||||
debug!("Compressing response: filename '{}' matches configured extension", filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the response matches configured MIME type
|
||||
if let Some(content_type) = response.headers().get(http::header::CONTENT_TYPE) {
|
||||
if let Ok(ct) = content_type.to_str() {
|
||||
if self.config.matches_mime_type(ct) {
|
||||
debug!("Compressing response: Content-Type '{}' matches configured MIME pattern", ct);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(content_type) = response.headers().get(http::header::CONTENT_TYPE)
|
||||
&& let Ok(ct) = content_type.to_str()
|
||||
&& self.config.matches_mime_type(ct)
|
||||
{
|
||||
debug!("Compressing response: Content-Type '{}' matches configured MIME pattern", ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default: don't compress (whitelist approach)
|
||||
|
||||
+33
-32
@@ -139,13 +139,13 @@ pub async fn start_http_server(
|
||||
};
|
||||
|
||||
// If address is IPv6 try to enable dual-stack; on failure, switch to IPv4 socket.
|
||||
if server_addr.is_ipv6() {
|
||||
if let Err(e) = socket.set_only_v6(false) {
|
||||
warn!("Failed to set IPV6_V6ONLY=false, attempting IPv4 fallback: {}", e);
|
||||
let ipv4_addr = SocketAddr::new(std::net::Ipv4Addr::UNSPECIFIED.into(), server_addr.port());
|
||||
server_addr = ipv4_addr;
|
||||
socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?;
|
||||
}
|
||||
if server_addr.is_ipv6()
|
||||
&& let Err(e) = socket.set_only_v6(false)
|
||||
{
|
||||
warn!("Failed to set IPV6_V6ONLY=false, attempting IPv4 fallback: {}", e);
|
||||
let ipv4_addr = SocketAddr::new(std::net::Ipv4Addr::UNSPECIFIED.into(), server_addr.port());
|
||||
server_addr = ipv4_addr;
|
||||
socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?;
|
||||
}
|
||||
|
||||
// Common setup for both IPv4 and successful dual-stack IPv6
|
||||
@@ -434,38 +434,38 @@ async fn setup_tls_acceptor(tls_path: &str) -> Result<Option<TlsAcceptor>> {
|
||||
debug!("Found TLS directory, checking for certificates");
|
||||
|
||||
// Make sure to use a modern encryption suite
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
let mtls_verifier = rustfs_utils::build_webpki_client_verifier(tls_path)?;
|
||||
|
||||
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
|
||||
if !cert_key_pairs.is_empty() {
|
||||
debug!("Found {} certificates, creating SNI-aware multi-cert resolver", cert_key_pairs.len());
|
||||
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path)
|
||||
&& !cert_key_pairs.is_empty()
|
||||
{
|
||||
debug!("Found {} certificates, creating SNI-aware multi-cert resolver", cert_key_pairs.len());
|
||||
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
// Create an SNI-enabled certificate resolver
|
||||
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
|
||||
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = if let Some(verifier) = mtls_verifier.clone() {
|
||||
ServerConfig::builder()
|
||||
.with_client_cert_verifier(verifier)
|
||||
.with_cert_resolver(Arc::new(resolver))
|
||||
} else {
|
||||
ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver))
|
||||
};
|
||||
// Configure the server to enable SNI support
|
||||
let mut server_config = if let Some(verifier) = mtls_verifier.clone() {
|
||||
ServerConfig::builder()
|
||||
.with_client_cert_verifier(verifier)
|
||||
.with_cert_resolver(Arc::new(resolver))
|
||||
} else {
|
||||
ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(resolver))
|
||||
};
|
||||
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
// Configure ALPN protocol priority
|
||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
|
||||
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
|
||||
// Log SNI requests
|
||||
if rustfs_utils::tls_key_log() {
|
||||
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
}
|
||||
|
||||
return Ok(Some(TlsAcceptor::from(Arc::new(server_config))));
|
||||
}
|
||||
|
||||
// 2. Revert to the traditional single-certificate mode
|
||||
@@ -520,7 +520,8 @@ struct ConnectionContext {
|
||||
/// 2. Build a complete service stack for this connection, including S3, RPC services, and all middleware.
|
||||
/// 3. Use Hyper to handle HTTP requests on this connection.
|
||||
/// 4. Incorporate connections into the management of elegant closures.
|
||||
#[instrument(skip_all, fields(peer_addr = %socket.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "unknown".to_string())))]
|
||||
#[instrument(skip_all, fields(peer_addr = %socket.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "unknown".to_string())
|
||||
))]
|
||||
fn process_connection(
|
||||
socket: TcpStream,
|
||||
tls_acceptor: Option<Arc<TlsAcceptor>>,
|
||||
|
||||
+329
-347
@@ -519,13 +519,13 @@ fn validate_list_object_unordered_with_delimiter(delimiter: Option<&Delimiter>,
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Ok(params) = from_bytes::<ListObjectUnorderedQuery>(query.as_bytes()) {
|
||||
if params.allow_unordered.as_deref() == Some("true") {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidArgument,
|
||||
"The allow-unordered parameter cannot be used when delimiter is specified.".to_string(),
|
||||
));
|
||||
}
|
||||
if let Ok(params) = from_bytes::<ListObjectUnorderedQuery>(query.as_bytes())
|
||||
&& params.allow_unordered.as_deref() == Some("true")
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidArgument,
|
||||
"The allow-unordered parameter cannot be used when delimiter is specified.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -735,8 +735,8 @@ impl FS {
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
if let Some(alg) = &input.checksum_algorithm
|
||||
&& let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
@@ -750,15 +750,15 @@ impl FS {
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
{
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,64 +977,63 @@ impl S3 for FS {
|
||||
|
||||
let mut reader = HashReader::new(reader, length, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
if let Some(ref sse_alg) = effective_sse {
|
||||
if is_managed_sse(sse_alg) {
|
||||
let material =
|
||||
create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size).await?;
|
||||
if let Some(ref sse_alg) = effective_sse
|
||||
&& is_managed_sse(sse_alg)
|
||||
{
|
||||
let material =
|
||||
create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size).await?;
|
||||
|
||||
let ManagedEncryptionMaterial {
|
||||
data_key,
|
||||
headers,
|
||||
kms_key_id: kms_key_used,
|
||||
} = material;
|
||||
let ManagedEncryptionMaterial {
|
||||
data_key,
|
||||
headers,
|
||||
kms_key_id: kms_key_used,
|
||||
} = material;
|
||||
|
||||
let key_bytes = data_key.plaintext_key;
|
||||
let nonce = data_key.nonce;
|
||||
let key_bytes = data_key.plaintext_key;
|
||||
let nonce = data_key.nonce;
|
||||
|
||||
src_info.user_defined.extend(headers.into_iter());
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
src_info.user_defined.extend(headers.into_iter());
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
// Apply SSE-C encryption if customer-provided key is specified
|
||||
if let (Some(sse_alg), Some(sse_key), Some(sse_md5)) = (&sse_customer_algorithm, &sse_customer_key, &sse_customer_key_md5)
|
||||
&& sse_alg.as_str() == "AES256"
|
||||
{
|
||||
if sse_alg.as_str() == "AES256" {
|
||||
let key_bytes = BASE64_STANDARD.decode(sse_key.as_str()).map_err(|e| {
|
||||
error!("Failed to decode SSE-C key: {}", e);
|
||||
ApiError::from(StorageError::other("Invalid SSE-C key"))
|
||||
})?;
|
||||
let key_bytes = BASE64_STANDARD.decode(sse_key.as_str()).map_err(|e| {
|
||||
error!("Failed to decode SSE-C key: {}", e);
|
||||
ApiError::from(StorageError::other("Invalid SSE-C key"))
|
||||
})?;
|
||||
|
||||
if key_bytes.len() != 32 {
|
||||
return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into());
|
||||
}
|
||||
|
||||
let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0);
|
||||
if computed_md5 != sse_md5.as_str() {
|
||||
return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into());
|
||||
}
|
||||
|
||||
// Store original size before encryption
|
||||
src_info
|
||||
.user_defined
|
||||
.insert("x-amz-server-side-encryption-customer-original-size".to_string(), actual_size.to_string());
|
||||
|
||||
// SAFETY: The length of `key_bytes` is checked to be 32 bytes above,
|
||||
// so this conversion cannot fail.
|
||||
let key_array: [u8; 32] = key_bytes.try_into().expect("key length already checked");
|
||||
// Generate deterministic nonce from bucket-key
|
||||
let nonce_source = format!("{bucket}-{key}");
|
||||
let nonce_hash = md5::compute(nonce_source.as_bytes());
|
||||
let nonce: [u8; 12] = nonce_hash.0[..12]
|
||||
.try_into()
|
||||
.expect("MD5 hash is always 16 bytes; taking first 12 bytes for nonce is safe");
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_array, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
if key_bytes.len() != 32 {
|
||||
return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into());
|
||||
}
|
||||
|
||||
let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0);
|
||||
if computed_md5 != sse_md5.as_str() {
|
||||
return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into());
|
||||
}
|
||||
|
||||
// Store original size before encryption
|
||||
src_info
|
||||
.user_defined
|
||||
.insert("x-amz-server-side-encryption-customer-original-size".to_string(), actual_size.to_string());
|
||||
|
||||
// SAFETY: The length of `key_bytes` is checked to be 32 bytes above,
|
||||
// so this conversion cannot fail.
|
||||
let key_array: [u8; 32] = key_bytes.try_into().expect("key length already checked");
|
||||
// Generate deterministic nonce from bucket-key
|
||||
let nonce_source = format!("{bucket}-{key}");
|
||||
let nonce_hash = md5::compute(nonce_source.as_bytes());
|
||||
let nonce: [u8; 12] = nonce_hash.0[..12]
|
||||
.try_into()
|
||||
.expect("MD5 hash is always 16 bytes; taking first 12 bytes for nonce is safe");
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_array, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
src_info.put_object_reader = Some(PutObjReader::new(reader));
|
||||
@@ -1246,15 +1245,14 @@ impl S3 for FS {
|
||||
|
||||
let restore_object = Uuid::new_v4().to_string();
|
||||
//if let Some(rreq) = rreq {
|
||||
if let Some(output_location) = &rreq.output_location {
|
||||
if let Some(s3) = &output_location.s3 {
|
||||
if !s3.bucket_name.is_empty() {
|
||||
header.insert(
|
||||
X_AMZ_RESTORE_OUTPUT_PATH,
|
||||
format!("{}{}{}", s3.bucket_name, s3.prefix, restore_object).parse().unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(output_location) = &rreq.output_location
|
||||
&& let Some(s3) = &output_location.s3
|
||||
&& !s3.bucket_name.is_empty()
|
||||
{
|
||||
header.insert(
|
||||
X_AMZ_RESTORE_OUTPUT_PATH,
|
||||
format!("{}{}{}", s3.bucket_name, s3.prefix, restore_object).parse().unwrap(),
|
||||
);
|
||||
}
|
||||
//}
|
||||
/*send_event(EventArgs {
|
||||
@@ -1730,24 +1728,23 @@ impl S3 for FS {
|
||||
};
|
||||
|
||||
for dobjs in delete_results.iter() {
|
||||
if let Some(dobj) = &dobjs.delete_object {
|
||||
if replicate_deletes
|
||||
&& (dobj.delete_marker_replication_status() == ReplicationStatusType::Pending
|
||||
|| dobj.version_purge_status() == VersionPurgeStatusType::Pending)
|
||||
{
|
||||
let mut dobj = dobj.clone();
|
||||
if is_dir_object(dobj.object_name.as_str()) && dobj.version_id.is_none() {
|
||||
dobj.version_id = Some(Uuid::nil());
|
||||
}
|
||||
|
||||
let deleted_object = DeletedObjectReplicationInfo {
|
||||
delete_object: dobj,
|
||||
bucket: bucket.clone(),
|
||||
event_type: REPLICATE_INCOMING_DELETE.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
schedule_replication_delete(deleted_object).await;
|
||||
if let Some(dobj) = &dobjs.delete_object
|
||||
&& replicate_deletes
|
||||
&& (dobj.delete_marker_replication_status() == ReplicationStatusType::Pending
|
||||
|| dobj.version_purge_status() == VersionPurgeStatusType::Pending)
|
||||
{
|
||||
let mut dobj = dobj.clone();
|
||||
if is_dir_object(dobj.object_name.as_str()) && dobj.version_id.is_none() {
|
||||
dobj.version_id = Some(Uuid::nil());
|
||||
}
|
||||
|
||||
let deleted_object = DeletedObjectReplicationInfo {
|
||||
delete_object: dobj,
|
||||
bucket: bucket.clone(),
|
||||
event_type: REPLICATE_INCOMING_DELETE.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
schedule_replication_delete(deleted_object).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1854,96 +1851,98 @@ impl S3 for FS {
|
||||
let cache_key = ConcurrencyManager::make_cache_key(&bucket, &key, version_id.as_deref());
|
||||
|
||||
// Only attempt cache lookup if caching is enabled and for objects without range/part requests
|
||||
if manager.is_cache_enabled() && part_number.is_none() && range.is_none() {
|
||||
if let Some(cached) = manager.get_cached_object(&cache_key).await {
|
||||
let cache_serve_duration = request_start.elapsed();
|
||||
if manager.is_cache_enabled()
|
||||
&& part_number.is_none()
|
||||
&& range.is_none()
|
||||
&& let Some(cached) = manager.get_cached_object(&cache_key).await
|
||||
{
|
||||
let cache_serve_duration = request_start.elapsed();
|
||||
|
||||
debug!("Serving object from response cache: {} (latency: {:?})", cache_key, cache_serve_duration);
|
||||
debug!("Serving object from response cache: {} (latency: {:?})", cache_key, cache_serve_duration);
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
use metrics::{counter, histogram};
|
||||
counter!("rustfs.get.object.cache.served.total").increment(1);
|
||||
histogram!("rustfs.get.object.cache.serve.duration.seconds").record(cache_serve_duration.as_secs_f64());
|
||||
histogram!("rustfs.get.object.cache.size.bytes").record(cached.body.len() as f64);
|
||||
}
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
use metrics::{counter, histogram};
|
||||
counter!("rustfs.get.object.cache.served.total").increment(1);
|
||||
histogram!("rustfs.get.object.cache.serve.duration.seconds").record(cache_serve_duration.as_secs_f64());
|
||||
histogram!("rustfs.get.object.cache.size.bytes").record(cached.body.len() as f64);
|
||||
}
|
||||
|
||||
// Build response from cached data with full metadata
|
||||
let body_data = cached.body.clone();
|
||||
let body = Some(StreamingBlob::wrap::<_, Infallible>(futures::stream::once(async move { Ok(body_data) })));
|
||||
// Build response from cached data with full metadata
|
||||
let body_data = cached.body.clone();
|
||||
let body = Some(StreamingBlob::wrap::<_, Infallible>(futures::stream::once(async move { Ok(body_data) })));
|
||||
|
||||
// Parse last_modified from RFC3339 string if available
|
||||
let last_modified = cached
|
||||
// Parse last_modified from RFC3339 string if available
|
||||
let last_modified = cached
|
||||
.last_modified
|
||||
.as_ref()
|
||||
.and_then(|s| match OffsetDateTime::parse(s, &Rfc3339) {
|
||||
Ok(dt) => Some(Timestamp::from(dt)),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse cached last_modified '{}': {}", s, e);
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Parse content_type
|
||||
let content_type = cached.content_type.as_ref().and_then(|ct| ContentType::from_str(ct).ok());
|
||||
|
||||
let output = GetObjectOutput {
|
||||
body,
|
||||
content_length: Some(cached.content_length),
|
||||
accept_ranges: Some("bytes".to_string()),
|
||||
e_tag: cached.e_tag.as_ref().map(|etag| to_s3s_etag(etag)),
|
||||
last_modified,
|
||||
content_type,
|
||||
cache_control: cached.cache_control.clone(),
|
||||
content_disposition: cached.content_disposition.clone(),
|
||||
content_encoding: cached.content_encoding.clone(),
|
||||
content_language: cached.content_language.clone(),
|
||||
version_id: cached.version_id.clone(),
|
||||
delete_marker: Some(cached.delete_marker),
|
||||
tag_count: cached.tag_count,
|
||||
metadata: if cached.user_metadata.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(cached.user_metadata.clone())
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// CRITICAL: Build ObjectInfo for event notification before calling complete().
|
||||
// This ensures S3 bucket notifications (s3:GetObject events) include proper
|
||||
// object metadata for event-driven workflows (Lambda, SNS, SQS).
|
||||
let event_info = ObjectInfo {
|
||||
bucket: bucket.clone(),
|
||||
name: key.clone(),
|
||||
storage_class: cached.storage_class.clone(),
|
||||
mod_time: cached
|
||||
.last_modified
|
||||
.as_ref()
|
||||
.and_then(|s| match OffsetDateTime::parse(s, &Rfc3339) {
|
||||
Ok(dt) => Some(Timestamp::from(dt)),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse cached last_modified '{}': {}", s, e);
|
||||
None
|
||||
}
|
||||
});
|
||||
.and_then(|s| OffsetDateTime::parse(s, &Rfc3339).ok()),
|
||||
size: cached.content_length,
|
||||
actual_size: cached.content_length,
|
||||
is_dir: false,
|
||||
user_defined: cached.user_metadata.clone(),
|
||||
version_id: cached.version_id.as_ref().and_then(|v| Uuid::parse_str(v).ok()),
|
||||
delete_marker: cached.delete_marker,
|
||||
content_type: cached.content_type.clone(),
|
||||
content_encoding: cached.content_encoding.clone(),
|
||||
etag: cached.e_tag.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Parse content_type
|
||||
let content_type = cached.content_type.as_ref().and_then(|ct| ContentType::from_str(ct).ok());
|
||||
// Set object info and version_id on helper for proper event notification
|
||||
let version_id_str = req.input.version_id.clone().unwrap_or_default();
|
||||
helper = helper.object(event_info).version_id(version_id_str);
|
||||
|
||||
let output = GetObjectOutput {
|
||||
body,
|
||||
content_length: Some(cached.content_length),
|
||||
accept_ranges: Some("bytes".to_string()),
|
||||
e_tag: cached.e_tag.as_ref().map(|etag| to_s3s_etag(etag)),
|
||||
last_modified,
|
||||
content_type,
|
||||
cache_control: cached.cache_control.clone(),
|
||||
content_disposition: cached.content_disposition.clone(),
|
||||
content_encoding: cached.content_encoding.clone(),
|
||||
content_language: cached.content_language.clone(),
|
||||
version_id: cached.version_id.clone(),
|
||||
delete_marker: Some(cached.delete_marker),
|
||||
tag_count: cached.tag_count,
|
||||
metadata: if cached.user_metadata.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(cached.user_metadata.clone())
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// CRITICAL: Build ObjectInfo for event notification before calling complete().
|
||||
// This ensures S3 bucket notifications (s3:GetObject events) include proper
|
||||
// object metadata for event-driven workflows (Lambda, SNS, SQS).
|
||||
let event_info = ObjectInfo {
|
||||
bucket: bucket.clone(),
|
||||
name: key.clone(),
|
||||
storage_class: cached.storage_class.clone(),
|
||||
mod_time: cached
|
||||
.last_modified
|
||||
.as_ref()
|
||||
.and_then(|s| OffsetDateTime::parse(s, &Rfc3339).ok()),
|
||||
size: cached.content_length,
|
||||
actual_size: cached.content_length,
|
||||
is_dir: false,
|
||||
user_defined: cached.user_metadata.clone(),
|
||||
version_id: cached.version_id.as_ref().and_then(|v| Uuid::parse_str(v).ok()),
|
||||
delete_marker: cached.delete_marker,
|
||||
content_type: cached.content_type.clone(),
|
||||
content_encoding: cached.content_encoding.clone(),
|
||||
etag: cached.e_tag.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Set object info and version_id on helper for proper event notification
|
||||
let version_id_str = req.input.version_id.clone().unwrap_or_default();
|
||||
helper = helper.object(event_info).version_id(version_id_str);
|
||||
|
||||
// Call helper.complete() for cache hits to ensure
|
||||
// S3 bucket notifications (s3:GetObject events) are triggered.
|
||||
// This ensures event-driven workflows (Lambda, SNS) work correctly
|
||||
// for both cache hits and misses.
|
||||
let result = Ok(S3Response::new(output));
|
||||
let _ = helper.complete(&result);
|
||||
return result;
|
||||
}
|
||||
// Call helper.complete() for cache hits to ensure
|
||||
// S3 bucket notifications (s3:GetObject events) are triggered.
|
||||
// This ensures event-driven workflows (Lambda, SNS) work correctly
|
||||
// for both cache hits and misses.
|
||||
let result = Ok(S3Response::new(output));
|
||||
let _ = helper.complete(&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: getObjectInArchiveFileHandler object = xxx.zip/xxx/xxx.xxx
|
||||
@@ -1954,10 +1953,10 @@ impl S3 for FS {
|
||||
|
||||
let part_number = part_number.map(|v| v as usize);
|
||||
|
||||
if let Some(part_num) = part_number {
|
||||
if part_num == 0 {
|
||||
return Err(s3_error!(InvalidArgument, "Invalid part number: part number must be greater than 0"));
|
||||
}
|
||||
if let Some(part_num) = part_number
|
||||
&& part_num == 0
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "Invalid part number: part number must be greater than 0"));
|
||||
}
|
||||
|
||||
let rs = range.map(|v| match v {
|
||||
@@ -2065,10 +2064,10 @@ impl S3 for FS {
|
||||
|
||||
let mut rs = rs;
|
||||
|
||||
if let Some(part_number) = part_number {
|
||||
if rs.is_none() {
|
||||
rs = HTTPRangeSpec::from_object_info(&info, part_number);
|
||||
}
|
||||
if let Some(part_number) = part_number
|
||||
&& rs.is_none()
|
||||
{
|
||||
rs = HTTPRangeSpec::from_object_info(&info, part_number);
|
||||
}
|
||||
|
||||
let mut content_length = info.get_actual_size().map_err(ApiError::from)?;
|
||||
@@ -2183,24 +2182,23 @@ impl S3 for FS {
|
||||
}
|
||||
}
|
||||
|
||||
if stored_sse_algorithm.is_none() {
|
||||
if let Some((key_bytes, nonce, original_size)) =
|
||||
if stored_sse_algorithm.is_none()
|
||||
&& let Some((key_bytes, nonce, original_size)) =
|
||||
decrypt_managed_encryption_key(&bucket, &key, &info.user_defined).await?
|
||||
{
|
||||
if info.parts.len() > 1 {
|
||||
let (reader, plain_size) = decrypt_multipart_managed_stream(final_stream, &info.parts, key_bytes, nonce)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
final_stream = reader;
|
||||
managed_original_size = Some(plain_size);
|
||||
} else {
|
||||
let warp_reader = WarpReader::new(final_stream);
|
||||
let decrypt_reader = DecryptReader::new(warp_reader, key_bytes, nonce);
|
||||
final_stream = Box::new(decrypt_reader);
|
||||
managed_original_size = original_size;
|
||||
}
|
||||
managed_encryption_applied = true;
|
||||
{
|
||||
if info.parts.len() > 1 {
|
||||
let (reader, plain_size) = decrypt_multipart_managed_stream(final_stream, &info.parts, key_bytes, nonce)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
final_stream = reader;
|
||||
managed_original_size = Some(plain_size);
|
||||
} else {
|
||||
let warp_reader = WarpReader::new(final_stream);
|
||||
let decrypt_reader = DecryptReader::new(warp_reader, key_bytes, nonce);
|
||||
final_stream = Box::new(decrypt_reader);
|
||||
managed_original_size = original_size;
|
||||
}
|
||||
managed_encryption_applied = true;
|
||||
}
|
||||
|
||||
// For SSE-C encrypted objects, use the original size instead of encrypted size
|
||||
@@ -2518,10 +2516,10 @@ impl S3 for FS {
|
||||
|
||||
let part_number = part_number.map(|v| v as usize);
|
||||
|
||||
if let Some(part_num) = part_number {
|
||||
if part_num == 0 {
|
||||
return Err(s3_error!(InvalidArgument, "part_number invalid"));
|
||||
}
|
||||
if let Some(part_num) = part_number
|
||||
&& part_num == 0
|
||||
{
|
||||
return Err(s3_error!(InvalidArgument, "part_number invalid"));
|
||||
}
|
||||
|
||||
let rs = range.map(|v| match v {
|
||||
@@ -2558,16 +2556,14 @@ impl S3 for FS {
|
||||
return Err(S3Error::new(S3ErrorCode::MethodNotAllowed));
|
||||
}
|
||||
|
||||
if let Some(match_etag) = if_none_match {
|
||||
if let Some(strong_etag) = match_etag.into_etag() {
|
||||
if info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::NotModified));
|
||||
}
|
||||
}
|
||||
if let Some(match_etag) = if_none_match
|
||||
&& let Some(strong_etag) = match_etag.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::NotModified));
|
||||
}
|
||||
|
||||
if let Some(modified_since) = if_modified_since {
|
||||
@@ -2581,22 +2577,21 @@ impl S3 for FS {
|
||||
}
|
||||
|
||||
if let Some(match_etag) = if_match {
|
||||
if let Some(strong_etag) = match_etag.into_etag() {
|
||||
if info
|
||||
if let Some(strong_etag) = match_etag.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag)
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::PreconditionFailed));
|
||||
}
|
||||
}
|
||||
} else if let Some(unmodified_since) = if_unmodified_since {
|
||||
if info.mod_time.is_some_and(|mod_time| {
|
||||
let give_time: OffsetDateTime = unmodified_since.into();
|
||||
mod_time > give_time.add(time::Duration::seconds(1))
|
||||
}) {
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::PreconditionFailed));
|
||||
}
|
||||
} else if let Some(unmodified_since) = if_unmodified_since
|
||||
&& info.mod_time.is_some_and(|mod_time| {
|
||||
let give_time: OffsetDateTime = unmodified_since.into();
|
||||
mod_time > give_time.add(time::Duration::seconds(1))
|
||||
})
|
||||
{
|
||||
return Err(S3Error::new(S3ErrorCode::PreconditionFailed));
|
||||
}
|
||||
|
||||
let event_info = info.clone();
|
||||
@@ -3080,10 +3075,10 @@ impl S3 for FS {
|
||||
let input = req.input;
|
||||
|
||||
// Save SSE-C parameters before moving input
|
||||
if let Some(ref storage_class) = input.storage_class {
|
||||
if !is_valid_storage_class(storage_class.as_str()) {
|
||||
return Err(s3_error!(InvalidStorageClass));
|
||||
}
|
||||
if let Some(ref storage_class) = input.storage_class
|
||||
&& !is_valid_storage_class(storage_class.as_str())
|
||||
{
|
||||
return Err(s3_error!(InvalidStorageClass));
|
||||
}
|
||||
let PutObjectInput {
|
||||
body,
|
||||
@@ -3116,27 +3111,23 @@ impl S3 for FS {
|
||||
match store.get_object_info(&bucket, &key, &ObjectOptions::default()).await {
|
||||
Ok(info) => {
|
||||
if !info.delete_marker {
|
||||
if let Some(ifmatch) = if_match {
|
||||
if let Some(strong_etag) = ifmatch.into_etag() {
|
||||
if info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
if let Some(ifmatch) = if_match
|
||||
&& let Some(strong_etag) = ifmatch.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
if let Some(ifnonematch) = if_none_match {
|
||||
if let Some(strong_etag) = ifnonematch.into_etag() {
|
||||
if info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
if let Some(ifnonematch) = if_none_match
|
||||
&& let Some(strong_etag) = ifnonematch.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3344,30 +3335,27 @@ impl S3 for FS {
|
||||
}
|
||||
|
||||
// Apply managed SSE (SSE-S3 or SSE-KMS) when requested
|
||||
if sse_customer_algorithm.is_none() {
|
||||
if let Some(sse_alg) = &effective_sse {
|
||||
if is_managed_sse(sse_alg) {
|
||||
let material =
|
||||
create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size)
|
||||
.await?;
|
||||
if sse_customer_algorithm.is_none()
|
||||
&& let Some(sse_alg) = &effective_sse
|
||||
&& is_managed_sse(sse_alg)
|
||||
{
|
||||
let material =
|
||||
create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size).await?;
|
||||
|
||||
let ManagedEncryptionMaterial {
|
||||
data_key,
|
||||
headers,
|
||||
kms_key_id: kms_key_used,
|
||||
} = material;
|
||||
let ManagedEncryptionMaterial {
|
||||
data_key,
|
||||
headers,
|
||||
kms_key_id: kms_key_used,
|
||||
} = material;
|
||||
|
||||
let key_bytes = data_key.plaintext_key;
|
||||
let nonce = data_key.nonce;
|
||||
let key_bytes = data_key.plaintext_key;
|
||||
let nonce = data_key.nonce;
|
||||
|
||||
metadata.extend(headers);
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
metadata.extend(headers);
|
||||
effective_kms_key_id = Some(kms_key_used.clone());
|
||||
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader =
|
||||
HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
}
|
||||
let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce);
|
||||
reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
@@ -3428,8 +3416,8 @@ impl S3 for FS {
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
if let Some(alg) = &input.checksum_algorithm
|
||||
&& let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
@@ -3443,15 +3431,15 @@ impl S3 for FS {
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
{
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3495,10 +3483,10 @@ impl S3 for FS {
|
||||
} = req.input.clone();
|
||||
|
||||
// Validate storage class if provided
|
||||
if let Some(ref storage_class) = storage_class {
|
||||
if !is_valid_storage_class(storage_class.as_str()) {
|
||||
return Err(s3_error!(InvalidStorageClass));
|
||||
}
|
||||
if let Some(ref storage_class) = storage_class
|
||||
&& !is_valid_storage_class(storage_class.as_str())
|
||||
{
|
||||
return Err(s3_error!(InvalidStorageClass));
|
||||
}
|
||||
|
||||
// mc cp step 3
|
||||
@@ -3654,10 +3642,10 @@ impl S3 for FS {
|
||||
let mut body_stream = body.ok_or_else(|| s3_error!(IncompleteBody))?;
|
||||
|
||||
if size.is_none() {
|
||||
if let Some(val) = req.headers.get(AMZ_DECODED_CONTENT_LENGTH) {
|
||||
if let Some(x) = atoi::atoi::<i64>(val.as_bytes()) {
|
||||
size = Some(x);
|
||||
}
|
||||
if let Some(val) = req.headers.get(AMZ_DECODED_CONTENT_LENGTH)
|
||||
&& let Some(x) = atoi::atoi::<i64>(val.as_bytes())
|
||||
{
|
||||
size = Some(x);
|
||||
}
|
||||
|
||||
if size.is_none() {
|
||||
@@ -3828,8 +3816,8 @@ impl S3 for FS {
|
||||
let mut checksum_sha256 = input.checksum_sha256;
|
||||
let mut checksum_crc64nvme = input.checksum_crc64nvme;
|
||||
|
||||
if let Some(alg) = &input.checksum_algorithm {
|
||||
if let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
if let Some(alg) = &input.checksum_algorithm
|
||||
&& let Some(Some(checksum_str)) = req.trailing_headers.as_ref().map(|trailer| {
|
||||
let key = match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => rustfs_rio::ChecksumType::CRC32.key(),
|
||||
ChecksumAlgorithm::CRC32C => rustfs_rio::ChecksumType::CRC32C.key(),
|
||||
@@ -3843,15 +3831,15 @@ impl S3 for FS {
|
||||
.get(key.unwrap_or_default())
|
||||
.and_then(|value| value.to_str().ok().map(|s| s.to_string()))
|
||||
})
|
||||
}) {
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
{
|
||||
match alg.as_str() {
|
||||
ChecksumAlgorithm::CRC32 => checksum_crc32 = checksum_str,
|
||||
ChecksumAlgorithm::CRC32C => checksum_crc32c = checksum_str,
|
||||
ChecksumAlgorithm::SHA1 => checksum_sha1 = checksum_str,
|
||||
ChecksumAlgorithm::SHA256 => checksum_sha256 = checksum_str,
|
||||
ChecksumAlgorithm::CRC64NVME => checksum_crc64nvme = checksum_str,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3949,16 +3937,14 @@ impl S3 for FS {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(if_none_match) = copy_source_if_none_match {
|
||||
if let Some(ref etag) = src_info.etag {
|
||||
if let Some(strong_etag) = if_none_match.into_etag() {
|
||||
if ETag::Strong(etag.clone()) == strong_etag {
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
// Weak ETag in If-None-Match is ignored (doesn't match)
|
||||
}
|
||||
if let Some(if_none_match) = copy_source_if_none_match
|
||||
&& let Some(ref etag) = src_info.etag
|
||||
&& let Some(strong_etag) = if_none_match.into_etag()
|
||||
&& ETag::Strong(etag.clone()) == strong_etag
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
// Weak ETag in If-None-Match is ignored (doesn't match)
|
||||
|
||||
// TODO: Implement proper time comparison for if_modified_since and if_unmodified_since
|
||||
// For now, we'll skip these conditions
|
||||
@@ -4157,10 +4143,10 @@ impl S3 for FS {
|
||||
|
||||
let max_uploads = max_uploads.map(|x| x as usize).unwrap_or(MAX_PARTS_COUNT);
|
||||
|
||||
if let Some(key_marker) = &key_marker {
|
||||
if !key_marker.starts_with(prefix.as_str()) {
|
||||
return Err(s3_error!(NotImplemented, "Invalid key marker"));
|
||||
}
|
||||
if let Some(key_marker) = &key_marker
|
||||
&& !key_marker.starts_with(prefix.as_str())
|
||||
{
|
||||
return Err(s3_error!(NotImplemented, "Invalid key marker"));
|
||||
}
|
||||
|
||||
let result = store
|
||||
@@ -4227,27 +4213,23 @@ impl S3 for FS {
|
||||
match store.get_object_info(&bucket, &key, &ObjectOptions::default()).await {
|
||||
Ok(info) => {
|
||||
if !info.delete_marker {
|
||||
if let Some(ifmatch) = if_match {
|
||||
if let Some(strong_etag) = ifmatch.into_etag() {
|
||||
if info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
if let Some(ifmatch) = if_match
|
||||
&& let Some(strong_etag) = ifmatch.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
if let Some(ifnonematch) = if_none_match {
|
||||
if let Some(strong_etag) = ifnonematch.into_etag() {
|
||||
if info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
if let Some(ifnonematch) = if_none_match
|
||||
&& let Some(strong_etag) = ifnonematch.into_etag()
|
||||
&& info
|
||||
.etag
|
||||
.as_ref()
|
||||
.is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag)
|
||||
{
|
||||
return Err(s3_error!(PreconditionFailed));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4852,11 +4834,11 @@ impl S3 for FS {
|
||||
let Some(input_cfg) = lifecycle_configuration else { return Err(s3_error!(InvalidArgument)) };
|
||||
|
||||
let rcfg = metadata_sys::get_object_lock_config(&bucket).await;
|
||||
if let Ok(rcfg) = rcfg {
|
||||
if let Err(err) = input_cfg.validate(&rcfg.0).await {
|
||||
//return Err(S3Error::with_message(S3ErrorCode::Custom("BucketLockValidateFailed".into()), err.to_string()));
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ValidateFailed".into()), err.to_string()));
|
||||
}
|
||||
if let Ok(rcfg) = rcfg
|
||||
&& let Err(err) = input_cfg.validate(&rcfg.0).await
|
||||
{
|
||||
//return Err(S3Error::with_message(S3ErrorCode::Custom("BucketLockValidateFailed".into()), err.to_string()));
|
||||
return Err(S3Error::with_message(S3ErrorCode::Custom("ValidateFailed".into()), err.to_string()));
|
||||
}
|
||||
|
||||
if let Err(err) = validate_transition_tier(&input_cfg).await {
|
||||
@@ -5735,23 +5717,23 @@ impl S3 for FS {
|
||||
|
||||
/// Auxiliary functions: extract prefixes and suffixes
|
||||
fn extract_prefix_suffix(filter: Option<&NotificationConfigurationFilter>) -> (String, String) {
|
||||
if let Some(filter) = filter {
|
||||
if let Some(filter_rules) = &filter.key {
|
||||
let mut prefix = String::new();
|
||||
let mut suffix = String::new();
|
||||
if let Some(rules) = &filter_rules.filter_rules {
|
||||
for rule in rules {
|
||||
if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) {
|
||||
match name.as_str() {
|
||||
"prefix" => prefix = value.clone(),
|
||||
"suffix" => suffix = value.clone(),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(filter) = filter
|
||||
&& let Some(filter_rules) = &filter.key
|
||||
{
|
||||
let mut prefix = String::new();
|
||||
let mut suffix = String::new();
|
||||
if let Some(rules) = &filter_rules.filter_rules {
|
||||
for rule in rules {
|
||||
if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) {
|
||||
match name.as_str() {
|
||||
"prefix" => prefix = value.clone(),
|
||||
"suffix" => suffix = value.clone(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (prefix, suffix);
|
||||
}
|
||||
return (prefix, suffix);
|
||||
}
|
||||
(String::new(), String::new())
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ impl OperationHelper {
|
||||
.req_path(req.uri.path().to_string())
|
||||
.req_query(extract_req_params(req));
|
||||
|
||||
if let Some(req_id) = req.headers.get("x-amz-request-id") {
|
||||
if let Ok(id_str) = req_id.to_str() {
|
||||
audit_builder = audit_builder.request_id(id_str);
|
||||
}
|
||||
if let Some(req_id) = req.headers.get("x-amz-request-id")
|
||||
&& let Ok(id_str) = req_id.to_str()
|
||||
{
|
||||
audit_builder = audit_builder.request_id(id_str);
|
||||
}
|
||||
|
||||
// initialize event builder
|
||||
@@ -194,15 +194,15 @@ impl Drop for OperationHelper {
|
||||
}
|
||||
|
||||
// Distribute event notification (only on success)
|
||||
if self.api_builder.0.status.as_deref() == Some("success") {
|
||||
if let Some(builder) = self.event_builder.take() {
|
||||
let event_args = builder.build();
|
||||
// Avoid generating notifications for copy requests
|
||||
if !event_args.is_replication_request() {
|
||||
spawn_background(async move {
|
||||
notifier_global::notify(event_args).await;
|
||||
});
|
||||
}
|
||||
if self.api_builder.0.status.as_deref() == Some("success")
|
||||
&& let Some(builder) = self.event_builder.take()
|
||||
{
|
||||
let event_args = builder.build();
|
||||
// Avoid generating notifications for copy requests
|
||||
if !event_args.is_replication_request() {
|
||||
spawn_background(async move {
|
||||
notifier_global::notify(event_args).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +64,12 @@ pub async fn del_opts(
|
||||
|
||||
let vid = vid.map(|v| v.as_str().trim().to_owned());
|
||||
|
||||
if let Some(ref id) = vid {
|
||||
if *id != Uuid::nil().to_string()
|
||||
&& let Err(err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
error!("del_opts: invalid version id: {} error: {}", id, err);
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
if let Some(ref id) = vid
|
||||
&& *id != Uuid::nil().to_string()
|
||||
&& let Err(err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
error!("del_opts: invalid version id: {} error: {}", id, err);
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
|
||||
let mut opts = put_opts_from_headers(headers, metadata.clone()).map_err(|err| {
|
||||
@@ -111,12 +110,11 @@ pub async fn get_opts(
|
||||
|
||||
let vid = vid.map(|v| v.as_str().trim().to_owned());
|
||||
|
||||
if let Some(ref id) = vid {
|
||||
if *id != Uuid::nil().to_string()
|
||||
&& let Err(_err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
if let Some(ref id) = vid
|
||||
&& *id != Uuid::nil().to_string()
|
||||
&& let Err(_err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
|
||||
let mut opts = get_default_opts(headers, HashMap::new(), false)
|
||||
@@ -187,12 +185,11 @@ pub async fn put_opts(
|
||||
|
||||
let vid = vid.map(|v| v.as_str().trim().to_owned());
|
||||
|
||||
if let Some(ref id) = vid {
|
||||
if *id != Uuid::nil().to_string()
|
||||
&& let Err(_err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
if let Some(ref id) = vid
|
||||
&& *id != Uuid::nil().to_string()
|
||||
&& let Err(_err) = Uuid::parse_str(id.as_str())
|
||||
{
|
||||
return Err(StorageError::InvalidVersionID(bucket.to_owned(), object.to_owned(), id.clone()));
|
||||
}
|
||||
|
||||
let mut opts = put_opts_from_headers(headers, metadata)
|
||||
@@ -512,12 +509,11 @@ fn skip_content_sha256_cksum(headers: &HeaderMap<HeaderValue>) -> bool {
|
||||
// such broken clients and content-length > 0.
|
||||
// For now, we'll assume strict compatibility is disabled
|
||||
// In a real implementation, you would check a global config
|
||||
if let Some(content_length) = headers.get("content-length") {
|
||||
if let Ok(length_str) = content_length.to_str() {
|
||||
if let Ok(length) = length_str.parse::<i64>() {
|
||||
return length > 0; // && !global_server_ctxt.strict_s3_compat
|
||||
}
|
||||
}
|
||||
if let Some(content_length) = headers.get("content-length")
|
||||
&& let Ok(length_str) = content_length.to_str()
|
||||
&& let Ok(length) = length_str.parse::<i64>()
|
||||
{
|
||||
return length > 0; // && !global_server_ctxt.strict_s3_compat
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -546,10 +542,10 @@ fn get_content_sha256_cksum(headers: &HeaderMap<HeaderValue>, service_type: Serv
|
||||
};
|
||||
|
||||
// We found 'X-Amz-Content-Sha256' return the captured value.
|
||||
if let Some(header_value) = content_sha256 {
|
||||
if let Ok(value) = header_value.to_str() {
|
||||
return value.to_string();
|
||||
}
|
||||
if let Some(header_value) = content_sha256
|
||||
&& let Ok(value) = header_value.to_str()
|
||||
{
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// We couldn't find 'X-Amz-Content-Sha256'.
|
||||
|
||||
@@ -75,10 +75,10 @@ fn increment_version(version: &str) -> Result<String, Box<dyn std::error::Error>
|
||||
let (major, minor, patch, pre_release) = parse_version(version)?;
|
||||
|
||||
// If there's a pre-release identifier, increment the pre-release version number
|
||||
if let Some(pre) = pre_release {
|
||||
if let Some(new_pre) = increment_pre_release(&pre) {
|
||||
return Ok(format!("{major}.{minor}.{patch}-{new_pre}"));
|
||||
}
|
||||
if let Some(pre) = pre_release
|
||||
&& let Some(new_pre) = increment_pre_release(&pre)
|
||||
{
|
||||
return Ok(format!("{major}.{minor}.{patch}-{new_pre}"));
|
||||
}
|
||||
|
||||
// Otherwise increment patch version number
|
||||
@@ -107,10 +107,10 @@ pub fn parse_version(version: &str) -> VersionParseResult {
|
||||
fn increment_pre_release(pre_release: &str) -> Option<String> {
|
||||
// Handle pre-release versions like "alpha.19"
|
||||
let parts: Vec<&str> = pre_release.split('.').collect();
|
||||
if parts.len() == 2 {
|
||||
if let Ok(num) = parts[1].parse::<u32>() {
|
||||
return Some(format!("{}.{}", parts[0], num + 1));
|
||||
}
|
||||
if parts.len() == 2
|
||||
&& let Ok(num) = parts[1].parse::<u32>()
|
||||
{
|
||||
return Some(format!("{}.{}", parts[0], num + 1));
|
||||
}
|
||||
|
||||
// Handle pre-release versions like "alpha19"
|
||||
|
||||
Reference in New Issue
Block a user