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::{HttpConnector, TokenCredentialProvider, http_connector};
28use crate::config::ConfigValue;
29use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
30use base64::Engine;
31use base64::prelude::BASE64_STANDARD;
32use itertools::Itertools;
33use md5::{Digest, Md5};
34use reqwest::header::{HeaderMap, HeaderValue};
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 skip_signature: ConfigValue<bool>,
178 copy_if_not_exists: Option<ConfigValue<S3CopyIfNotExists>>,
180 conditional_put: ConfigValue<S3ConditionalPut>,
182 disable_tagging: ConfigValue<bool>,
184 encryption_type: Option<ConfigValue<S3EncryptionType>>,
186 encryption_kms_key_id: Option<String>,
187 encryption_bucket_key_enabled: Option<ConfigValue<bool>>,
188 encryption_customer_key_base64: Option<String>,
190 request_payer: ConfigValue<bool>,
192 http_connector: Option<Arc<dyn HttpConnector>>,
194}
195
196#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
208#[non_exhaustive]
209pub enum AmazonS3ConfigKey {
210 AccessKeyId,
218
219 SecretAccessKey,
227
228 Region,
236
237 DefaultRegion,
245
246 Bucket,
256
257 Endpoint,
267
268 S3Endpoint,
275
276 Token,
286
287 ImdsV1Fallback,
295
296 VirtualHostedStyleRequest,
304
305 UnsignedPayload,
313
314 Checksum,
318
319 MetadataEndpoint,
327
328 ContainerCredentialsRelativeUri,
338
339 ContainerCredentialsFullUri,
349
350 ContainerAuthorizationTokenFile,
360
361 WebIdentityTokenFile,
369
370 RoleArn,
378
379 RoleSessionName,
385
386 StsEndpoint,
394
395 CopyIfNotExists,
403
404 ConditionalPut,
412
413 SkipSignature,
421
422 DisableTagging,
431
432 S3Express,
438
439 RequestPayer,
445
446 Client(ClientConfigKey),
448
449 Encryption(S3EncryptionConfigKey),
451}
452
453impl AsRef<str> for AmazonS3ConfigKey {
454 fn as_ref(&self) -> &str {
455 match self {
456 Self::AccessKeyId => "aws_access_key_id",
457 Self::SecretAccessKey => "aws_secret_access_key",
458 Self::Region => "aws_region",
459 Self::Bucket => "aws_bucket",
460 Self::Endpoint => "aws_endpoint",
461 Self::S3Endpoint => "aws_endpoint_url_s3",
462 Self::Token => "aws_session_token",
463 Self::ImdsV1Fallback => "aws_imdsv1_fallback",
464 Self::VirtualHostedStyleRequest => "aws_virtual_hosted_style_request",
465 Self::S3Express => "aws_s3_express",
466 Self::DefaultRegion => "aws_default_region",
467 Self::MetadataEndpoint => "aws_metadata_endpoint",
468 Self::UnsignedPayload => "aws_unsigned_payload",
469 Self::Checksum => "aws_checksum_algorithm",
470 Self::ContainerCredentialsRelativeUri => "aws_container_credentials_relative_uri",
471 Self::ContainerCredentialsFullUri => "aws_container_credentials_full_uri",
472 Self::ContainerAuthorizationTokenFile => "aws_container_authorization_token_file",
473 Self::WebIdentityTokenFile => "aws_web_identity_token_file",
474 Self::RoleArn => "aws_role_arn",
475 Self::RoleSessionName => "aws_role_session_name",
476 Self::StsEndpoint => "aws_endpoint_url_sts",
477 Self::SkipSignature => "aws_skip_signature",
478 Self::CopyIfNotExists => "aws_copy_if_not_exists",
479 Self::ConditionalPut => "aws_conditional_put",
480 Self::DisableTagging => "aws_disable_tagging",
481 Self::RequestPayer => "aws_request_payer",
482 Self::Client(opt) => opt.as_ref(),
483 Self::Encryption(opt) => opt.as_ref(),
484 }
485 }
486}
487
488impl FromStr for AmazonS3ConfigKey {
489 type Err = crate::Error;
490
491 fn from_str(s: &str) -> Result<Self, Self::Err> {
492 match s {
493 "aws_access_key_id" | "access_key_id" => Ok(Self::AccessKeyId),
494 "aws_secret_access_key" | "secret_access_key" => Ok(Self::SecretAccessKey),
495 "aws_default_region" | "default_region" => Ok(Self::DefaultRegion),
496 "aws_region" | "region" => Ok(Self::Region),
497 "aws_bucket" | "aws_bucket_name" | "bucket_name" | "bucket" => Ok(Self::Bucket),
498 "aws_endpoint_url" | "aws_endpoint" | "endpoint_url" | "endpoint" => Ok(Self::Endpoint),
499 "aws_endpoint_url_s3" => Ok(Self::S3Endpoint),
500 "aws_session_token" | "aws_token" | "session_token" | "token" => Ok(Self::Token),
501 "aws_virtual_hosted_style_request" | "virtual_hosted_style_request" => {
502 Ok(Self::VirtualHostedStyleRequest)
503 }
504 "aws_s3_express" | "s3_express" => Ok(Self::S3Express),
505 "aws_imdsv1_fallback" | "imdsv1_fallback" => Ok(Self::ImdsV1Fallback),
506 "aws_metadata_endpoint" | "metadata_endpoint" => Ok(Self::MetadataEndpoint),
507 "aws_unsigned_payload" | "unsigned_payload" => Ok(Self::UnsignedPayload),
508 "aws_checksum_algorithm" | "checksum_algorithm" => Ok(Self::Checksum),
509 "aws_container_credentials_relative_uri" | "container_credentials_relative_uri" => {
510 Ok(Self::ContainerCredentialsRelativeUri)
511 }
512 "aws_container_credentials_full_uri" | "container_credentials_full_uri" => {
513 Ok(Self::ContainerCredentialsFullUri)
514 }
515 "aws_container_authorization_token_file" | "container_authorization_token_file" => {
516 Ok(Self::ContainerAuthorizationTokenFile)
517 }
518 "aws_web_identity_token_file" | "web_identity_token_file" => {
519 Ok(Self::WebIdentityTokenFile)
520 }
521 "aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
522 "aws_role_session_name" | "role_session_name" => Ok(Self::RoleSessionName),
523 "aws_endpoint_url_sts" | "endpoint_url_sts" => Ok(Self::StsEndpoint),
524 "aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
525 "aws_copy_if_not_exists" | "copy_if_not_exists" => Ok(Self::CopyIfNotExists),
526 "aws_conditional_put" | "conditional_put" => Ok(Self::ConditionalPut),
527 "aws_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
528 "aws_request_payer" | "request_payer" => Ok(Self::RequestPayer),
529 "aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
531 "aws_server_side_encryption" | "server_side_encryption" => Ok(Self::Encryption(
532 S3EncryptionConfigKey::ServerSideEncryption,
533 )),
534 "aws_sse_kms_key_id" | "sse_kms_key_id" => {
535 Ok(Self::Encryption(S3EncryptionConfigKey::KmsKeyId))
536 }
537 "aws_sse_bucket_key_enabled" | "sse_bucket_key_enabled" => {
538 Ok(Self::Encryption(S3EncryptionConfigKey::BucketKeyEnabled))
539 }
540 "aws_sse_customer_key_base64" | "sse_customer_key_base64" => Ok(Self::Encryption(
541 S3EncryptionConfigKey::CustomerEncryptionKey,
542 )),
543 _ => match s.strip_prefix("aws_").unwrap_or(s).parse() {
544 Ok(key) => Ok(Self::Client(key)),
545 Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
546 },
547 }
548 }
549}
550
551impl AmazonS3Builder {
552 pub fn new() -> Self {
554 Default::default()
555 }
556
557 pub fn from_env() -> Self {
588 let mut builder: Self = Default::default();
589 for (os_key, os_value) in std::env::vars_os() {
590 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
591 if key.starts_with("AWS_") {
592 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
593 builder = builder.with_config(config_key, value);
594 }
595 }
596 }
597 }
598 builder
599 }
600
601 pub fn with_url(mut self, url: impl Into<String>) -> Self {
622 self.url = Some(url.into());
623 self
624 }
625
626 pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
628 match key {
629 AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
630 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
631 AmazonS3ConfigKey::Region => self.region = Some(value.into()),
632 AmazonS3ConfigKey::Bucket => self.bucket_name = Some(value.into()),
633 AmazonS3ConfigKey::Endpoint => self.endpoint = Some(value.into()),
634 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint = Some(value.into()),
635 AmazonS3ConfigKey::Token => self.token = Some(value.into()),
636 AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
637 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
638 self.virtual_hosted_style_request.parse(value)
639 }
640 AmazonS3ConfigKey::S3Express => self.s3_express.parse(value),
641 AmazonS3ConfigKey::DefaultRegion => {
642 self.region = self.region.or_else(|| Some(value.into()))
643 }
644 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
645 AmazonS3ConfigKey::UnsignedPayload => self.unsigned_payload.parse(value),
646 AmazonS3ConfigKey::Checksum => {
647 self.checksum_algorithm = Some(ConfigValue::Deferred(value.into()))
648 }
649 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
650 self.container_credentials_relative_uri = Some(value.into())
651 }
652 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
653 self.container_credentials_full_uri = Some(value.into());
654 }
655 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
656 self.container_authorization_token_file = Some(value.into());
657 }
658 AmazonS3ConfigKey::WebIdentityTokenFile => {
659 self.web_identity_token_file = Some(value.into());
660 }
661 AmazonS3ConfigKey::RoleArn => {
662 self.role_arn = Some(value.into());
663 }
664 AmazonS3ConfigKey::RoleSessionName => {
665 self.role_session_name = Some(value.into());
666 }
667 AmazonS3ConfigKey::StsEndpoint => {
668 self.sts_endpoint = Some(value.into());
669 }
670 AmazonS3ConfigKey::Client(key) => {
671 self.client_options = self.client_options.with_config(key, value)
672 }
673 AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
674 AmazonS3ConfigKey::DisableTagging => self.disable_tagging.parse(value),
675 AmazonS3ConfigKey::CopyIfNotExists => {
676 self.copy_if_not_exists = Some(ConfigValue::Deferred(value.into()))
677 }
678 AmazonS3ConfigKey::ConditionalPut => {
679 self.conditional_put = ConfigValue::Deferred(value.into())
680 }
681 AmazonS3ConfigKey::RequestPayer => {
682 self.request_payer = ConfigValue::Deferred(value.into())
683 }
684 AmazonS3ConfigKey::Encryption(key) => match key {
685 S3EncryptionConfigKey::ServerSideEncryption => {
686 self.encryption_type = Some(ConfigValue::Deferred(value.into()))
687 }
688 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id = Some(value.into()),
689 S3EncryptionConfigKey::BucketKeyEnabled => {
690 self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into()))
691 }
692 S3EncryptionConfigKey::CustomerEncryptionKey => {
693 self.encryption_customer_key_base64 = Some(value.into())
694 }
695 },
696 };
697 self
698 }
699
700 pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
712 match key {
713 AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
714 AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
715 AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
716 AmazonS3ConfigKey::Bucket => self.bucket_name.clone(),
717 AmazonS3ConfigKey::Endpoint => self.endpoint.clone(),
718 AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint.clone(),
719 AmazonS3ConfigKey::Token => self.token.clone(),
720 AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
721 AmazonS3ConfigKey::VirtualHostedStyleRequest => {
722 Some(self.virtual_hosted_style_request.to_string())
723 }
724 AmazonS3ConfigKey::S3Express => Some(self.s3_express.to_string()),
725 AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
726 AmazonS3ConfigKey::UnsignedPayload => Some(self.unsigned_payload.to_string()),
727 AmazonS3ConfigKey::Checksum => {
728 self.checksum_algorithm.as_ref().map(ToString::to_string)
729 }
730 AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
731 AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
732 self.container_credentials_relative_uri.clone()
733 }
734 AmazonS3ConfigKey::ContainerCredentialsFullUri => {
735 self.container_credentials_full_uri.clone()
736 }
737 AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
738 self.container_authorization_token_file.clone()
739 }
740 AmazonS3ConfigKey::WebIdentityTokenFile => self.web_identity_token_file.clone(),
741 AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
742 AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
743 AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
744 AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
745 AmazonS3ConfigKey::CopyIfNotExists => {
746 self.copy_if_not_exists.as_ref().map(ToString::to_string)
747 }
748 AmazonS3ConfigKey::ConditionalPut => Some(self.conditional_put.to_string()),
749 AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
750 AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
751 AmazonS3ConfigKey::Encryption(key) => match key {
752 S3EncryptionConfigKey::ServerSideEncryption => {
753 self.encryption_type.as_ref().map(ToString::to_string)
754 }
755 S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id.clone(),
756 S3EncryptionConfigKey::BucketKeyEnabled => self
757 .encryption_bucket_key_enabled
758 .as_ref()
759 .map(ToString::to_string),
760 S3EncryptionConfigKey::CustomerEncryptionKey => {
761 self.encryption_customer_key_base64.clone()
762 }
763 },
764 }
765 }
766
767 fn parse_url(&mut self, url: &str) -> Result<()> {
772 let parsed = Url::parse(url).map_err(|source| {
773 let url = url.into();
774 Error::UnableToParseUrl { url, source }
775 })?;
776
777 let host = parsed
778 .host_str()
779 .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
780
781 match parsed.scheme() {
782 "s3" | "s3a" => self.bucket_name = Some(host.to_string()),
783 "https" => match host.splitn(4, '.').collect_tuple() {
784 Some(("s3", region, "amazonaws", "com")) => {
785 self.region = Some(region.to_string());
786 let bucket = parsed.path_segments().into_iter().flatten().next();
787 if let Some(bucket) = bucket {
788 self.bucket_name = Some(bucket.into());
789 }
790 }
791 Some((bucket, "s3", "amazonaws", "com")) => {
792 self.bucket_name = Some(bucket.to_string());
793 self.virtual_hosted_style_request = true.into();
794 }
795 Some((bucket, "s3", region, "amazonaws.com")) => {
796 self.bucket_name = Some(bucket.to_string());
797 self.region = Some(region.to_string());
798 self.virtual_hosted_style_request = true.into();
799 }
800 Some((account, "r2", "cloudflarestorage", "com")) => {
801 self.region = Some("auto".to_string());
802 let endpoint = format!("https://{account}.r2.cloudflarestorage.com");
803 self.endpoint = Some(endpoint);
804
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 _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
811 },
812 scheme => {
813 let scheme = scheme.into();
814 return Err(Error::UnknownUrlScheme { scheme }.into());
815 }
816 };
817 Ok(())
818 }
819
820 pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
824 self.access_key_id = Some(access_key_id.into());
825 self
826 }
827
828 pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
830 self.secret_access_key = Some(secret_access_key.into());
831 self
832 }
833
834 pub fn with_token(mut self, token: impl Into<String>) -> Self {
838 self.token = Some(token.into());
839 self
840 }
841
842 pub fn with_region(mut self, region: impl Into<String>) -> Self {
844 self.region = Some(region.into());
845 self
846 }
847
848 pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
850 self.bucket_name = Some(bucket_name.into());
851 self
852 }
853
854 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
870 self.endpoint = Some(endpoint.into());
871 self
872 }
873
874 pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
876 self.credentials = Some(credentials);
877 self
878 }
879
880 pub fn with_allow_http(mut self, allow_http: bool) -> Self {
894 self.client_options = self.client_options.with_allow_http(allow_http);
895 self
896 }
897
898 pub fn with_virtual_hosted_style_request(mut self, virtual_hosted_style_request: bool) -> Self {
909 self.virtual_hosted_style_request = virtual_hosted_style_request.into();
910 self
911 }
912
913 pub fn with_s3_express(mut self, s3_express: bool) -> Self {
915 self.s3_express = s3_express.into();
916 self
917 }
918
919 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
921 self.retry_config = retry_config;
922 self
923 }
924
925 pub fn with_imdsv1_fallback(mut self) -> Self {
937 self.imdsv1_fallback = true.into();
938 self
939 }
940
941 pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
947 self.unsigned_payload = unsigned_payload.into();
948 self
949 }
950
951 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
955 self.skip_signature = skip_signature.into();
956 self
957 }
958
959 pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
963 self.checksum_algorithm = Some(checksum_algorithm.into());
965 self
966 }
967
968 pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
974 self.metadata_endpoint = Some(endpoint.into());
975 self
976 }
977
978 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
980 self.client_options = self.client_options.with_proxy_url(proxy_url);
981 self
982 }
983
984 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
986 self.client_options = self
987 .client_options
988 .with_proxy_ca_certificate(proxy_ca_certificate);
989 self
990 }
991
992 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
994 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
995 self
996 }
997
998 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1000 self.client_options = options;
1001 self
1002 }
1003
1004 pub fn with_copy_if_not_exists(mut self, config: S3CopyIfNotExists) -> Self {
1006 self.copy_if_not_exists = Some(config.into());
1007 self
1008 }
1009
1010 pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1013 self.conditional_put = config.into();
1014 self
1015 }
1016
1017 pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1019 self.disable_tagging = ignore.into();
1020 self
1021 }
1022
1023 pub fn with_sse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1025 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseKms));
1026 if let Some(kms_key_id) = kms_key_id.into().into() {
1027 self.encryption_kms_key_id = Some(kms_key_id);
1028 }
1029 self
1030 }
1031
1032 pub fn with_dsse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1034 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::DsseKms));
1035 if let Some(kms_key_id) = kms_key_id.into().into() {
1036 self.encryption_kms_key_id = Some(kms_key_id);
1037 }
1038 self
1039 }
1040
1041 pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self {
1044 self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC));
1045 self.encryption_customer_key_base64 = customer_key_base64.into().into();
1046 self
1047 }
1048
1049 pub fn with_bucket_key(mut self, enabled: bool) -> Self {
1056 self.encryption_bucket_key_enabled = Some(ConfigValue::Parsed(enabled));
1057 self
1058 }
1059
1060 pub fn with_request_payer(mut self, enabled: bool) -> Self {
1064 self.request_payer = ConfigValue::Parsed(enabled);
1065 self
1066 }
1067
1068 pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1072 self.http_connector = Some(Arc::new(connector));
1073 self
1074 }
1075
1076 pub fn build(mut self) -> Result<AmazonS3> {
1079 if let Some(url) = self.url.take() {
1080 self.parse_url(&url)?;
1081 }
1082
1083 let http = http_connector(self.http_connector)?;
1084
1085 let bucket = self.bucket_name.ok_or(Error::MissingBucketName)?;
1086 let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
1087 let checksum = self.checksum_algorithm.map(|x| x.get()).transpose()?;
1088 let copy_if_not_exists = self.copy_if_not_exists.map(|x| x.get()).transpose()?;
1089
1090 let credentials = if let Some(credentials) = self.credentials {
1091 credentials
1092 } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
1093 match (self.access_key_id, self.secret_access_key, self.token) {
1094 (Some(key_id), Some(secret_key), token) => {
1095 debug!("Using Static credential provider");
1096 let credential = AwsCredential {
1097 key_id,
1098 secret_key,
1099 token,
1100 };
1101 Arc::new(StaticCredentialProvider::new(credential)) as _
1102 }
1103 (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
1104 (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
1105 (None, None, _) => unreachable!(),
1106 }
1107 } else if let (Some(token_path), Some(role_arn)) =
1108 (self.web_identity_token_file, self.role_arn)
1109 {
1110 debug!("Using WebIdentity credential provider");
1111
1112 let session_name = self
1113 .role_session_name
1114 .clone()
1115 .unwrap_or_else(|| "WebIdentitySession".to_string());
1116
1117 let endpoint = self
1118 .sts_endpoint
1119 .clone()
1120 .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
1121
1122 let options = self.client_options.clone().with_allow_http(false);
1124
1125 let token = WebIdentityProvider {
1126 token_path: token_path.clone(),
1127 session_name,
1128 role_arn: role_arn.clone(),
1129 endpoint,
1130 };
1131
1132 Arc::new(TokenCredentialProvider::new(
1133 token,
1134 http.connect(&options)?,
1135 self.retry_config.clone(),
1136 )) as _
1137 } else if let Some(uri) = self.container_credentials_relative_uri {
1138 debug!("Using Task credential provider");
1139
1140 let options = self.client_options.clone().with_allow_http(true);
1141
1142 Arc::new(TaskCredentialProvider {
1143 url: format!("http://169.254.170.2{uri}"),
1144 retry: self.retry_config.clone(),
1145 client: http.connect(&options)?,
1147 cache: Default::default(),
1148 }) as _
1149 } else if let (Some(full_uri), Some(token_file)) = (
1150 self.container_credentials_full_uri,
1151 self.container_authorization_token_file,
1152 ) {
1153 debug!("Using EKS Pod Identity credential provider");
1154
1155 let options = self.client_options.clone().with_allow_http(true);
1156
1157 Arc::new(EKSPodCredentialProvider {
1158 url: full_uri,
1159 token_file,
1160 retry: self.retry_config.clone(),
1161 client: http.connect(&options)?,
1162 cache: Default::default(),
1163 }) as _
1164 } else {
1165 debug!("Using Instance credential provider");
1166
1167 let token = InstanceCredentialProvider {
1168 imdsv1_fallback: self.imdsv1_fallback.get()?,
1169 metadata_endpoint: self
1170 .metadata_endpoint
1171 .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
1172 };
1173
1174 Arc::new(TokenCredentialProvider::new(
1175 token,
1176 http.connect(&self.client_options.metadata_options())?,
1177 self.retry_config.clone(),
1178 )) as _
1179 };
1180
1181 let (session_provider, zonal_endpoint) = match self.s3_express.get()? {
1182 true => {
1183 let zone = parse_bucket_az(&bucket).ok_or_else(|| {
1184 let bucket = bucket.clone();
1185 Error::ZoneSuffix { bucket }
1186 })?;
1187
1188 let endpoint = format!("https://{bucket}.s3express-{zone}.{region}.amazonaws.com");
1190
1191 let session = Arc::new(
1192 TokenCredentialProvider::new(
1193 SessionProvider {
1194 endpoint: endpoint.clone(),
1195 region: region.clone(),
1196 credentials: Arc::clone(&credentials),
1197 },
1198 http.connect(&self.client_options)?,
1199 self.retry_config.clone(),
1200 )
1201 .with_min_ttl(Duration::from_secs(60)), );
1203 (Some(session as _), Some(endpoint))
1204 }
1205 false => (None, None),
1206 };
1207
1208 let endpoint = self.s3_endpoint.or(self.endpoint);
1210
1211 let virtual_hosted = self.virtual_hosted_style_request.get()?;
1214 let bucket_endpoint = match (&endpoint, zonal_endpoint, virtual_hosted) {
1215 (Some(endpoint), _, true) => endpoint.clone(),
1216 (Some(endpoint), _, false) => format!("{}/{}", endpoint.trim_end_matches("/"), bucket),
1217 (None, Some(endpoint), _) => endpoint,
1218 (None, None, true) => format!("https://{bucket}.s3.{region}.amazonaws.com"),
1219 (None, None, false) => format!("https://s3.{region}.amazonaws.com/{bucket}"),
1220 };
1221
1222 let encryption_headers = if let Some(encryption_type) = self.encryption_type {
1223 S3EncryptionHeaders::try_new(
1224 &encryption_type.get()?,
1225 self.encryption_kms_key_id,
1226 self.encryption_bucket_key_enabled
1227 .map(|val| val.get())
1228 .transpose()?,
1229 self.encryption_customer_key_base64,
1230 )?
1231 } else {
1232 S3EncryptionHeaders::default()
1233 };
1234
1235 let config = S3Config {
1236 region,
1237 bucket,
1238 bucket_endpoint,
1239 credentials,
1240 session_provider,
1241 retry_config: self.retry_config,
1242 client_options: self.client_options,
1243 sign_payload: !self.unsigned_payload.get()?,
1244 skip_signature: self.skip_signature.get()?,
1245 disable_tagging: self.disable_tagging.get()?,
1246 checksum,
1247 copy_if_not_exists,
1248 conditional_put: self.conditional_put.get()?,
1249 encryption_headers,
1250 request_payer: self.request_payer.get()?,
1251 };
1252
1253 let http_client = http.connect(&config.client_options)?;
1254 let client = Arc::new(S3Client::new(config, http_client));
1255
1256 Ok(AmazonS3 { client })
1257 }
1258}
1259
1260fn parse_bucket_az(bucket: &str) -> Option<&str> {
1264 let base = bucket
1265 .strip_suffix("--x-s3")
1266 .or_else(|| bucket.strip_suffix("--xa-s3"))?;
1267 Some(base.rsplit_once("--")?.1)
1268}
1269
1270#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1280#[non_exhaustive]
1281pub enum S3EncryptionConfigKey {
1282 ServerSideEncryption,
1294 KmsKeyId,
1304 BucketKeyEnabled,
1311
1312 CustomerEncryptionKey,
1320}
1321
1322impl AsRef<str> for S3EncryptionConfigKey {
1323 fn as_ref(&self) -> &str {
1324 match self {
1325 Self::ServerSideEncryption => "aws_server_side_encryption",
1326 Self::KmsKeyId => "aws_sse_kms_key_id",
1327 Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled",
1328 Self::CustomerEncryptionKey => "aws_sse_customer_key_base64",
1329 }
1330 }
1331}
1332
1333#[derive(Debug, Clone)]
1334enum S3EncryptionType {
1335 S3,
1336 SseKms,
1337 DsseKms,
1338 SseC,
1339}
1340
1341impl crate::config::Parse for S3EncryptionType {
1342 fn parse(s: &str) -> Result<Self> {
1343 match s {
1344 "AES256" => Ok(Self::S3),
1345 "aws:kms" => Ok(Self::SseKms),
1346 "aws:kms:dsse" => Ok(Self::DsseKms),
1347 "sse-c" => Ok(Self::SseC),
1348 _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()),
1349 }
1350 }
1351}
1352
1353impl From<&S3EncryptionType> for &'static str {
1354 fn from(value: &S3EncryptionType) -> Self {
1355 match value {
1356 S3EncryptionType::S3 => "AES256",
1357 S3EncryptionType::SseKms => "aws:kms",
1358 S3EncryptionType::DsseKms => "aws:kms:dsse",
1359 S3EncryptionType::SseC => "sse-c",
1360 }
1361 }
1362}
1363
1364impl std::fmt::Display for S3EncryptionType {
1365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1366 f.write_str(self.into())
1367 }
1368}
1369
1370#[derive(Default, Clone, Debug)]
1376pub(super) struct S3EncryptionHeaders(pub HeaderMap);
1377
1378impl S3EncryptionHeaders {
1379 fn try_new(
1380 encryption_type: &S3EncryptionType,
1381 encryption_kms_key_id: Option<String>,
1382 bucket_key_enabled: Option<bool>,
1383 encryption_customer_key_base64: Option<String>,
1384 ) -> Result<Self> {
1385 let mut headers = HeaderMap::new();
1386 match encryption_type {
1387 S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => {
1388 headers.insert(
1389 "x-amz-server-side-encryption",
1390 HeaderValue::from_static(encryption_type.into()),
1391 );
1392 if let Some(key_id) = encryption_kms_key_id {
1393 headers.insert(
1394 "x-amz-server-side-encryption-aws-kms-key-id",
1395 key_id
1396 .try_into()
1397 .map_err(|err| Error::InvalidEncryptionHeader {
1398 header: "kms-key-id",
1399 source: Box::new(err),
1400 })?,
1401 );
1402 }
1403 if let Some(bucket_key_enabled) = bucket_key_enabled {
1404 headers.insert(
1405 "x-amz-server-side-encryption-bucket-key-enabled",
1406 HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }),
1407 );
1408 }
1409 }
1410 S3EncryptionType::SseC => {
1411 headers.insert(
1412 "x-amz-server-side-encryption-customer-algorithm",
1413 HeaderValue::from_static("AES256"),
1414 );
1415 if let Some(key) = encryption_customer_key_base64 {
1416 let mut header_value: HeaderValue =
1417 key.clone()
1418 .try_into()
1419 .map_err(|err| Error::InvalidEncryptionHeader {
1420 header: "x-amz-server-side-encryption-customer-key",
1421 source: Box::new(err),
1422 })?;
1423 header_value.set_sensitive(true);
1424 headers.insert("x-amz-server-side-encryption-customer-key", header_value);
1425
1426 let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| {
1427 Error::InvalidEncryptionHeader {
1428 header: "x-amz-server-side-encryption-customer-key",
1429 source: Box::new(err),
1430 }
1431 })?;
1432 let mut hasher = Md5::new();
1433 hasher.update(decoded_key);
1434 let md5 = BASE64_STANDARD.encode(hasher.finalize());
1435 let mut md5_header_value: HeaderValue =
1436 md5.try_into()
1437 .map_err(|err| Error::InvalidEncryptionHeader {
1438 header: "x-amz-server-side-encryption-customer-key-MD5",
1439 source: Box::new(err),
1440 })?;
1441 md5_header_value.set_sensitive(true);
1442 headers.insert(
1443 "x-amz-server-side-encryption-customer-key-MD5",
1444 md5_header_value,
1445 );
1446 } else {
1447 return Err(Error::InvalidEncryptionHeader {
1448 header: "x-amz-server-side-encryption-customer-key",
1449 source: Box::new(std::io::Error::new(
1450 std::io::ErrorKind::InvalidInput,
1451 "Missing customer key",
1452 )),
1453 }
1454 .into());
1455 }
1456 }
1457 }
1458 Ok(Self(headers))
1459 }
1460}
1461
1462impl From<S3EncryptionHeaders> for HeaderMap {
1463 fn from(headers: S3EncryptionHeaders) -> Self {
1464 headers.0
1465 }
1466}
1467
1468#[cfg(test)]
1469mod tests {
1470 use super::*;
1471 use std::collections::HashMap;
1472
1473 #[test]
1474 fn s3_test_config_from_map() {
1475 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1476 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1477 let aws_default_region = "object_store:fake_default_region".to_string();
1478 let aws_endpoint = "object_store:fake_endpoint".to_string();
1479 let aws_session_token = "object_store:fake_session_token".to_string();
1480 let options = HashMap::from([
1481 ("aws_access_key_id", aws_access_key_id.clone()),
1482 ("aws_secret_access_key", aws_secret_access_key),
1483 ("aws_default_region", aws_default_region.clone()),
1484 ("aws_endpoint", aws_endpoint.clone()),
1485 ("aws_session_token", aws_session_token.clone()),
1486 ("aws_unsigned_payload", "true".to_string()),
1487 ("aws_checksum_algorithm", "sha256".to_string()),
1488 ]);
1489
1490 let builder = options
1491 .into_iter()
1492 .fold(AmazonS3Builder::new(), |builder, (key, value)| {
1493 builder.with_config(key.parse().unwrap(), value)
1494 })
1495 .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
1496
1497 assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
1498 assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
1499 assert_eq!(builder.region.unwrap(), aws_default_region);
1500 assert_eq!(builder.endpoint.unwrap(), aws_endpoint);
1501 assert_eq!(builder.token.unwrap(), aws_session_token);
1502 assert_eq!(
1503 builder.checksum_algorithm.unwrap().get().unwrap(),
1504 Checksum::SHA256
1505 );
1506 assert!(builder.unsigned_payload.get().unwrap());
1507 }
1508
1509 #[test]
1510 fn s3_test_endpoint_url_s3_config() {
1511 let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1513 assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1514
1515 let s3 = AmazonS3Builder::new()
1517 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1518 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1519 .with_bucket_name("test-bucket")
1520 .build()
1521 .unwrap();
1522 assert_eq!(
1523 s3.client.config.bucket_endpoint,
1524 "http://s3-specific-endpoint/test-bucket"
1525 );
1526
1527 let s3 = AmazonS3Builder::new()
1529 .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1530 .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1531 .with_bucket_name("test-bucket")
1532 .build()
1533 .unwrap();
1534 assert_eq!(
1535 s3.client.config.bucket_endpoint,
1536 "http://s3-specific-endpoint/test-bucket"
1537 );
1538 }
1539
1540 #[test]
1541 fn s3_test_config_get_value() {
1542 let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1543 let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1544 let aws_default_region = "object_store:fake_default_region".to_string();
1545 let aws_endpoint = "object_store:fake_endpoint".to_string();
1546 let aws_session_token = "object_store:fake_session_token".to_string();
1547
1548 let builder = AmazonS3Builder::new()
1549 .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
1550 .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
1551 .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
1552 .with_config(AmazonS3ConfigKey::Endpoint, &aws_endpoint)
1553 .with_config(AmazonS3ConfigKey::Token, &aws_session_token)
1554 .with_config(AmazonS3ConfigKey::UnsignedPayload, "true")
1555 .with_config("aws_server_side_encryption".parse().unwrap(), "AES256")
1556 .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id")
1557 .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true")
1558 .with_config(
1559 "aws_sse_customer_key_base64".parse().unwrap(),
1560 "some_customer_key",
1561 );
1562
1563 assert_eq!(
1564 builder
1565 .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
1566 .unwrap(),
1567 aws_access_key_id
1568 );
1569 assert_eq!(
1570 builder
1571 .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
1572 .unwrap(),
1573 aws_secret_access_key
1574 );
1575 assert_eq!(
1576 builder
1577 .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
1578 .unwrap(),
1579 aws_default_region
1580 );
1581 assert_eq!(
1582 builder
1583 .get_config_value(&AmazonS3ConfigKey::Endpoint)
1584 .unwrap(),
1585 aws_endpoint
1586 );
1587 assert_eq!(
1588 builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
1589 aws_session_token
1590 );
1591 assert_eq!(
1592 builder
1593 .get_config_value(&AmazonS3ConfigKey::UnsignedPayload)
1594 .unwrap(),
1595 "true"
1596 );
1597 assert_eq!(
1598 builder
1599 .get_config_value(&"aws_server_side_encryption".parse().unwrap())
1600 .unwrap(),
1601 "AES256"
1602 );
1603 assert_eq!(
1604 builder
1605 .get_config_value(&"aws_sse_kms_key_id".parse().unwrap())
1606 .unwrap(),
1607 "some_key_id"
1608 );
1609 assert_eq!(
1610 builder
1611 .get_config_value(&"aws_sse_bucket_key_enabled".parse().unwrap())
1612 .unwrap(),
1613 "true"
1614 );
1615 assert_eq!(
1616 builder
1617 .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap())
1618 .unwrap(),
1619 "some_customer_key"
1620 );
1621 }
1622
1623 #[test]
1624 fn s3_default_region() {
1625 let builder = AmazonS3Builder::new()
1626 .with_bucket_name("foo")
1627 .build()
1628 .unwrap();
1629 assert_eq!(builder.client.config.region, "us-east-1");
1630 }
1631
1632 #[test]
1633 fn s3_test_bucket_endpoint() {
1634 let builder = AmazonS3Builder::new()
1635 .with_endpoint("http://some.host:1234")
1636 .with_bucket_name("foo")
1637 .build()
1638 .unwrap();
1639 assert_eq!(
1640 builder.client.config.bucket_endpoint,
1641 "http://some.host:1234/foo"
1642 );
1643
1644 let builder = AmazonS3Builder::new()
1645 .with_endpoint("http://some.host:1234/")
1646 .with_bucket_name("foo")
1647 .build()
1648 .unwrap();
1649 assert_eq!(
1650 builder.client.config.bucket_endpoint,
1651 "http://some.host:1234/foo"
1652 );
1653 }
1654
1655 #[test]
1656 fn s3_test_urls() {
1657 let mut builder = AmazonS3Builder::new();
1658 builder.parse_url("s3://bucket/path").unwrap();
1659 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1660
1661 let mut builder = AmazonS3Builder::new();
1662 builder
1663 .parse_url("s3://buckets.can.have.dots/path")
1664 .unwrap();
1665 assert_eq!(
1666 builder.bucket_name,
1667 Some("buckets.can.have.dots".to_string())
1668 );
1669
1670 let mut builder = AmazonS3Builder::new();
1671 builder
1672 .parse_url("https://s3.region.amazonaws.com")
1673 .unwrap();
1674 assert_eq!(builder.region, Some("region".to_string()));
1675
1676 let mut builder = AmazonS3Builder::new();
1677 builder
1678 .parse_url("https://s3.region.amazonaws.com/bucket")
1679 .unwrap();
1680 assert_eq!(builder.region, Some("region".to_string()));
1681 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1682
1683 let mut builder = AmazonS3Builder::new();
1684 builder
1685 .parse_url("https://s3.region.amazonaws.com/bucket.with.dot/path")
1686 .unwrap();
1687 assert_eq!(builder.region, Some("region".to_string()));
1688 assert_eq!(builder.bucket_name, Some("bucket.with.dot".to_string()));
1689
1690 let mut builder = AmazonS3Builder::new();
1691 builder
1692 .parse_url("https://bucket.s3.amazonaws.com")
1693 .unwrap();
1694 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1695 assert!(builder.virtual_hosted_style_request.get().unwrap());
1696
1697 let mut builder = AmazonS3Builder::new();
1698 builder
1699 .parse_url("https://bucket.s3.region.amazonaws.com")
1700 .unwrap();
1701 assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1702 assert_eq!(builder.region, Some("region".to_string()));
1703 assert!(builder.virtual_hosted_style_request.get().unwrap());
1704
1705 let mut builder = AmazonS3Builder::new();
1706 builder
1707 .parse_url("https://account123.r2.cloudflarestorage.com/bucket-123")
1708 .unwrap();
1709
1710 assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1711 assert_eq!(builder.region, Some("auto".to_string()));
1712 assert_eq!(
1713 builder.endpoint,
1714 Some("https://account123.r2.cloudflarestorage.com".to_string())
1715 );
1716
1717 let err_cases = [
1718 "mailto://bucket/path",
1719 "https://s3.bucket.mydomain.com",
1720 "https://s3.bucket.foo.amazonaws.com",
1721 "https://bucket.mydomain.region.amazonaws.com",
1722 "https://bucket.s3.region.bar.amazonaws.com",
1723 "https://bucket.foo.s3.amazonaws.com",
1724 ];
1725 let mut builder = AmazonS3Builder::new();
1726 for case in err_cases {
1727 builder.parse_url(case).unwrap_err();
1728 }
1729 }
1730
1731 #[tokio::test]
1732 async fn s3_test_proxy_url() {
1733 let s3 = AmazonS3Builder::new()
1734 .with_access_key_id("access_key_id")
1735 .with_secret_access_key("secret_access_key")
1736 .with_region("region")
1737 .with_bucket_name("bucket_name")
1738 .with_allow_http(true)
1739 .with_proxy_url("https://example.com")
1740 .build();
1741
1742 assert!(s3.is_ok());
1743
1744 let err = AmazonS3Builder::new()
1745 .with_access_key_id("access_key_id")
1746 .with_secret_access_key("secret_access_key")
1747 .with_region("region")
1748 .with_bucket_name("bucket_name")
1749 .with_allow_http(true)
1750 .with_proxy_url("dxx:ddd\\example.com")
1752 .build()
1753 .unwrap_err()
1754 .to_string();
1755
1756 assert_eq!("Generic HTTP client error: builder error", err);
1757 }
1758
1759 #[test]
1760 fn test_invalid_config() {
1761 let err = AmazonS3Builder::new()
1762 .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
1763 .with_bucket_name("bucket")
1764 .with_region("region")
1765 .build()
1766 .unwrap_err()
1767 .to_string();
1768
1769 assert_eq!(
1770 err,
1771 "Generic Config error: failed to parse \"enabled\" as boolean"
1772 );
1773
1774 let err = AmazonS3Builder::new()
1775 .with_config(AmazonS3ConfigKey::Checksum, "md5")
1776 .with_bucket_name("bucket")
1777 .with_region("region")
1778 .build()
1779 .unwrap_err()
1780 .to_string();
1781
1782 assert_eq!(
1783 err,
1784 "Generic Config error: \"md5\" is not a valid checksum algorithm"
1785 );
1786 }
1787
1788 #[test]
1789 fn test_parse_bucket_az() {
1790 let cases = [
1791 ("bucket-base-name--usw2-az1--x-s3", Some("usw2-az1")),
1792 ("bucket-base--name--azid--x-s3", Some("azid")),
1793 ("bucket-base-name--use1-az4--xa-s3", Some("use1-az4")),
1794 ("bucket-base--name--azid--xa-s3", Some("azid")),
1795 ("bucket-base-name", None),
1796 ("bucket-base-name--x-s3", None),
1797 ("bucket-base-name--xa-s3", None),
1798 ];
1799
1800 for (bucket, expected) in cases {
1801 assert_eq!(parse_bucket_az(bucket), expected)
1802 }
1803 }
1804
1805 #[test]
1806 fn aws_test_client_opts() {
1807 let key = "AWS_PROXY_URL";
1808 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
1809 assert_eq!(
1810 AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
1811 config_key
1812 );
1813 } else {
1814 panic!("{key} not propagated as ClientConfigKey");
1815 }
1816 }
1817
1818 #[test]
1819 fn test_builder_eks_with_config() {
1820 let builder = AmazonS3Builder::new()
1821 .with_bucket_name("some-bucket")
1822 .with_config(
1823 AmazonS3ConfigKey::ContainerCredentialsFullUri,
1824 "https://127.0.0.1/eks-credentials",
1825 )
1826 .with_config(
1827 AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
1828 "/tmp/fake-bearer-token",
1829 );
1830
1831 let s3 = builder.build().expect("should build successfully");
1832 let creds = &s3.client.config.credentials;
1833 let debug_str = format!("{creds:?}");
1834 assert!(
1835 debug_str.contains("EKSPodCredentialProvider"),
1836 "expected EKS provider but got: {debug_str}"
1837 );
1838 }
1839
1840 #[test]
1841 fn test_builder_web_identity_with_config() {
1842 let builder = AmazonS3Builder::new()
1843 .with_bucket_name("some-bucket")
1844 .with_config(
1845 AmazonS3ConfigKey::WebIdentityTokenFile,
1846 "/tmp/fake-token-file",
1847 )
1848 .with_config(
1849 AmazonS3ConfigKey::RoleArn,
1850 "arn:aws:iam::123456789012:role/test-role",
1851 )
1852 .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
1853 .with_config(
1854 AmazonS3ConfigKey::StsEndpoint,
1855 "https://sts.us-west-2.amazonaws.com",
1856 );
1857
1858 assert_eq!(
1859 builder
1860 .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
1861 .unwrap(),
1862 "/tmp/fake-token-file"
1863 );
1864 assert_eq!(
1865 builder
1866 .get_config_value(&AmazonS3ConfigKey::RoleArn)
1867 .unwrap(),
1868 "arn:aws:iam::123456789012:role/test-role"
1869 );
1870 assert_eq!(
1871 builder
1872 .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
1873 .unwrap(),
1874 "TestSession"
1875 );
1876 assert_eq!(
1877 builder
1878 .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
1879 .unwrap(),
1880 "https://sts.us-west-2.amazonaws.com"
1881 );
1882
1883 let s3 = builder.build().expect("should build successfully");
1884 let creds = &s3.client.config.credentials;
1885 let debug_str = format!("{creds:?}");
1886 assert!(
1887 debug_str.contains("TokenCredentialProvider"),
1888 "expected TokenCredentialProvider but got: {debug_str}"
1889 );
1890 }
1891}