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::{CryptoProvider, HttpConnector, TokenCredentialProvider, http_connector};
28use crate::config::ConfigValue;
29use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
30use base64::Engine;
31use base64::prelude::BASE64_STANDARD;
32use http::header::{HeaderMap, HeaderValue};
33use itertools::Itertools;
34use md5::{Digest, Md5};
35use serde::{Deserialize, Serialize};
36use std::str::FromStr;
37use std::sync::Arc;
38use std::time::Duration;
39use tracing::debug;
40use url::Url;
41
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    /// The [`CryptoProvider`] to use
177    crypto: Option<Arc<dyn CryptoProvider>>,
178    /// Skip signing requests
179    skip_signature: ConfigValue<bool>,
180    /// Copy if not exists
181    copy_if_not_exists: Option<ConfigValue<S3CopyIfNotExists>>,
182    /// Put precondition
183    conditional_put: ConfigValue<S3ConditionalPut>,
184    /// Ignore tags
185    disable_tagging: ConfigValue<bool>,
186    /// Disable bulk delete
187    disable_bulk_delete: ConfigValue<bool>,
188    /// Encryption (See [`S3EncryptionConfigKey`])
189    encryption_type: Option<ConfigValue<S3EncryptionType>>,
190    encryption_kms_key_id: Option<String>,
191    encryption_bucket_key_enabled: Option<ConfigValue<bool>>,
192    /// base64-encoded 256-bit customer encryption key for SSE-C.
193    encryption_customer_key_base64: Option<String>,
194    /// When set to true, charge requester for bucket operations
195    request_payer: ConfigValue<RequesterPayer>,
196    /// The [`HttpConnector`] to use
197    http_connector: Option<Arc<dyn HttpConnector>>,
198}
199
200/// Configuration keys for [`AmazonS3Builder`]
201///
202/// Configuration via keys can be done via [`AmazonS3Builder::with_config`]
203///
204/// # Example
205/// ```
206/// # use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey};
207/// let builder = AmazonS3Builder::new()
208///     .with_config("aws_access_key_id".parse().unwrap(), "my-access-key-id")
209///     .with_config(AmazonS3ConfigKey::DefaultRegion, "my-default-region");
210/// ```
211#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
212#[non_exhaustive]
213pub enum AmazonS3ConfigKey {
214    /// AWS Access Key
215    ///
216    /// See [`AmazonS3Builder::with_access_key_id`] for details.
217    ///
218    /// Supported keys:
219    /// - `aws_access_key_id`
220    /// - `access_key_id`
221    AccessKeyId,
222
223    /// Secret Access Key
224    ///
225    /// See [`AmazonS3Builder::with_secret_access_key`] for details.
226    ///
227    /// Supported keys:
228    /// - `aws_secret_access_key`
229    /// - `secret_access_key`
230    SecretAccessKey,
231
232    /// Region
233    ///
234    /// See [`AmazonS3Builder::with_region`] for details.
235    ///
236    /// Supported keys:
237    /// - `aws_region`
238    /// - `region`
239    Region,
240
241    /// Default region
242    ///
243    /// See [`AmazonS3Builder::with_region`] for details.
244    ///
245    /// Supported keys:
246    /// - `aws_default_region`
247    /// - `default_region`
248    DefaultRegion,
249
250    /// Bucket name
251    ///
252    /// See [`AmazonS3Builder::with_bucket_name`] for details.
253    ///
254    /// Supported keys:
255    /// - `aws_bucket`
256    /// - `aws_bucket_name`
257    /// - `bucket`
258    /// - `bucket_name`
259    Bucket,
260
261    /// Sets custom endpoint for communicating with AWS S3.
262    ///
263    /// See [`AmazonS3Builder::with_endpoint`] for details.
264    ///
265    /// Supported keys:
266    /// - `aws_endpoint`
267    /// - `aws_endpoint_url`
268    /// - `endpoint`
269    /// - `endpoint_url`
270    Endpoint,
271
272    /// Service-specific S3 endpoint URL
273    ///
274    /// When set, takes precedence over [`Endpoint`](Self::Endpoint) in the build method.
275    ///
276    /// Supported keys:
277    /// - `aws_endpoint_url_s3`
278    S3Endpoint,
279
280    /// Token to use for requests (passed to underlying provider)
281    ///
282    /// See [`AmazonS3Builder::with_token`] for details.
283    ///
284    /// Supported keys:
285    /// - `aws_session_token`
286    /// - `aws_token`
287    /// - `session_token`
288    /// - `token`
289    Token,
290
291    /// Fall back to ImdsV1
292    ///
293    /// See [`AmazonS3Builder::with_imdsv1_fallback`] for details.
294    ///
295    /// Supported keys:
296    /// - `aws_imdsv1_fallback`
297    /// - `imdsv1_fallback`
298    ImdsV1Fallback,
299
300    /// If virtual hosted style request has to be used
301    ///
302    /// See [`AmazonS3Builder::with_virtual_hosted_style_request`] for details.
303    ///
304    /// Supported keys:
305    /// - `aws_virtual_hosted_style_request`
306    /// - `virtual_hosted_style_request`
307    VirtualHostedStyleRequest,
308
309    /// Avoid computing payload checksum when calculating signature.
310    ///
311    /// See [`AmazonS3Builder::with_unsigned_payload`] for details.
312    ///
313    /// Supported keys:
314    /// - `aws_unsigned_payload`
315    /// - `unsigned_payload`
316    UnsignedPayload,
317
318    /// Set the checksum algorithm for this client
319    ///
320    /// See [`AmazonS3Builder::with_checksum_algorithm`] for details.
321    Checksum,
322
323    /// Set the instance metadata endpoint
324    ///
325    /// See [`AmazonS3Builder::with_metadata_endpoint`] for details.
326    ///
327    /// Supported keys:
328    /// - `aws_metadata_endpoint`
329    /// - `metadata_endpoint`
330    MetadataEndpoint,
331
332    /// Set the container credentials relative URI when used in ECS
333    ///
334    /// <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
335    ///
336    /// Supported keys:
337    /// - `aws_container_credentials_relative_uri`
338    /// - `container_credentials_relative_uri`
339    ///
340    /// Example: `/v2/credentials/abc123`
341    ContainerCredentialsRelativeUri,
342
343    /// Set the container credentials full URI when used in EKS
344    ///
345    /// <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
346    ///
347    /// Supported keys:
348    /// - `aws_container_credentials_full_uri`
349    /// - `container_credentials_full_uri`
350    ///
351    /// Example: `http://169.254.170.2/v2/credentials/abc123`
352    ContainerCredentialsFullUri,
353
354    /// Set the authorization token in plain text when used in EKS to authenticate with ContainerCredentialsFullUri
355    ///
356    /// <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
357    ///
358    /// Supported keys:
359    /// - `aws_container_authorization_token_file`
360    /// - `container_authorization_token_file`
361    ///
362    /// Example: `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`
363    ContainerAuthorizationTokenFile,
364
365    /// Web identity token file path for AssumeRoleWithWebIdentity
366    ///
367    /// Supported keys:
368    /// - `aws_web_identity_token_file`
369    /// - `web_identity_token_file`
370    ///
371    /// Example: `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`
372    WebIdentityTokenFile,
373
374    /// Role ARN to assume when using web identity token
375    ///
376    /// Supported keys:
377    /// - `aws_role_arn`
378    /// - `role_arn`
379    ///
380    /// Example: `arn:aws:iam::123456789012:role/MyWebIdentityRole`
381    RoleArn,
382
383    /// Session name for web identity role assumption
384    ///
385    /// Supported keys:
386    /// - `aws_role_session_name`
387    /// - `role_session_name`
388    RoleSessionName,
389
390    /// Custom STS endpoint for web identity token exchange
391    ///
392    /// Defaults to `https://sts.{region}.amazonaws.com`
393    ///
394    /// Supported keys:
395    /// - `aws_endpoint_url_sts`
396    /// - `endpoint_url_sts`
397    StsEndpoint,
398
399    /// Configure how to provide `copy_if_not_exists`
400    ///
401    /// See [`S3CopyIfNotExists`] for details.
402    ///
403    /// Supported keys:
404    /// - `aws_copy_if_not_exists`
405    /// - `copy_if_not_exists`
406    CopyIfNotExists,
407
408    /// Configure how to provide conditional put operations
409    ///
410    /// See [`S3ConditionalPut`] for details.
411    ///
412    /// Supported keys:
413    /// - `aws_conditional_put`
414    /// - `conditional_put`
415    ConditionalPut,
416
417    /// Skip signing request
418    ///
419    /// See [`AmazonS3Builder::with_skip_signature`] for details.
420    ///
421    /// Supported keys:
422    /// - `aws_skip_signature`
423    /// - `skip_signature`
424    SkipSignature,
425
426    /// Disable tagging objects
427    ///
428    /// If set to `true` will ignore any tags provided to [`put_opts`](crate::ObjectStore::put_opts).
429    /// This can be desirable if not supported by the backing store
430    ///
431    /// Supported keys:
432    /// - `aws_disable_tagging`
433    /// - `disable_tagging`
434    DisableTagging,
435
436    /// Disable bulk delete (`DeleteObjects`, `POST /?delete`)
437    ///
438    /// If set to `true`, [`delete`](crate::ObjectStoreExt::delete) and
439    /// [`delete_stream`](crate::ObjectStore::delete_stream) will issue
440    /// single-object `DELETE /key` requests instead of the bulk `DeleteObjects`
441    /// API (`POST /?delete`). Use this for S3-compatible providers that do not
442    /// implement `DeleteObjects` (e.g. Alibaba Cloud OSS).
443    ///
444    /// Supported keys:
445    /// - `aws_disable_bulk_delete`
446    /// - `disable_bulk_delete`
447    DisableBulkDelete,
448
449    /// Enable Support for S3 Express One Zone
450    ///
451    /// Supported keys:
452    /// - `aws_s3_express`
453    /// - `s3_express`
454    S3Express,
455
456    /// Enable Support for S3 Requester Pays
457    ///
458    /// Supported keys:
459    /// - `aws_request_payer`
460    /// - `request_payer`
461    RequestPayer,
462
463    /// Client options
464    Client(ClientConfigKey),
465
466    /// Encryption options
467    Encryption(S3EncryptionConfigKey),
468}
469
470impl AsRef<str> for AmazonS3ConfigKey {
471    fn as_ref(&self) -> &str {
472        match self {
473            Self::AccessKeyId => "aws_access_key_id",
474            Self::SecretAccessKey => "aws_secret_access_key",
475            Self::Region => "aws_region",
476            Self::Bucket => "aws_bucket",
477            Self::Endpoint => "aws_endpoint",
478            Self::S3Endpoint => "aws_endpoint_url_s3",
479            Self::Token => "aws_session_token",
480            Self::ImdsV1Fallback => "aws_imdsv1_fallback",
481            Self::VirtualHostedStyleRequest => "aws_virtual_hosted_style_request",
482            Self::S3Express => "aws_s3_express",
483            Self::DefaultRegion => "aws_default_region",
484            Self::MetadataEndpoint => "aws_metadata_endpoint",
485            Self::UnsignedPayload => "aws_unsigned_payload",
486            Self::Checksum => "aws_checksum_algorithm",
487            Self::ContainerCredentialsRelativeUri => "aws_container_credentials_relative_uri",
488            Self::ContainerCredentialsFullUri => "aws_container_credentials_full_uri",
489            Self::ContainerAuthorizationTokenFile => "aws_container_authorization_token_file",
490            Self::WebIdentityTokenFile => "aws_web_identity_token_file",
491            Self::RoleArn => "aws_role_arn",
492            Self::RoleSessionName => "aws_role_session_name",
493            Self::StsEndpoint => "aws_endpoint_url_sts",
494            Self::SkipSignature => "aws_skip_signature",
495            Self::CopyIfNotExists => "aws_copy_if_not_exists",
496            Self::ConditionalPut => "aws_conditional_put",
497            Self::DisableTagging => "aws_disable_tagging",
498            Self::DisableBulkDelete => "aws_disable_bulk_delete",
499            Self::RequestPayer => "aws_request_payer",
500            Self::Client(opt) => opt.as_ref(),
501            Self::Encryption(opt) => opt.as_ref(),
502        }
503    }
504}
505
506impl FromStr for AmazonS3ConfigKey {
507    type Err = crate::Error;
508
509    fn from_str(s: &str) -> Result<Self, Self::Err> {
510        match s {
511            "aws_access_key_id" | "access_key_id" => Ok(Self::AccessKeyId),
512            "aws_secret_access_key" | "secret_access_key" => Ok(Self::SecretAccessKey),
513            "aws_default_region" | "default_region" => Ok(Self::DefaultRegion),
514            "aws_region" | "region" => Ok(Self::Region),
515            "aws_bucket" | "aws_bucket_name" | "bucket_name" | "bucket" => Ok(Self::Bucket),
516            "aws_endpoint_url" | "aws_endpoint" | "endpoint_url" | "endpoint" => Ok(Self::Endpoint),
517            "aws_endpoint_url_s3" => Ok(Self::S3Endpoint),
518            "aws_session_token" | "aws_token" | "session_token" | "token" => Ok(Self::Token),
519            "aws_virtual_hosted_style_request" | "virtual_hosted_style_request" => {
520                Ok(Self::VirtualHostedStyleRequest)
521            }
522            "aws_s3_express" | "s3_express" => Ok(Self::S3Express),
523            "aws_imdsv1_fallback" | "imdsv1_fallback" => Ok(Self::ImdsV1Fallback),
524            "aws_metadata_endpoint" | "metadata_endpoint" => Ok(Self::MetadataEndpoint),
525            "aws_unsigned_payload" | "unsigned_payload" => Ok(Self::UnsignedPayload),
526            "aws_checksum_algorithm" | "checksum_algorithm" => Ok(Self::Checksum),
527            "aws_container_credentials_relative_uri" | "container_credentials_relative_uri" => {
528                Ok(Self::ContainerCredentialsRelativeUri)
529            }
530            "aws_container_credentials_full_uri" | "container_credentials_full_uri" => {
531                Ok(Self::ContainerCredentialsFullUri)
532            }
533            "aws_container_authorization_token_file" | "container_authorization_token_file" => {
534                Ok(Self::ContainerAuthorizationTokenFile)
535            }
536            "aws_web_identity_token_file" | "web_identity_token_file" => {
537                Ok(Self::WebIdentityTokenFile)
538            }
539            "aws_role_arn" | "role_arn" => Ok(Self::RoleArn),
540            "aws_role_session_name" | "role_session_name" => Ok(Self::RoleSessionName),
541            "aws_endpoint_url_sts" | "endpoint_url_sts" => Ok(Self::StsEndpoint),
542            "aws_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
543            "aws_copy_if_not_exists" | "copy_if_not_exists" => Ok(Self::CopyIfNotExists),
544            "aws_conditional_put" | "conditional_put" => Ok(Self::ConditionalPut),
545            "aws_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
546            "aws_disable_bulk_delete" | "disable_bulk_delete" => Ok(Self::DisableBulkDelete),
547            "aws_request_payer" | "request_payer" => Ok(Self::RequestPayer),
548            // Backwards compatibility
549            "aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
550            "aws_server_side_encryption" | "server_side_encryption" => Ok(Self::Encryption(
551                S3EncryptionConfigKey::ServerSideEncryption,
552            )),
553            "aws_sse_kms_key_id" | "sse_kms_key_id" => {
554                Ok(Self::Encryption(S3EncryptionConfigKey::KmsKeyId))
555            }
556            "aws_sse_bucket_key_enabled" | "sse_bucket_key_enabled" => {
557                Ok(Self::Encryption(S3EncryptionConfigKey::BucketKeyEnabled))
558            }
559            "aws_sse_customer_key_base64" | "sse_customer_key_base64" => Ok(Self::Encryption(
560                S3EncryptionConfigKey::CustomerEncryptionKey,
561            )),
562            _ => match s.strip_prefix("aws_").unwrap_or(s).parse() {
563                Ok(key) => Ok(Self::Client(key)),
564                Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
565            },
566        }
567    }
568}
569
570impl AmazonS3Builder {
571    /// Create a new [`AmazonS3Builder`] with default values.
572    pub fn new() -> Self {
573        Default::default()
574    }
575
576    /// Fill the [`AmazonS3Builder`] with regular AWS environment variables
577    ///
578    /// All environment variables starting with `AWS_` will be evaluated.
579    /// Names must match acceptable input to [`AmazonS3ConfigKey::from_str`].
580    ///
581    /// Some examples of variables extracted from environment:
582    /// * `AWS_ACCESS_KEY_ID` -> access_key_id
583    /// * `AWS_SECRET_ACCESS_KEY` -> secret_access_key
584    /// * `AWS_DEFAULT_REGION` -> region
585    /// * `AWS_ENDPOINT` -> endpoint
586    /// * `AWS_ENDPOINT_URL_S3` -> s3_endpoint (takes precedence over endpoint in build)
587    /// * `AWS_SESSION_TOKEN` -> token
588    /// * `AWS_WEB_IDENTITY_TOKEN_FILE` -> path to file containing web identity token for AssumeRoleWithWebIdentity
589    /// * `AWS_ROLE_ARN` -> ARN of the role to assume when using web identity token
590    /// * `AWS_ROLE_SESSION_NAME` -> optional session name for web identity role assumption (defaults to "WebIdentitySession")
591    /// * `AWS_ENDPOINT_URL_STS` -> optional custom STS endpoint for web identity token exchange (defaults to "https://sts.{region}.amazonaws.com")
592    /// * `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` -> <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html>
593    /// * `AWS_CONTAINER_CREDENTIALS_FULL_URI` -> <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
594    /// * `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` -> <https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html>
595    /// * `AWS_ALLOW_HTTP` -> set to "true" to permit HTTP connections without TLS
596    /// * `AWS_REQUEST_PAYER` -> set to "requester" or "true" to permit operations on requester-pays buckets.
597    ///
598    /// # Example
599    /// ```
600    /// use object_store::aws::AmazonS3Builder;
601    ///
602    /// let s3 = AmazonS3Builder::from_env()
603    ///     .with_bucket_name("foo")
604    ///     .build();
605    /// ```
606    pub fn from_env() -> Self {
607        let mut builder: Self = Default::default();
608        for (os_key, os_value) in std::env::vars_os() {
609            if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
610                if key.starts_with("AWS_") {
611                    if let Ok(config_key) = key.to_ascii_lowercase().parse() {
612                        builder = builder.with_config(config_key, value);
613                    }
614                }
615            }
616        }
617        builder
618    }
619
620    /// Parse available connection info form a well-known storage URL.
621    ///
622    /// The supported url schemes are:
623    ///
624    /// - `s3://<bucket>/<path>`
625    /// - `s3a://<bucket>/<path>`
626    /// - `https://s3.<region>.amazonaws.com/<bucket>`
627    /// - `https://<bucket>.s3.<region>.amazonaws.com`
628    /// - `https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket`
629    ///
630    /// Note: Settings derived from the URL will override any others set on this builder
631    ///
632    /// # Example
633    /// ```
634    /// use object_store::aws::AmazonS3Builder;
635    ///
636    /// let s3 = AmazonS3Builder::from_env()
637    ///     .with_url("s3://bucket/path")
638    ///     .build();
639    /// ```
640    pub fn with_url(mut self, url: impl Into<String>) -> Self {
641        self.url = Some(url.into());
642        self
643    }
644
645    /// Set an option on the builder via a key - value pair.
646    pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
647        match key {
648            AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
649            AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
650            AmazonS3ConfigKey::Region => self.region = Some(value.into()),
651            AmazonS3ConfigKey::Bucket => self.bucket_name = Some(value.into()),
652            AmazonS3ConfigKey::Endpoint => self.endpoint = Some(value.into()),
653            AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint = Some(value.into()),
654            AmazonS3ConfigKey::Token => self.token = Some(value.into()),
655            AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
656            AmazonS3ConfigKey::VirtualHostedStyleRequest => {
657                self.virtual_hosted_style_request.parse(value)
658            }
659            AmazonS3ConfigKey::S3Express => self.s3_express.parse(value),
660            AmazonS3ConfigKey::DefaultRegion => {
661                self.region = self.region.or_else(|| Some(value.into()))
662            }
663            AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
664            AmazonS3ConfigKey::UnsignedPayload => self.unsigned_payload.parse(value),
665            AmazonS3ConfigKey::Checksum => {
666                self.checksum_algorithm = Some(ConfigValue::Deferred(value.into()))
667            }
668            AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
669                self.container_credentials_relative_uri = Some(value.into())
670            }
671            AmazonS3ConfigKey::ContainerCredentialsFullUri => {
672                self.container_credentials_full_uri = Some(value.into());
673            }
674            AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
675                self.container_authorization_token_file = Some(value.into());
676            }
677            AmazonS3ConfigKey::WebIdentityTokenFile => {
678                self.web_identity_token_file = Some(value.into());
679            }
680            AmazonS3ConfigKey::RoleArn => {
681                self.role_arn = Some(value.into());
682            }
683            AmazonS3ConfigKey::RoleSessionName => {
684                self.role_session_name = Some(value.into());
685            }
686            AmazonS3ConfigKey::StsEndpoint => {
687                self.sts_endpoint = Some(value.into());
688            }
689            AmazonS3ConfigKey::Client(key) => {
690                self.client_options = self.client_options.with_config(key, value)
691            }
692            AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
693            AmazonS3ConfigKey::DisableTagging => self.disable_tagging.parse(value),
694            AmazonS3ConfigKey::DisableBulkDelete => self.disable_bulk_delete.parse(value),
695            AmazonS3ConfigKey::CopyIfNotExists => {
696                self.copy_if_not_exists = Some(ConfigValue::Deferred(value.into()))
697            }
698            AmazonS3ConfigKey::ConditionalPut => {
699                self.conditional_put = ConfigValue::Deferred(value.into())
700            }
701            AmazonS3ConfigKey::RequestPayer => self.request_payer.parse(value),
702            AmazonS3ConfigKey::Encryption(key) => match key {
703                S3EncryptionConfigKey::ServerSideEncryption => {
704                    self.encryption_type = Some(ConfigValue::Deferred(value.into()))
705                }
706                S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id = Some(value.into()),
707                S3EncryptionConfigKey::BucketKeyEnabled => {
708                    self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into()))
709                }
710                S3EncryptionConfigKey::CustomerEncryptionKey => {
711                    self.encryption_customer_key_base64 = Some(value.into())
712                }
713            },
714        };
715        self
716    }
717
718    /// Get config value via a [`AmazonS3ConfigKey`].
719    ///
720    /// # Example
721    /// ```
722    /// use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey};
723    ///
724    /// let builder = AmazonS3Builder::from_env()
725    ///     .with_bucket_name("foo");
726    /// let bucket_name = builder.get_config_value(&AmazonS3ConfigKey::Bucket).unwrap_or_default();
727    /// assert_eq!("foo", &bucket_name);
728    /// ```
729    pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
730        match key {
731            AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
732            AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
733            AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
734            AmazonS3ConfigKey::Bucket => self.bucket_name.clone(),
735            AmazonS3ConfigKey::Endpoint => self.endpoint.clone(),
736            AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint.clone(),
737            AmazonS3ConfigKey::Token => self.token.clone(),
738            AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
739            AmazonS3ConfigKey::VirtualHostedStyleRequest => {
740                Some(self.virtual_hosted_style_request.to_string())
741            }
742            AmazonS3ConfigKey::S3Express => Some(self.s3_express.to_string()),
743            AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
744            AmazonS3ConfigKey::UnsignedPayload => Some(self.unsigned_payload.to_string()),
745            AmazonS3ConfigKey::Checksum => {
746                self.checksum_algorithm.as_ref().map(ToString::to_string)
747            }
748            AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
749            AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
750                self.container_credentials_relative_uri.clone()
751            }
752            AmazonS3ConfigKey::ContainerCredentialsFullUri => {
753                self.container_credentials_full_uri.clone()
754            }
755            AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
756                self.container_authorization_token_file.clone()
757            }
758            AmazonS3ConfigKey::WebIdentityTokenFile => self.web_identity_token_file.clone(),
759            AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
760            AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
761            AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
762            AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
763            AmazonS3ConfigKey::CopyIfNotExists => {
764                self.copy_if_not_exists.as_ref().map(ToString::to_string)
765            }
766            AmazonS3ConfigKey::ConditionalPut => Some(self.conditional_put.to_string()),
767            AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
768            AmazonS3ConfigKey::DisableBulkDelete => Some(self.disable_bulk_delete.to_string()),
769            AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
770            AmazonS3ConfigKey::Encryption(key) => match key {
771                S3EncryptionConfigKey::ServerSideEncryption => {
772                    self.encryption_type.as_ref().map(ToString::to_string)
773                }
774                S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id.clone(),
775                S3EncryptionConfigKey::BucketKeyEnabled => self
776                    .encryption_bucket_key_enabled
777                    .as_ref()
778                    .map(ToString::to_string),
779                S3EncryptionConfigKey::CustomerEncryptionKey => {
780                    self.encryption_customer_key_base64.clone()
781                }
782            },
783        }
784    }
785
786    /// Sets properties on this builder based on a URL
787    ///
788    /// This is a separate member function to allow fallible computation to
789    /// be deferred until [`Self::build`] which in turn allows deriving [`Clone`]
790    fn parse_url(&mut self, url: &str) -> Result<()> {
791        let parsed = Url::parse(url).map_err(|source| {
792            let url = url.into();
793            Error::UnableToParseUrl { url, source }
794        })?;
795
796        let host = parsed
797            .host_str()
798            .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
799
800        match parsed.scheme() {
801            "s3" | "s3a" => self.bucket_name = Some(host.to_string()),
802            "https" => match host.splitn(4, '.').collect_tuple() {
803                Some(("s3", region, "amazonaws", "com")) => {
804                    self.region = Some(region.to_string());
805                    let bucket = parsed.path_segments().into_iter().flatten().next();
806                    if let Some(bucket) = bucket {
807                        self.bucket_name = Some(bucket.into());
808                    }
809                }
810                Some((bucket, "s3", "amazonaws", "com")) => {
811                    self.bucket_name = Some(bucket.to_string());
812                    self.virtual_hosted_style_request = true.into();
813                }
814                Some((bucket, "s3", region, "amazonaws.com")) => {
815                    self.bucket_name = Some(bucket.to_string());
816                    self.region = Some(region.to_string());
817                    self.virtual_hosted_style_request = true.into();
818                }
819                Some((account, "r2", "cloudflarestorage", "com")) => {
820                    self.region = Some("auto".to_string());
821                    let endpoint = format!("https://{account}.r2.cloudflarestorage.com");
822                    self.endpoint = Some(endpoint);
823
824                    let bucket = parsed.path_segments().into_iter().flatten().next();
825                    if let Some(bucket) = bucket {
826                        self.bucket_name = Some(bucket.into());
827                    }
828                }
829                _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
830            },
831            scheme => {
832                let scheme = scheme.into();
833                return Err(Error::UnknownUrlScheme { scheme }.into());
834            }
835        };
836        Ok(())
837    }
838
839    /// Set the AWS Access Key
840    ///
841    /// Examples: `AKIAIOSFODNN7EXAMPLE`, `ASIA4ZP5EXAMPLETOKEN`
842    pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
843        self.access_key_id = Some(access_key_id.into());
844        self
845    }
846
847    /// Set the AWS Secret Access Key
848    pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
849        self.secret_access_key = Some(secret_access_key.into());
850        self
851    }
852
853    /// Set the AWS Session Token to use for requests
854    ///
855    /// Should not be used in combination with [`Self::with_allow_http`].
856    pub fn with_token(mut self, token: impl Into<String>) -> Self {
857        self.token = Some(token.into());
858        self
859    }
860
861    /// Set the region, defaults to `us-east-1`
862    pub fn with_region(mut self, region: impl Into<String>) -> Self {
863        self.region = Some(region.into());
864        self
865    }
866
867    /// Set the bucket_name (required)
868    pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
869        self.bucket_name = Some(bucket_name.into());
870        self
871    }
872
873    /// Sets the endpoint for communicating with AWS S3.
874    ///
875    /// Defaults to the [region endpoint]. See  [`Self::with_region`] for further details.
876    ///
877    /// For example, this might be set to `"http://localhost:4566:`
878    /// for testing against a localstack instance.
879    ///
880    /// The `endpoint` field should be consistent with [`Self::with_virtual_hosted_style_request`].
881    /// I.e. if `virtual_hosted_style_request` is set to true then `endpoint`
882    /// should have the bucket name included.
883    ///
884    /// By default, only HTTPS schemes are enabled.
885    /// To connect to an HTTP endpoint, enable [`Self::with_allow_http`].
886    ///
887    /// [region endpoint]: https://docs.aws.amazon.com/general/latest/gr/s3.html
888    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
889        self.endpoint = Some(endpoint.into());
890        self
891    }
892
893    /// Set the credential provider overriding any other options
894    pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
895        self.credentials = Some(credentials);
896        self
897    }
898
899    /// The [`CryptoProvider`] to use
900    pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
901        self.crypto = Some(provider);
902        self
903    }
904
905    /// Sets what protocol is allowed.
906    ///
907    /// If `allow_http` is :
908    /// * false (default):  Only HTTPS are allowed
909    /// * true:  HTTP and HTTPS are allowed
910    ///
911    /// <div class="warning">
912    ///
913    /// **Warning**
914    ///
915    /// If you enable this option, attackers may be able to read the data you request.
916    ///
917    /// </div>
918    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
919        self.client_options = self.client_options.with_allow_http(allow_http);
920        self
921    }
922
923    /// Sets if virtual hosted style request has to be used.
924    ///
925    /// If `virtual_hosted_style_request` is:
926    /// * false (default):  Path style request is used
927    /// * true:  Virtual hosted style request is used
928    ///
929    /// If the `endpoint` is provided then it should be
930    /// consistent with `virtual_hosted_style_request`.
931    /// I.e. if `virtual_hosted_style_request` is set to true
932    /// then `endpoint` should have bucket name included.
933    pub fn with_virtual_hosted_style_request(mut self, virtual_hosted_style_request: bool) -> Self {
934        self.virtual_hosted_style_request = virtual_hosted_style_request.into();
935        self
936    }
937
938    /// Configure this as an S3 Express One Zone Bucket
939    pub fn with_s3_express(mut self, s3_express: bool) -> Self {
940        self.s3_express = s3_express.into();
941        self
942    }
943
944    /// Set the retry configuration
945    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
946        self.retry_config = retry_config;
947        self
948    }
949
950    /// By default instance credentials will only be fetched over [IMDSv2], as AWS recommends
951    /// against having IMDSv1 enabled on EC2 instances as it is vulnerable to [SSRF attack]
952    ///
953    /// However, certain deployment environments, such as those running old versions of kube2iam,
954    /// may not support IMDSv2. This option will enable automatic fallback to using IMDSv1
955    /// if the token endpoint returns a 403 error indicating that IMDSv2 is not supported.
956    ///
957    /// This option has no effect if not using instance credentials
958    ///
959    /// [IMDSv2]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
960    /// [SSRF attack]: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
961    pub fn with_imdsv1_fallback(mut self) -> Self {
962        self.imdsv1_fallback = true.into();
963        self
964    }
965
966    /// Sets if unsigned payload option has to be used.
967    ///
968    /// See [unsigned payload option](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html)
969    /// * false (default): Signed payload option is used, where the checksum for the request body is computed and included when constructing a canonical request.
970    /// * true: Unsigned payload option is used. `UNSIGNED-PAYLOAD` literal is included when constructing a canonical request,
971    pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
972        self.unsigned_payload = unsigned_payload.into();
973        self
974    }
975
976    /// If enabled, [`AmazonS3`] will not fetch credentials and will not sign requests
977    ///
978    /// This can be useful when interacting with public S3 buckets that deny authorized requests
979    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
980        self.skip_signature = skip_signature.into();
981        self
982    }
983
984    /// Sets the [checksum algorithm] which has to be used for object integrity check during upload.
985    ///
986    /// [checksum algorithm]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
987    pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
988        // Convert to String to enable deferred parsing of config
989        self.checksum_algorithm = Some(checksum_algorithm.into());
990        self
991    }
992
993    /// Set the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html),
994    /// used primarily within AWS EC2.
995    ///
996    /// This defaults to the IPv4 endpoint: http://169.254.169.254.
997    /// One can alternatively use the IPv6 endpoint http://fd00:ec2::254.
998    pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
999        self.metadata_endpoint = Some(endpoint.into());
1000        self
1001    }
1002
1003    /// Set the proxy_url to be used by the underlying client
1004    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
1005        self.client_options = self.client_options.with_proxy_url(proxy_url);
1006        self
1007    }
1008
1009    /// Set a trusted proxy CA certificate
1010    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
1011        self.client_options = self
1012            .client_options
1013            .with_proxy_ca_certificate(proxy_ca_certificate);
1014        self
1015    }
1016
1017    /// Set a list of hosts to exclude from proxy connections
1018    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
1019        self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
1020        self
1021    }
1022
1023    /// Sets the client options, overriding any already set
1024    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1025        self.client_options = options;
1026        self
1027    }
1028
1029    /// Configure how to provide `copy_if_not_exists`
1030    pub fn with_copy_if_not_exists(mut self, config: S3CopyIfNotExists) -> Self {
1031        self.copy_if_not_exists = Some(config.into());
1032        self
1033    }
1034
1035    /// Configure how to provide conditional put operations.
1036    /// if not set, the default value will be `S3ConditionalPut::ETagMatch`
1037    pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1038        self.conditional_put = config.into();
1039        self
1040    }
1041
1042    /// If set to `true` will ignore any tags provided to [`put_opts`](crate::ObjectStore::put_opts)
1043    pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1044        self.disable_tagging = ignore.into();
1045        self
1046    }
1047
1048    /// If set to `true`, [`delete`](crate::ObjectStoreExt::delete) and
1049    /// [`delete_stream`](crate::ObjectStore::delete_stream) will issue
1050    /// single-object `DELETE /key` requests instead of the bulk `DeleteObjects`
1051    /// API (`POST /?delete`).
1052    ///
1053    /// The bulk `DeleteObjects` API is more efficient but is not implemented by
1054    /// all S3-compatible providers (e.g. Alibaba Cloud OSS). Setting this to
1055    /// `true` restores the single-object delete behaviour that works against
1056    /// every S3-compatible provider, at the cost of throughput when deleting
1057    /// many objects via [`delete_stream`](crate::ObjectStore::delete_stream).
1058    pub fn with_disable_bulk_delete(mut self, disable: bool) -> Self {
1059        self.disable_bulk_delete = disable.into();
1060        self
1061    }
1062
1063    /// Use SSE-KMS for server side encryption.
1064    pub fn with_sse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1065        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseKms));
1066        if let Some(kms_key_id) = kms_key_id.into().into() {
1067            self.encryption_kms_key_id = Some(kms_key_id);
1068        }
1069        self
1070    }
1071
1072    /// Use dual server side encryption for server side encryption.
1073    pub fn with_dsse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1074        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::DsseKms));
1075        if let Some(kms_key_id) = kms_key_id.into().into() {
1076            self.encryption_kms_key_id = Some(kms_key_id);
1077        }
1078        self
1079    }
1080
1081    /// Use SSE-C for server side encryption.
1082    /// Must pass the *base64-encoded* 256-bit customer encryption key.
1083    pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self {
1084        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC));
1085        self.encryption_customer_key_base64 = customer_key_base64.into().into();
1086        self
1087    }
1088
1089    /// Set whether to enable bucket key for server side encryption. This overrides
1090    /// the bucket default setting for bucket keys.
1091    ///
1092    /// When bucket keys are disabled, each object is encrypted with a unique data key.
1093    /// When bucket keys are enabled, a single data key is used for the entire bucket,
1094    /// reducing overhead of encryption.
1095    pub fn with_bucket_key(mut self, enabled: bool) -> Self {
1096        self.encryption_bucket_key_enabled = Some(ConfigValue::Parsed(enabled));
1097        self
1098    }
1099
1100    /// Set whether to charge requester for bucket operations.
1101    ///
1102    /// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/RequesterPaysBuckets.html>
1103    pub fn with_request_payer(mut self, enabled: bool) -> Self {
1104        self.request_payer = ConfigValue::Parsed(enabled.into());
1105        self
1106    }
1107
1108    /// The [`HttpConnector`] to use
1109    ///
1110    /// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
1111    pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1112        self.http_connector = Some(Arc::new(connector));
1113        self
1114    }
1115
1116    /// Create a [`AmazonS3`] instance from the provided values,
1117    /// consuming `self`.
1118    pub fn build(mut self) -> Result<AmazonS3> {
1119        if let Some(url) = self.url.take() {
1120            self.parse_url(&url)?;
1121        }
1122
1123        let http = http_connector(self.http_connector)?;
1124
1125        let bucket = self.bucket_name.ok_or(Error::MissingBucketName)?;
1126        let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
1127        let checksum = self.checksum_algorithm.map(|x| x.get()).transpose()?;
1128        let copy_if_not_exists = self.copy_if_not_exists.map(|x| x.get()).transpose()?;
1129
1130        let credentials = if let Some(credentials) = self.credentials {
1131            credentials
1132        } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
1133            match (self.access_key_id, self.secret_access_key, self.token) {
1134                (Some(key_id), Some(secret_key), token) => {
1135                    debug!("Using Static credential provider");
1136                    let credential = AwsCredential {
1137                        key_id,
1138                        secret_key,
1139                        token,
1140                    };
1141                    Arc::new(StaticCredentialProvider::new(credential)) as _
1142                }
1143                (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
1144                (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
1145                (None, None, _) => unreachable!(),
1146            }
1147        } else if let (Some(token_path), Some(role_arn)) =
1148            (self.web_identity_token_file, self.role_arn)
1149        {
1150            debug!("Using WebIdentity credential provider");
1151
1152            let session_name = self
1153                .role_session_name
1154                .clone()
1155                .unwrap_or_else(|| "WebIdentitySession".to_string());
1156
1157            let endpoint = self
1158                .sts_endpoint
1159                .clone()
1160                .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
1161
1162            // Disallow non-HTTPs requests
1163            let options = self.client_options.clone().with_allow_http(false);
1164
1165            let token = WebIdentityProvider {
1166                token_path: token_path.clone(),
1167                session_name,
1168                role_arn: role_arn.clone(),
1169                endpoint,
1170            };
1171
1172            Arc::new(TokenCredentialProvider::new(
1173                token,
1174                http.connect(&options)?,
1175                self.retry_config.clone(),
1176            )) as _
1177        } else if let Some(uri) = self.container_credentials_relative_uri {
1178            debug!("Using Task credential provider");
1179
1180            let options = self.client_options.clone().with_allow_http(true);
1181
1182            Arc::new(TaskCredentialProvider {
1183                url: format!("http://169.254.170.2{uri}"),
1184                retry: self.retry_config.clone(),
1185                // The instance metadata endpoint is access over HTTP
1186                client: http.connect(&options)?,
1187                cache: Default::default(),
1188            }) as _
1189        } else if let (Some(full_uri), Some(token_file)) = (
1190            self.container_credentials_full_uri,
1191            self.container_authorization_token_file,
1192        ) {
1193            debug!("Using EKS Pod Identity credential provider");
1194
1195            let options = self.client_options.clone().with_allow_http(true);
1196
1197            Arc::new(EKSPodCredentialProvider {
1198                url: full_uri,
1199                token_file,
1200                retry: self.retry_config.clone(),
1201                client: http.connect(&options)?,
1202                cache: Default::default(),
1203            }) as _
1204        } else {
1205            debug!("Using Instance credential provider");
1206
1207            let token = InstanceCredentialProvider {
1208                imdsv1_fallback: self.imdsv1_fallback.get()?,
1209                metadata_endpoint: self
1210                    .metadata_endpoint
1211                    .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
1212            };
1213
1214            Arc::new(TokenCredentialProvider::new(
1215                token,
1216                http.connect(&self.client_options.metadata_options())?,
1217                self.retry_config.clone(),
1218            )) as _
1219        };
1220
1221        let (session_provider, zonal_endpoint) = match self.s3_express.get()? {
1222            true => {
1223                let zone = parse_bucket_az(&bucket).ok_or_else(|| {
1224                    let bucket = bucket.clone();
1225                    Error::ZoneSuffix { bucket }
1226                })?;
1227
1228                // https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-Regions-and-Zones.html
1229                let endpoint = format!("https://{bucket}.s3express-{zone}.{region}.amazonaws.com");
1230
1231                let session = Arc::new(
1232                    TokenCredentialProvider::new(
1233                        SessionProvider {
1234                            endpoint: endpoint.clone(),
1235                            region: region.clone(),
1236                            credentials: Arc::clone(&credentials),
1237                            crypto: self.crypto.clone(),
1238                        },
1239                        http.connect(&self.client_options)?,
1240                        self.retry_config.clone(),
1241                    )
1242                    .with_min_ttl(Duration::from_secs(60)), // Credentials only valid for 5 minutes
1243                );
1244                (Some(session as _), Some(endpoint))
1245            }
1246            false => (None, None),
1247        };
1248
1249        // S3-specific endpoint takes precedence over generic endpoint
1250        let endpoint = self.s3_endpoint.or(self.endpoint);
1251
1252        // If `endpoint` is provided it's assumed to be consistent with `virtual_hosted_style_request` or `s3_express`.
1253        // For example, if `virtual_hosted_style_request` is true then `endpoint` should have bucket name included.
1254        let virtual_hosted = self.virtual_hosted_style_request.get()?;
1255        let bucket_endpoint = match (&endpoint, zonal_endpoint, virtual_hosted) {
1256            (Some(endpoint), _, true) => endpoint.clone(),
1257            (Some(endpoint), _, false) => format!("{}/{}", endpoint.trim_end_matches("/"), bucket),
1258            (None, Some(endpoint), _) => endpoint,
1259            (None, None, true) => format!("https://{bucket}.s3.{region}.amazonaws.com"),
1260            (None, None, false) => format!("https://s3.{region}.amazonaws.com/{bucket}"),
1261        };
1262
1263        let encryption_headers = if let Some(encryption_type) = self.encryption_type {
1264            S3EncryptionHeaders::try_new(
1265                &encryption_type.get()?,
1266                self.encryption_kms_key_id,
1267                self.encryption_bucket_key_enabled
1268                    .map(|val| val.get())
1269                    .transpose()?,
1270                self.encryption_customer_key_base64,
1271            )?
1272        } else {
1273            S3EncryptionHeaders::default()
1274        };
1275
1276        let config = S3Config {
1277            region,
1278            bucket,
1279            bucket_endpoint,
1280            credentials,
1281            crypto: self.crypto,
1282            session_provider,
1283            retry_config: self.retry_config,
1284            client_options: self.client_options,
1285            sign_payload: !self.unsigned_payload.get()?,
1286            skip_signature: self.skip_signature.get()?,
1287            disable_tagging: self.disable_tagging.get()?,
1288            disable_bulk_delete: self.disable_bulk_delete.get()?,
1289            checksum,
1290            copy_if_not_exists,
1291            conditional_put: self.conditional_put.get()?,
1292            encryption_headers,
1293            request_payer: self.request_payer.get()?.into(),
1294        };
1295
1296        let http_client = http.connect(&config.client_options)?;
1297        let client = Arc::new(S3Client::new(config, http_client));
1298
1299        Ok(AmazonS3 { client })
1300    }
1301}
1302
1303/// Extracts the AZ from a S3 Express One Zone bucket name
1304///
1305/// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html>
1306fn parse_bucket_az(bucket: &str) -> Option<&str> {
1307    let base = bucket
1308        .strip_suffix("--x-s3")
1309        .or_else(|| bucket.strip_suffix("--xa-s3"))?;
1310    Some(base.rsplit_once("--")?.1)
1311}
1312
1313/// Captures `AWS_REQUEST_PAYER`.
1314///
1315/// Parses either as `"requester"` (case-insensitive) meaning `true`, or as a [`bool`] (i.e. `"true"`, `"1"`, `"no"`, etc.).
1316#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1317struct RequesterPayer(bool);
1318
1319impl crate::config::Parse for RequesterPayer {
1320    fn parse(v: &str) -> Result<Self> {
1321        if v.eq_ignore_ascii_case("requester") {
1322            Ok(Self(true))
1323        } else {
1324            Ok(Self(<bool as crate::config::Parse>::parse(v)?))
1325        }
1326    }
1327}
1328
1329impl From<bool> for RequesterPayer {
1330    fn from(value: bool) -> Self {
1331        Self(value)
1332    }
1333}
1334
1335impl From<RequesterPayer> for bool {
1336    fn from(value: RequesterPayer) -> Self {
1337        value.0
1338    }
1339}
1340
1341impl std::fmt::Display for RequesterPayer {
1342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1343        self.0.fmt(f)
1344    }
1345}
1346
1347/// Encryption configuration options for S3.
1348///
1349/// These options are used to configure server-side encryption for S3 objects.
1350/// To configure them, pass them to [`AmazonS3Builder::with_config`].
1351///
1352/// [SSE-S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html
1353/// [SSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
1354/// [DSSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html
1355/// [SSE-C]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
1356#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1357#[non_exhaustive]
1358pub enum S3EncryptionConfigKey {
1359    /// Type of encryption to use.
1360    ///
1361    /// If set, must be one of
1362    /// - "AES256" (SSE-S3),
1363    /// - "aws:kms" (SSE-KMS),
1364    /// - "aws:kms:dsse" (DSSE-KMS) or
1365    /// - "sse-c"
1366    ///
1367    /// Supported keys:
1368    /// - `aws_server_side_encryption`
1369    /// - `server_side_encryption`
1370    ServerSideEncryption,
1371    /// The KMS key ID to use for server-side encryption.
1372    ///
1373    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "aws:kms" or "aws:kms:dsse".
1374    ///
1375    /// Supported keys:
1376    /// - `aws_sse_kms_key_id`
1377    /// - `sse_kms_key_id`
1378    ///
1379    /// Example: `arn:aws:kms:us-east-1:123456789012:key/abcd-1234-efgh-5678`
1380    KmsKeyId,
1381    /// If set to true, will use the bucket's default KMS key for server-side encryption.
1382    /// If set to false, will disable the use of the bucket's default KMS key for server-side encryption.
1383    ///
1384    /// Supported keys:
1385    /// - `aws_sse_bucket_key_enabled`
1386    /// - `sse_bucket_key_enabled`
1387    BucketKeyEnabled,
1388
1389    /// The base64 encoded, 256-bit customer encryption key to use for server-side encryption.
1390    ///
1391    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "sse-c".
1392    ///
1393    /// Supported keys:
1394    /// - `aws_sse_customer_key_base64`
1395    /// - `sse_customer_key_base64`
1396    CustomerEncryptionKey,
1397}
1398
1399impl AsRef<str> for S3EncryptionConfigKey {
1400    fn as_ref(&self) -> &str {
1401        match self {
1402            Self::ServerSideEncryption => "aws_server_side_encryption",
1403            Self::KmsKeyId => "aws_sse_kms_key_id",
1404            Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled",
1405            Self::CustomerEncryptionKey => "aws_sse_customer_key_base64",
1406        }
1407    }
1408}
1409
1410#[derive(Debug, Clone)]
1411enum S3EncryptionType {
1412    S3,
1413    SseKms,
1414    DsseKms,
1415    SseC,
1416}
1417
1418impl crate::config::Parse for S3EncryptionType {
1419    fn parse(s: &str) -> Result<Self> {
1420        match s {
1421            "AES256" => Ok(Self::S3),
1422            "aws:kms" => Ok(Self::SseKms),
1423            "aws:kms:dsse" => Ok(Self::DsseKms),
1424            "sse-c" => Ok(Self::SseC),
1425            _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()),
1426        }
1427    }
1428}
1429
1430impl From<&S3EncryptionType> for &'static str {
1431    fn from(value: &S3EncryptionType) -> Self {
1432        match value {
1433            S3EncryptionType::S3 => "AES256",
1434            S3EncryptionType::SseKms => "aws:kms",
1435            S3EncryptionType::DsseKms => "aws:kms:dsse",
1436            S3EncryptionType::SseC => "sse-c",
1437        }
1438    }
1439}
1440
1441impl std::fmt::Display for S3EncryptionType {
1442    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1443        f.write_str(self.into())
1444    }
1445}
1446
1447/// A sequence of headers to be sent for write requests that specify server-side
1448/// encryption.
1449///
1450/// Whether these headers are sent depends on both the kind of encryption set
1451/// and the kind of request being made.
1452#[derive(Default, Clone, Debug)]
1453pub(super) struct S3EncryptionHeaders(pub HeaderMap);
1454
1455impl S3EncryptionHeaders {
1456    fn try_new(
1457        encryption_type: &S3EncryptionType,
1458        encryption_kms_key_id: Option<String>,
1459        bucket_key_enabled: Option<bool>,
1460        encryption_customer_key_base64: Option<String>,
1461    ) -> Result<Self> {
1462        let mut headers = HeaderMap::new();
1463        match encryption_type {
1464            S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => {
1465                headers.insert(
1466                    "x-amz-server-side-encryption",
1467                    HeaderValue::from_static(encryption_type.into()),
1468                );
1469                if let Some(key_id) = encryption_kms_key_id {
1470                    headers.insert(
1471                        "x-amz-server-side-encryption-aws-kms-key-id",
1472                        key_id
1473                            .try_into()
1474                            .map_err(|err| Error::InvalidEncryptionHeader {
1475                                header: "kms-key-id",
1476                                source: Box::new(err),
1477                            })?,
1478                    );
1479                }
1480                if let Some(bucket_key_enabled) = bucket_key_enabled {
1481                    headers.insert(
1482                        "x-amz-server-side-encryption-bucket-key-enabled",
1483                        HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }),
1484                    );
1485                }
1486            }
1487            S3EncryptionType::SseC => {
1488                headers.insert(
1489                    "x-amz-server-side-encryption-customer-algorithm",
1490                    HeaderValue::from_static("AES256"),
1491                );
1492                if let Some(key) = encryption_customer_key_base64 {
1493                    let mut header_value: HeaderValue =
1494                        key.clone()
1495                            .try_into()
1496                            .map_err(|err| Error::InvalidEncryptionHeader {
1497                                header: "x-amz-server-side-encryption-customer-key",
1498                                source: Box::new(err),
1499                            })?;
1500                    header_value.set_sensitive(true);
1501                    headers.insert("x-amz-server-side-encryption-customer-key", header_value);
1502
1503                    let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| {
1504                        Error::InvalidEncryptionHeader {
1505                            header: "x-amz-server-side-encryption-customer-key",
1506                            source: Box::new(err),
1507                        }
1508                    })?;
1509                    let mut hasher = Md5::new();
1510                    hasher.update(decoded_key);
1511                    let md5 = BASE64_STANDARD.encode(hasher.finalize());
1512                    let mut md5_header_value: HeaderValue =
1513                        md5.try_into()
1514                            .map_err(|err| Error::InvalidEncryptionHeader {
1515                                header: "x-amz-server-side-encryption-customer-key-MD5",
1516                                source: Box::new(err),
1517                            })?;
1518                    md5_header_value.set_sensitive(true);
1519                    headers.insert(
1520                        "x-amz-server-side-encryption-customer-key-MD5",
1521                        md5_header_value,
1522                    );
1523                } else {
1524                    return Err(Error::InvalidEncryptionHeader {
1525                        header: "x-amz-server-side-encryption-customer-key",
1526                        source: Box::new(std::io::Error::new(
1527                            std::io::ErrorKind::InvalidInput,
1528                            "Missing customer key",
1529                        )),
1530                    }
1531                    .into());
1532                }
1533            }
1534        }
1535        Ok(Self(headers))
1536    }
1537}
1538
1539impl From<S3EncryptionHeaders> for HeaderMap {
1540    fn from(headers: S3EncryptionHeaders) -> Self {
1541        headers.0
1542    }
1543}
1544
1545#[cfg(test)]
1546mod tests {
1547    use super::*;
1548    use std::collections::HashMap;
1549
1550    #[test]
1551    fn s3_test_config_from_map() {
1552        let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1553        let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1554        let aws_default_region = "object_store:fake_default_region".to_string();
1555        let aws_endpoint = "object_store:fake_endpoint".to_string();
1556        let aws_session_token = "object_store:fake_session_token".to_string();
1557        let options = HashMap::from([
1558            ("aws_access_key_id", aws_access_key_id.clone()),
1559            ("aws_secret_access_key", aws_secret_access_key),
1560            ("aws_default_region", aws_default_region.clone()),
1561            ("aws_endpoint", aws_endpoint.clone()),
1562            ("aws_session_token", aws_session_token.clone()),
1563            ("aws_unsigned_payload", "true".to_string()),
1564            ("aws_checksum_algorithm", "sha256".to_string()),
1565        ]);
1566
1567        let builder = options
1568            .into_iter()
1569            .fold(AmazonS3Builder::new(), |builder, (key, value)| {
1570                builder.with_config(key.parse().unwrap(), value)
1571            })
1572            .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
1573
1574        assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
1575        assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
1576        assert_eq!(builder.region.unwrap(), aws_default_region);
1577        assert_eq!(builder.endpoint.unwrap(), aws_endpoint);
1578        assert_eq!(builder.token.unwrap(), aws_session_token);
1579        assert_eq!(
1580            builder.checksum_algorithm.unwrap().get().unwrap(),
1581            Checksum::SHA256
1582        );
1583        assert!(builder.unsigned_payload.get().unwrap());
1584    }
1585
1586    #[cfg(feature = "reqwest")]
1587    #[test]
1588    fn s3_test_endpoint_url_s3_config() {
1589        // Verify aws_endpoint_url_s3 parses to S3Endpoint config key
1590        let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1591        assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1592
1593        // Verify S3Endpoint takes precedence over Endpoint in build, regardless of order
1594        let s3 = AmazonS3Builder::new()
1595            .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1596            .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1597            .with_bucket_name("test-bucket")
1598            .build()
1599            .unwrap();
1600        assert_eq!(
1601            s3.client.config.bucket_endpoint,
1602            "http://s3-specific-endpoint/test-bucket"
1603        );
1604
1605        // Verify precedence works even when S3Endpoint is set first
1606        let s3 = AmazonS3Builder::new()
1607            .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1608            .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1609            .with_bucket_name("test-bucket")
1610            .build()
1611            .unwrap();
1612        assert_eq!(
1613            s3.client.config.bucket_endpoint,
1614            "http://s3-specific-endpoint/test-bucket"
1615        );
1616    }
1617
1618    #[test]
1619    fn s3_test_config_get_value() {
1620        let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1621        let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1622        let aws_default_region = "object_store:fake_default_region".to_string();
1623        let aws_endpoint = "object_store:fake_endpoint".to_string();
1624        let aws_session_token = "object_store:fake_session_token".to_string();
1625
1626        let builder = AmazonS3Builder::new()
1627            .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
1628            .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
1629            .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
1630            .with_config(AmazonS3ConfigKey::Endpoint, &aws_endpoint)
1631            .with_config(AmazonS3ConfigKey::Token, &aws_session_token)
1632            .with_config(AmazonS3ConfigKey::UnsignedPayload, "true")
1633            .with_config("aws_server_side_encryption".parse().unwrap(), "AES256")
1634            .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id")
1635            .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true")
1636            .with_config(
1637                "aws_sse_customer_key_base64".parse().unwrap(),
1638                "some_customer_key",
1639            );
1640
1641        assert_eq!(
1642            builder
1643                .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
1644                .unwrap(),
1645            aws_access_key_id
1646        );
1647        assert_eq!(
1648            builder
1649                .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
1650                .unwrap(),
1651            aws_secret_access_key
1652        );
1653        assert_eq!(
1654            builder
1655                .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
1656                .unwrap(),
1657            aws_default_region
1658        );
1659        assert_eq!(
1660            builder
1661                .get_config_value(&AmazonS3ConfigKey::Endpoint)
1662                .unwrap(),
1663            aws_endpoint
1664        );
1665        assert_eq!(
1666            builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
1667            aws_session_token
1668        );
1669        assert_eq!(
1670            builder
1671                .get_config_value(&AmazonS3ConfigKey::UnsignedPayload)
1672                .unwrap(),
1673            "true"
1674        );
1675        assert_eq!(
1676            builder
1677                .get_config_value(&"aws_server_side_encryption".parse().unwrap())
1678                .unwrap(),
1679            "AES256"
1680        );
1681        assert_eq!(
1682            builder
1683                .get_config_value(&"aws_sse_kms_key_id".parse().unwrap())
1684                .unwrap(),
1685            "some_key_id"
1686        );
1687        assert_eq!(
1688            builder
1689                .get_config_value(&"aws_sse_bucket_key_enabled".parse().unwrap())
1690                .unwrap(),
1691            "true"
1692        );
1693        assert_eq!(
1694            builder
1695                .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap())
1696                .unwrap(),
1697            "some_customer_key"
1698        );
1699    }
1700
1701    #[cfg(feature = "reqwest")]
1702    #[test]
1703    fn s3_default_region() {
1704        let builder = AmazonS3Builder::new()
1705            .with_bucket_name("foo")
1706            .build()
1707            .unwrap();
1708        assert_eq!(builder.client.config.region, "us-east-1");
1709    }
1710
1711    #[cfg(feature = "reqwest")]
1712    #[test]
1713    fn s3_test_bucket_endpoint() {
1714        let builder = AmazonS3Builder::new()
1715            .with_endpoint("http://some.host:1234")
1716            .with_bucket_name("foo")
1717            .build()
1718            .unwrap();
1719        assert_eq!(
1720            builder.client.config.bucket_endpoint,
1721            "http://some.host:1234/foo"
1722        );
1723
1724        let builder = AmazonS3Builder::new()
1725            .with_endpoint("http://some.host:1234/")
1726            .with_bucket_name("foo")
1727            .build()
1728            .unwrap();
1729        assert_eq!(
1730            builder.client.config.bucket_endpoint,
1731            "http://some.host:1234/foo"
1732        );
1733    }
1734
1735    #[test]
1736    fn s3_test_urls() {
1737        let mut builder = AmazonS3Builder::new();
1738        builder.parse_url("s3://bucket/path").unwrap();
1739        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1740
1741        let mut builder = AmazonS3Builder::new();
1742        builder
1743            .parse_url("s3://buckets.can.have.dots/path")
1744            .unwrap();
1745        assert_eq!(
1746            builder.bucket_name,
1747            Some("buckets.can.have.dots".to_string())
1748        );
1749
1750        let mut builder = AmazonS3Builder::new();
1751        builder
1752            .parse_url("https://s3.region.amazonaws.com")
1753            .unwrap();
1754        assert_eq!(builder.region, Some("region".to_string()));
1755
1756        let mut builder = AmazonS3Builder::new();
1757        builder
1758            .parse_url("https://s3.region.amazonaws.com/bucket")
1759            .unwrap();
1760        assert_eq!(builder.region, Some("region".to_string()));
1761        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1762
1763        let mut builder = AmazonS3Builder::new();
1764        builder
1765            .parse_url("https://s3.region.amazonaws.com/bucket.with.dot/path")
1766            .unwrap();
1767        assert_eq!(builder.region, Some("region".to_string()));
1768        assert_eq!(builder.bucket_name, Some("bucket.with.dot".to_string()));
1769
1770        let mut builder = AmazonS3Builder::new();
1771        builder
1772            .parse_url("https://bucket.s3.amazonaws.com")
1773            .unwrap();
1774        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1775        assert!(builder.virtual_hosted_style_request.get().unwrap());
1776
1777        let mut builder = AmazonS3Builder::new();
1778        builder
1779            .parse_url("https://bucket.s3.region.amazonaws.com")
1780            .unwrap();
1781        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1782        assert_eq!(builder.region, Some("region".to_string()));
1783        assert!(builder.virtual_hosted_style_request.get().unwrap());
1784
1785        let mut builder = AmazonS3Builder::new();
1786        builder
1787            .parse_url("https://account123.r2.cloudflarestorage.com/bucket-123")
1788            .unwrap();
1789
1790        assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1791        assert_eq!(builder.region, Some("auto".to_string()));
1792        assert_eq!(
1793            builder.endpoint,
1794            Some("https://account123.r2.cloudflarestorage.com".to_string())
1795        );
1796
1797        let err_cases = [
1798            "mailto://bucket/path",
1799            "https://s3.bucket.mydomain.com",
1800            "https://s3.bucket.foo.amazonaws.com",
1801            "https://bucket.mydomain.region.amazonaws.com",
1802            "https://bucket.s3.region.bar.amazonaws.com",
1803            "https://bucket.foo.s3.amazonaws.com",
1804        ];
1805        let mut builder = AmazonS3Builder::new();
1806        for case in err_cases {
1807            builder.parse_url(case).unwrap_err();
1808        }
1809    }
1810
1811    #[cfg(feature = "reqwest")]
1812    #[tokio::test]
1813    async fn s3_test_proxy_url() {
1814        let s3 = AmazonS3Builder::new()
1815            .with_access_key_id("access_key_id")
1816            .with_secret_access_key("secret_access_key")
1817            .with_region("region")
1818            .with_bucket_name("bucket_name")
1819            .with_allow_http(true)
1820            .with_proxy_url("https://example.com")
1821            .build();
1822
1823        assert!(s3.is_ok());
1824
1825        let err = AmazonS3Builder::new()
1826            .with_access_key_id("access_key_id")
1827            .with_secret_access_key("secret_access_key")
1828            .with_region("region")
1829            .with_bucket_name("bucket_name")
1830            .with_allow_http(true)
1831            // use invalid url
1832            .with_proxy_url("dxx:ddd\\example.com")
1833            .build()
1834            .unwrap_err()
1835            .to_string();
1836
1837        assert_eq!("Generic HTTP client error: builder error", err);
1838    }
1839
1840    #[cfg(feature = "reqwest")]
1841    #[test]
1842    fn test_invalid_config() {
1843        let err = AmazonS3Builder::new()
1844            .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
1845            .with_bucket_name("bucket")
1846            .with_region("region")
1847            .build()
1848            .unwrap_err()
1849            .to_string();
1850
1851        assert_eq!(
1852            err,
1853            "Generic Config error: failed to parse \"enabled\" as boolean"
1854        );
1855
1856        let err = AmazonS3Builder::new()
1857            .with_config(AmazonS3ConfigKey::Checksum, "md5")
1858            .with_bucket_name("bucket")
1859            .with_region("region")
1860            .build()
1861            .unwrap_err()
1862            .to_string();
1863
1864        assert_eq!(
1865            err,
1866            "Generic Config error: \"md5\" is not a valid checksum algorithm"
1867        );
1868
1869        let err = AmazonS3Builder::new()
1870            .with_config(AmazonS3ConfigKey::RequestPayer, "requestr")
1871            .with_bucket_name("bucket")
1872            .with_region("region")
1873            .build()
1874            .unwrap_err()
1875            .to_string();
1876
1877        assert_eq!(
1878            err,
1879            "Generic Config error: failed to parse \"requestr\" as boolean"
1880        );
1881    }
1882
1883    #[cfg(feature = "reqwest")]
1884    #[test]
1885    fn test_request_payer_config() {
1886        let s3 = AmazonS3Builder::new()
1887            .with_config(AmazonS3ConfigKey::RequestPayer, "requester")
1888            .with_bucket_name("bucket")
1889            .with_region("region")
1890            .build()
1891            .unwrap();
1892        assert!(s3.client.config.request_payer);
1893
1894        let s3 = AmazonS3Builder::new()
1895            .with_config(AmazonS3ConfigKey::RequestPayer, "REQUESTER")
1896            .with_bucket_name("bucket")
1897            .with_region("region")
1898            .build()
1899            .unwrap();
1900        assert!(s3.client.config.request_payer);
1901
1902        let s3 = AmazonS3Builder::new()
1903            .with_config(AmazonS3ConfigKey::RequestPayer, "true")
1904            .with_bucket_name("bucket")
1905            .with_region("region")
1906            .build()
1907            .unwrap();
1908        assert!(s3.client.config.request_payer);
1909
1910        let s3 = AmazonS3Builder::new()
1911            .with_config(AmazonS3ConfigKey::RequestPayer, "false")
1912            .with_bucket_name("bucket")
1913            .with_region("region")
1914            .build()
1915            .unwrap();
1916        assert!(!s3.client.config.request_payer);
1917
1918        let s3 = AmazonS3Builder::new()
1919            .with_request_payer(true)
1920            .with_bucket_name("bucket")
1921            .with_region("region")
1922            .build()
1923            .unwrap();
1924        assert!(s3.client.config.request_payer);
1925    }
1926
1927    #[test]
1928    fn test_parse_bucket_az() {
1929        let cases = [
1930            ("bucket-base-name--usw2-az1--x-s3", Some("usw2-az1")),
1931            ("bucket-base--name--azid--x-s3", Some("azid")),
1932            ("bucket-base-name--use1-az4--xa-s3", Some("use1-az4")),
1933            ("bucket-base--name--azid--xa-s3", Some("azid")),
1934            ("bucket-base-name", None),
1935            ("bucket-base-name--x-s3", None),
1936            ("bucket-base-name--xa-s3", None),
1937        ];
1938
1939        for (bucket, expected) in cases {
1940            assert_eq!(parse_bucket_az(bucket), expected)
1941        }
1942    }
1943
1944    #[test]
1945    fn aws_test_client_opts() {
1946        let key = "AWS_PROXY_URL";
1947        if let Ok(config_key) = key.to_ascii_lowercase().parse() {
1948            assert_eq!(
1949                AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
1950                config_key
1951            );
1952        } else {
1953            panic!("{key} not propagated as ClientConfigKey");
1954        }
1955    }
1956
1957    #[cfg(feature = "reqwest")]
1958    #[test]
1959    fn test_builder_eks_with_config() {
1960        let builder = AmazonS3Builder::new()
1961            .with_bucket_name("some-bucket")
1962            .with_config(
1963                AmazonS3ConfigKey::ContainerCredentialsFullUri,
1964                "https://127.0.0.1/eks-credentials",
1965            )
1966            .with_config(
1967                AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
1968                "/tmp/fake-bearer-token",
1969            );
1970
1971        let s3 = builder.build().expect("should build successfully");
1972        let creds = &s3.client.config.credentials;
1973        let debug_str = format!("{creds:?}");
1974        assert!(
1975            debug_str.contains("EKSPodCredentialProvider"),
1976            "expected EKS provider but got: {debug_str}"
1977        );
1978    }
1979
1980    #[cfg(feature = "reqwest")]
1981    #[test]
1982    fn test_builder_web_identity_with_config() {
1983        let builder = AmazonS3Builder::new()
1984            .with_bucket_name("some-bucket")
1985            .with_config(
1986                AmazonS3ConfigKey::WebIdentityTokenFile,
1987                "/tmp/fake-token-file",
1988            )
1989            .with_config(
1990                AmazonS3ConfigKey::RoleArn,
1991                "arn:aws:iam::123456789012:role/test-role",
1992            )
1993            .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
1994            .with_config(
1995                AmazonS3ConfigKey::StsEndpoint,
1996                "https://sts.us-west-2.amazonaws.com",
1997            );
1998
1999        assert_eq!(
2000            builder
2001                .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
2002                .unwrap(),
2003            "/tmp/fake-token-file"
2004        );
2005        assert_eq!(
2006            builder
2007                .get_config_value(&AmazonS3ConfigKey::RoleArn)
2008                .unwrap(),
2009            "arn:aws:iam::123456789012:role/test-role"
2010        );
2011        assert_eq!(
2012            builder
2013                .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
2014                .unwrap(),
2015            "TestSession"
2016        );
2017        assert_eq!(
2018            builder
2019                .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
2020                .unwrap(),
2021            "https://sts.us-west-2.amazonaws.com"
2022        );
2023
2024        let s3 = builder.build().expect("should build successfully");
2025        let creds = &s3.client.config.credentials;
2026        let debug_str = format!("{creds:?}");
2027        assert!(
2028            debug_str.contains("TokenCredentialProvider"),
2029            "expected TokenCredentialProvider but got: {debug_str}"
2030        );
2031    }
2032}