Skip to main content

object_store/aws/
builder.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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
42/// Default metadata endpoint
43static DEFAULT_METADATA_ENDPOINT: &str = "http://169.254.169.254";
44
45/// A specialized `Error` for object store-related errors
46#[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/// Configure a connection to Amazon S3 using the specified credentials in
110/// the specified Amazon region and bucket.
111///
112/// # Example
113/// ```
114/// # let REGION = "foo";
115/// # let BUCKET_NAME = "foo";
116/// # let ACCESS_KEY_ID = "foo";
117/// # let SECRET_KEY = "foo";
118/// # use object_store::aws::AmazonS3Builder;
119/// let s3 = AmazonS3Builder::new()
120///  .with_region(REGION)
121///  .with_bucket_name(BUCKET_NAME)
122///  .with_access_key_id(ACCESS_KEY_ID)
123///  .with_secret_access_key(SECRET_KEY)
124///  .build();
125/// ```
126#[derive(Debug, Default, Clone)]
127pub struct AmazonS3Builder {
128    /// Access key id
129    access_key_id: Option<String>,
130    /// Secret access_key
131    secret_access_key: Option<String>,
132    /// Region
133    region: Option<String>,
134    /// Bucket name
135    bucket_name: Option<String>,
136    /// Endpoint for communicating with AWS S3
137    endpoint: Option<String>,
138    /// Service-specific S3 endpoint URL (takes precedence over endpoint)
139    s3_endpoint: Option<String>,
140    /// Token to use for requests
141    token: Option<String>,
142    /// Url
143    url: Option<String>,
144    /// Retry config
145    retry_config: RetryConfig,
146    /// When set to true, fallback to IMDSv1
147    imdsv1_fallback: ConfigValue<bool>,
148    /// When set to true, virtual hosted style request has to be used
149    virtual_hosted_style_request: ConfigValue<bool>,
150    /// When set to true, S3 express is used
151    s3_express: ConfigValue<bool>,
152    /// When set to true, unsigned payload option has to be used
153    unsigned_payload: ConfigValue<bool>,
154    /// Checksum algorithm which has to be used for object integrity check during upload
155    checksum_algorithm: Option<ConfigValue<Checksum>>,
156    /// Metadata endpoint, see <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html>
157    metadata_endpoint: Option<String>,
158    /// Container credentials URL, see <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
159    container_credentials_relative_uri: Option<String>,
160    /// Container credentials full URL, see <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
161    container_credentials_full_uri: Option<String>,
162    /// Container authorization token file, see <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
163    container_authorization_token_file: Option<String>,
164    /// Web identity token file path for AssumeRoleWithWebIdentity
165    web_identity_token_file: Option<String>,
166    /// Role ARN to assume when using web identity token
167    role_arn: Option<String>,
168    /// Session name for web identity role assumption
169    role_session_name: Option<String>,
170    /// Custom STS endpoint for web identity token exchange
171    sts_endpoint: Option<String>,
172    /// Client options
173    client_options: ClientOptions,
174    /// Credentials
175    credentials: Option<AwsCredentialProvider>,
176    /// Skip signing requests
177    skip_signature: ConfigValue<bool>,
178    /// Copy if not exists
179    copy_if_not_exists: Option<ConfigValue<S3CopyIfNotExists>>,
180    /// Put precondition
181    conditional_put: ConfigValue<S3ConditionalPut>,
182    /// Ignore tags
183    disable_tagging: ConfigValue<bool>,
184    /// Encryption (See [`S3EncryptionConfigKey`])
185    encryption_type: Option<ConfigValue<S3EncryptionType>>,
186    encryption_kms_key_id: Option<String>,
187    encryption_bucket_key_enabled: Option<ConfigValue<bool>>,
188    /// base64-encoded 256-bit customer encryption key for SSE-C.
189    encryption_customer_key_base64: Option<String>,
190    /// When set to true, charge requester for bucket operations
191    request_payer: ConfigValue<bool>,
192    /// The [`HttpConnector`] to use
193    http_connector: Option<Arc<dyn HttpConnector>>,
194}
195
196/// Configuration keys for [`AmazonS3Builder`]
197///
198/// Configuration via keys can be done via [`AmazonS3Builder::with_config`]
199///
200/// # Example
201/// ```
202/// # use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey};
203/// let builder = AmazonS3Builder::new()
204///     .with_config("aws_access_key_id".parse().unwrap(), "my-access-key-id")
205///     .with_config(AmazonS3ConfigKey::DefaultRegion, "my-default-region");
206/// ```
207#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
208#[non_exhaustive]
209pub enum AmazonS3ConfigKey {
210    /// AWS Access Key
211    ///
212    /// See [`AmazonS3Builder::with_access_key_id`] for details.
213    ///
214    /// Supported keys:
215    /// - `aws_access_key_id`
216    /// - `access_key_id`
217    AccessKeyId,
218
219    /// Secret Access Key
220    ///
221    /// See [`AmazonS3Builder::with_secret_access_key`] for details.
222    ///
223    /// Supported keys:
224    /// - `aws_secret_access_key`
225    /// - `secret_access_key`
226    SecretAccessKey,
227
228    /// Region
229    ///
230    /// See [`AmazonS3Builder::with_region`] for details.
231    ///
232    /// Supported keys:
233    /// - `aws_region`
234    /// - `region`
235    Region,
236
237    /// Default region
238    ///
239    /// See [`AmazonS3Builder::with_region`] for details.
240    ///
241    /// Supported keys:
242    /// - `aws_default_region`
243    /// - `default_region`
244    DefaultRegion,
245
246    /// Bucket name
247    ///
248    /// See [`AmazonS3Builder::with_bucket_name`] for details.
249    ///
250    /// Supported keys:
251    /// - `aws_bucket`
252    /// - `aws_bucket_name`
253    /// - `bucket`
254    /// - `bucket_name`
255    Bucket,
256
257    /// Sets custom endpoint for communicating with AWS S3.
258    ///
259    /// See [`AmazonS3Builder::with_endpoint`] for details.
260    ///
261    /// Supported keys:
262    /// - `aws_endpoint`
263    /// - `aws_endpoint_url`
264    /// - `endpoint`
265    /// - `endpoint_url`
266    Endpoint,
267
268    /// Service-specific S3 endpoint URL
269    ///
270    /// When set, takes precedence over [`Endpoint`](Self::Endpoint) in the build method.
271    ///
272    /// Supported keys:
273    /// - `aws_endpoint_url_s3`
274    S3Endpoint,
275
276    /// Token to use for requests (passed to underlying provider)
277    ///
278    /// See [`AmazonS3Builder::with_token`] for details.
279    ///
280    /// Supported keys:
281    /// - `aws_session_token`
282    /// - `aws_token`
283    /// - `session_token`
284    /// - `token`
285    Token,
286
287    /// Fall back to ImdsV1
288    ///
289    /// See [`AmazonS3Builder::with_imdsv1_fallback`] for details.
290    ///
291    /// Supported keys:
292    /// - `aws_imdsv1_fallback`
293    /// - `imdsv1_fallback`
294    ImdsV1Fallback,
295
296    /// If virtual hosted style request has to be used
297    ///
298    /// See [`AmazonS3Builder::with_virtual_hosted_style_request`] for details.
299    ///
300    /// Supported keys:
301    /// - `aws_virtual_hosted_style_request`
302    /// - `virtual_hosted_style_request`
303    VirtualHostedStyleRequest,
304
305    /// Avoid computing payload checksum when calculating signature.
306    ///
307    /// See [`AmazonS3Builder::with_unsigned_payload`] for details.
308    ///
309    /// Supported keys:
310    /// - `aws_unsigned_payload`
311    /// - `unsigned_payload`
312    UnsignedPayload,
313
314    /// Set the checksum algorithm for this client
315    ///
316    /// See [`AmazonS3Builder::with_checksum_algorithm`] for details.
317    Checksum,
318
319    /// Set the instance metadata endpoint
320    ///
321    /// See [`AmazonS3Builder::with_metadata_endpoint`] for details.
322    ///
323    /// Supported keys:
324    /// - `aws_metadata_endpoint`
325    /// - `metadata_endpoint`
326    MetadataEndpoint,
327
328    /// Set the container credentials relative URI when used in ECS
329    ///
330    /// <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
331    ///
332    /// Supported keys:
333    /// - `aws_container_credentials_relative_uri`
334    /// - `container_credentials_relative_uri`
335    ///
336    /// Example: `/v2/credentials/abc123`
337    ContainerCredentialsRelativeUri,
338
339    /// Set the container credentials full URI when used in EKS
340    ///
341    /// <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
342    ///
343    /// Supported keys:
344    /// - `aws_container_credentials_full_uri`
345    /// - `container_credentials_full_uri`
346    ///
347    /// Example: `http://169.254.170.2/v2/credentials/abc123`
348    ContainerCredentialsFullUri,
349
350    /// Set the authorization token in plain text when used in EKS to authenticate with ContainerCredentialsFullUri
351    ///
352    /// <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
353    ///
354    /// Supported keys:
355    /// - `aws_container_authorization_token_file`
356    /// - `container_authorization_token_file`
357    ///
358    /// Example: `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`
359    ContainerAuthorizationTokenFile,
360
361    /// Web identity token file path for AssumeRoleWithWebIdentity
362    ///
363    /// Supported keys:
364    /// - `aws_web_identity_token_file`
365    /// - `web_identity_token_file`
366    ///
367    /// Example: `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`
368    WebIdentityTokenFile,
369
370    /// Role ARN to assume when using web identity token
371    ///
372    /// Supported keys:
373    /// - `aws_role_arn`
374    /// - `role_arn`
375    ///
376    /// Example: `arn:aws:iam::123456789012:role/MyWebIdentityRole`
377    RoleArn,
378
379    /// Session name for web identity role assumption
380    ///
381    /// Supported keys:
382    /// - `aws_role_session_name`
383    /// - `role_session_name`
384    RoleSessionName,
385
386    /// Custom STS endpoint for web identity token exchange
387    ///
388    /// Defaults to `https://sts.{region}.amazonaws.com`
389    ///
390    /// Supported keys:
391    /// - `aws_endpoint_url_sts`
392    /// - `endpoint_url_sts`
393    StsEndpoint,
394
395    /// Configure how to provide `copy_if_not_exists`
396    ///
397    /// See [`S3CopyIfNotExists`] for details.
398    ///
399    /// Supported keys:
400    /// - `aws_copy_if_not_exists`
401    /// - `copy_if_not_exists`
402    CopyIfNotExists,
403
404    /// Configure how to provide conditional put operations
405    ///
406    /// See [`S3ConditionalPut`] for details.
407    ///
408    /// Supported keys:
409    /// - `aws_conditional_put`
410    /// - `conditional_put`
411    ConditionalPut,
412
413    /// Skip signing request
414    ///
415    /// See [`AmazonS3Builder::with_skip_signature`] for details.
416    ///
417    /// Supported keys:
418    /// - `aws_skip_signature`
419    /// - `skip_signature`
420    SkipSignature,
421
422    /// Disable tagging objects
423    ///
424    /// If set to `true` will ignore any tags provided to [`put_opts`](crate::ObjectStore::put_opts).
425    /// This can be desirable if not supported by the backing store
426    ///
427    /// Supported keys:
428    /// - `aws_disable_tagging`
429    /// - `disable_tagging`
430    DisableTagging,
431
432    /// Enable Support for S3 Express One Zone
433    ///
434    /// Supported keys:
435    /// - `aws_s3_express`
436    /// - `s3_express`
437    S3Express,
438
439    /// Enable Support for S3 Requester Pays
440    ///
441    /// Supported keys:
442    /// - `aws_request_payer`
443    /// - `request_payer`
444    RequestPayer,
445
446    /// Client options
447    Client(ClientConfigKey),
448
449    /// Encryption options
450    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            // Backwards compatibility
530            "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    /// Create a new [`AmazonS3Builder`] with default values.
553    pub fn new() -> Self {
554        Default::default()
555    }
556
557    /// Fill the [`AmazonS3Builder`] with regular AWS environment variables
558    ///
559    /// All environment variables starting with `AWS_` will be evaluated.
560    /// Names must match acceptable input to [`AmazonS3ConfigKey::from_str`].
561    ///
562    /// Some examples of variables extracted from environment:
563    /// * `AWS_ACCESS_KEY_ID` -> access_key_id
564    /// * `AWS_SECRET_ACCESS_KEY` -> secret_access_key
565    /// * `AWS_DEFAULT_REGION` -> region
566    /// * `AWS_ENDPOINT` -> endpoint
567    /// * `AWS_ENDPOINT_URL_S3` -> s3_endpoint (takes precedence over endpoint in build)
568    /// * `AWS_SESSION_TOKEN` -> token
569    /// * `AWS_WEB_IDENTITY_TOKEN_FILE` -> path to file containing web identity token for AssumeRoleWithWebIdentity
570    /// * `AWS_ROLE_ARN` -> ARN of the role to assume when using web identity token
571    /// * `AWS_ROLE_SESSION_NAME` -> optional session name for web identity role assumption (defaults to "WebIdentitySession")
572    /// * `AWS_ENDPOINT_URL_STS` -> optional custom STS endpoint for web identity token exchange (defaults to "https://sts.{region}.amazonaws.com")
573    /// * `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` -> <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
574    /// * `AWS_CONTAINER_CREDENTIALS_FULL_URI` -> <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
575    /// * `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` -> <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
576    /// * `AWS_ALLOW_HTTP` -> set to "true" to permit HTTP connections without TLS
577    /// * `AWS_REQUEST_PAYER` -> set to "true" to permit operations on requester-pays buckets.
578    ///
579    /// # Example
580    /// ```
581    /// use object_store::aws::AmazonS3Builder;
582    ///
583    /// let s3 = AmazonS3Builder::from_env()
584    ///     .with_bucket_name("foo")
585    ///     .build();
586    /// ```
587    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    /// Parse available connection info form a well-known storage URL.
602    ///
603    /// The supported url schemes are:
604    ///
605    /// - `s3://<bucket>/<path>`
606    /// - `s3a://<bucket>/<path>`
607    /// - `https://s3.<region>.amazonaws.com/<bucket>`
608    /// - `https://<bucket>.s3.<region>.amazonaws.com`
609    /// - `https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket`
610    ///
611    /// Note: Settings derived from the URL will override any others set on this builder
612    ///
613    /// # Example
614    /// ```
615    /// use object_store::aws::AmazonS3Builder;
616    ///
617    /// let s3 = AmazonS3Builder::from_env()
618    ///     .with_url("s3://bucket/path")
619    ///     .build();
620    /// ```
621    pub fn with_url(mut self, url: impl Into<String>) -> Self {
622        self.url = Some(url.into());
623        self
624    }
625
626    /// Set an option on the builder via a key - value pair.
627    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    /// Get config value via a [`AmazonS3ConfigKey`].
701    ///
702    /// # Example
703    /// ```
704    /// use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey};
705    ///
706    /// let builder = AmazonS3Builder::from_env()
707    ///     .with_bucket_name("foo");
708    /// let bucket_name = builder.get_config_value(&AmazonS3ConfigKey::Bucket).unwrap_or_default();
709    /// assert_eq!("foo", &bucket_name);
710    /// ```
711    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    /// Sets properties on this builder based on a URL
768    ///
769    /// This is a separate member function to allow fallible computation to
770    /// be deferred until [`Self::build`] which in turn allows deriving [`Clone`]
771    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    /// Set the AWS Access Key
821    ///
822    /// Examples: `AKIAIOSFODNN7EXAMPLE`, `ASIA4ZP5EXAMPLETOKEN`
823    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    /// Set the AWS Secret Access Key
829    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    /// Set the AWS Session Token to use for requests
835    ///
836    /// Should not be used in combination with [`Self::with_allow_http`].
837    pub fn with_token(mut self, token: impl Into<String>) -> Self {
838        self.token = Some(token.into());
839        self
840    }
841
842    /// Set the region, defaults to `us-east-1`
843    pub fn with_region(mut self, region: impl Into<String>) -> Self {
844        self.region = Some(region.into());
845        self
846    }
847
848    /// Set the bucket_name (required)
849    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    /// Sets the endpoint for communicating with AWS S3.
855    ///
856    /// Defaults to the [region endpoint]. See  [`Self::with_region`] for further details.
857    ///
858    /// For example, this might be set to `"http://localhost:4566:`
859    /// for testing against a localstack instance.
860    ///
861    /// The `endpoint` field should be consistent with [`Self::with_virtual_hosted_style_request`].
862    /// I.e. if `virtual_hosted_style_request` is set to true then `endpoint`
863    /// should have the bucket name included.
864    ///
865    /// By default, only HTTPS schemes are enabled.
866    /// To connect to an HTTP endpoint, enable [`Self::with_allow_http`].
867    ///
868    /// [region endpoint]: https://docs.aws.amazon.com/general/latest/gr/s3.html
869    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
870        self.endpoint = Some(endpoint.into());
871        self
872    }
873
874    /// Set the credential provider overriding any other options
875    pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
876        self.credentials = Some(credentials);
877        self
878    }
879
880    /// Sets what protocol is allowed.
881    ///
882    /// If `allow_http` is :
883    /// * false (default):  Only HTTPS are allowed
884    /// * true:  HTTP and HTTPS are allowed
885    ///
886    /// <div class="warning">
887    ///
888    /// **Warning**
889    ///
890    /// If you enable this option, attackers may be able to read the data you request.
891    ///
892    /// </div>
893    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    /// Sets if virtual hosted style request has to be used.
899    ///
900    /// If `virtual_hosted_style_request` is:
901    /// * false (default):  Path style request is used
902    /// * true:  Virtual hosted style request is used
903    ///
904    /// If the `endpoint` is provided then it should be
905    /// consistent with `virtual_hosted_style_request`.
906    /// I.e. if `virtual_hosted_style_request` is set to true
907    /// then `endpoint` should have bucket name included.
908    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    /// Configure this as an S3 Express One Zone Bucket
914    pub fn with_s3_express(mut self, s3_express: bool) -> Self {
915        self.s3_express = s3_express.into();
916        self
917    }
918
919    /// Set the retry configuration
920    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
921        self.retry_config = retry_config;
922        self
923    }
924
925    /// By default instance credentials will only be fetched over [IMDSv2], as AWS recommends
926    /// against having IMDSv1 enabled on EC2 instances as it is vulnerable to [SSRF attack]
927    ///
928    /// However, certain deployment environments, such as those running old versions of kube2iam,
929    /// may not support IMDSv2. This option will enable automatic fallback to using IMDSv1
930    /// if the token endpoint returns a 403 error indicating that IMDSv2 is not supported.
931    ///
932    /// This option has no effect if not using instance credentials
933    ///
934    /// [IMDSv2]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
935    /// [SSRF attack]: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
936    pub fn with_imdsv1_fallback(mut self) -> Self {
937        self.imdsv1_fallback = true.into();
938        self
939    }
940
941    /// Sets if unsigned payload option has to be used.
942    ///
943    /// See [unsigned payload option](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html)
944    /// * false (default): Signed payload option is used, where the checksum for the request body is computed and included when constructing a canonical request.
945    /// * true: Unsigned payload option is used. `UNSIGNED-PAYLOAD` literal is included when constructing a canonical request,
946    pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
947        self.unsigned_payload = unsigned_payload.into();
948        self
949    }
950
951    /// If enabled, [`AmazonS3`] will not fetch credentials and will not sign requests
952    ///
953    /// This can be useful when interacting with public S3 buckets that deny authorized requests
954    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
955        self.skip_signature = skip_signature.into();
956        self
957    }
958
959    /// Sets the [checksum algorithm] which has to be used for object integrity check during upload.
960    ///
961    /// [checksum algorithm]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
962    pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
963        // Convert to String to enable deferred parsing of config
964        self.checksum_algorithm = Some(checksum_algorithm.into());
965        self
966    }
967
968    /// Set the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html),
969    /// used primarily within AWS EC2.
970    ///
971    /// This defaults to the IPv4 endpoint: http://169.254.169.254.
972    /// One can alternatively use the IPv6 endpoint http://fd00:ec2::254.
973    pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
974        self.metadata_endpoint = Some(endpoint.into());
975        self
976    }
977
978    /// Set the proxy_url to be used by the underlying client
979    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    /// Set a trusted proxy CA certificate
985    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    /// Set a list of hosts to exclude from proxy connections
993    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    /// Sets the client options, overriding any already set
999    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1000        self.client_options = options;
1001        self
1002    }
1003
1004    /// Configure how to provide `copy_if_not_exists`
1005    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    /// Configure how to provide conditional put operations.
1011    /// if not set, the default value will be `S3ConditionalPut::ETagMatch`
1012    pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1013        self.conditional_put = config.into();
1014        self
1015    }
1016
1017    /// If set to `true` will ignore any tags provided to [`put_opts`](crate::ObjectStore::put_opts)
1018    pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1019        self.disable_tagging = ignore.into();
1020        self
1021    }
1022
1023    /// Use SSE-KMS for server side encryption.
1024    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    /// Use dual server side encryption for server side encryption.
1033    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    /// Use SSE-C for server side encryption.
1042    /// Must pass the *base64-encoded* 256-bit customer encryption key.
1043    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    /// Set whether to enable bucket key for server side encryption. This overrides
1050    /// the bucket default setting for bucket keys.
1051    ///
1052    /// When bucket keys are disabled, each object is encrypted with a unique data key.
1053    /// When bucket keys are enabled, a single data key is used for the entire bucket,
1054    /// reducing overhead of encryption.
1055    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    /// Set whether to charge requester for bucket operations.
1061    ///
1062    /// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/RequesterPaysBuckets.html>
1063    pub fn with_request_payer(mut self, enabled: bool) -> Self {
1064        self.request_payer = ConfigValue::Parsed(enabled);
1065        self
1066    }
1067
1068    /// The [`HttpConnector`] to use
1069    ///
1070    /// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
1071    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    /// Create a [`AmazonS3`] instance from the provided values,
1077    /// consuming `self`.
1078    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            // Disallow non-HTTPs requests
1123            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                // The instance metadata endpoint is access over HTTP
1146                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                // https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-Regions-and-Zones.html
1189                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)), // Credentials only valid for 5 minutes
1202                );
1203                (Some(session as _), Some(endpoint))
1204            }
1205            false => (None, None),
1206        };
1207
1208        // S3-specific endpoint takes precedence over generic endpoint
1209        let endpoint = self.s3_endpoint.or(self.endpoint);
1210
1211        // If `endpoint` is provided it's assumed to be consistent with `virtual_hosted_style_request` or `s3_express`.
1212        // For example, if `virtual_hosted_style_request` is true then `endpoint` should have bucket name included.
1213        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
1260/// Extracts the AZ from a S3 Express One Zone bucket name
1261///
1262/// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html>
1263fn 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/// Encryption configuration options for S3.
1271///
1272/// These options are used to configure server-side encryption for S3 objects.
1273/// To configure them, pass them to [`AmazonS3Builder::with_config`].
1274///
1275/// [SSE-S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html
1276/// [SSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
1277/// [DSSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html
1278/// [SSE-C]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
1279#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1280#[non_exhaustive]
1281pub enum S3EncryptionConfigKey {
1282    /// Type of encryption to use.
1283    ///
1284    /// If set, must be one of
1285    /// - "AES256" (SSE-S3),
1286    /// - "aws:kms" (SSE-KMS),
1287    /// - "aws:kms:dsse" (DSSE-KMS) or
1288    /// - "sse-c"
1289    ///
1290    /// Supported keys:
1291    /// - `aws_server_side_encryption`
1292    /// - `server_side_encryption`
1293    ServerSideEncryption,
1294    /// The KMS key ID to use for server-side encryption.
1295    ///
1296    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "aws:kms" or "aws:kms:dsse".
1297    ///
1298    /// Supported keys:
1299    /// - `aws_sse_kms_key_id`
1300    /// - `sse_kms_key_id`
1301    ///
1302    /// Example: `arn:aws:kms:us-east-1:123456789012:key/abcd-1234-efgh-5678`
1303    KmsKeyId,
1304    /// If set to true, will use the bucket's default KMS key for server-side encryption.
1305    /// If set to false, will disable the use of the bucket's default KMS key for server-side encryption.
1306    ///
1307    /// Supported keys:
1308    /// - `aws_sse_bucket_key_enabled`
1309    /// - `sse_bucket_key_enabled`
1310    BucketKeyEnabled,
1311
1312    /// The base64 encoded, 256-bit customer encryption key to use for server-side encryption.
1313    ///
1314    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "sse-c".
1315    ///
1316    /// Supported keys:
1317    /// - `aws_sse_customer_key_base64`
1318    /// - `sse_customer_key_base64`
1319    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/// A sequence of headers to be sent for write requests that specify server-side
1371/// encryption.
1372///
1373/// Whether these headers are sent depends on both the kind of encryption set
1374/// and the kind of request being made.
1375#[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        // Verify aws_endpoint_url_s3 parses to S3Endpoint config key
1512        let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1513        assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1514
1515        // Verify S3Endpoint takes precedence over Endpoint in build, regardless of order
1516        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        // Verify precedence works even when S3Endpoint is set first
1528        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            // use invalid url
1751            .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}