1use crate::aws::client::{S3Client, S3Config};
19use crate::aws::credential::{
20 EKSPodCredentialProvider, InstanceCredentialProvider, SessionProvider, TaskCredentialProvider,
21 WebIdentityProvider,
22};
23use crate::aws::{
24 AmazonS3, AwsCredential, AwsCredentialProvider, Checksum, S3ConditionalPut, S3CopyIfNotExists,
25 STORE,
26};
27use crate::client::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector};
28use crate::config::ConfigValue;
29use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
30use base64::Engine;
31use base64::prelude::BASE64_STANDARD;
32use http::header::{HeaderMap, HeaderValue};
33use itertools::Itertools;
34use md5::{Digest, Md5};
35use serde::{Deserialize, Serialize};
36use std::str::FromStr;
37use std::sync::Arc;
38use std::time::Duration;
39use tracing::debug;
40use url::Url;
41
42static DEFAULT_METADATA_ENDPOINT: &str = "http://169.254.169.254";
44
45#[derive(Debug, thiserror::Error)]
47enum Error {
48 #[error("Missing bucket name")]
49 MissingBucketName,
50
51 #[error("Missing AccessKeyId")]
52 MissingAccessKeyId,
53
54 #[error("Missing SecretAccessKey")]
55 MissingSecretAccessKey,
56
57 #[error("Unable parse source url. Url: {}, Error: {}", url, source)]
58 UnableToParseUrl {
59 source: url::ParseError,
60 url: String,
61 },
62
63 #[error(
64 "Unknown url scheme cannot be parsed into storage location: {}",
65 scheme
66 )]
67 UnknownUrlScheme { scheme: String },
68
69 #[error("URL did not match any known pattern for scheme: {}", url)]
70 UrlNotRecognised { url: String },
71
72 #[error("Configuration key: '{}' is not known.", key)]
73 UnknownConfigurationKey { key: String },
74
75 #[error("Invalid Zone suffix for bucket '{bucket}'")]
76 ZoneSuffix { bucket: String },
77
78 #[error(
79 "Invalid encryption type: {}. Valid values are \"AES256\", \"sse:kms\", \"sse:kms:dsse\" and \"sse-c\".",
80 passed
81 )]
82 InvalidEncryptionType { passed: String },
83
84 #[error(
85 "Invalid encryption header values. Header: {}, source: {}",
86 header,
87 source
88 )]
89 InvalidEncryptionHeader {
90 header: &'static str,
91 source: Box<dyn std::error::Error + Send + Sync + 'static>,
92 },
93}
94
95impl From<Error> for crate::Error {
96 fn from(source: Error) -> Self {
97 match source {
98 Error::UnknownConfigurationKey { key } => {
99 Self::UnknownConfigurationKey { store: STORE, key }
100 }
101 _ => Self::Generic {
102 store: STORE,
103 source: Box::new(source),
104 },
105 }
106 }
107}
108
109#[derive(Debug, Default, Clone)]
127pub struct AmazonS3Builder {
128 access_key_id: Option<String>,
130 secret_access_key: Option<String>,
132 region: Option<String>,
134 bucket_name: Option<String>,
136 endpoint: Option<String>,
138 s3_endpoint: Option<String>,
140 token: Option<String>,
142 url: Option<String>,
144 retry_config: RetryConfig,
146 imdsv1_fallback: ConfigValue<bool>,
148 virtual_hosted_style_request: ConfigValue<bool>,
150 s3_express: ConfigValue<bool>,
152 unsigned_payload: ConfigValue<bool>,
154 checksum_algorithm: Option<ConfigValue<Checksum>>,
156 metadata_endpoint: Option<String>,
158 container_credentials_relative_uri: Option<String>,
160 container_credentials_full_uri: Option<String>,
162 container_authorization_token_file: Option<String>,
164 web_identity_token_file: Option<String>,
166 role_arn: Option<String>,
168 role_session_name: Option<String>,
170 sts_endpoint: Option<String>,
172 client_options: ClientOptions,
174 credentials: Option<AwsCredentialProvider>,
176 crypto: Option<Arc<dyn CryptoProvider>>,
178 skip_signature: ConfigValue<bool>,
180 copy_if_not_exists: Option<ConfigValue<S3CopyIfNotExists>>,
182 conditional_put: ConfigValue<S3ConditionalPut>,
184 disable_tagging: ConfigValue<bool>,
186 disable_bulk_delete: ConfigValue<bool>,
188 encryption_type: Option<ConfigValue<S3EncryptionType>>,
190 encryption_kms_key_id: Option<String>,
191 encryption_bucket_key_enabled: Option<ConfigValue<bool>>,
192 encryption_customer_key_base64: Option<String>,
194 request_payer: ConfigValue<RequesterPayer>,
196 http_connector: Option<Arc<dyn HttpConnector>>,
198}
199
200#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
212#[non_exhaustive]
213pub enum AmazonS3ConfigKey {
214 AccessKeyId,
222
223 SecretAccessKey,
231
232 Region,
240
241 DefaultRegion,
249
250 Bucket,
260
261 Endpoint,
271
272 S3Endpoint,
279
280 Token,
290
291 ImdsV1Fallback,
299
300 VirtualHostedStyleRequest,
308
309 UnsignedPayload,
317
318 Checksum,
322
323 MetadataEndpoint,
331
332 ContainerCredentialsRelativeUri,
342
343 ContainerCredentialsFullUri,
353
354 ContainerAuthorizationTokenFile,
364
365 WebIdentityTokenFile,
373
374 RoleArn,
382
383 RoleSessionName,
389
390 StsEndpoint,
398
399 CopyIfNotExists,
407
408 ConditionalPut,
416
417 SkipSignature,
425
426 DisableTagging,
435
436 DisableBulkDelete,
448
449 S3Express,
455
456 RequestPayer,
462
463 Client(ClientConfigKey),
465
466 Encryption(S3EncryptionConfigKey),
468}
469
470impl AsRef<str> for AmazonS3ConfigKey {
471 fn as_ref(&self) -> &str {
472 match self {
473 Self::AccessKeyId => "aws_access_key_id",
474 Self::SecretAccessKey => "aws_secret_access_key",
475 Self::Region => "aws_region",
476 Self::Bucket => "aws_bucket",
477 Self::Endpoint => "aws_endpoint",
478 Self::S3Endpoint => "aws_endpoint_url_s3",
479 Self::Token => "aws_session_token",
480 Self::ImdsV1Fallback => "aws_imdsv1_fallback",
481 Self::VirtualHostedStyleRequest => "aws_virtual_hosted_style_request",
482 Self::S3Express => "aws_s3_express",
483 Self::DefaultRegion => "aws_default_region",
484 Self::MetadataEndpoint => "aws_metadata_endpoint",
485 Self::UnsignedPayload => "aws_unsigned_payload",
486 Self::Checksum => "aws_checksum_algorithm",
487 Self::ContainerCredentialsRelativeUri => "aws_container_credentials_relative_uri",
488 Self::ContainerCredentialsFullUri => "aws_container_credentials_full_uri",
489 Self::ContainerAuthorizationTokenFile => "aws_container_authorization_token_file",
490 Self::WebIdentityTokenFile => "aws_web_identity_token_file",
491 Self::RoleArn => "aws_role_arn",
492 Self::RoleSessionName => "aws_role_session_name",
493 Self::StsEndpoint => "aws_endpoint_url_sts",
494 Self::SkipSignature => "aws_skip_signature",
495 Self::CopyIfNotExists => "aws_copy_if_not_exists",
496 Self::ConditionalPut => "aws_conditional_put",
497 Self::DisableTagging => "aws_disable_tagging",
498 Self::DisableBulkDelete => "aws_disable_bulk_delete",
499 Self::RequestPayer => "aws_request_payer",
500 Self::Client(opt) => opt.as_ref(),
501 Self::Encryption(opt) => opt.as_ref(),
502 }
503 }
504}
505
506impl FromStr for AmazonS3ConfigKey {
507 type Err = crate::Error;
508
509 fn from_str(s: &str) -> Result<Self, Self::Err> {
510 match s {
511 "aws_access_key_id" | "access_key_id" => Ok(Self::AccessKeyId),
512 "aws_secret_access_key" | "secret_access_key" => Ok(Self::SecretAccessKey),
513 "aws_default_region" | "default_region" => Ok(Self::DefaultRegion),
514 "aws_region" | "region" => Ok(Self::Region),
515 "aws_bucket" | "aws_bucket_name" | "bucket_name" | "bucket" => Ok(Self::Bucket),
516 "aws_endpoint_url" | "aws_endpoint" | "endpoint_url" | "endpoint" => Ok(Self::Endpoint),
517 "aws_endpoint_url_s3" => Ok(Self::S3Endpoint),
518 "aws_session_token" | "aws_token" | "session_token" | "token" => Ok(Self::Token),
519 "aws_virtual_hosted_style_request" | "virtual_hosted_style_request" => {
520 Ok(Self::VirtualHostedStyleRequest)
521 }
522 "aws_s3_express" | "s3_express" => Ok(Self::S3Express),
523 "aws_imdsv1_fallback" | "imdsv1_fallback" => Ok(Self::ImdsV1Fallback),
524 "aws_metadata_endpoint" | "metadata_endpoint" => Ok(Self::MetadataEndpoint),
525 "aws_unsigned_payload" | "unsigned_payload" => Ok(Self::UnsignedPayload),
526 "aws_checksum_algorithm" | "checksum_algorithm" => Ok(Self::Checksum),
527 "aws_container_credentials_relative_uri" | "container_credentials_relative_uri" => {
528 Ok(Self::ContainerCredentialsRelativeUri)
529 }
530 "aws_container_credentials_full_uri" | "container_credentials_full_uri" => {
531 Ok(Self::ContainerCredentialsFullUri)
532 }
533 "aws_container_authorization_token_file" | "container_authorization_token_file" => {
534 Ok(Self::ContainerAuthorizationTokenFile)
535 }
536 "aws_web_identity_token_file" | "web_identity_token_file" => {
537 Ok(Self::WebIdentityTokenFile)
538 }
539 "aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
540 "aws_role_session_name" | "role_session_name" => Ok(Self::RoleSessionName),
541 "aws_endpoint_url_sts" | "endpoint_url_sts" => Ok(Self::StsEndpoint),
542 "aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
543 "aws_copy_if_not_exists" | "copy_if_not_exists" => Ok(Self::CopyIfNotExists),
544 "aws_conditional_put" | "conditional_put" => Ok(Self::ConditionalPut),
545 "aws_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
546 "aws_disable_bulk_delete" | "disable_bulk_delete" => Ok(Self::DisableBulkDelete),
547 "aws_request_payer" | "request_payer" => Ok(Self::RequestPayer),
548 "aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
550 "aws_server_side_encryption" | "server_side_encryption" => Ok(Self::Encryption(
551 S3EncryptionConfigKey::ServerSideEncryption,
552 )),
553 "aws_sse_kms_key_id" | "sse_kms_key_id" => {
554 Ok(Self::Encryption(S3EncryptionConfigKey::KmsKeyId))
555 }
556 "aws_sse_bucket_key_enabled" | "sse_bucket_key_enabled" => {
557 Ok(Self::Encryption(S3EncryptionConfigKey::BucketKeyEnabled))
558 }
559 "aws_sse_customer_key_base64" | "sse_customer_key_base64" => Ok(Self::Encryption(
560 S3EncryptionConfigKey::CustomerEncryptionKey,
561 )),
562 _ => match s.strip_prefix("aws_").unwrap_or(s).parse() {
563 Ok(key) => Ok(Self::Client(key)),
564 Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
565 },
566 }
567 }
568}
569
570impl AmazonS3Builder {
571 pub fn new() -> Self {
573 Default::default()
574 }
575
576 pub fn from_env() -> Self {
607 let mut builder: Self = Default::default();
608 for (os_key, os_value) in std::env::vars_os() {
609 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
610 if key.starts_with("AWS_") {
611 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
612 builder = builder.with_config(config_key, value);
613 }
614 }
615 }
616 }
617 builder
618 }
619
620 pub fn with_url(mut self, url: impl Into<String>) -> Self {
644 self.url = Some(url.into());
645 self
646 }
647
648 pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
650 match key {
651 AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
652 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
653 AmazonS3ConfigKey::Region => self.region = Some(value.into()),
654 AmazonS3ConfigKey::Bucket => self.bucket_name = Some(value.into()),
655 AmazonS3ConfigKey::Endpoint => self.endpoint = Some(value.into()),
656 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint = Some(value.into()),
657 AmazonS3ConfigKey::Token => self.token = Some(value.into()),
658 AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
659 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
660 self.virtual_hosted_style_request.parse(value)
661 }
662 AmazonS3ConfigKey::S3Express => self.s3_express.parse(value),
663 AmazonS3ConfigKey::DefaultRegion => {
664 self.region = self.region.or_else(|| Some(value.into()))
665 }
666 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
667 AmazonS3ConfigKey::UnsignedPayload => self.unsigned_payload.parse(value),
668 AmazonS3ConfigKey::Checksum => {
669 self.checksum_algorithm = Some(ConfigValue::Deferred(value.into()))
670 }
671 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
672 self.container_credentials_relative_uri = Some(value.into())
673 }
674 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
675 self.container_credentials_full_uri = Some(value.into());
676 }
677 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
678 self.container_authorization_token_file = Some(value.into());
679 }
680 AmazonS3ConfigKey::WebIdentityTokenFile => {
681 self.web_identity_token_file = Some(value.into());
682 }
683 AmazonS3ConfigKey::RoleArn => {
684 self.role_arn = Some(value.into());
685 }
686 AmazonS3ConfigKey::RoleSessionName => {
687 self.role_session_name = Some(value.into());
688 }
689 AmazonS3ConfigKey::StsEndpoint => {
690 self.sts_endpoint = Some(value.into());
691 }
692 AmazonS3ConfigKey::Client(key) => {
693 self.client_options = self.client_options.with_config(key, value)
694 }
695 AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
696 AmazonS3ConfigKey::DisableTagging => self.disable_tagging.parse(value),
697 AmazonS3ConfigKey::DisableBulkDelete => self.disable_bulk_delete.parse(value),
698 AmazonS3ConfigKey::CopyIfNotExists => {
699 self.copy_if_not_exists = Some(ConfigValue::Deferred(value.into()))
700 }
701 AmazonS3ConfigKey::ConditionalPut => {
702 self.conditional_put = ConfigValue::Deferred(value.into())
703 }
704 AmazonS3ConfigKey::RequestPayer => self.request_payer.parse(value),
705 AmazonS3ConfigKey::Encryption(key) => match key {
706 S3EncryptionConfigKey::ServerSideEncryption => {
707 self.encryption_type = Some(ConfigValue::Deferred(value.into()))
708 }
709 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id = Some(value.into()),
710 S3EncryptionConfigKey::BucketKeyEnabled => {
711 self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into()))
712 }
713 S3EncryptionConfigKey::CustomerEncryptionKey => {
714 self.encryption_customer_key_base64 = Some(value.into())
715 }
716 },
717 };
718 self
719 }
720
721 pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
733 match key {
734 AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
735 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
736 AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
737 AmazonS3ConfigKey::Bucket => self.bucket_name.clone(),
738 AmazonS3ConfigKey::Endpoint => self.endpoint.clone(),
739 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint.clone(),
740 AmazonS3ConfigKey::Token => self.token.clone(),
741 AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
742 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
743 Some(self.virtual_hosted_style_request.to_string())
744 }
745 AmazonS3ConfigKey::S3Express => Some(self.s3_express.to_string()),
746 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
747 AmazonS3ConfigKey::UnsignedPayload => Some(self.unsigned_payload.to_string()),
748 AmazonS3ConfigKey::Checksum => {
749 self.checksum_algorithm.as_ref().map(ToString::to_string)
750 }
751 AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
752 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
753 self.container_credentials_relative_uri.clone()
754 }
755 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
756 self.container_credentials_full_uri.clone()
757 }
758 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
759 self.container_authorization_token_file.clone()
760 }
761 AmazonS3ConfigKey::WebIdentityTokenFile => self.web_identity_token_file.clone(),
762 AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
763 AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
764 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
765 AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
766 AmazonS3ConfigKey::CopyIfNotExists => {
767 self.copy_if_not_exists.as_ref().map(ToString::to_string)
768 }
769 AmazonS3ConfigKey::ConditionalPut => Some(self.conditional_put.to_string()),
770 AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
771 AmazonS3ConfigKey::DisableBulkDelete => Some(self.disable_bulk_delete.to_string()),
772 AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
773 AmazonS3ConfigKey::Encryption(key) => match key {
774 S3EncryptionConfigKey::ServerSideEncryption => {
775 self.encryption_type.as_ref().map(ToString::to_string)
776 }
777 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id.clone(),
778 S3EncryptionConfigKey::BucketKeyEnabled => self
779 .encryption_bucket_key_enabled
780 .as_ref()
781 .map(ToString::to_string),
782 S3EncryptionConfigKey::CustomerEncryptionKey => {
783 self.encryption_customer_key_base64.clone()
784 }
785 },
786 }
787 }
788
789 fn parse_url(&mut self, url: &str) -> Result<()> {
794 let parsed = Url::parse(url).map_err(|source| {
795 let url = url.into();
796 Error::UnableToParseUrl { url, source }
797 })?;
798
799 let host = parsed
800 .host_str()
801 .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
802
803 match parsed.scheme() {
804 "s3" | "s3a" => self.bucket_name = Some(host.to_string()),
805 "https" => match host.splitn(4, '.').collect_tuple() {
806 Some(("s3", region, "amazonaws", "com")) => {
807 self.region = Some(region.to_string());
808 let bucket = parsed.path_segments().into_iter().flatten().next();
809 if let Some(bucket) = bucket {
810 self.bucket_name = Some(bucket.into());
811 }
812 }
813 Some(("s3", "dualstack", region, "amazonaws.com")) => {
815 self.region = Some(region.to_string());
816 self.endpoint = Some(format!("https://s3.dualstack.{region}.amazonaws.com"));
818 let bucket = parsed.path_segments().into_iter().flatten().next();
819 if let Some(bucket) = bucket {
820 self.bucket_name = Some(bucket.into());
821 }
822 }
823 Some((bucket, "s3", "amazonaws", "com")) => {
824 self.bucket_name = Some(bucket.to_string());
825 self.virtual_hosted_style_request = true.into();
826 }
827 Some((bucket, "s3", "dualstack", rest)) => {
830 let region = rest
831 .strip_suffix(".amazonaws.com")
832 .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
833 self.bucket_name = Some(bucket.to_string());
834 self.region = Some(region.to_string());
835 self.endpoint = Some(format!(
837 "https://{bucket}.s3.dualstack.{region}.amazonaws.com"
838 ));
839 self.virtual_hosted_style_request = true.into();
840 }
841 Some((bucket, "s3", region, "amazonaws.com")) => {
842 self.bucket_name = Some(bucket.to_string());
843 self.region = Some(region.to_string());
844 self.virtual_hosted_style_request = true.into();
845 }
846 Some((account, "r2", "cloudflarestorage", "com")) => {
847 self.region = Some("auto".to_string());
848 let endpoint = format!("https://{account}.r2.cloudflarestorage.com");
849 self.endpoint = Some(endpoint);
850
851 let bucket = parsed.path_segments().into_iter().flatten().next();
852 if let Some(bucket) = bucket {
853 self.bucket_name = Some(bucket.into());
854 }
855 }
856 Some((account, jurisdiction, "r2", "cloudflarestorage.com")) => {
857 self.region = Some("auto".to_string());
858 let endpoint =
859 format!("https://{account}.{jurisdiction}.r2.cloudflarestorage.com");
860 self.endpoint = Some(endpoint);
861
862 let bucket = parsed.path_segments().into_iter().flatten().next();
863 if let Some(bucket) = bucket {
864 self.bucket_name = Some(bucket.into());
865 }
866 }
867 _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
868 },
869 scheme => {
870 let scheme = scheme.into();
871 return Err(Error::UnknownUrlScheme { scheme }.into());
872 }
873 };
874 Ok(())
875 }
876
877 pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
881 self.access_key_id = Some(access_key_id.into());
882 self
883 }
884
885 pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
887 self.secret_access_key = Some(secret_access_key.into());
888 self
889 }
890
891 pub fn with_token(mut self, token: impl Into<String>) -> Self {
895 self.token = Some(token.into());
896 self
897 }
898
899 pub fn with_region(mut self, region: impl Into<String>) -> Self {
901 self.region = Some(region.into());
902 self
903 }
904
905 pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
907 self.bucket_name = Some(bucket_name.into());
908 self
909 }
910
911 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
927 self.endpoint = Some(endpoint.into());
928 self
929 }
930
931 pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
933 self.credentials = Some(credentials);
934 self
935 }
936
937 pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
939 self.crypto = Some(provider);
940 self
941 }
942
943 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
957 self.client_options = self.client_options.with_allow_http(allow_http);
958 self
959 }
960
961 pub fn with_virtual_hosted_style_request(mut self, virtual_hosted_style_request: bool) -> Self {
972 self.virtual_hosted_style_request = virtual_hosted_style_request.into();
973 self
974 }
975
976 pub fn with_s3_express(mut self, s3_express: bool) -> Self {
978 self.s3_express = s3_express.into();
979 self
980 }
981
982 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
984 self.retry_config = retry_config;
985 self
986 }
987
988 pub fn with_imdsv1_fallback(mut self) -> Self {
1000 self.imdsv1_fallback = true.into();
1001 self
1002 }
1003
1004 pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
1010 self.unsigned_payload = unsigned_payload.into();
1011 self
1012 }
1013
1014 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
1018 self.skip_signature = skip_signature.into();
1019 self
1020 }
1021
1022 pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
1026 self.checksum_algorithm = Some(checksum_algorithm.into());
1028 self
1029 }
1030
1031 pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
1037 self.metadata_endpoint = Some(endpoint.into());
1038 self
1039 }
1040
1041 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
1043 self.client_options = self.client_options.with_proxy_url(proxy_url);
1044 self
1045 }
1046
1047 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
1049 self.client_options = self
1050 .client_options
1051 .with_proxy_ca_certificate(proxy_ca_certificate);
1052 self
1053 }
1054
1055 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
1057 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
1058 self
1059 }
1060
1061 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1063 self.client_options = options;
1064 self
1065 }
1066
1067 pub fn with_copy_if_not_exists(mut self, config: S3CopyIfNotExists) -> Self {
1069 self.copy_if_not_exists = Some(config.into());
1070 self
1071 }
1072
1073 pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1076 self.conditional_put = config.into();
1077 self
1078 }
1079
1080 pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1082 self.disable_tagging = ignore.into();
1083 self
1084 }
1085
1086 pub fn with_disable_bulk_delete(mut self, disable: bool) -> Self {
1097 self.disable_bulk_delete = disable.into();
1098 self
1099 }
1100
1101 pub fn with_sse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1103 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseKms));
1104 if let Some(kms_key_id) = kms_key_id.into().into() {
1105 self.encryption_kms_key_id = Some(kms_key_id);
1106 }
1107 self
1108 }
1109
1110 pub fn with_dsse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1112 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::DsseKms));
1113 if let Some(kms_key_id) = kms_key_id.into().into() {
1114 self.encryption_kms_key_id = Some(kms_key_id);
1115 }
1116 self
1117 }
1118
1119 pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self {
1122 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC));
1123 self.encryption_customer_key_base64 = customer_key_base64.into().into();
1124 self
1125 }
1126
1127 pub fn with_bucket_key(mut self, enabled: bool) -> Self {
1134 self.encryption_bucket_key_enabled = Some(ConfigValue::Parsed(enabled));
1135 self
1136 }
1137
1138 pub fn with_request_payer(mut self, enabled: bool) -> Self {
1142 self.request_payer = ConfigValue::Parsed(enabled.into());
1143 self
1144 }
1145
1146 pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1150 self.http_connector = Some(Arc::new(connector));
1151 self
1152 }
1153
1154 pub fn build(mut self) -> Result<AmazonS3> {
1157 if let Some(url) = self.url.take() {
1158 self.parse_url(&url)?;
1159 }
1160
1161 let http = http_connector(self.http_connector)?;
1162
1163 let bucket = self.bucket_name.ok_or(Error::MissingBucketName)?;
1164 let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
1165 let checksum = self.checksum_algorithm.map(|x| x.get()).transpose()?;
1166 let copy_if_not_exists = self.copy_if_not_exists.map(|x| x.get()).transpose()?;
1167
1168 let credentials = if let Some(credentials) = self.credentials {
1169 credentials
1170 } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
1171 match (self.access_key_id, self.secret_access_key, self.token) {
1172 (Some(key_id), Some(secret_key), token) => {
1173 debug!("Using Static credential provider");
1174 let credential = AwsCredential {
1175 key_id,
1176 secret_key,
1177 token,
1178 };
1179 Arc::new(StaticCredentialProvider::new(credential)) as _
1180 }
1181 (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
1182 (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
1183 (None, None, _) => unreachable!(),
1184 }
1185 } else if let (Some(token_path), Some(role_arn)) =
1186 (self.web_identity_token_file, self.role_arn)
1187 {
1188 debug!("Using WebIdentity credential provider");
1189
1190 let session_name = self
1191 .role_session_name
1192 .clone()
1193 .unwrap_or_else(|| "WebIdentitySession".to_string());
1194
1195 let endpoint = self
1196 .sts_endpoint
1197 .clone()
1198 .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
1199
1200 let options = self.client_options.clone().with_allow_http(false);
1202
1203 let token = WebIdentityProvider {
1204 token_path: token_path.clone(),
1205 session_name,
1206 role_arn: role_arn.clone(),
1207 endpoint,
1208 };
1209
1210 Arc::new(TokenCredentialProvider::new(
1211 token,
1212 http.connect(&options)?,
1213 self.retry_config.clone(),
1214 )) as _
1215 } else if let Some(uri) = self.container_credentials_relative_uri {
1216 debug!("Using Task credential provider");
1217
1218 let options = self.client_options.clone().with_allow_http(true);
1219
1220 Arc::new(TaskCredentialProvider {
1221 url: format!("http://169.254.170.2{uri}"),
1222 retry: self.retry_config.clone(),
1223 client: http.connect(&options)?,
1225 cache: Default::default(),
1226 }) as _
1227 } else if let (Some(full_uri), Some(token_file)) = (
1228 self.container_credentials_full_uri,
1229 self.container_authorization_token_file,
1230 ) {
1231 debug!("Using EKS Pod Identity credential provider");
1232
1233 let options = self.client_options.clone().with_allow_http(true);
1234
1235 Arc::new(EKSPodCredentialProvider {
1236 url: full_uri,
1237 token_file,
1238 retry: self.retry_config.clone(),
1239 client: http.connect(&options)?,
1240 cache: Default::default(),
1241 }) as _
1242 } else {
1243 debug!("Using Instance credential provider");
1244
1245 let token = InstanceCredentialProvider {
1246 imdsv1_fallback: self.imdsv1_fallback.get()?,
1247 metadata_endpoint: self
1248 .metadata_endpoint
1249 .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
1250 };
1251
1252 Arc::new(TokenCredentialProvider::new(
1253 token,
1254 http.connect(&self.client_options.metadata_options())?,
1255 self.retry_config.clone(),
1256 )) as _
1257 };
1258
1259 let (session_provider, zonal_endpoint) = match self.s3_express.get()? {
1260 true => {
1261 let zone = parse_bucket_az(&bucket).ok_or_else(|| {
1262 let bucket = bucket.clone();
1263 Error::ZoneSuffix { bucket }
1264 })?;
1265
1266 let endpoint = format!("https://{bucket}.s3express-{zone}.{region}.amazonaws.com");
1268
1269 let session = Arc::new(
1270 TokenCredentialProvider::new(
1271 SessionProvider {
1272 endpoint: endpoint.clone(),
1273 region: region.clone(),
1274 credentials: Arc::clone(&credentials),
1275 crypto: self.crypto.clone(),
1276 },
1277 http.connect(&self.client_options)?,
1278 self.retry_config.clone(),
1279 )
1280 .with_min_ttl(Duration::from_secs(60)), );
1282 (Some(session as _), Some(endpoint))
1283 }
1284 false => (None, None),
1285 };
1286
1287 let endpoint = self.s3_endpoint.or(self.endpoint);
1289
1290 let virtual_hosted = self.virtual_hosted_style_request.get()?;
1293 let bucket_endpoint = match (&endpoint, zonal_endpoint, virtual_hosted) {
1294 (Some(endpoint), _, true) => endpoint.clone(),
1295 (Some(endpoint), _, false) => format!("{}/{}", endpoint.trim_end_matches("/"), bucket),
1296 (None, Some(endpoint), _) => endpoint,
1297 (None, None, true) => format!("https://{bucket}.s3.{region}.amazonaws.com"),
1298 (None, None, false) => format!("https://s3.{region}.amazonaws.com/{bucket}"),
1299 };
1300
1301 let encryption_headers = if let Some(encryption_type) = self.encryption_type {
1302 S3EncryptionHeaders::try_new(
1303 &encryption_type.get()?,
1304 self.encryption_kms_key_id,
1305 self.encryption_bucket_key_enabled
1306 .map(|val| val.get())
1307 .transpose()?,
1308 self.encryption_customer_key_base64,
1309 )?
1310 } else {
1311 S3EncryptionHeaders::default()
1312 };
1313
1314 let config = S3Config {
1315 region,
1316 bucket,
1317 bucket_endpoint,
1318 credentials,
1319 crypto: self.crypto,
1320 session_provider,
1321 retry_config: self.retry_config,
1322 client_options: self.client_options,
1323 sign_payload: !self.unsigned_payload.get()?,
1324 skip_signature: self.skip_signature.get()?,
1325 disable_tagging: self.disable_tagging.get()?,
1326 disable_bulk_delete: self.disable_bulk_delete.get()?,
1327 checksum,
1328 copy_if_not_exists,
1329 conditional_put: self.conditional_put.get()?,
1330 encryption_headers,
1331 request_payer: self.request_payer.get()?.into(),
1332 };
1333
1334 let http_client = http.connect(&config.client_options)?;
1335 let client = Arc::new(S3Client::new(config, http_client));
1336
1337 Ok(AmazonS3 { client })
1338 }
1339}
1340
1341fn parse_bucket_az(bucket: &str) -> Option<&str> {
1345 let base = bucket
1346 .strip_suffix("--x-s3")
1347 .or_else(|| bucket.strip_suffix("--xa-s3"))?;
1348 Some(base.rsplit_once("--")?.1)
1349}
1350
1351#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1355struct RequesterPayer(bool);
1356
1357impl crate::config::Parse for RequesterPayer {
1358 fn parse(v: &str) -> Result<Self> {
1359 if v.eq_ignore_ascii_case("requester") {
1360 Ok(Self(true))
1361 } else {
1362 Ok(Self(<bool as crate::config::Parse>::parse(v)?))
1363 }
1364 }
1365}
1366
1367impl From<bool> for RequesterPayer {
1368 fn from(value: bool) -> Self {
1369 Self(value)
1370 }
1371}
1372
1373impl From<RequesterPayer> for bool {
1374 fn from(value: RequesterPayer) -> Self {
1375 value.0
1376 }
1377}
1378
1379impl std::fmt::Display for RequesterPayer {
1380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1381 self.0.fmt(f)
1382 }
1383}
1384
1385#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1395#[non_exhaustive]
1396pub enum S3EncryptionConfigKey {
1397 ServerSideEncryption,
1409 KmsKeyId,
1419 BucketKeyEnabled,
1426
1427 CustomerEncryptionKey,
1435}
1436
1437impl AsRef<str> for S3EncryptionConfigKey {
1438 fn as_ref(&self) -> &str {
1439 match self {
1440 Self::ServerSideEncryption => "aws_server_side_encryption",
1441 Self::KmsKeyId => "aws_sse_kms_key_id",
1442 Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled",
1443 Self::CustomerEncryptionKey => "aws_sse_customer_key_base64",
1444 }
1445 }
1446}
1447
1448#[derive(Debug, Clone)]
1449enum S3EncryptionType {
1450 S3,
1451 SseKms,
1452 DsseKms,
1453 SseC,
1454}
1455
1456impl crate::config::Parse for S3EncryptionType {
1457 fn parse(s: &str) -> Result<Self> {
1458 match s {
1459 "AES256" => Ok(Self::S3),
1460 "aws:kms" => Ok(Self::SseKms),
1461 "aws:kms:dsse" => Ok(Self::DsseKms),
1462 "sse-c" => Ok(Self::SseC),
1463 _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()),
1464 }
1465 }
1466}
1467
1468impl From<&S3EncryptionType> for &'static str {
1469 fn from(value: &S3EncryptionType) -> Self {
1470 match value {
1471 S3EncryptionType::S3 => "AES256",
1472 S3EncryptionType::SseKms => "aws:kms",
1473 S3EncryptionType::DsseKms => "aws:kms:dsse",
1474 S3EncryptionType::SseC => "sse-c",
1475 }
1476 }
1477}
1478
1479impl std::fmt::Display for S3EncryptionType {
1480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1481 f.write_str(self.into())
1482 }
1483}
1484
1485#[derive(Default, Clone, Debug)]
1491pub(super) struct S3EncryptionHeaders(pub HeaderMap);
1492
1493impl S3EncryptionHeaders {
1494 fn try_new(
1495 encryption_type: &S3EncryptionType,
1496 encryption_kms_key_id: Option<String>,
1497 bucket_key_enabled: Option<bool>,
1498 encryption_customer_key_base64: Option<String>,
1499 ) -> Result<Self> {
1500 let mut headers = HeaderMap::new();
1501 match encryption_type {
1502 S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => {
1503 headers.insert(
1504 "x-amz-server-side-encryption",
1505 HeaderValue::from_static(encryption_type.into()),
1506 );
1507 if let Some(key_id) = encryption_kms_key_id {
1508 headers.insert(
1509 "x-amz-server-side-encryption-aws-kms-key-id",
1510 key_id
1511 .try_into()
1512 .map_err(|err| Error::InvalidEncryptionHeader {
1513 header: "kms-key-id",
1514 source: Box::new(err),
1515 })?,
1516 );
1517 }
1518 if let Some(bucket_key_enabled) = bucket_key_enabled {
1519 headers.insert(
1520 "x-amz-server-side-encryption-bucket-key-enabled",
1521 HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }),
1522 );
1523 }
1524 }
1525 S3EncryptionType::SseC => {
1526 headers.insert(
1527 "x-amz-server-side-encryption-customer-algorithm",
1528 HeaderValue::from_static("AES256"),
1529 );
1530 if let Some(key) = encryption_customer_key_base64 {
1531 let mut header_value: HeaderValue =
1532 key.clone()
1533 .try_into()
1534 .map_err(|err| Error::InvalidEncryptionHeader {
1535 header: "x-amz-server-side-encryption-customer-key",
1536 source: Box::new(err),
1537 })?;
1538 header_value.set_sensitive(true);
1539 headers.insert("x-amz-server-side-encryption-customer-key", header_value);
1540
1541 let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| {
1542 Error::InvalidEncryptionHeader {
1543 header: "x-amz-server-side-encryption-customer-key",
1544 source: Box::new(err),
1545 }
1546 })?;
1547 let mut hasher = Md5::new();
1548 hasher.update(decoded_key);
1549 let md5 = BASE64_STANDARD.encode(hasher.finalize());
1550 let mut md5_header_value: HeaderValue =
1551 md5.try_into()
1552 .map_err(|err| Error::InvalidEncryptionHeader {
1553 header: "x-amz-server-side-encryption-customer-key-MD5",
1554 source: Box::new(err),
1555 })?;
1556 md5_header_value.set_sensitive(true);
1557 headers.insert(
1558 "x-amz-server-side-encryption-customer-key-MD5",
1559 md5_header_value,
1560 );
1561 } else {
1562 return Err(Error::InvalidEncryptionHeader {
1563 header: "x-amz-server-side-encryption-customer-key",
1564 source: Box::new(std::io::Error::new(
1565 std::io::ErrorKind::InvalidInput,
1566 "Missing customer key",
1567 )),
1568 }
1569 .into());
1570 }
1571 }
1572 }
1573 Ok(Self(headers))
1574 }
1575}
1576
1577impl From<S3EncryptionHeaders> for HeaderMap {
1578 fn from(headers: S3EncryptionHeaders) -> Self {
1579 headers.0
1580 }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585 use super::*;
1586 use std::collections::HashMap;
1587
1588 #[test]
1589 fn s3_test_config_from_map() {
1590 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1591 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1592 let aws_default_region = "object_store:fake_default_region".to_string();
1593 let aws_endpoint = "object_store:fake_endpoint".to_string();
1594 let aws_session_token = "object_store:fake_session_token".to_string();
1595 let options = HashMap::from([
1596 ("aws_access_key_id", aws_access_key_id.clone()),
1597 ("aws_secret_access_key", aws_secret_access_key),
1598 ("aws_default_region", aws_default_region.clone()),
1599 ("aws_endpoint", aws_endpoint.clone()),
1600 ("aws_session_token", aws_session_token.clone()),
1601 ("aws_unsigned_payload", "true".to_string()),
1602 ("aws_checksum_algorithm", "sha256".to_string()),
1603 ]);
1604
1605 let builder = options
1606 .into_iter()
1607 .fold(AmazonS3Builder::new(), |builder, (key, value)| {
1608 builder.with_config(key.parse().unwrap(), value)
1609 })
1610 .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
1611
1612 assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
1613 assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
1614 assert_eq!(builder.region.unwrap(), aws_default_region);
1615 assert_eq!(builder.endpoint.unwrap(), aws_endpoint);
1616 assert_eq!(builder.token.unwrap(), aws_session_token);
1617 assert_eq!(
1618 builder.checksum_algorithm.unwrap().get().unwrap(),
1619 Checksum::SHA256
1620 );
1621 assert!(builder.unsigned_payload.get().unwrap());
1622 }
1623
1624 #[cfg(feature = "reqwest")]
1625 #[test]
1626 fn s3_test_endpoint_url_s3_config() {
1627 let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1629 assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1630
1631 let s3 = AmazonS3Builder::new()
1633 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1634 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1635 .with_bucket_name("test-bucket")
1636 .build()
1637 .unwrap();
1638 assert_eq!(
1639 s3.client.config.bucket_endpoint,
1640 "http://s3-specific-endpoint/test-bucket"
1641 );
1642
1643 let s3 = AmazonS3Builder::new()
1645 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1646 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1647 .with_bucket_name("test-bucket")
1648 .build()
1649 .unwrap();
1650 assert_eq!(
1651 s3.client.config.bucket_endpoint,
1652 "http://s3-specific-endpoint/test-bucket"
1653 );
1654 }
1655
1656 #[test]
1657 fn s3_test_config_get_value() {
1658 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1659 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1660 let aws_default_region = "object_store:fake_default_region".to_string();
1661 let aws_endpoint = "object_store:fake_endpoint".to_string();
1662 let aws_session_token = "object_store:fake_session_token".to_string();
1663
1664 let builder = AmazonS3Builder::new()
1665 .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
1666 .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
1667 .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
1668 .with_config(AmazonS3ConfigKey::Endpoint, &aws_endpoint)
1669 .with_config(AmazonS3ConfigKey::Token, &aws_session_token)
1670 .with_config(AmazonS3ConfigKey::UnsignedPayload, "true")
1671 .with_config("aws_server_side_encryption".parse().unwrap(), "AES256")
1672 .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id")
1673 .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true")
1674 .with_config(
1675 "aws_sse_customer_key_base64".parse().unwrap(),
1676 "some_customer_key",
1677 );
1678
1679 assert_eq!(
1680 builder
1681 .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
1682 .unwrap(),
1683 aws_access_key_id
1684 );
1685 assert_eq!(
1686 builder
1687 .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
1688 .unwrap(),
1689 aws_secret_access_key
1690 );
1691 assert_eq!(
1692 builder
1693 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
1694 .unwrap(),
1695 aws_default_region
1696 );
1697 assert_eq!(
1698 builder
1699 .get_config_value(&AmazonS3ConfigKey::Endpoint)
1700 .unwrap(),
1701 aws_endpoint
1702 );
1703 assert_eq!(
1704 builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
1705 aws_session_token
1706 );
1707 assert_eq!(
1708 builder
1709 .get_config_value(&AmazonS3ConfigKey::UnsignedPayload)
1710 .unwrap(),
1711 "true"
1712 );
1713 assert_eq!(
1714 builder
1715 .get_config_value(&"aws_server_side_encryption".parse().unwrap())
1716 .unwrap(),
1717 "AES256"
1718 );
1719 assert_eq!(
1720 builder
1721 .get_config_value(&"aws_sse_kms_key_id".parse().unwrap())
1722 .unwrap(),
1723 "some_key_id"
1724 );
1725 assert_eq!(
1726 builder
1727 .get_config_value(&"aws_sse_bucket_key_enabled".parse().unwrap())
1728 .unwrap(),
1729 "true"
1730 );
1731 assert_eq!(
1732 builder
1733 .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap())
1734 .unwrap(),
1735 "some_customer_key"
1736 );
1737 }
1738
1739 #[cfg(feature = "reqwest")]
1740 #[test]
1741 fn s3_default_region() {
1742 let builder = AmazonS3Builder::new()
1743 .with_bucket_name("foo")
1744 .build()
1745 .unwrap();
1746 assert_eq!(builder.client.config.region, "us-east-1");
1747 }
1748
1749 #[cfg(feature = "reqwest")]
1750 #[test]
1751 fn s3_test_bucket_endpoint() {
1752 let builder = AmazonS3Builder::new()
1753 .with_endpoint("http://some.host:1234")
1754 .with_bucket_name("foo")
1755 .build()
1756 .unwrap();
1757 assert_eq!(
1758 builder.client.config.bucket_endpoint,
1759 "http://some.host:1234/foo"
1760 );
1761
1762 let builder = AmazonS3Builder::new()
1763 .with_endpoint("http://some.host:1234/")
1764 .with_bucket_name("foo")
1765 .build()
1766 .unwrap();
1767 assert_eq!(
1768 builder.client.config.bucket_endpoint,
1769 "http://some.host:1234/foo"
1770 );
1771 }
1772
1773 #[test]
1774 fn s3_test_urls() {
1775 let mut builder = AmazonS3Builder::new();
1776 builder.parse_url("s3://bucket/path").unwrap();
1777 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1778
1779 let mut builder = AmazonS3Builder::new();
1780 builder
1781 .parse_url("s3://buckets.can.have.dots/path")
1782 .unwrap();
1783 assert_eq!(
1784 builder.bucket_name,
1785 Some("buckets.can.have.dots".to_string())
1786 );
1787
1788 let mut builder = AmazonS3Builder::new();
1789 builder
1790 .parse_url("https://s3.region.amazonaws.com")
1791 .unwrap();
1792 assert_eq!(builder.region, Some("region".to_string()));
1793
1794 let mut builder = AmazonS3Builder::new();
1795 builder
1796 .parse_url("https://s3.region.amazonaws.com/bucket")
1797 .unwrap();
1798 assert_eq!(builder.region, Some("region".to_string()));
1799 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1800
1801 let mut builder = AmazonS3Builder::new();
1802 builder
1803 .parse_url("https://s3.region.amazonaws.com/bucket.with.dot/path")
1804 .unwrap();
1805 assert_eq!(builder.region, Some("region".to_string()));
1806 assert_eq!(builder.bucket_name, Some("bucket.with.dot".to_string()));
1807
1808 let mut builder = AmazonS3Builder::new();
1809 builder
1810 .parse_url("https://bucket.s3.amazonaws.com")
1811 .unwrap();
1812 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1813 assert!(builder.virtual_hosted_style_request.get().unwrap());
1814
1815 let mut builder = AmazonS3Builder::new();
1816 builder
1817 .parse_url("https://bucket.s3.region.amazonaws.com")
1818 .unwrap();
1819 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1820 assert_eq!(builder.region, Some("region".to_string()));
1821 assert!(builder.virtual_hosted_style_request.get().unwrap());
1822
1823 let mut builder = AmazonS3Builder::new();
1825 builder
1826 .parse_url("https://s3.dualstack.us-east-1.amazonaws.com/bucket/path")
1827 .unwrap();
1828 assert_eq!(builder.region, Some("us-east-1".to_string()));
1829 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1830 assert_eq!(
1831 builder.endpoint,
1832 Some("https://s3.dualstack.us-east-1.amazonaws.com".to_string())
1833 );
1834 assert!(!builder.virtual_hosted_style_request.get().unwrap());
1835
1836 let mut builder = AmazonS3Builder::new();
1838 builder
1839 .parse_url("https://bucket.s3.dualstack.us-east-1.amazonaws.com")
1840 .unwrap();
1841 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1842 assert_eq!(builder.region, Some("us-east-1".to_string()));
1843 assert_eq!(
1844 builder.endpoint,
1845 Some("https://bucket.s3.dualstack.us-east-1.amazonaws.com".to_string())
1846 );
1847 assert!(builder.virtual_hosted_style_request.get().unwrap());
1848
1849 let mut builder = AmazonS3Builder::new();
1850 builder
1851 .parse_url("https://account123.r2.cloudflarestorage.com/bucket-123")
1852 .unwrap();
1853
1854 assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1855 assert_eq!(builder.region, Some("auto".to_string()));
1856 assert_eq!(
1857 builder.endpoint,
1858 Some("https://account123.r2.cloudflarestorage.com".to_string())
1859 );
1860
1861 let mut builder = AmazonS3Builder::new();
1862 builder
1863 .parse_url("https://account123.eu.r2.cloudflarestorage.com/bucket-123")
1864 .unwrap();
1865
1866 assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1867 assert_eq!(builder.region, Some("auto".to_string()));
1868 assert_eq!(
1869 builder.endpoint,
1870 Some("https://account123.eu.r2.cloudflarestorage.com".to_string())
1871 );
1872
1873 let err_cases = [
1874 "mailto://bucket/path",
1875 "https://s3.bucket.mydomain.com",
1876 "https://s3.bucket.foo.amazonaws.com",
1877 "https://bucket.mydomain.region.amazonaws.com",
1878 "https://bucket.s3.region.bar.amazonaws.com",
1879 "https://bucket.foo.s3.amazonaws.com",
1880 "https://account.r2.eu.cloudflarestorage.com/bucket",
1881 ];
1882 let mut builder = AmazonS3Builder::new();
1883 for case in err_cases {
1884 builder.parse_url(case).unwrap_err();
1885 }
1886 }
1887
1888 #[cfg(feature = "reqwest")]
1889 #[tokio::test]
1890 async fn s3_test_proxy_url() {
1891 let s3 = AmazonS3Builder::new()
1892 .with_access_key_id("access_key_id")
1893 .with_secret_access_key("secret_access_key")
1894 .with_region("region")
1895 .with_bucket_name("bucket_name")
1896 .with_allow_http(true)
1897 .with_proxy_url("https://example.com")
1898 .build();
1899
1900 assert!(s3.is_ok());
1901
1902 let err = AmazonS3Builder::new()
1903 .with_access_key_id("access_key_id")
1904 .with_secret_access_key("secret_access_key")
1905 .with_region("region")
1906 .with_bucket_name("bucket_name")
1907 .with_allow_http(true)
1908 .with_proxy_url("dxx:ddd\\example.com")
1910 .build()
1911 .unwrap_err()
1912 .to_string();
1913
1914 assert_eq!("Generic HTTP client error: builder error", err);
1915 }
1916
1917 #[cfg(feature = "reqwest")]
1918 #[test]
1919 fn test_invalid_config() {
1920 let err = AmazonS3Builder::new()
1921 .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
1922 .with_bucket_name("bucket")
1923 .with_region("region")
1924 .build()
1925 .unwrap_err()
1926 .to_string();
1927
1928 assert_eq!(
1929 err,
1930 "Generic Config error: failed to parse \"enabled\" as boolean"
1931 );
1932
1933 let err = AmazonS3Builder::new()
1934 .with_config(AmazonS3ConfigKey::Checksum, "md5")
1935 .with_bucket_name("bucket")
1936 .with_region("region")
1937 .build()
1938 .unwrap_err()
1939 .to_string();
1940
1941 assert_eq!(
1942 err,
1943 "Generic Config error: \"md5\" is not a valid checksum algorithm"
1944 );
1945
1946 let err = AmazonS3Builder::new()
1947 .with_config(AmazonS3ConfigKey::RequestPayer, "requestr")
1948 .with_bucket_name("bucket")
1949 .with_region("region")
1950 .build()
1951 .unwrap_err()
1952 .to_string();
1953
1954 assert_eq!(
1955 err,
1956 "Generic Config error: failed to parse \"requestr\" as boolean"
1957 );
1958 }
1959
1960 #[cfg(feature = "reqwest")]
1961 #[test]
1962 fn test_request_payer_config() {
1963 let s3 = AmazonS3Builder::new()
1964 .with_config(AmazonS3ConfigKey::RequestPayer, "requester")
1965 .with_bucket_name("bucket")
1966 .with_region("region")
1967 .build()
1968 .unwrap();
1969 assert!(s3.client.config.request_payer);
1970
1971 let s3 = AmazonS3Builder::new()
1972 .with_config(AmazonS3ConfigKey::RequestPayer, "REQUESTER")
1973 .with_bucket_name("bucket")
1974 .with_region("region")
1975 .build()
1976 .unwrap();
1977 assert!(s3.client.config.request_payer);
1978
1979 let s3 = AmazonS3Builder::new()
1980 .with_config(AmazonS3ConfigKey::RequestPayer, "true")
1981 .with_bucket_name("bucket")
1982 .with_region("region")
1983 .build()
1984 .unwrap();
1985 assert!(s3.client.config.request_payer);
1986
1987 let s3 = AmazonS3Builder::new()
1988 .with_config(AmazonS3ConfigKey::RequestPayer, "false")
1989 .with_bucket_name("bucket")
1990 .with_region("region")
1991 .build()
1992 .unwrap();
1993 assert!(!s3.client.config.request_payer);
1994
1995 let s3 = AmazonS3Builder::new()
1996 .with_request_payer(true)
1997 .with_bucket_name("bucket")
1998 .with_region("region")
1999 .build()
2000 .unwrap();
2001 assert!(s3.client.config.request_payer);
2002 }
2003
2004 #[test]
2005 fn test_parse_bucket_az() {
2006 let cases = [
2007 ("bucket-base-name--usw2-az1--x-s3", Some("usw2-az1")),
2008 ("bucket-base--name--azid--x-s3", Some("azid")),
2009 ("bucket-base-name--use1-az4--xa-s3", Some("use1-az4")),
2010 ("bucket-base--name--azid--xa-s3", Some("azid")),
2011 ("bucket-base-name", None),
2012 ("bucket-base-name--x-s3", None),
2013 ("bucket-base-name--xa-s3", None),
2014 ];
2015
2016 for (bucket, expected) in cases {
2017 assert_eq!(parse_bucket_az(bucket), expected)
2018 }
2019 }
2020
2021 #[test]
2022 fn aws_test_client_opts() {
2023 let key = "AWS_PROXY_URL";
2024 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
2025 assert_eq!(
2026 AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
2027 config_key
2028 );
2029 } else {
2030 panic!("{key} not propagated as ClientConfigKey");
2031 }
2032 }
2033
2034 #[cfg(feature = "reqwest")]
2035 #[test]
2036 fn test_builder_eks_with_config() {
2037 let builder = AmazonS3Builder::new()
2038 .with_bucket_name("some-bucket")
2039 .with_config(
2040 AmazonS3ConfigKey::ContainerCredentialsFullUri,
2041 "https://127.0.0.1/eks-credentials",
2042 )
2043 .with_config(
2044 AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
2045 "/tmp/fake-bearer-token",
2046 );
2047
2048 let s3 = builder.build().expect("should build successfully");
2049 let creds = &s3.client.config.credentials;
2050 let debug_str = format!("{creds:?}");
2051 assert!(
2052 debug_str.contains("EKSPodCredentialProvider"),
2053 "expected EKS provider but got: {debug_str}"
2054 );
2055 }
2056
2057 #[cfg(feature = "reqwest")]
2058 #[test]
2059 fn test_builder_web_identity_with_config() {
2060 let builder = AmazonS3Builder::new()
2061 .with_bucket_name("some-bucket")
2062 .with_config(
2063 AmazonS3ConfigKey::WebIdentityTokenFile,
2064 "/tmp/fake-token-file",
2065 )
2066 .with_config(
2067 AmazonS3ConfigKey::RoleArn,
2068 "arn:aws:iam::123456789012:role/test-role",
2069 )
2070 .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
2071 .with_config(
2072 AmazonS3ConfigKey::StsEndpoint,
2073 "https://sts.us-west-2.amazonaws.com",
2074 );
2075
2076 assert_eq!(
2077 builder
2078 .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
2079 .unwrap(),
2080 "/tmp/fake-token-file"
2081 );
2082 assert_eq!(
2083 builder
2084 .get_config_value(&AmazonS3ConfigKey::RoleArn)
2085 .unwrap(),
2086 "arn:aws:iam::123456789012:role/test-role"
2087 );
2088 assert_eq!(
2089 builder
2090 .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
2091 .unwrap(),
2092 "TestSession"
2093 );
2094 assert_eq!(
2095 builder
2096 .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
2097 .unwrap(),
2098 "https://sts.us-west-2.amazonaws.com"
2099 );
2100
2101 let s3 = builder.build().expect("should build successfully");
2102 let creds = &s3.client.config.credentials;
2103 let debug_str = format!("{creds:?}");
2104 assert!(
2105 debug_str.contains("TokenCredentialProvider"),
2106 "expected TokenCredentialProvider but got: {debug_str}"
2107 );
2108 }
2109}