// 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::io::{Error, ErrorKind}; use http::HeaderMap; const X_AMZ_VERSION_ID: &str = "x-amz-version-id"; const X_OSS_VERSION_ID: &str = "x-oss-version-id"; const X_COS_VERSION_ID: &str = "x-cos-version-id"; const X_OBS_VERSION_ID: &str = "x-obs-version-id"; const MAX_REMOTE_VERSION_ID_LEN: usize = 1024; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[allow(dead_code, reason = "bucket versioning states kept as a complete vocabulary (backlog#1823)")] pub(crate) enum BucketVersioningState { Unknown, Disabled, Suspended, Enabled, } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum RemoteVersion { Unknown, Disabled, SuspendedNull, Exact(String), } impl RemoteVersion { pub(crate) fn exact_id(&self) -> Option<&str> { match self { Self::SuspendedNull => Some("null"), Self::Exact(version_id) => Some(version_id), Self::Unknown | Self::Disabled => None, } } #[allow(dead_code, reason = "MinIO-parity accessor with no caller in this port (backlog#1823)")] pub(crate) fn exact_request_id(&self) -> Result, Error> { match self { Self::Unknown => Err(Error::new( ErrorKind::InvalidData, "remote object version is unknown; exact version routing is unsafe", )), Self::Disabled => Ok(None), Self::SuspendedNull => Ok(Some("null")), Self::Exact(version_id) => Ok(Some(version_id)), } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ConditionalCreateCapability { Unsupported, IfNoneMatchStar, GenerationMatchZero, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct ProviderVersionCapabilities { raw_version_header: Option<&'static str>, pub(crate) bucket_versioning_state: bool, pub(crate) list_object_versions: bool, pub(crate) conditional_create: ConditionalCreateCapability, pub(crate) exact_get_delete: bool, } impl ProviderVersionCapabilities { pub(crate) fn for_tier_type(tier_type: &str) -> Self { if tier_type.eq_ignore_ascii_case("s3") || tier_type.eq_ignore_ascii_case("rustfs") || tier_type.eq_ignore_ascii_case("minio") || tier_type.eq_ignore_ascii_case("r2") || tier_type.eq_ignore_ascii_case("wasabi") { let list_object_versions = tier_type.eq_ignore_ascii_case("s3") || tier_type.eq_ignore_ascii_case("rustfs") || tier_type.eq_ignore_ascii_case("minio") || tier_type.eq_ignore_ascii_case("r2"); Self { raw_version_header: Some(X_AMZ_VERSION_ID), bucket_versioning_state: list_object_versions, list_object_versions, conditional_create: if tier_type.eq_ignore_ascii_case("s3") || tier_type.eq_ignore_ascii_case("r2") { ConditionalCreateCapability::IfNoneMatchStar } else { ConditionalCreateCapability::Unsupported }, exact_get_delete: true, } } else if tier_type.eq_ignore_ascii_case("aliyun") { Self { raw_version_header: Some(X_OSS_VERSION_ID), bucket_versioning_state: false, list_object_versions: false, conditional_create: ConditionalCreateCapability::Unsupported, exact_get_delete: true, } } else if tier_type.eq_ignore_ascii_case("tencent") { Self { raw_version_header: Some(X_COS_VERSION_ID), bucket_versioning_state: false, list_object_versions: false, conditional_create: ConditionalCreateCapability::Unsupported, exact_get_delete: true, } } else if tier_type.eq_ignore_ascii_case("huaweicloud") { Self { raw_version_header: Some(X_OBS_VERSION_ID), bucket_versioning_state: false, list_object_versions: false, conditional_create: ConditionalCreateCapability::Unsupported, exact_get_delete: true, } } else if tier_type.eq_ignore_ascii_case("gcs") { Self { raw_version_header: None, bucket_versioning_state: false, list_object_versions: false, conditional_create: ConditionalCreateCapability::GenerationMatchZero, exact_get_delete: false, } } else { Self { raw_version_header: None, bucket_versioning_state: false, list_object_versions: false, conditional_create: ConditionalCreateCapability::Unsupported, exact_get_delete: false, } } } pub(crate) fn raw_version_id(self, headers: &HeaderMap) -> Result, Error> { let Some(header_name) = self.raw_version_header else { return Ok(None); }; let Some(value) = headers.get(header_name) else { return Ok(None); }; let value = value .to_str() .map_err(|_| Error::new(ErrorKind::InvalidData, "remote object version id is not valid ASCII"))?; validate_remote_version_id(value)?; Ok(Some(value)) } pub(crate) fn remote_version(self, headers: &HeaderMap, versioning: BucketVersioningState) -> Result { let Some(value) = self.raw_version_id(headers)? else { return Ok(match versioning { BucketVersioningState::Disabled => RemoteVersion::Disabled, BucketVersioningState::Unknown | BucketVersioningState::Suspended | BucketVersioningState::Enabled => { RemoteVersion::Unknown } }); }; if value == "null" { return Ok(RemoteVersion::SuspendedNull); } Ok(RemoteVersion::Exact(value.to_string())) } } pub(crate) fn validate_remote_version_id(version_id: &str) -> Result<(), Error> { if version_id.is_empty() { return Err(Error::new( ErrorKind::InvalidData, "remote tier returned an empty object version id header", )); } if version_id.len() > MAX_REMOTE_VERSION_ID_LEN { return Err(Error::new( ErrorKind::InvalidData, "remote tier returned an oversized object version id header", )); } if version_id.chars().any(char::is_control) { return Err(Error::new( ErrorKind::InvalidData, "remote tier returned an object version id containing control characters", )); } Ok(()) } #[cfg(test)] mod tests { use super::{BucketVersioningState, ConditionalCreateCapability, ProviderVersionCapabilities, RemoteVersion}; use http::{HeaderMap, HeaderValue}; #[test] fn provider_version_header_matrix_preserves_opaque_versions() { for (tier_type, header_name) in [ ("s3", "x-amz-version-id"), ("S3", "x-amz-version-id"), ("rustfs", "x-amz-version-id"), ("RustFS", "x-amz-version-id"), ("minio", "x-amz-version-id"), ("MinIO", "x-amz-version-id"), ("r2", "x-amz-version-id"), ("R2", "x-amz-version-id"), ("wasabi", "x-amz-version-id"), ("Wasabi", "x-amz-version-id"), ("aliyun", "x-oss-version-id"), ("Aliyun", "x-oss-version-id"), ("tencent", "x-cos-version-id"), ("Tencent", "x-cos-version-id"), ("huaweicloud", "x-obs-version-id"), ("Huaweicloud", "x-obs-version-id"), ] { let mut headers = HeaderMap::new(); headers.insert(header_name, HeaderValue::from_static("opaque.version_01")); let capabilities = ProviderVersionCapabilities::for_tier_type(tier_type); assert_eq!(capabilities.raw_version_id(&headers).expect("raw version"), Some("opaque.version_01")); assert_eq!( capabilities .remote_version(&headers, BucketVersioningState::Enabled) .expect("remote version"), RemoteVersion::Exact("opaque.version_01".to_string()) ); } } #[test] fn provider_version_header_matrix_does_not_cross_read_sibling_headers() { let mut headers = HeaderMap::new(); headers.insert("x-amz-version-id", HeaderValue::from_static("aws-version")); headers.insert("x-cos-version-id", HeaderValue::from_static("cos-version")); assert_eq!( ProviderVersionCapabilities::for_tier_type("s3") .raw_version_id(&headers) .expect("aws raw version"), Some("aws-version") ); assert_eq!( ProviderVersionCapabilities::for_tier_type("tencent") .raw_version_id(&headers) .expect("cos raw version"), Some("cos-version") ); } #[test] fn provider_version_missing_header_is_unknown_until_bucket_state_is_known() { let headers = HeaderMap::new(); let capabilities = ProviderVersionCapabilities::for_tier_type("aliyun"); assert_eq!( capabilities .remote_version(&headers, BucketVersioningState::Unknown) .expect("unknown versioning"), RemoteVersion::Unknown ); assert_eq!( capabilities .remote_version(&headers, BucketVersioningState::Disabled) .expect("disabled versioning"), RemoteVersion::Disabled ); } #[test] fn provider_capability_matrix_is_conservative_and_provider_specific() { for (tier_type, state, list, conditional_create, exact_get_delete) in [ ("s3", true, true, ConditionalCreateCapability::IfNoneMatchStar, true), ("rustfs", true, true, ConditionalCreateCapability::Unsupported, true), ("minio", true, true, ConditionalCreateCapability::Unsupported, true), ("r2", true, true, ConditionalCreateCapability::IfNoneMatchStar, true), ("wasabi", false, false, ConditionalCreateCapability::Unsupported, true), ("aliyun", false, false, ConditionalCreateCapability::Unsupported, true), ("tencent", false, false, ConditionalCreateCapability::Unsupported, true), ("huaweicloud", false, false, ConditionalCreateCapability::Unsupported, true), ("gcs", false, false, ConditionalCreateCapability::GenerationMatchZero, false), ("azure", false, false, ConditionalCreateCapability::Unsupported, false), ("unsupported", false, false, ConditionalCreateCapability::Unsupported, false), ] { let capabilities = ProviderVersionCapabilities::for_tier_type(tier_type); assert_eq!(capabilities.bucket_versioning_state, state, "{tier_type} versioning state"); assert_eq!(capabilities.list_object_versions, list, "{tier_type} version listing"); assert_eq!(capabilities.conditional_create, conditional_create, "{tier_type} conditional create"); assert_eq!(capabilities.exact_get_delete, exact_get_delete, "{tier_type} exact routing"); } } #[test] fn remote_version_states_preserve_unknown_disabled_suspended_and_exact() { let capabilities = ProviderVersionCapabilities::for_tier_type("s3"); let empty = HeaderMap::new(); let mut null = HeaderMap::new(); null.insert("x-amz-version-id", HeaderValue::from_static("null")); let mut exact = HeaderMap::new(); exact.insert("x-amz-version-id", HeaderValue::from_static("opaque.generation-7")); for (headers, state, expected) in [ (&empty, BucketVersioningState::Unknown, RemoteVersion::Unknown), (&empty, BucketVersioningState::Disabled, RemoteVersion::Disabled), (&empty, BucketVersioningState::Suspended, RemoteVersion::Unknown), (&empty, BucketVersioningState::Enabled, RemoteVersion::Unknown), (&null, BucketVersioningState::Suspended, RemoteVersion::SuspendedNull), ( &exact, BucketVersioningState::Enabled, RemoteVersion::Exact("opaque.generation-7".to_string()), ), ] { assert_eq!( capabilities .remote_version(headers, state) .expect("version state should normalize"), expected ); } } #[test] fn exact_request_routing_fails_closed_for_unknown_versions() { for (version, expected) in [ (RemoteVersion::Disabled, None), (RemoteVersion::SuspendedNull, Some("null")), (RemoteVersion::Exact("opaque-v1".to_string()), Some("opaque-v1")), ] { assert_eq!(version.exact_request_id().expect("known version state"), expected); } assert!(RemoteVersion::Unknown.exact_request_id().is_err()); } #[test] fn provider_version_rejects_empty_or_oversized_headers() { let oversized = "v".repeat(1025); for bad in ["", oversized.as_str()] { let mut headers = HeaderMap::new(); headers.insert("x-oss-version-id", HeaderValue::from_str(bad).expect("test header value")); assert!( ProviderVersionCapabilities::for_tier_type("aliyun") .raw_version_id(&headers) .is_err() ); } } }