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 {
641 self.url = Some(url.into());
642 self
643 }
644
645 pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
647 match key {
648 AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
649 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
650 AmazonS3ConfigKey::Region => self.region = Some(value.into()),
651 AmazonS3ConfigKey::Bucket => self.bucket_name = Some(value.into()),
652 AmazonS3ConfigKey::Endpoint => self.endpoint = Some(value.into()),
653 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint = Some(value.into()),
654 AmazonS3ConfigKey::Token => self.token = Some(value.into()),
655 AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
656 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
657 self.virtual_hosted_style_request.parse(value)
658 }
659 AmazonS3ConfigKey::S3Express => self.s3_express.parse(value),
660 AmazonS3ConfigKey::DefaultRegion => {
661 self.region = self.region.or_else(|| Some(value.into()))
662 }
663 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
664 AmazonS3ConfigKey::UnsignedPayload => self.unsigned_payload.parse(value),
665 AmazonS3ConfigKey::Checksum => {
666 self.checksum_algorithm = Some(ConfigValue::Deferred(value.into()))
667 }
668 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
669 self.container_credentials_relative_uri = Some(value.into())
670 }
671 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
672 self.container_credentials_full_uri = Some(value.into());
673 }
674 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
675 self.container_authorization_token_file = Some(value.into());
676 }
677 AmazonS3ConfigKey::WebIdentityTokenFile => {
678 self.web_identity_token_file = Some(value.into());
679 }
680 AmazonS3ConfigKey::RoleArn => {
681 self.role_arn = Some(value.into());
682 }
683 AmazonS3ConfigKey::RoleSessionName => {
684 self.role_session_name = Some(value.into());
685 }
686 AmazonS3ConfigKey::StsEndpoint => {
687 self.sts_endpoint = Some(value.into());
688 }
689 AmazonS3ConfigKey::Client(key) => {
690 self.client_options = self.client_options.with_config(key, value)
691 }
692 AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
693 AmazonS3ConfigKey::DisableTagging => self.disable_tagging.parse(value),
694 AmazonS3ConfigKey::DisableBulkDelete => self.disable_bulk_delete.parse(value),
695 AmazonS3ConfigKey::CopyIfNotExists => {
696 self.copy_if_not_exists = Some(ConfigValue::Deferred(value.into()))
697 }
698 AmazonS3ConfigKey::ConditionalPut => {
699 self.conditional_put = ConfigValue::Deferred(value.into())
700 }
701 AmazonS3ConfigKey::RequestPayer => self.request_payer.parse(value),
702 AmazonS3ConfigKey::Encryption(key) => match key {
703 S3EncryptionConfigKey::ServerSideEncryption => {
704 self.encryption_type = Some(ConfigValue::Deferred(value.into()))
705 }
706 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id = Some(value.into()),
707 S3EncryptionConfigKey::BucketKeyEnabled => {
708 self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into()))
709 }
710 S3EncryptionConfigKey::CustomerEncryptionKey => {
711 self.encryption_customer_key_base64 = Some(value.into())
712 }
713 },
714 };
715 self
716 }
717
718 pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
730 match key {
731 AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
732 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
733 AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
734 AmazonS3ConfigKey::Bucket => self.bucket_name.clone(),
735 AmazonS3ConfigKey::Endpoint => self.endpoint.clone(),
736 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint.clone(),
737 AmazonS3ConfigKey::Token => self.token.clone(),
738 AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
739 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
740 Some(self.virtual_hosted_style_request.to_string())
741 }
742 AmazonS3ConfigKey::S3Express => Some(self.s3_express.to_string()),
743 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
744 AmazonS3ConfigKey::UnsignedPayload => Some(self.unsigned_payload.to_string()),
745 AmazonS3ConfigKey::Checksum => {
746 self.checksum_algorithm.as_ref().map(ToString::to_string)
747 }
748 AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
749 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
750 self.container_credentials_relative_uri.clone()
751 }
752 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
753 self.container_credentials_full_uri.clone()
754 }
755 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
756 self.container_authorization_token_file.clone()
757 }
758 AmazonS3ConfigKey::WebIdentityTokenFile => self.web_identity_token_file.clone(),
759 AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
760 AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
761 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
762 AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
763 AmazonS3ConfigKey::CopyIfNotExists => {
764 self.copy_if_not_exists.as_ref().map(ToString::to_string)
765 }
766 AmazonS3ConfigKey::ConditionalPut => Some(self.conditional_put.to_string()),
767 AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
768 AmazonS3ConfigKey::DisableBulkDelete => Some(self.disable_bulk_delete.to_string()),
769 AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
770 AmazonS3ConfigKey::Encryption(key) => match key {
771 S3EncryptionConfigKey::ServerSideEncryption => {
772 self.encryption_type.as_ref().map(ToString::to_string)
773 }
774 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id.clone(),
775 S3EncryptionConfigKey::BucketKeyEnabled => self
776 .encryption_bucket_key_enabled
777 .as_ref()
778 .map(ToString::to_string),
779 S3EncryptionConfigKey::CustomerEncryptionKey => {
780 self.encryption_customer_key_base64.clone()
781 }
782 },
783 }
784 }
785
786 fn parse_url(&mut self, url: &str) -> Result<()> {
791 let parsed = Url::parse(url).map_err(|source| {
792 let url = url.into();
793 Error::UnableToParseUrl { url, source }
794 })?;
795
796 let host = parsed
797 .host_str()
798 .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
799
800 match parsed.scheme() {
801 "s3" | "s3a" => self.bucket_name = Some(host.to_string()),
802 "https" => match host.splitn(4, '.').collect_tuple() {
803 Some(("s3", region, "amazonaws", "com")) => {
804 self.region = Some(region.to_string());
805 let bucket = parsed.path_segments().into_iter().flatten().next();
806 if let Some(bucket) = bucket {
807 self.bucket_name = Some(bucket.into());
808 }
809 }
810 Some((bucket, "s3", "amazonaws", "com")) => {
811 self.bucket_name = Some(bucket.to_string());
812 self.virtual_hosted_style_request = true.into();
813 }
814 Some((bucket, "s3", region, "amazonaws.com")) => {
815 self.bucket_name = Some(bucket.to_string());
816 self.region = Some(region.to_string());
817 self.virtual_hosted_style_request = true.into();
818 }
819 Some((account, "r2", "cloudflarestorage", "com")) => {
820 self.region = Some("auto".to_string());
821 let endpoint = format!("https://{account}.r2.cloudflarestorage.com");
822 self.endpoint = Some(endpoint);
823
824 let bucket = parsed.path_segments().into_iter().flatten().next();
825 if let Some(bucket) = bucket {
826 self.bucket_name = Some(bucket.into());
827 }
828 }
829 _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
830 },
831 scheme => {
832 let scheme = scheme.into();
833 return Err(Error::UnknownUrlScheme { scheme }.into());
834 }
835 };
836 Ok(())
837 }
838
839 pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
843 self.access_key_id = Some(access_key_id.into());
844 self
845 }
846
847 pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
849 self.secret_access_key = Some(secret_access_key.into());
850 self
851 }
852
853 pub fn with_token(mut self, token: impl Into<String>) -> Self {
857 self.token = Some(token.into());
858 self
859 }
860
861 pub fn with_region(mut self, region: impl Into<String>) -> Self {
863 self.region = Some(region.into());
864 self
865 }
866
867 pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
869 self.bucket_name = Some(bucket_name.into());
870 self
871 }
872
873 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
889 self.endpoint = Some(endpoint.into());
890 self
891 }
892
893 pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
895 self.credentials = Some(credentials);
896 self
897 }
898
899 pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
901 self.crypto = Some(provider);
902 self
903 }
904
905 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
919 self.client_options = self.client_options.with_allow_http(allow_http);
920 self
921 }
922
923 pub fn with_virtual_hosted_style_request(mut self, virtual_hosted_style_request: bool) -> Self {
934 self.virtual_hosted_style_request = virtual_hosted_style_request.into();
935 self
936 }
937
938 pub fn with_s3_express(mut self, s3_express: bool) -> Self {
940 self.s3_express = s3_express.into();
941 self
942 }
943
944 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
946 self.retry_config = retry_config;
947 self
948 }
949
950 pub fn with_imdsv1_fallback(mut self) -> Self {
962 self.imdsv1_fallback = true.into();
963 self
964 }
965
966 pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
972 self.unsigned_payload = unsigned_payload.into();
973 self
974 }
975
976 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
980 self.skip_signature = skip_signature.into();
981 self
982 }
983
984 pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
988 self.checksum_algorithm = Some(checksum_algorithm.into());
990 self
991 }
992
993 pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
999 self.metadata_endpoint = Some(endpoint.into());
1000 self
1001 }
1002
1003 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
1005 self.client_options = self.client_options.with_proxy_url(proxy_url);
1006 self
1007 }
1008
1009 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
1011 self.client_options = self
1012 .client_options
1013 .with_proxy_ca_certificate(proxy_ca_certificate);
1014 self
1015 }
1016
1017 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
1019 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
1020 self
1021 }
1022
1023 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1025 self.client_options = options;
1026 self
1027 }
1028
1029 pub fn with_copy_if_not_exists(mut self, config: S3CopyIfNotExists) -> Self {
1031 self.copy_if_not_exists = Some(config.into());
1032 self
1033 }
1034
1035 pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1038 self.conditional_put = config.into();
1039 self
1040 }
1041
1042 pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1044 self.disable_tagging = ignore.into();
1045 self
1046 }
1047
1048 pub fn with_disable_bulk_delete(mut self, disable: bool) -> Self {
1059 self.disable_bulk_delete = disable.into();
1060 self
1061 }
1062
1063 pub fn with_sse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1065 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseKms));
1066 if let Some(kms_key_id) = kms_key_id.into().into() {
1067 self.encryption_kms_key_id = Some(kms_key_id);
1068 }
1069 self
1070 }
1071
1072 pub fn with_dsse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1074 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::DsseKms));
1075 if let Some(kms_key_id) = kms_key_id.into().into() {
1076 self.encryption_kms_key_id = Some(kms_key_id);
1077 }
1078 self
1079 }
1080
1081 pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self {
1084 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC));
1085 self.encryption_customer_key_base64 = customer_key_base64.into().into();
1086 self
1087 }
1088
1089 pub fn with_bucket_key(mut self, enabled: bool) -> Self {
1096 self.encryption_bucket_key_enabled = Some(ConfigValue::Parsed(enabled));
1097 self
1098 }
1099
1100 pub fn with_request_payer(mut self, enabled: bool) -> Self {
1104 self.request_payer = ConfigValue::Parsed(enabled.into());
1105 self
1106 }
1107
1108 pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1112 self.http_connector = Some(Arc::new(connector));
1113 self
1114 }
1115
1116 pub fn build(mut self) -> Result<AmazonS3> {
1119 if let Some(url) = self.url.take() {
1120 self.parse_url(&url)?;
1121 }
1122
1123 let http = http_connector(self.http_connector)?;
1124
1125 let bucket = self.bucket_name.ok_or(Error::MissingBucketName)?;
1126 let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
1127 let checksum = self.checksum_algorithm.map(|x| x.get()).transpose()?;
1128 let copy_if_not_exists = self.copy_if_not_exists.map(|x| x.get()).transpose()?;
1129
1130 let credentials = if let Some(credentials) = self.credentials {
1131 credentials
1132 } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
1133 match (self.access_key_id, self.secret_access_key, self.token) {
1134 (Some(key_id), Some(secret_key), token) => {
1135 debug!("Using Static credential provider");
1136 let credential = AwsCredential {
1137 key_id,
1138 secret_key,
1139 token,
1140 };
1141 Arc::new(StaticCredentialProvider::new(credential)) as _
1142 }
1143 (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
1144 (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
1145 (None, None, _) => unreachable!(),
1146 }
1147 } else if let (Some(token_path), Some(role_arn)) =
1148 (self.web_identity_token_file, self.role_arn)
1149 {
1150 debug!("Using WebIdentity credential provider");
1151
1152 let session_name = self
1153 .role_session_name
1154 .clone()
1155 .unwrap_or_else(|| "WebIdentitySession".to_string());
1156
1157 let endpoint = self
1158 .sts_endpoint
1159 .clone()
1160 .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
1161
1162 let options = self.client_options.clone().with_allow_http(false);
1164
1165 let token = WebIdentityProvider {
1166 token_path: token_path.clone(),
1167 session_name,
1168 role_arn: role_arn.clone(),
1169 endpoint,
1170 };
1171
1172 Arc::new(TokenCredentialProvider::new(
1173 token,
1174 http.connect(&options)?,
1175 self.retry_config.clone(),
1176 )) as _
1177 } else if let Some(uri) = self.container_credentials_relative_uri {
1178 debug!("Using Task credential provider");
1179
1180 let options = self.client_options.clone().with_allow_http(true);
1181
1182 Arc::new(TaskCredentialProvider {
1183 url: format!("http://169.254.170.2{uri}"),
1184 retry: self.retry_config.clone(),
1185 client: http.connect(&options)?,
1187 cache: Default::default(),
1188 }) as _
1189 } else if let (Some(full_uri), Some(token_file)) = (
1190 self.container_credentials_full_uri,
1191 self.container_authorization_token_file,
1192 ) {
1193 debug!("Using EKS Pod Identity credential provider");
1194
1195 let options = self.client_options.clone().with_allow_http(true);
1196
1197 Arc::new(EKSPodCredentialProvider {
1198 url: full_uri,
1199 token_file,
1200 retry: self.retry_config.clone(),
1201 client: http.connect(&options)?,
1202 cache: Default::default(),
1203 }) as _
1204 } else {
1205 debug!("Using Instance credential provider");
1206
1207 let token = InstanceCredentialProvider {
1208 imdsv1_fallback: self.imdsv1_fallback.get()?,
1209 metadata_endpoint: self
1210 .metadata_endpoint
1211 .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
1212 };
1213
1214 Arc::new(TokenCredentialProvider::new(
1215 token,
1216 http.connect(&self.client_options.metadata_options())?,
1217 self.retry_config.clone(),
1218 )) as _
1219 };
1220
1221 let (session_provider, zonal_endpoint) = match self.s3_express.get()? {
1222 true => {
1223 let zone = parse_bucket_az(&bucket).ok_or_else(|| {
1224 let bucket = bucket.clone();
1225 Error::ZoneSuffix { bucket }
1226 })?;
1227
1228 let endpoint = format!("https://{bucket}.s3express-{zone}.{region}.amazonaws.com");
1230
1231 let session = Arc::new(
1232 TokenCredentialProvider::new(
1233 SessionProvider {
1234 endpoint: endpoint.clone(),
1235 region: region.clone(),
1236 credentials: Arc::clone(&credentials),
1237 crypto: self.crypto.clone(),
1238 },
1239 http.connect(&self.client_options)?,
1240 self.retry_config.clone(),
1241 )
1242 .with_min_ttl(Duration::from_secs(60)), );
1244 (Some(session as _), Some(endpoint))
1245 }
1246 false => (None, None),
1247 };
1248
1249 let endpoint = self.s3_endpoint.or(self.endpoint);
1251
1252 let virtual_hosted = self.virtual_hosted_style_request.get()?;
1255 let bucket_endpoint = match (&endpoint, zonal_endpoint, virtual_hosted) {
1256 (Some(endpoint), _, true) => endpoint.clone(),
1257 (Some(endpoint), _, false) => format!("{}/{}", endpoint.trim_end_matches("/"), bucket),
1258 (None, Some(endpoint), _) => endpoint,
1259 (None, None, true) => format!("https://{bucket}.s3.{region}.amazonaws.com"),
1260 (None, None, false) => format!("https://s3.{region}.amazonaws.com/{bucket}"),
1261 };
1262
1263 let encryption_headers = if let Some(encryption_type) = self.encryption_type {
1264 S3EncryptionHeaders::try_new(
1265 &encryption_type.get()?,
1266 self.encryption_kms_key_id,
1267 self.encryption_bucket_key_enabled
1268 .map(|val| val.get())
1269 .transpose()?,
1270 self.encryption_customer_key_base64,
1271 )?
1272 } else {
1273 S3EncryptionHeaders::default()
1274 };
1275
1276 let config = S3Config {
1277 region,
1278 bucket,
1279 bucket_endpoint,
1280 credentials,
1281 crypto: self.crypto,
1282 session_provider,
1283 retry_config: self.retry_config,
1284 client_options: self.client_options,
1285 sign_payload: !self.unsigned_payload.get()?,
1286 skip_signature: self.skip_signature.get()?,
1287 disable_tagging: self.disable_tagging.get()?,
1288 disable_bulk_delete: self.disable_bulk_delete.get()?,
1289 checksum,
1290 copy_if_not_exists,
1291 conditional_put: self.conditional_put.get()?,
1292 encryption_headers,
1293 request_payer: self.request_payer.get()?.into(),
1294 };
1295
1296 let http_client = http.connect(&config.client_options)?;
1297 let client = Arc::new(S3Client::new(config, http_client));
1298
1299 Ok(AmazonS3 { client })
1300 }
1301}
1302
1303fn parse_bucket_az(bucket: &str) -> Option<&str> {
1307 let base = bucket
1308 .strip_suffix("--x-s3")
1309 .or_else(|| bucket.strip_suffix("--xa-s3"))?;
1310 Some(base.rsplit_once("--")?.1)
1311}
1312
1313#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1317struct RequesterPayer(bool);
1318
1319impl crate::config::Parse for RequesterPayer {
1320 fn parse(v: &str) -> Result<Self> {
1321 if v.eq_ignore_ascii_case("requester") {
1322 Ok(Self(true))
1323 } else {
1324 Ok(Self(<bool as crate::config::Parse>::parse(v)?))
1325 }
1326 }
1327}
1328
1329impl From<bool> for RequesterPayer {
1330 fn from(value: bool) -> Self {
1331 Self(value)
1332 }
1333}
1334
1335impl From<RequesterPayer> for bool {
1336 fn from(value: RequesterPayer) -> Self {
1337 value.0
1338 }
1339}
1340
1341impl std::fmt::Display for RequesterPayer {
1342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1343 self.0.fmt(f)
1344 }
1345}
1346
1347#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1357#[non_exhaustive]
1358pub enum S3EncryptionConfigKey {
1359 ServerSideEncryption,
1371 KmsKeyId,
1381 BucketKeyEnabled,
1388
1389 CustomerEncryptionKey,
1397}
1398
1399impl AsRef<str> for S3EncryptionConfigKey {
1400 fn as_ref(&self) -> &str {
1401 match self {
1402 Self::ServerSideEncryption => "aws_server_side_encryption",
1403 Self::KmsKeyId => "aws_sse_kms_key_id",
1404 Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled",
1405 Self::CustomerEncryptionKey => "aws_sse_customer_key_base64",
1406 }
1407 }
1408}
1409
1410#[derive(Debug, Clone)]
1411enum S3EncryptionType {
1412 S3,
1413 SseKms,
1414 DsseKms,
1415 SseC,
1416}
1417
1418impl crate::config::Parse for S3EncryptionType {
1419 fn parse(s: &str) -> Result<Self> {
1420 match s {
1421 "AES256" => Ok(Self::S3),
1422 "aws:kms" => Ok(Self::SseKms),
1423 "aws:kms:dsse" => Ok(Self::DsseKms),
1424 "sse-c" => Ok(Self::SseC),
1425 _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()),
1426 }
1427 }
1428}
1429
1430impl From<&S3EncryptionType> for &'static str {
1431 fn from(value: &S3EncryptionType) -> Self {
1432 match value {
1433 S3EncryptionType::S3 => "AES256",
1434 S3EncryptionType::SseKms => "aws:kms",
1435 S3EncryptionType::DsseKms => "aws:kms:dsse",
1436 S3EncryptionType::SseC => "sse-c",
1437 }
1438 }
1439}
1440
1441impl std::fmt::Display for S3EncryptionType {
1442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1443 f.write_str(self.into())
1444 }
1445}
1446
1447#[derive(Default, Clone, Debug)]
1453pub(super) struct S3EncryptionHeaders(pub HeaderMap);
1454
1455impl S3EncryptionHeaders {
1456 fn try_new(
1457 encryption_type: &S3EncryptionType,
1458 encryption_kms_key_id: Option<String>,
1459 bucket_key_enabled: Option<bool>,
1460 encryption_customer_key_base64: Option<String>,
1461 ) -> Result<Self> {
1462 let mut headers = HeaderMap::new();
1463 match encryption_type {
1464 S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => {
1465 headers.insert(
1466 "x-amz-server-side-encryption",
1467 HeaderValue::from_static(encryption_type.into()),
1468 );
1469 if let Some(key_id) = encryption_kms_key_id {
1470 headers.insert(
1471 "x-amz-server-side-encryption-aws-kms-key-id",
1472 key_id
1473 .try_into()
1474 .map_err(|err| Error::InvalidEncryptionHeader {
1475 header: "kms-key-id",
1476 source: Box::new(err),
1477 })?,
1478 );
1479 }
1480 if let Some(bucket_key_enabled) = bucket_key_enabled {
1481 headers.insert(
1482 "x-amz-server-side-encryption-bucket-key-enabled",
1483 HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }),
1484 );
1485 }
1486 }
1487 S3EncryptionType::SseC => {
1488 headers.insert(
1489 "x-amz-server-side-encryption-customer-algorithm",
1490 HeaderValue::from_static("AES256"),
1491 );
1492 if let Some(key) = encryption_customer_key_base64 {
1493 let mut header_value: HeaderValue =
1494 key.clone()
1495 .try_into()
1496 .map_err(|err| Error::InvalidEncryptionHeader {
1497 header: "x-amz-server-side-encryption-customer-key",
1498 source: Box::new(err),
1499 })?;
1500 header_value.set_sensitive(true);
1501 headers.insert("x-amz-server-side-encryption-customer-key", header_value);
1502
1503 let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| {
1504 Error::InvalidEncryptionHeader {
1505 header: "x-amz-server-side-encryption-customer-key",
1506 source: Box::new(err),
1507 }
1508 })?;
1509 let mut hasher = Md5::new();
1510 hasher.update(decoded_key);
1511 let md5 = BASE64_STANDARD.encode(hasher.finalize());
1512 let mut md5_header_value: HeaderValue =
1513 md5.try_into()
1514 .map_err(|err| Error::InvalidEncryptionHeader {
1515 header: "x-amz-server-side-encryption-customer-key-MD5",
1516 source: Box::new(err),
1517 })?;
1518 md5_header_value.set_sensitive(true);
1519 headers.insert(
1520 "x-amz-server-side-encryption-customer-key-MD5",
1521 md5_header_value,
1522 );
1523 } else {
1524 return Err(Error::InvalidEncryptionHeader {
1525 header: "x-amz-server-side-encryption-customer-key",
1526 source: Box::new(std::io::Error::new(
1527 std::io::ErrorKind::InvalidInput,
1528 "Missing customer key",
1529 )),
1530 }
1531 .into());
1532 }
1533 }
1534 }
1535 Ok(Self(headers))
1536 }
1537}
1538
1539impl From<S3EncryptionHeaders> for HeaderMap {
1540 fn from(headers: S3EncryptionHeaders) -> Self {
1541 headers.0
1542 }
1543}
1544
1545#[cfg(test)]
1546mod tests {
1547 use super::*;
1548 use std::collections::HashMap;
1549
1550 #[test]
1551 fn s3_test_config_from_map() {
1552 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1553 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1554 let aws_default_region = "object_store:fake_default_region".to_string();
1555 let aws_endpoint = "object_store:fake_endpoint".to_string();
1556 let aws_session_token = "object_store:fake_session_token".to_string();
1557 let options = HashMap::from([
1558 ("aws_access_key_id", aws_access_key_id.clone()),
1559 ("aws_secret_access_key", aws_secret_access_key),
1560 ("aws_default_region", aws_default_region.clone()),
1561 ("aws_endpoint", aws_endpoint.clone()),
1562 ("aws_session_token", aws_session_token.clone()),
1563 ("aws_unsigned_payload", "true".to_string()),
1564 ("aws_checksum_algorithm", "sha256".to_string()),
1565 ]);
1566
1567 let builder = options
1568 .into_iter()
1569 .fold(AmazonS3Builder::new(), |builder, (key, value)| {
1570 builder.with_config(key.parse().unwrap(), value)
1571 })
1572 .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
1573
1574 assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
1575 assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
1576 assert_eq!(builder.region.unwrap(), aws_default_region);
1577 assert_eq!(builder.endpoint.unwrap(), aws_endpoint);
1578 assert_eq!(builder.token.unwrap(), aws_session_token);
1579 assert_eq!(
1580 builder.checksum_algorithm.unwrap().get().unwrap(),
1581 Checksum::SHA256
1582 );
1583 assert!(builder.unsigned_payload.get().unwrap());
1584 }
1585
1586 #[cfg(feature = "reqwest")]
1587 #[test]
1588 fn s3_test_endpoint_url_s3_config() {
1589 let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1591 assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1592
1593 let s3 = AmazonS3Builder::new()
1595 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1596 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1597 .with_bucket_name("test-bucket")
1598 .build()
1599 .unwrap();
1600 assert_eq!(
1601 s3.client.config.bucket_endpoint,
1602 "http://s3-specific-endpoint/test-bucket"
1603 );
1604
1605 let s3 = AmazonS3Builder::new()
1607 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1608 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1609 .with_bucket_name("test-bucket")
1610 .build()
1611 .unwrap();
1612 assert_eq!(
1613 s3.client.config.bucket_endpoint,
1614 "http://s3-specific-endpoint/test-bucket"
1615 );
1616 }
1617
1618 #[test]
1619 fn s3_test_config_get_value() {
1620 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1621 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1622 let aws_default_region = "object_store:fake_default_region".to_string();
1623 let aws_endpoint = "object_store:fake_endpoint".to_string();
1624 let aws_session_token = "object_store:fake_session_token".to_string();
1625
1626 let builder = AmazonS3Builder::new()
1627 .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
1628 .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
1629 .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
1630 .with_config(AmazonS3ConfigKey::Endpoint, &aws_endpoint)
1631 .with_config(AmazonS3ConfigKey::Token, &aws_session_token)
1632 .with_config(AmazonS3ConfigKey::UnsignedPayload, "true")
1633 .with_config("aws_server_side_encryption".parse().unwrap(), "AES256")
1634 .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id")
1635 .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true")
1636 .with_config(
1637 "aws_sse_customer_key_base64".parse().unwrap(),
1638 "some_customer_key",
1639 );
1640
1641 assert_eq!(
1642 builder
1643 .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
1644 .unwrap(),
1645 aws_access_key_id
1646 );
1647 assert_eq!(
1648 builder
1649 .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
1650 .unwrap(),
1651 aws_secret_access_key
1652 );
1653 assert_eq!(
1654 builder
1655 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
1656 .unwrap(),
1657 aws_default_region
1658 );
1659 assert_eq!(
1660 builder
1661 .get_config_value(&AmazonS3ConfigKey::Endpoint)
1662 .unwrap(),
1663 aws_endpoint
1664 );
1665 assert_eq!(
1666 builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
1667 aws_session_token
1668 );
1669 assert_eq!(
1670 builder
1671 .get_config_value(&AmazonS3ConfigKey::UnsignedPayload)
1672 .unwrap(),
1673 "true"
1674 );
1675 assert_eq!(
1676 builder
1677 .get_config_value(&"aws_server_side_encryption".parse().unwrap())
1678 .unwrap(),
1679 "AES256"
1680 );
1681 assert_eq!(
1682 builder
1683 .get_config_value(&"aws_sse_kms_key_id".parse().unwrap())
1684 .unwrap(),
1685 "some_key_id"
1686 );
1687 assert_eq!(
1688 builder
1689 .get_config_value(&"aws_sse_bucket_key_enabled".parse().unwrap())
1690 .unwrap(),
1691 "true"
1692 );
1693 assert_eq!(
1694 builder
1695 .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap())
1696 .unwrap(),
1697 "some_customer_key"
1698 );
1699 }
1700
1701 #[cfg(feature = "reqwest")]
1702 #[test]
1703 fn s3_default_region() {
1704 let builder = AmazonS3Builder::new()
1705 .with_bucket_name("foo")
1706 .build()
1707 .unwrap();
1708 assert_eq!(builder.client.config.region, "us-east-1");
1709 }
1710
1711 #[cfg(feature = "reqwest")]
1712 #[test]
1713 fn s3_test_bucket_endpoint() {
1714 let builder = AmazonS3Builder::new()
1715 .with_endpoint("http://some.host:1234")
1716 .with_bucket_name("foo")
1717 .build()
1718 .unwrap();
1719 assert_eq!(
1720 builder.client.config.bucket_endpoint,
1721 "http://some.host:1234/foo"
1722 );
1723
1724 let builder = AmazonS3Builder::new()
1725 .with_endpoint("http://some.host:1234/")
1726 .with_bucket_name("foo")
1727 .build()
1728 .unwrap();
1729 assert_eq!(
1730 builder.client.config.bucket_endpoint,
1731 "http://some.host:1234/foo"
1732 );
1733 }
1734
1735 #[test]
1736 fn s3_test_urls() {
1737 let mut builder = AmazonS3Builder::new();
1738 builder.parse_url("s3://bucket/path").unwrap();
1739 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1740
1741 let mut builder = AmazonS3Builder::new();
1742 builder
1743 .parse_url("s3://buckets.can.have.dots/path")
1744 .unwrap();
1745 assert_eq!(
1746 builder.bucket_name,
1747 Some("buckets.can.have.dots".to_string())
1748 );
1749
1750 let mut builder = AmazonS3Builder::new();
1751 builder
1752 .parse_url("https://s3.region.amazonaws.com")
1753 .unwrap();
1754 assert_eq!(builder.region, Some("region".to_string()));
1755
1756 let mut builder = AmazonS3Builder::new();
1757 builder
1758 .parse_url("https://s3.region.amazonaws.com/bucket")
1759 .unwrap();
1760 assert_eq!(builder.region, Some("region".to_string()));
1761 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1762
1763 let mut builder = AmazonS3Builder::new();
1764 builder
1765 .parse_url("https://s3.region.amazonaws.com/bucket.with.dot/path")
1766 .unwrap();
1767 assert_eq!(builder.region, Some("region".to_string()));
1768 assert_eq!(builder.bucket_name, Some("bucket.with.dot".to_string()));
1769
1770 let mut builder = AmazonS3Builder::new();
1771 builder
1772 .parse_url("https://bucket.s3.amazonaws.com")
1773 .unwrap();
1774 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1775 assert!(builder.virtual_hosted_style_request.get().unwrap());
1776
1777 let mut builder = AmazonS3Builder::new();
1778 builder
1779 .parse_url("https://bucket.s3.region.amazonaws.com")
1780 .unwrap();
1781 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1782 assert_eq!(builder.region, Some("region".to_string()));
1783 assert!(builder.virtual_hosted_style_request.get().unwrap());
1784
1785 let mut builder = AmazonS3Builder::new();
1786 builder
1787 .parse_url("https://account123.r2.cloudflarestorage.com/bucket-123")
1788 .unwrap();
1789
1790 assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1791 assert_eq!(builder.region, Some("auto".to_string()));
1792 assert_eq!(
1793 builder.endpoint,
1794 Some("https://account123.r2.cloudflarestorage.com".to_string())
1795 );
1796
1797 let err_cases = [
1798 "mailto://bucket/path",
1799 "https://s3.bucket.mydomain.com",
1800 "https://s3.bucket.foo.amazonaws.com",
1801 "https://bucket.mydomain.region.amazonaws.com",
1802 "https://bucket.s3.region.bar.amazonaws.com",
1803 "https://bucket.foo.s3.amazonaws.com",
1804 ];
1805 let mut builder = AmazonS3Builder::new();
1806 for case in err_cases {
1807 builder.parse_url(case).unwrap_err();
1808 }
1809 }
1810
1811 #[cfg(feature = "reqwest")]
1812 #[tokio::test]
1813 async fn s3_test_proxy_url() {
1814 let s3 = AmazonS3Builder::new()
1815 .with_access_key_id("access_key_id")
1816 .with_secret_access_key("secret_access_key")
1817 .with_region("region")
1818 .with_bucket_name("bucket_name")
1819 .with_allow_http(true)
1820 .with_proxy_url("https://example.com")
1821 .build();
1822
1823 assert!(s3.is_ok());
1824
1825 let err = AmazonS3Builder::new()
1826 .with_access_key_id("access_key_id")
1827 .with_secret_access_key("secret_access_key")
1828 .with_region("region")
1829 .with_bucket_name("bucket_name")
1830 .with_allow_http(true)
1831 .with_proxy_url("dxx:ddd\\example.com")
1833 .build()
1834 .unwrap_err()
1835 .to_string();
1836
1837 assert_eq!("Generic HTTP client error: builder error", err);
1838 }
1839
1840 #[cfg(feature = "reqwest")]
1841 #[test]
1842 fn test_invalid_config() {
1843 let err = AmazonS3Builder::new()
1844 .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
1845 .with_bucket_name("bucket")
1846 .with_region("region")
1847 .build()
1848 .unwrap_err()
1849 .to_string();
1850
1851 assert_eq!(
1852 err,
1853 "Generic Config error: failed to parse \"enabled\" as boolean"
1854 );
1855
1856 let err = AmazonS3Builder::new()
1857 .with_config(AmazonS3ConfigKey::Checksum, "md5")
1858 .with_bucket_name("bucket")
1859 .with_region("region")
1860 .build()
1861 .unwrap_err()
1862 .to_string();
1863
1864 assert_eq!(
1865 err,
1866 "Generic Config error: \"md5\" is not a valid checksum algorithm"
1867 );
1868
1869 let err = AmazonS3Builder::new()
1870 .with_config(AmazonS3ConfigKey::RequestPayer, "requestr")
1871 .with_bucket_name("bucket")
1872 .with_region("region")
1873 .build()
1874 .unwrap_err()
1875 .to_string();
1876
1877 assert_eq!(
1878 err,
1879 "Generic Config error: failed to parse \"requestr\" as boolean"
1880 );
1881 }
1882
1883 #[cfg(feature = "reqwest")]
1884 #[test]
1885 fn test_request_payer_config() {
1886 let s3 = AmazonS3Builder::new()
1887 .with_config(AmazonS3ConfigKey::RequestPayer, "requester")
1888 .with_bucket_name("bucket")
1889 .with_region("region")
1890 .build()
1891 .unwrap();
1892 assert!(s3.client.config.request_payer);
1893
1894 let s3 = AmazonS3Builder::new()
1895 .with_config(AmazonS3ConfigKey::RequestPayer, "REQUESTER")
1896 .with_bucket_name("bucket")
1897 .with_region("region")
1898 .build()
1899 .unwrap();
1900 assert!(s3.client.config.request_payer);
1901
1902 let s3 = AmazonS3Builder::new()
1903 .with_config(AmazonS3ConfigKey::RequestPayer, "true")
1904 .with_bucket_name("bucket")
1905 .with_region("region")
1906 .build()
1907 .unwrap();
1908 assert!(s3.client.config.request_payer);
1909
1910 let s3 = AmazonS3Builder::new()
1911 .with_config(AmazonS3ConfigKey::RequestPayer, "false")
1912 .with_bucket_name("bucket")
1913 .with_region("region")
1914 .build()
1915 .unwrap();
1916 assert!(!s3.client.config.request_payer);
1917
1918 let s3 = AmazonS3Builder::new()
1919 .with_request_payer(true)
1920 .with_bucket_name("bucket")
1921 .with_region("region")
1922 .build()
1923 .unwrap();
1924 assert!(s3.client.config.request_payer);
1925 }
1926
1927 #[test]
1928 fn test_parse_bucket_az() {
1929 let cases = [
1930 ("bucket-base-name--usw2-az1--x-s3", Some("usw2-az1")),
1931 ("bucket-base--name--azid--x-s3", Some("azid")),
1932 ("bucket-base-name--use1-az4--xa-s3", Some("use1-az4")),
1933 ("bucket-base--name--azid--xa-s3", Some("azid")),
1934 ("bucket-base-name", None),
1935 ("bucket-base-name--x-s3", None),
1936 ("bucket-base-name--xa-s3", None),
1937 ];
1938
1939 for (bucket, expected) in cases {
1940 assert_eq!(parse_bucket_az(bucket), expected)
1941 }
1942 }
1943
1944 #[test]
1945 fn aws_test_client_opts() {
1946 let key = "AWS_PROXY_URL";
1947 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
1948 assert_eq!(
1949 AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
1950 config_key
1951 );
1952 } else {
1953 panic!("{key} not propagated as ClientConfigKey");
1954 }
1955 }
1956
1957 #[cfg(feature = "reqwest")]
1958 #[test]
1959 fn test_builder_eks_with_config() {
1960 let builder = AmazonS3Builder::new()
1961 .with_bucket_name("some-bucket")
1962 .with_config(
1963 AmazonS3ConfigKey::ContainerCredentialsFullUri,
1964 "https://127.0.0.1/eks-credentials",
1965 )
1966 .with_config(
1967 AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
1968 "/tmp/fake-bearer-token",
1969 );
1970
1971 let s3 = builder.build().expect("should build successfully");
1972 let creds = &s3.client.config.credentials;
1973 let debug_str = format!("{creds:?}");
1974 assert!(
1975 debug_str.contains("EKSPodCredentialProvider"),
1976 "expected EKS provider but got: {debug_str}"
1977 );
1978 }
1979
1980 #[cfg(feature = "reqwest")]
1981 #[test]
1982 fn test_builder_web_identity_with_config() {
1983 let builder = AmazonS3Builder::new()
1984 .with_bucket_name("some-bucket")
1985 .with_config(
1986 AmazonS3ConfigKey::WebIdentityTokenFile,
1987 "/tmp/fake-token-file",
1988 )
1989 .with_config(
1990 AmazonS3ConfigKey::RoleArn,
1991 "arn:aws:iam::123456789012:role/test-role",
1992 )
1993 .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
1994 .with_config(
1995 AmazonS3ConfigKey::StsEndpoint,
1996 "https://sts.us-west-2.amazonaws.com",
1997 );
1998
1999 assert_eq!(
2000 builder
2001 .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
2002 .unwrap(),
2003 "/tmp/fake-token-file"
2004 );
2005 assert_eq!(
2006 builder
2007 .get_config_value(&AmazonS3ConfigKey::RoleArn)
2008 .unwrap(),
2009 "arn:aws:iam::123456789012:role/test-role"
2010 );
2011 assert_eq!(
2012 builder
2013 .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
2014 .unwrap(),
2015 "TestSession"
2016 );
2017 assert_eq!(
2018 builder
2019 .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
2020 .unwrap(),
2021 "https://sts.us-west-2.amazonaws.com"
2022 );
2023
2024 let s3 = builder.build().expect("should build successfully");
2025 let creds = &s3.client.config.credentials;
2026 let debug_str = format!("{creds:?}");
2027 assert!(
2028 debug_str.contains("TokenCredentialProvider"),
2029 "expected TokenCredentialProvider but got: {debug_str}"
2030 );
2031 }
2032}