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://s3.dualstack.<region>.amazonaws.com/<bucket>`
629    /// - `https://<bucket>.s3.dualstack.<region>.amazonaws.com`
630    /// - `https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket`
631    /// - `https://ACCOUNT_ID.JURISDICTION.r2.cloudflarestorage.com/bucket`
632    ///
633    /// Note: Settings derived from the URL will override any others set on this builder
634    ///
635    /// # Example
636    /// ```
637    /// use object_store::aws::AmazonS3Builder;
638    ///
639    /// let s3 = AmazonS3Builder::from_env()
640    ///     .with_url("s3://bucket/path")
641    ///     .build();
642    /// ```
643    pub fn with_url(mut self, url: impl Into<String>) -> Self {
644        self.url = Some(url.into());
645        self
646    }
647
648    /// Set an option on the builder via a key - value pair.
649    pub fn with_config(mut self, key: AmazonS3ConfigKey, value: impl Into<String>) -> Self {
650        match key {
651            AmazonS3ConfigKey::AccessKeyId => self.access_key_id = Some(value.into()),
652            AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key = Some(value.into()),
653            AmazonS3ConfigKey::Region => self.region = Some(value.into()),
654            AmazonS3ConfigKey::Bucket => self.bucket_name = Some(value.into()),
655            AmazonS3ConfigKey::Endpoint => self.endpoint = Some(value.into()),
656            AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint = Some(value.into()),
657            AmazonS3ConfigKey::Token => self.token = Some(value.into()),
658            AmazonS3ConfigKey::ImdsV1Fallback => self.imdsv1_fallback.parse(value),
659            AmazonS3ConfigKey::VirtualHostedStyleRequest => {
660                self.virtual_hosted_style_request.parse(value)
661            }
662            AmazonS3ConfigKey::S3Express => self.s3_express.parse(value),
663            AmazonS3ConfigKey::DefaultRegion => {
664                self.region = self.region.or_else(|| Some(value.into()))
665            }
666            AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint = Some(value.into()),
667            AmazonS3ConfigKey::UnsignedPayload => self.unsigned_payload.parse(value),
668            AmazonS3ConfigKey::Checksum => {
669                self.checksum_algorithm = Some(ConfigValue::Deferred(value.into()))
670            }
671            AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
672                self.container_credentials_relative_uri = Some(value.into())
673            }
674            AmazonS3ConfigKey::ContainerCredentialsFullUri => {
675                self.container_credentials_full_uri = Some(value.into());
676            }
677            AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
678                self.container_authorization_token_file = Some(value.into());
679            }
680            AmazonS3ConfigKey::WebIdentityTokenFile => {
681                self.web_identity_token_file = Some(value.into());
682            }
683            AmazonS3ConfigKey::RoleArn => {
684                self.role_arn = Some(value.into());
685            }
686            AmazonS3ConfigKey::RoleSessionName => {
687                self.role_session_name = Some(value.into());
688            }
689            AmazonS3ConfigKey::StsEndpoint => {
690                self.sts_endpoint = Some(value.into());
691            }
692            AmazonS3ConfigKey::Client(key) => {
693                self.client_options = self.client_options.with_config(key, value)
694            }
695            AmazonS3ConfigKey::SkipSignature => self.skip_signature.parse(value),
696            AmazonS3ConfigKey::DisableTagging => self.disable_tagging.parse(value),
697            AmazonS3ConfigKey::DisableBulkDelete => self.disable_bulk_delete.parse(value),
698            AmazonS3ConfigKey::CopyIfNotExists => {
699                self.copy_if_not_exists = Some(ConfigValue::Deferred(value.into()))
700            }
701            AmazonS3ConfigKey::ConditionalPut => {
702                self.conditional_put = ConfigValue::Deferred(value.into())
703            }
704            AmazonS3ConfigKey::RequestPayer => self.request_payer.parse(value),
705            AmazonS3ConfigKey::Encryption(key) => match key {
706                S3EncryptionConfigKey::ServerSideEncryption => {
707                    self.encryption_type = Some(ConfigValue::Deferred(value.into()))
708                }
709                S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id = Some(value.into()),
710                S3EncryptionConfigKey::BucketKeyEnabled => {
711                    self.encryption_bucket_key_enabled = Some(ConfigValue::Deferred(value.into()))
712                }
713                S3EncryptionConfigKey::CustomerEncryptionKey => {
714                    self.encryption_customer_key_base64 = Some(value.into())
715                }
716            },
717        };
718        self
719    }
720
721    /// Get config value via a [`AmazonS3ConfigKey`].
722    ///
723    /// # Example
724    /// ```
725    /// use object_store::aws::{AmazonS3Builder, AmazonS3ConfigKey};
726    ///
727    /// let builder = AmazonS3Builder::from_env()
728    ///     .with_bucket_name("foo");
729    /// let bucket_name = builder.get_config_value(&AmazonS3ConfigKey::Bucket).unwrap_or_default();
730    /// assert_eq!("foo", &bucket_name);
731    /// ```
732    pub fn get_config_value(&self, key: &AmazonS3ConfigKey) -> Option<String> {
733        match key {
734            AmazonS3ConfigKey::AccessKeyId => self.access_key_id.clone(),
735            AmazonS3ConfigKey::SecretAccessKey => self.secret_access_key.clone(),
736            AmazonS3ConfigKey::Region | AmazonS3ConfigKey::DefaultRegion => self.region.clone(),
737            AmazonS3ConfigKey::Bucket => self.bucket_name.clone(),
738            AmazonS3ConfigKey::Endpoint => self.endpoint.clone(),
739            AmazonS3ConfigKey::S3Endpoint => self.s3_endpoint.clone(),
740            AmazonS3ConfigKey::Token => self.token.clone(),
741            AmazonS3ConfigKey::ImdsV1Fallback => Some(self.imdsv1_fallback.to_string()),
742            AmazonS3ConfigKey::VirtualHostedStyleRequest => {
743                Some(self.virtual_hosted_style_request.to_string())
744            }
745            AmazonS3ConfigKey::S3Express => Some(self.s3_express.to_string()),
746            AmazonS3ConfigKey::MetadataEndpoint => self.metadata_endpoint.clone(),
747            AmazonS3ConfigKey::UnsignedPayload => Some(self.unsigned_payload.to_string()),
748            AmazonS3ConfigKey::Checksum => {
749                self.checksum_algorithm.as_ref().map(ToString::to_string)
750            }
751            AmazonS3ConfigKey::Client(key) => self.client_options.get_config_value(key),
752            AmazonS3ConfigKey::ContainerCredentialsRelativeUri => {
753                self.container_credentials_relative_uri.clone()
754            }
755            AmazonS3ConfigKey::ContainerCredentialsFullUri => {
756                self.container_credentials_full_uri.clone()
757            }
758            AmazonS3ConfigKey::ContainerAuthorizationTokenFile => {
759                self.container_authorization_token_file.clone()
760            }
761            AmazonS3ConfigKey::WebIdentityTokenFile => self.web_identity_token_file.clone(),
762            AmazonS3ConfigKey::RoleArn => self.role_arn.clone(),
763            AmazonS3ConfigKey::RoleSessionName => self.role_session_name.clone(),
764            AmazonS3ConfigKey::StsEndpoint => self.sts_endpoint.clone(),
765            AmazonS3ConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
766            AmazonS3ConfigKey::CopyIfNotExists => {
767                self.copy_if_not_exists.as_ref().map(ToString::to_string)
768            }
769            AmazonS3ConfigKey::ConditionalPut => Some(self.conditional_put.to_string()),
770            AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
771            AmazonS3ConfigKey::DisableBulkDelete => Some(self.disable_bulk_delete.to_string()),
772            AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
773            AmazonS3ConfigKey::Encryption(key) => match key {
774                S3EncryptionConfigKey::ServerSideEncryption => {
775                    self.encryption_type.as_ref().map(ToString::to_string)
776                }
777                S3EncryptionConfigKey::KmsKeyId => self.encryption_kms_key_id.clone(),
778                S3EncryptionConfigKey::BucketKeyEnabled => self
779                    .encryption_bucket_key_enabled
780                    .as_ref()
781                    .map(ToString::to_string),
782                S3EncryptionConfigKey::CustomerEncryptionKey => {
783                    self.encryption_customer_key_base64.clone()
784                }
785            },
786        }
787    }
788
789    /// Sets properties on this builder based on a URL
790    ///
791    /// This is a separate member function to allow fallible computation to
792    /// be deferred until [`Self::build`] which in turn allows deriving [`Clone`]
793    fn parse_url(&mut self, url: &str) -> Result<()> {
794        let parsed = Url::parse(url).map_err(|source| {
795            let url = url.into();
796            Error::UnableToParseUrl { url, source }
797        })?;
798
799        let host = parsed
800            .host_str()
801            .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
802
803        match parsed.scheme() {
804            "s3" | "s3a" => self.bucket_name = Some(host.to_string()),
805            "https" => match host.splitn(4, '.').collect_tuple() {
806                Some(("s3", region, "amazonaws", "com")) => {
807                    self.region = Some(region.to_string());
808                    let bucket = parsed.path_segments().into_iter().flatten().next();
809                    if let Some(bucket) = bucket {
810                        self.bucket_name = Some(bucket.into());
811                    }
812                }
813                // Path-style dual-stack endpoint, e.g. `s3.dualstack.<region>.amazonaws.com`.
814                Some(("s3", "dualstack", region, "amazonaws.com")) => {
815                    self.region = Some(region.to_string());
816                    // Override endpoint to explicitly preserve dualstack address
817                    self.endpoint = Some(format!("https://s3.dualstack.{region}.amazonaws.com"));
818                    let bucket = parsed.path_segments().into_iter().flatten().next();
819                    if let Some(bucket) = bucket {
820                        self.bucket_name = Some(bucket.into());
821                    }
822                }
823                Some((bucket, "s3", "amazonaws", "com")) => {
824                    self.bucket_name = Some(bucket.to_string());
825                    self.virtual_hosted_style_request = true.into();
826                }
827                // Virtual-hosted dual-stack endpoint, e.g.
828                // `<bucket>.s3.dualstack.<region>.amazonaws.com`
829                Some((bucket, "s3", "dualstack", rest)) => {
830                    let region = rest
831                        .strip_suffix(".amazonaws.com")
832                        .ok_or_else(|| Error::UrlNotRecognised { url: url.into() })?;
833                    self.bucket_name = Some(bucket.to_string());
834                    self.region = Some(region.to_string());
835                    // Override endpoint to explicitly preserve dualstack address
836                    self.endpoint = Some(format!(
837                        "https://{bucket}.s3.dualstack.{region}.amazonaws.com"
838                    ));
839                    self.virtual_hosted_style_request = true.into();
840                }
841                Some((bucket, "s3", region, "amazonaws.com")) => {
842                    self.bucket_name = Some(bucket.to_string());
843                    self.region = Some(region.to_string());
844                    self.virtual_hosted_style_request = true.into();
845                }
846                Some((account, "r2", "cloudflarestorage", "com")) => {
847                    self.region = Some("auto".to_string());
848                    let endpoint = format!("https://{account}.r2.cloudflarestorage.com");
849                    self.endpoint = Some(endpoint);
850
851                    let bucket = parsed.path_segments().into_iter().flatten().next();
852                    if let Some(bucket) = bucket {
853                        self.bucket_name = Some(bucket.into());
854                    }
855                }
856                Some((account, jurisdiction, "r2", "cloudflarestorage.com")) => {
857                    self.region = Some("auto".to_string());
858                    let endpoint =
859                        format!("https://{account}.{jurisdiction}.r2.cloudflarestorage.com");
860                    self.endpoint = Some(endpoint);
861
862                    let bucket = parsed.path_segments().into_iter().flatten().next();
863                    if let Some(bucket) = bucket {
864                        self.bucket_name = Some(bucket.into());
865                    }
866                }
867                _ => return Err(Error::UrlNotRecognised { url: url.into() }.into()),
868            },
869            scheme => {
870                let scheme = scheme.into();
871                return Err(Error::UnknownUrlScheme { scheme }.into());
872            }
873        };
874        Ok(())
875    }
876
877    /// Set the AWS Access Key
878    ///
879    /// Examples: `AKIAIOSFODNN7EXAMPLE`, `ASIA4ZP5EXAMPLETOKEN`
880    pub fn with_access_key_id(mut self, access_key_id: impl Into<String>) -> Self {
881        self.access_key_id = Some(access_key_id.into());
882        self
883    }
884
885    /// Set the AWS Secret Access Key
886    pub fn with_secret_access_key(mut self, secret_access_key: impl Into<String>) -> Self {
887        self.secret_access_key = Some(secret_access_key.into());
888        self
889    }
890
891    /// Set the AWS Session Token to use for requests
892    ///
893    /// Should not be used in combination with [`Self::with_allow_http`].
894    pub fn with_token(mut self, token: impl Into<String>) -> Self {
895        self.token = Some(token.into());
896        self
897    }
898
899    /// Set the region, defaults to `us-east-1`
900    pub fn with_region(mut self, region: impl Into<String>) -> Self {
901        self.region = Some(region.into());
902        self
903    }
904
905    /// Set the bucket_name (required)
906    pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
907        self.bucket_name = Some(bucket_name.into());
908        self
909    }
910
911    /// Sets the endpoint for communicating with AWS S3.
912    ///
913    /// Defaults to the [region endpoint]. See  [`Self::with_region`] for further details.
914    ///
915    /// For example, this might be set to `"http://localhost:4566:`
916    /// for testing against a localstack instance.
917    ///
918    /// The `endpoint` field should be consistent with [`Self::with_virtual_hosted_style_request`].
919    /// I.e. if `virtual_hosted_style_request` is set to true then `endpoint`
920    /// should have the bucket name included.
921    ///
922    /// By default, only HTTPS schemes are enabled.
923    /// To connect to an HTTP endpoint, enable [`Self::with_allow_http`].
924    ///
925    /// [region endpoint]: https://docs.aws.amazon.com/general/latest/gr/s3.html
926    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
927        self.endpoint = Some(endpoint.into());
928        self
929    }
930
931    /// Set the credential provider overriding any other options
932    pub fn with_credentials(mut self, credentials: AwsCredentialProvider) -> Self {
933        self.credentials = Some(credentials);
934        self
935    }
936
937    /// The [`CryptoProvider`] to use
938    pub fn with_crypto_provider(mut self, provider: Arc<dyn CryptoProvider>) -> Self {
939        self.crypto = Some(provider);
940        self
941    }
942
943    /// Sets what protocol is allowed.
944    ///
945    /// If `allow_http` is :
946    /// * false (default):  Only HTTPS are allowed
947    /// * true:  HTTP and HTTPS are allowed
948    ///
949    /// <div class="warning">
950    ///
951    /// **Warning**
952    ///
953    /// If you enable this option, attackers may be able to read the data you request.
954    ///
955    /// </div>
956    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
957        self.client_options = self.client_options.with_allow_http(allow_http);
958        self
959    }
960
961    /// Sets if virtual hosted style request has to be used.
962    ///
963    /// If `virtual_hosted_style_request` is:
964    /// * false (default):  Path style request is used
965    /// * true:  Virtual hosted style request is used
966    ///
967    /// If the `endpoint` is provided then it should be
968    /// consistent with `virtual_hosted_style_request`.
969    /// I.e. if `virtual_hosted_style_request` is set to true
970    /// then `endpoint` should have bucket name included.
971    pub fn with_virtual_hosted_style_request(mut self, virtual_hosted_style_request: bool) -> Self {
972        self.virtual_hosted_style_request = virtual_hosted_style_request.into();
973        self
974    }
975
976    /// Configure this as an S3 Express One Zone Bucket
977    pub fn with_s3_express(mut self, s3_express: bool) -> Self {
978        self.s3_express = s3_express.into();
979        self
980    }
981
982    /// Set the retry configuration
983    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
984        self.retry_config = retry_config;
985        self
986    }
987
988    /// By default instance credentials will only be fetched over [IMDSv2], as AWS recommends
989    /// against having IMDSv1 enabled on EC2 instances as it is vulnerable to [SSRF attack]
990    ///
991    /// However, certain deployment environments, such as those running old versions of kube2iam,
992    /// may not support IMDSv2. This option will enable automatic fallback to using IMDSv1
993    /// if the token endpoint returns a 403 error indicating that IMDSv2 is not supported.
994    ///
995    /// This option has no effect if not using instance credentials
996    ///
997    /// [IMDSv2]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
998    /// [SSRF attack]: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
999    pub fn with_imdsv1_fallback(mut self) -> Self {
1000        self.imdsv1_fallback = true.into();
1001        self
1002    }
1003
1004    /// Sets if unsigned payload option has to be used.
1005    ///
1006    /// See [unsigned payload option](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html)
1007    /// * false (default): Signed payload option is used, where the checksum for the request body is computed and included when constructing a canonical request.
1008    /// * true: Unsigned payload option is used. `UNSIGNED-PAYLOAD` literal is included when constructing a canonical request,
1009    pub fn with_unsigned_payload(mut self, unsigned_payload: bool) -> Self {
1010        self.unsigned_payload = unsigned_payload.into();
1011        self
1012    }
1013
1014    /// If enabled, [`AmazonS3`] will not fetch credentials and will not sign requests
1015    ///
1016    /// This can be useful when interacting with public S3 buckets that deny authorized requests
1017    pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
1018        self.skip_signature = skip_signature.into();
1019        self
1020    }
1021
1022    /// Sets the [checksum algorithm] which has to be used for object integrity check during upload.
1023    ///
1024    /// [checksum algorithm]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
1025    pub fn with_checksum_algorithm(mut self, checksum_algorithm: Checksum) -> Self {
1026        // Convert to String to enable deferred parsing of config
1027        self.checksum_algorithm = Some(checksum_algorithm.into());
1028        self
1029    }
1030
1031    /// Set the [instance metadata endpoint](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html),
1032    /// used primarily within AWS EC2.
1033    ///
1034    /// This defaults to the IPv4 endpoint: http://169.254.169.254.
1035    /// One can alternatively use the IPv6 endpoint http://fd00:ec2::254.
1036    pub fn with_metadata_endpoint(mut self, endpoint: impl Into<String>) -> Self {
1037        self.metadata_endpoint = Some(endpoint.into());
1038        self
1039    }
1040
1041    /// Set the proxy_url to be used by the underlying client
1042    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
1043        self.client_options = self.client_options.with_proxy_url(proxy_url);
1044        self
1045    }
1046
1047    /// Set a trusted proxy CA certificate
1048    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
1049        self.client_options = self
1050            .client_options
1051            .with_proxy_ca_certificate(proxy_ca_certificate);
1052        self
1053    }
1054
1055    /// Set a list of hosts to exclude from proxy connections
1056    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
1057        self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
1058        self
1059    }
1060
1061    /// Sets the client options, overriding any already set
1062    pub fn with_client_options(mut self, options: ClientOptions) -> Self {
1063        self.client_options = options;
1064        self
1065    }
1066
1067    /// Configure how to provide `copy_if_not_exists`
1068    pub fn with_copy_if_not_exists(mut self, config: S3CopyIfNotExists) -> Self {
1069        self.copy_if_not_exists = Some(config.into());
1070        self
1071    }
1072
1073    /// Configure how to provide conditional put operations.
1074    /// if not set, the default value will be `S3ConditionalPut::ETagMatch`
1075    pub fn with_conditional_put(mut self, config: S3ConditionalPut) -> Self {
1076        self.conditional_put = config.into();
1077        self
1078    }
1079
1080    /// If set to `true` will ignore any tags provided to [`put_opts`](crate::ObjectStore::put_opts)
1081    pub fn with_disable_tagging(mut self, ignore: bool) -> Self {
1082        self.disable_tagging = ignore.into();
1083        self
1084    }
1085
1086    /// If set to `true`, [`delete`](crate::ObjectStoreExt::delete) and
1087    /// [`delete_stream`](crate::ObjectStore::delete_stream) will issue
1088    /// single-object `DELETE /key` requests instead of the bulk `DeleteObjects`
1089    /// API (`POST /?delete`).
1090    ///
1091    /// The bulk `DeleteObjects` API is more efficient but is not implemented by
1092    /// all S3-compatible providers (e.g. Alibaba Cloud OSS). Setting this to
1093    /// `true` restores the single-object delete behaviour that works against
1094    /// every S3-compatible provider, at the cost of throughput when deleting
1095    /// many objects via [`delete_stream`](crate::ObjectStore::delete_stream).
1096    pub fn with_disable_bulk_delete(mut self, disable: bool) -> Self {
1097        self.disable_bulk_delete = disable.into();
1098        self
1099    }
1100
1101    /// Use SSE-KMS for server side encryption.
1102    pub fn with_sse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1103        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseKms));
1104        if let Some(kms_key_id) = kms_key_id.into().into() {
1105            self.encryption_kms_key_id = Some(kms_key_id);
1106        }
1107        self
1108    }
1109
1110    /// Use dual server side encryption for server side encryption.
1111    pub fn with_dsse_kms_encryption(mut self, kms_key_id: impl Into<String>) -> Self {
1112        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::DsseKms));
1113        if let Some(kms_key_id) = kms_key_id.into().into() {
1114            self.encryption_kms_key_id = Some(kms_key_id);
1115        }
1116        self
1117    }
1118
1119    /// Use SSE-C for server side encryption.
1120    /// Must pass the *base64-encoded* 256-bit customer encryption key.
1121    pub fn with_ssec_encryption(mut self, customer_key_base64: impl Into<String>) -> Self {
1122        self.encryption_type = Some(ConfigValue::Parsed(S3EncryptionType::SseC));
1123        self.encryption_customer_key_base64 = customer_key_base64.into().into();
1124        self
1125    }
1126
1127    /// Set whether to enable bucket key for server side encryption. This overrides
1128    /// the bucket default setting for bucket keys.
1129    ///
1130    /// When bucket keys are disabled, each object is encrypted with a unique data key.
1131    /// When bucket keys are enabled, a single data key is used for the entire bucket,
1132    /// reducing overhead of encryption.
1133    pub fn with_bucket_key(mut self, enabled: bool) -> Self {
1134        self.encryption_bucket_key_enabled = Some(ConfigValue::Parsed(enabled));
1135        self
1136    }
1137
1138    /// Set whether to charge requester for bucket operations.
1139    ///
1140    /// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/RequesterPaysBuckets.html>
1141    pub fn with_request_payer(mut self, enabled: bool) -> Self {
1142        self.request_payer = ConfigValue::Parsed(enabled.into());
1143        self
1144    }
1145
1146    /// The [`HttpConnector`] to use
1147    ///
1148    /// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
1149    pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
1150        self.http_connector = Some(Arc::new(connector));
1151        self
1152    }
1153
1154    /// Create a [`AmazonS3`] instance from the provided values,
1155    /// consuming `self`.
1156    pub fn build(mut self) -> Result<AmazonS3> {
1157        if let Some(url) = self.url.take() {
1158            self.parse_url(&url)?;
1159        }
1160
1161        let http = http_connector(self.http_connector)?;
1162
1163        let bucket = self.bucket_name.ok_or(Error::MissingBucketName)?;
1164        let region = self.region.unwrap_or_else(|| "us-east-1".to_string());
1165        let checksum = self.checksum_algorithm.map(|x| x.get()).transpose()?;
1166        let copy_if_not_exists = self.copy_if_not_exists.map(|x| x.get()).transpose()?;
1167
1168        let credentials = if let Some(credentials) = self.credentials {
1169            credentials
1170        } else if self.access_key_id.is_some() || self.secret_access_key.is_some() {
1171            match (self.access_key_id, self.secret_access_key, self.token) {
1172                (Some(key_id), Some(secret_key), token) => {
1173                    debug!("Using Static credential provider");
1174                    let credential = AwsCredential {
1175                        key_id,
1176                        secret_key,
1177                        token,
1178                    };
1179                    Arc::new(StaticCredentialProvider::new(credential)) as _
1180                }
1181                (None, Some(_), _) => return Err(Error::MissingAccessKeyId.into()),
1182                (Some(_), None, _) => return Err(Error::MissingSecretAccessKey.into()),
1183                (None, None, _) => unreachable!(),
1184            }
1185        } else if let (Some(token_path), Some(role_arn)) =
1186            (self.web_identity_token_file, self.role_arn)
1187        {
1188            debug!("Using WebIdentity credential provider");
1189
1190            let session_name = self
1191                .role_session_name
1192                .clone()
1193                .unwrap_or_else(|| "WebIdentitySession".to_string());
1194
1195            let endpoint = self
1196                .sts_endpoint
1197                .clone()
1198                .unwrap_or_else(|| format!("https://sts.{region}.amazonaws.com"));
1199
1200            // Disallow non-HTTPs requests
1201            let options = self.client_options.clone().with_allow_http(false);
1202
1203            let token = WebIdentityProvider {
1204                token_path: token_path.clone(),
1205                session_name,
1206                role_arn: role_arn.clone(),
1207                endpoint,
1208            };
1209
1210            Arc::new(TokenCredentialProvider::new(
1211                token,
1212                http.connect(&options)?,
1213                self.retry_config.clone(),
1214            )) as _
1215        } else if let Some(uri) = self.container_credentials_relative_uri {
1216            debug!("Using Task credential provider");
1217
1218            let options = self.client_options.clone().with_allow_http(true);
1219
1220            Arc::new(TaskCredentialProvider {
1221                url: format!("http://169.254.170.2{uri}"),
1222                retry: self.retry_config.clone(),
1223                // The instance metadata endpoint is access over HTTP
1224                client: http.connect(&options)?,
1225                cache: Default::default(),
1226            }) as _
1227        } else if let (Some(full_uri), Some(token_file)) = (
1228            self.container_credentials_full_uri,
1229            self.container_authorization_token_file,
1230        ) {
1231            debug!("Using EKS Pod Identity credential provider");
1232
1233            let options = self.client_options.clone().with_allow_http(true);
1234
1235            Arc::new(EKSPodCredentialProvider {
1236                url: full_uri,
1237                token_file,
1238                retry: self.retry_config.clone(),
1239                client: http.connect(&options)?,
1240                cache: Default::default(),
1241            }) as _
1242        } else {
1243            debug!("Using Instance credential provider");
1244
1245            let token = InstanceCredentialProvider {
1246                imdsv1_fallback: self.imdsv1_fallback.get()?,
1247                metadata_endpoint: self
1248                    .metadata_endpoint
1249                    .unwrap_or_else(|| DEFAULT_METADATA_ENDPOINT.into()),
1250            };
1251
1252            Arc::new(TokenCredentialProvider::new(
1253                token,
1254                http.connect(&self.client_options.metadata_options())?,
1255                self.retry_config.clone(),
1256            )) as _
1257        };
1258
1259        let (session_provider, zonal_endpoint) = match self.s3_express.get()? {
1260            true => {
1261                let zone = parse_bucket_az(&bucket).ok_or_else(|| {
1262                    let bucket = bucket.clone();
1263                    Error::ZoneSuffix { bucket }
1264                })?;
1265
1266                // https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-express-Regions-and-Zones.html
1267                let endpoint = format!("https://{bucket}.s3express-{zone}.{region}.amazonaws.com");
1268
1269                let session = Arc::new(
1270                    TokenCredentialProvider::new(
1271                        SessionProvider {
1272                            endpoint: endpoint.clone(),
1273                            region: region.clone(),
1274                            credentials: Arc::clone(&credentials),
1275                            crypto: self.crypto.clone(),
1276                        },
1277                        http.connect(&self.client_options)?,
1278                        self.retry_config.clone(),
1279                    )
1280                    .with_min_ttl(Duration::from_secs(60)), // Credentials only valid for 5 minutes
1281                );
1282                (Some(session as _), Some(endpoint))
1283            }
1284            false => (None, None),
1285        };
1286
1287        // S3-specific endpoint takes precedence over generic endpoint
1288        let endpoint = self.s3_endpoint.or(self.endpoint);
1289
1290        // If `endpoint` is provided it's assumed to be consistent with `virtual_hosted_style_request` or `s3_express`.
1291        // For example, if `virtual_hosted_style_request` is true then `endpoint` should have bucket name included.
1292        let virtual_hosted = self.virtual_hosted_style_request.get()?;
1293        let bucket_endpoint = match (&endpoint, zonal_endpoint, virtual_hosted) {
1294            (Some(endpoint), _, true) => endpoint.clone(),
1295            (Some(endpoint), _, false) => format!("{}/{}", endpoint.trim_end_matches("/"), bucket),
1296            (None, Some(endpoint), _) => endpoint,
1297            (None, None, true) => format!("https://{bucket}.s3.{region}.amazonaws.com"),
1298            (None, None, false) => format!("https://s3.{region}.amazonaws.com/{bucket}"),
1299        };
1300
1301        let encryption_headers = if let Some(encryption_type) = self.encryption_type {
1302            S3EncryptionHeaders::try_new(
1303                &encryption_type.get()?,
1304                self.encryption_kms_key_id,
1305                self.encryption_bucket_key_enabled
1306                    .map(|val| val.get())
1307                    .transpose()?,
1308                self.encryption_customer_key_base64,
1309            )?
1310        } else {
1311            S3EncryptionHeaders::default()
1312        };
1313
1314        let config = S3Config {
1315            region,
1316            bucket,
1317            bucket_endpoint,
1318            credentials,
1319            crypto: self.crypto,
1320            session_provider,
1321            retry_config: self.retry_config,
1322            client_options: self.client_options,
1323            sign_payload: !self.unsigned_payload.get()?,
1324            skip_signature: self.skip_signature.get()?,
1325            disable_tagging: self.disable_tagging.get()?,
1326            disable_bulk_delete: self.disable_bulk_delete.get()?,
1327            checksum,
1328            copy_if_not_exists,
1329            conditional_put: self.conditional_put.get()?,
1330            encryption_headers,
1331            request_payer: self.request_payer.get()?.into(),
1332        };
1333
1334        let http_client = http.connect(&config.client_options)?;
1335        let client = Arc::new(S3Client::new(config, http_client));
1336
1337        Ok(AmazonS3 { client })
1338    }
1339}
1340
1341/// Extracts the AZ from a S3 Express One Zone bucket name
1342///
1343/// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/directory-bucket-naming-rules.html>
1344fn parse_bucket_az(bucket: &str) -> Option<&str> {
1345    let base = bucket
1346        .strip_suffix("--x-s3")
1347        .or_else(|| bucket.strip_suffix("--xa-s3"))?;
1348    Some(base.rsplit_once("--")?.1)
1349}
1350
1351/// Captures `AWS_REQUEST_PAYER`.
1352///
1353/// Parses either as `"requester"` (case-insensitive) meaning `true`, or as a [`bool`] (i.e. `"true"`, `"1"`, `"no"`, etc.).
1354#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1355struct RequesterPayer(bool);
1356
1357impl crate::config::Parse for RequesterPayer {
1358    fn parse(v: &str) -> Result<Self> {
1359        if v.eq_ignore_ascii_case("requester") {
1360            Ok(Self(true))
1361        } else {
1362            Ok(Self(<bool as crate::config::Parse>::parse(v)?))
1363        }
1364    }
1365}
1366
1367impl From<bool> for RequesterPayer {
1368    fn from(value: bool) -> Self {
1369        Self(value)
1370    }
1371}
1372
1373impl From<RequesterPayer> for bool {
1374    fn from(value: RequesterPayer) -> Self {
1375        value.0
1376    }
1377}
1378
1379impl std::fmt::Display for RequesterPayer {
1380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1381        self.0.fmt(f)
1382    }
1383}
1384
1385/// Encryption configuration options for S3.
1386///
1387/// These options are used to configure server-side encryption for S3 objects.
1388/// To configure them, pass them to [`AmazonS3Builder::with_config`].
1389///
1390/// [SSE-S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html
1391/// [SSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
1392/// [DSSE-KMS]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html
1393/// [SSE-C]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
1394#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
1395#[non_exhaustive]
1396pub enum S3EncryptionConfigKey {
1397    /// Type of encryption to use.
1398    ///
1399    /// If set, must be one of
1400    /// - "AES256" (SSE-S3),
1401    /// - "aws:kms" (SSE-KMS),
1402    /// - "aws:kms:dsse" (DSSE-KMS) or
1403    /// - "sse-c"
1404    ///
1405    /// Supported keys:
1406    /// - `aws_server_side_encryption`
1407    /// - `server_side_encryption`
1408    ServerSideEncryption,
1409    /// The KMS key ID to use for server-side encryption.
1410    ///
1411    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "aws:kms" or "aws:kms:dsse".
1412    ///
1413    /// Supported keys:
1414    /// - `aws_sse_kms_key_id`
1415    /// - `sse_kms_key_id`
1416    ///
1417    /// Example: `arn:aws:kms:us-east-1:123456789012:key/abcd-1234-efgh-5678`
1418    KmsKeyId,
1419    /// If set to true, will use the bucket's default KMS key for server-side encryption.
1420    /// If set to false, will disable the use of the bucket's default KMS key for server-side encryption.
1421    ///
1422    /// Supported keys:
1423    /// - `aws_sse_bucket_key_enabled`
1424    /// - `sse_bucket_key_enabled`
1425    BucketKeyEnabled,
1426
1427    /// The base64 encoded, 256-bit customer encryption key to use for server-side encryption.
1428    ///
1429    /// If set, [ServerSideEncryption](Self::ServerSideEncryption) must be "sse-c".
1430    ///
1431    /// Supported keys:
1432    /// - `aws_sse_customer_key_base64`
1433    /// - `sse_customer_key_base64`
1434    CustomerEncryptionKey,
1435}
1436
1437impl AsRef<str> for S3EncryptionConfigKey {
1438    fn as_ref(&self) -> &str {
1439        match self {
1440            Self::ServerSideEncryption => "aws_server_side_encryption",
1441            Self::KmsKeyId => "aws_sse_kms_key_id",
1442            Self::BucketKeyEnabled => "aws_sse_bucket_key_enabled",
1443            Self::CustomerEncryptionKey => "aws_sse_customer_key_base64",
1444        }
1445    }
1446}
1447
1448#[derive(Debug, Clone)]
1449enum S3EncryptionType {
1450    S3,
1451    SseKms,
1452    DsseKms,
1453    SseC,
1454}
1455
1456impl crate::config::Parse for S3EncryptionType {
1457    fn parse(s: &str) -> Result<Self> {
1458        match s {
1459            "AES256" => Ok(Self::S3),
1460            "aws:kms" => Ok(Self::SseKms),
1461            "aws:kms:dsse" => Ok(Self::DsseKms),
1462            "sse-c" => Ok(Self::SseC),
1463            _ => Err(Error::InvalidEncryptionType { passed: s.into() }.into()),
1464        }
1465    }
1466}
1467
1468impl From<&S3EncryptionType> for &'static str {
1469    fn from(value: &S3EncryptionType) -> Self {
1470        match value {
1471            S3EncryptionType::S3 => "AES256",
1472            S3EncryptionType::SseKms => "aws:kms",
1473            S3EncryptionType::DsseKms => "aws:kms:dsse",
1474            S3EncryptionType::SseC => "sse-c",
1475        }
1476    }
1477}
1478
1479impl std::fmt::Display for S3EncryptionType {
1480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1481        f.write_str(self.into())
1482    }
1483}
1484
1485/// A sequence of headers to be sent for write requests that specify server-side
1486/// encryption.
1487///
1488/// Whether these headers are sent depends on both the kind of encryption set
1489/// and the kind of request being made.
1490#[derive(Default, Clone, Debug)]
1491pub(super) struct S3EncryptionHeaders(pub HeaderMap);
1492
1493impl S3EncryptionHeaders {
1494    fn try_new(
1495        encryption_type: &S3EncryptionType,
1496        encryption_kms_key_id: Option<String>,
1497        bucket_key_enabled: Option<bool>,
1498        encryption_customer_key_base64: Option<String>,
1499    ) -> Result<Self> {
1500        let mut headers = HeaderMap::new();
1501        match encryption_type {
1502            S3EncryptionType::S3 | S3EncryptionType::SseKms | S3EncryptionType::DsseKms => {
1503                headers.insert(
1504                    "x-amz-server-side-encryption",
1505                    HeaderValue::from_static(encryption_type.into()),
1506                );
1507                if let Some(key_id) = encryption_kms_key_id {
1508                    headers.insert(
1509                        "x-amz-server-side-encryption-aws-kms-key-id",
1510                        key_id
1511                            .try_into()
1512                            .map_err(|err| Error::InvalidEncryptionHeader {
1513                                header: "kms-key-id",
1514                                source: Box::new(err),
1515                            })?,
1516                    );
1517                }
1518                if let Some(bucket_key_enabled) = bucket_key_enabled {
1519                    headers.insert(
1520                        "x-amz-server-side-encryption-bucket-key-enabled",
1521                        HeaderValue::from_static(if bucket_key_enabled { "true" } else { "false" }),
1522                    );
1523                }
1524            }
1525            S3EncryptionType::SseC => {
1526                headers.insert(
1527                    "x-amz-server-side-encryption-customer-algorithm",
1528                    HeaderValue::from_static("AES256"),
1529                );
1530                if let Some(key) = encryption_customer_key_base64 {
1531                    let mut header_value: HeaderValue =
1532                        key.clone()
1533                            .try_into()
1534                            .map_err(|err| Error::InvalidEncryptionHeader {
1535                                header: "x-amz-server-side-encryption-customer-key",
1536                                source: Box::new(err),
1537                            })?;
1538                    header_value.set_sensitive(true);
1539                    headers.insert("x-amz-server-side-encryption-customer-key", header_value);
1540
1541                    let decoded_key = BASE64_STANDARD.decode(key.as_bytes()).map_err(|err| {
1542                        Error::InvalidEncryptionHeader {
1543                            header: "x-amz-server-side-encryption-customer-key",
1544                            source: Box::new(err),
1545                        }
1546                    })?;
1547                    let mut hasher = Md5::new();
1548                    hasher.update(decoded_key);
1549                    let md5 = BASE64_STANDARD.encode(hasher.finalize());
1550                    let mut md5_header_value: HeaderValue =
1551                        md5.try_into()
1552                            .map_err(|err| Error::InvalidEncryptionHeader {
1553                                header: "x-amz-server-side-encryption-customer-key-MD5",
1554                                source: Box::new(err),
1555                            })?;
1556                    md5_header_value.set_sensitive(true);
1557                    headers.insert(
1558                        "x-amz-server-side-encryption-customer-key-MD5",
1559                        md5_header_value,
1560                    );
1561                } else {
1562                    return Err(Error::InvalidEncryptionHeader {
1563                        header: "x-amz-server-side-encryption-customer-key",
1564                        source: Box::new(std::io::Error::new(
1565                            std::io::ErrorKind::InvalidInput,
1566                            "Missing customer key",
1567                        )),
1568                    }
1569                    .into());
1570                }
1571            }
1572        }
1573        Ok(Self(headers))
1574    }
1575}
1576
1577impl From<S3EncryptionHeaders> for HeaderMap {
1578    fn from(headers: S3EncryptionHeaders) -> Self {
1579        headers.0
1580    }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585    use super::*;
1586    use std::collections::HashMap;
1587
1588    #[test]
1589    fn s3_test_config_from_map() {
1590        let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1591        let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1592        let aws_default_region = "object_store:fake_default_region".to_string();
1593        let aws_endpoint = "object_store:fake_endpoint".to_string();
1594        let aws_session_token = "object_store:fake_session_token".to_string();
1595        let options = HashMap::from([
1596            ("aws_access_key_id", aws_access_key_id.clone()),
1597            ("aws_secret_access_key", aws_secret_access_key),
1598            ("aws_default_region", aws_default_region.clone()),
1599            ("aws_endpoint", aws_endpoint.clone()),
1600            ("aws_session_token", aws_session_token.clone()),
1601            ("aws_unsigned_payload", "true".to_string()),
1602            ("aws_checksum_algorithm", "sha256".to_string()),
1603        ]);
1604
1605        let builder = options
1606            .into_iter()
1607            .fold(AmazonS3Builder::new(), |builder, (key, value)| {
1608                builder.with_config(key.parse().unwrap(), value)
1609            })
1610            .with_config(AmazonS3ConfigKey::SecretAccessKey, "new-secret-key");
1611
1612        assert_eq!(builder.access_key_id.unwrap(), aws_access_key_id.as_str());
1613        assert_eq!(builder.secret_access_key.unwrap(), "new-secret-key");
1614        assert_eq!(builder.region.unwrap(), aws_default_region);
1615        assert_eq!(builder.endpoint.unwrap(), aws_endpoint);
1616        assert_eq!(builder.token.unwrap(), aws_session_token);
1617        assert_eq!(
1618            builder.checksum_algorithm.unwrap().get().unwrap(),
1619            Checksum::SHA256
1620        );
1621        assert!(builder.unsigned_payload.get().unwrap());
1622    }
1623
1624    #[cfg(feature = "reqwest")]
1625    #[test]
1626    fn s3_test_endpoint_url_s3_config() {
1627        // Verify aws_endpoint_url_s3 parses to S3Endpoint config key
1628        let key: AmazonS3ConfigKey = "aws_endpoint_url_s3".parse().unwrap();
1629        assert!(matches!(key, AmazonS3ConfigKey::S3Endpoint));
1630
1631        // Verify S3Endpoint takes precedence over Endpoint in build, regardless of order
1632        let s3 = AmazonS3Builder::new()
1633            .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1634            .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1635            .with_bucket_name("test-bucket")
1636            .build()
1637            .unwrap();
1638        assert_eq!(
1639            s3.client.config.bucket_endpoint,
1640            "http://s3-specific-endpoint/test-bucket"
1641        );
1642
1643        // Verify precedence works even when S3Endpoint is set first
1644        let s3 = AmazonS3Builder::new()
1645            .with_config(AmazonS3ConfigKey::S3Endpoint, "http://s3-specific-endpoint")
1646            .with_config(AmazonS3ConfigKey::Endpoint, "http://generic-endpoint")
1647            .with_bucket_name("test-bucket")
1648            .build()
1649            .unwrap();
1650        assert_eq!(
1651            s3.client.config.bucket_endpoint,
1652            "http://s3-specific-endpoint/test-bucket"
1653        );
1654    }
1655
1656    #[test]
1657    fn s3_test_config_get_value() {
1658        let aws_access_key_id = "object_store:fake_access_key_id".to_string();
1659        let aws_secret_access_key = "object_store:fake_secret_key".to_string();
1660        let aws_default_region = "object_store:fake_default_region".to_string();
1661        let aws_endpoint = "object_store:fake_endpoint".to_string();
1662        let aws_session_token = "object_store:fake_session_token".to_string();
1663
1664        let builder = AmazonS3Builder::new()
1665            .with_config(AmazonS3ConfigKey::AccessKeyId, &aws_access_key_id)
1666            .with_config(AmazonS3ConfigKey::SecretAccessKey, &aws_secret_access_key)
1667            .with_config(AmazonS3ConfigKey::DefaultRegion, &aws_default_region)
1668            .with_config(AmazonS3ConfigKey::Endpoint, &aws_endpoint)
1669            .with_config(AmazonS3ConfigKey::Token, &aws_session_token)
1670            .with_config(AmazonS3ConfigKey::UnsignedPayload, "true")
1671            .with_config("aws_server_side_encryption".parse().unwrap(), "AES256")
1672            .with_config("aws_sse_kms_key_id".parse().unwrap(), "some_key_id")
1673            .with_config("aws_sse_bucket_key_enabled".parse().unwrap(), "true")
1674            .with_config(
1675                "aws_sse_customer_key_base64".parse().unwrap(),
1676                "some_customer_key",
1677            );
1678
1679        assert_eq!(
1680            builder
1681                .get_config_value(&AmazonS3ConfigKey::AccessKeyId)
1682                .unwrap(),
1683            aws_access_key_id
1684        );
1685        assert_eq!(
1686            builder
1687                .get_config_value(&AmazonS3ConfigKey::SecretAccessKey)
1688                .unwrap(),
1689            aws_secret_access_key
1690        );
1691        assert_eq!(
1692            builder
1693                .get_config_value(&AmazonS3ConfigKey::DefaultRegion)
1694                .unwrap(),
1695            aws_default_region
1696        );
1697        assert_eq!(
1698            builder
1699                .get_config_value(&AmazonS3ConfigKey::Endpoint)
1700                .unwrap(),
1701            aws_endpoint
1702        );
1703        assert_eq!(
1704            builder.get_config_value(&AmazonS3ConfigKey::Token).unwrap(),
1705            aws_session_token
1706        );
1707        assert_eq!(
1708            builder
1709                .get_config_value(&AmazonS3ConfigKey::UnsignedPayload)
1710                .unwrap(),
1711            "true"
1712        );
1713        assert_eq!(
1714            builder
1715                .get_config_value(&"aws_server_side_encryption".parse().unwrap())
1716                .unwrap(),
1717            "AES256"
1718        );
1719        assert_eq!(
1720            builder
1721                .get_config_value(&"aws_sse_kms_key_id".parse().unwrap())
1722                .unwrap(),
1723            "some_key_id"
1724        );
1725        assert_eq!(
1726            builder
1727                .get_config_value(&"aws_sse_bucket_key_enabled".parse().unwrap())
1728                .unwrap(),
1729            "true"
1730        );
1731        assert_eq!(
1732            builder
1733                .get_config_value(&"aws_sse_customer_key_base64".parse().unwrap())
1734                .unwrap(),
1735            "some_customer_key"
1736        );
1737    }
1738
1739    #[cfg(feature = "reqwest")]
1740    #[test]
1741    fn s3_default_region() {
1742        let builder = AmazonS3Builder::new()
1743            .with_bucket_name("foo")
1744            .build()
1745            .unwrap();
1746        assert_eq!(builder.client.config.region, "us-east-1");
1747    }
1748
1749    #[cfg(feature = "reqwest")]
1750    #[test]
1751    fn s3_test_bucket_endpoint() {
1752        let builder = AmazonS3Builder::new()
1753            .with_endpoint("http://some.host:1234")
1754            .with_bucket_name("foo")
1755            .build()
1756            .unwrap();
1757        assert_eq!(
1758            builder.client.config.bucket_endpoint,
1759            "http://some.host:1234/foo"
1760        );
1761
1762        let builder = AmazonS3Builder::new()
1763            .with_endpoint("http://some.host:1234/")
1764            .with_bucket_name("foo")
1765            .build()
1766            .unwrap();
1767        assert_eq!(
1768            builder.client.config.bucket_endpoint,
1769            "http://some.host:1234/foo"
1770        );
1771    }
1772
1773    #[test]
1774    fn s3_test_urls() {
1775        let mut builder = AmazonS3Builder::new();
1776        builder.parse_url("s3://bucket/path").unwrap();
1777        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1778
1779        let mut builder = AmazonS3Builder::new();
1780        builder
1781            .parse_url("s3://buckets.can.have.dots/path")
1782            .unwrap();
1783        assert_eq!(
1784            builder.bucket_name,
1785            Some("buckets.can.have.dots".to_string())
1786        );
1787
1788        let mut builder = AmazonS3Builder::new();
1789        builder
1790            .parse_url("https://s3.region.amazonaws.com")
1791            .unwrap();
1792        assert_eq!(builder.region, Some("region".to_string()));
1793
1794        let mut builder = AmazonS3Builder::new();
1795        builder
1796            .parse_url("https://s3.region.amazonaws.com/bucket")
1797            .unwrap();
1798        assert_eq!(builder.region, Some("region".to_string()));
1799        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1800
1801        let mut builder = AmazonS3Builder::new();
1802        builder
1803            .parse_url("https://s3.region.amazonaws.com/bucket.with.dot/path")
1804            .unwrap();
1805        assert_eq!(builder.region, Some("region".to_string()));
1806        assert_eq!(builder.bucket_name, Some("bucket.with.dot".to_string()));
1807
1808        let mut builder = AmazonS3Builder::new();
1809        builder
1810            .parse_url("https://bucket.s3.amazonaws.com")
1811            .unwrap();
1812        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1813        assert!(builder.virtual_hosted_style_request.get().unwrap());
1814
1815        let mut builder = AmazonS3Builder::new();
1816        builder
1817            .parse_url("https://bucket.s3.region.amazonaws.com")
1818            .unwrap();
1819        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1820        assert_eq!(builder.region, Some("region".to_string()));
1821        assert!(builder.virtual_hosted_style_request.get().unwrap());
1822
1823        // Path-style dual-stack endpoint
1824        let mut builder = AmazonS3Builder::new();
1825        builder
1826            .parse_url("https://s3.dualstack.us-east-1.amazonaws.com/bucket/path")
1827            .unwrap();
1828        assert_eq!(builder.region, Some("us-east-1".to_string()));
1829        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1830        assert_eq!(
1831            builder.endpoint,
1832            Some("https://s3.dualstack.us-east-1.amazonaws.com".to_string())
1833        );
1834        assert!(!builder.virtual_hosted_style_request.get().unwrap());
1835
1836        // Virtual-hosted dual-stack endpoint
1837        let mut builder = AmazonS3Builder::new();
1838        builder
1839            .parse_url("https://bucket.s3.dualstack.us-east-1.amazonaws.com")
1840            .unwrap();
1841        assert_eq!(builder.bucket_name, Some("bucket".to_string()));
1842        assert_eq!(builder.region, Some("us-east-1".to_string()));
1843        assert_eq!(
1844            builder.endpoint,
1845            Some("https://bucket.s3.dualstack.us-east-1.amazonaws.com".to_string())
1846        );
1847        assert!(builder.virtual_hosted_style_request.get().unwrap());
1848
1849        let mut builder = AmazonS3Builder::new();
1850        builder
1851            .parse_url("https://account123.r2.cloudflarestorage.com/bucket-123")
1852            .unwrap();
1853
1854        assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1855        assert_eq!(builder.region, Some("auto".to_string()));
1856        assert_eq!(
1857            builder.endpoint,
1858            Some("https://account123.r2.cloudflarestorage.com".to_string())
1859        );
1860
1861        let mut builder = AmazonS3Builder::new();
1862        builder
1863            .parse_url("https://account123.eu.r2.cloudflarestorage.com/bucket-123")
1864            .unwrap();
1865
1866        assert_eq!(builder.bucket_name, Some("bucket-123".to_string()));
1867        assert_eq!(builder.region, Some("auto".to_string()));
1868        assert_eq!(
1869            builder.endpoint,
1870            Some("https://account123.eu.r2.cloudflarestorage.com".to_string())
1871        );
1872
1873        let err_cases = [
1874            "mailto://bucket/path",
1875            "https://s3.bucket.mydomain.com",
1876            "https://s3.bucket.foo.amazonaws.com",
1877            "https://bucket.mydomain.region.amazonaws.com",
1878            "https://bucket.s3.region.bar.amazonaws.com",
1879            "https://bucket.foo.s3.amazonaws.com",
1880            "https://account.r2.eu.cloudflarestorage.com/bucket",
1881        ];
1882        let mut builder = AmazonS3Builder::new();
1883        for case in err_cases {
1884            builder.parse_url(case).unwrap_err();
1885        }
1886    }
1887
1888    #[cfg(feature = "reqwest")]
1889    #[tokio::test]
1890    async fn s3_test_proxy_url() {
1891        let s3 = AmazonS3Builder::new()
1892            .with_access_key_id("access_key_id")
1893            .with_secret_access_key("secret_access_key")
1894            .with_region("region")
1895            .with_bucket_name("bucket_name")
1896            .with_allow_http(true)
1897            .with_proxy_url("https://example.com")
1898            .build();
1899
1900        assert!(s3.is_ok());
1901
1902        let err = AmazonS3Builder::new()
1903            .with_access_key_id("access_key_id")
1904            .with_secret_access_key("secret_access_key")
1905            .with_region("region")
1906            .with_bucket_name("bucket_name")
1907            .with_allow_http(true)
1908            // use invalid url
1909            .with_proxy_url("dxx:ddd\\example.com")
1910            .build()
1911            .unwrap_err()
1912            .to_string();
1913
1914        assert_eq!("Generic HTTP client error: builder error", err);
1915    }
1916
1917    #[cfg(feature = "reqwest")]
1918    #[test]
1919    fn test_invalid_config() {
1920        let err = AmazonS3Builder::new()
1921            .with_config(AmazonS3ConfigKey::ImdsV1Fallback, "enabled")
1922            .with_bucket_name("bucket")
1923            .with_region("region")
1924            .build()
1925            .unwrap_err()
1926            .to_string();
1927
1928        assert_eq!(
1929            err,
1930            "Generic Config error: failed to parse \"enabled\" as boolean"
1931        );
1932
1933        let err = AmazonS3Builder::new()
1934            .with_config(AmazonS3ConfigKey::Checksum, "md5")
1935            .with_bucket_name("bucket")
1936            .with_region("region")
1937            .build()
1938            .unwrap_err()
1939            .to_string();
1940
1941        assert_eq!(
1942            err,
1943            "Generic Config error: \"md5\" is not a valid checksum algorithm"
1944        );
1945
1946        let err = AmazonS3Builder::new()
1947            .with_config(AmazonS3ConfigKey::RequestPayer, "requestr")
1948            .with_bucket_name("bucket")
1949            .with_region("region")
1950            .build()
1951            .unwrap_err()
1952            .to_string();
1953
1954        assert_eq!(
1955            err,
1956            "Generic Config error: failed to parse \"requestr\" as boolean"
1957        );
1958    }
1959
1960    #[cfg(feature = "reqwest")]
1961    #[test]
1962    fn test_request_payer_config() {
1963        let s3 = AmazonS3Builder::new()
1964            .with_config(AmazonS3ConfigKey::RequestPayer, "requester")
1965            .with_bucket_name("bucket")
1966            .with_region("region")
1967            .build()
1968            .unwrap();
1969        assert!(s3.client.config.request_payer);
1970
1971        let s3 = AmazonS3Builder::new()
1972            .with_config(AmazonS3ConfigKey::RequestPayer, "REQUESTER")
1973            .with_bucket_name("bucket")
1974            .with_region("region")
1975            .build()
1976            .unwrap();
1977        assert!(s3.client.config.request_payer);
1978
1979        let s3 = AmazonS3Builder::new()
1980            .with_config(AmazonS3ConfigKey::RequestPayer, "true")
1981            .with_bucket_name("bucket")
1982            .with_region("region")
1983            .build()
1984            .unwrap();
1985        assert!(s3.client.config.request_payer);
1986
1987        let s3 = AmazonS3Builder::new()
1988            .with_config(AmazonS3ConfigKey::RequestPayer, "false")
1989            .with_bucket_name("bucket")
1990            .with_region("region")
1991            .build()
1992            .unwrap();
1993        assert!(!s3.client.config.request_payer);
1994
1995        let s3 = AmazonS3Builder::new()
1996            .with_request_payer(true)
1997            .with_bucket_name("bucket")
1998            .with_region("region")
1999            .build()
2000            .unwrap();
2001        assert!(s3.client.config.request_payer);
2002    }
2003
2004    #[test]
2005    fn test_parse_bucket_az() {
2006        let cases = [
2007            ("bucket-base-name--usw2-az1--x-s3", Some("usw2-az1")),
2008            ("bucket-base--name--azid--x-s3", Some("azid")),
2009            ("bucket-base-name--use1-az4--xa-s3", Some("use1-az4")),
2010            ("bucket-base--name--azid--xa-s3", Some("azid")),
2011            ("bucket-base-name", None),
2012            ("bucket-base-name--x-s3", None),
2013            ("bucket-base-name--xa-s3", None),
2014        ];
2015
2016        for (bucket, expected) in cases {
2017            assert_eq!(parse_bucket_az(bucket), expected)
2018        }
2019    }
2020
2021    #[test]
2022    fn aws_test_client_opts() {
2023        let key = "AWS_PROXY_URL";
2024        if let Ok(config_key) = key.to_ascii_lowercase().parse() {
2025            assert_eq!(
2026                AmazonS3ConfigKey::Client(ClientConfigKey::ProxyUrl),
2027                config_key
2028            );
2029        } else {
2030            panic!("{key} not propagated as ClientConfigKey");
2031        }
2032    }
2033
2034    #[cfg(feature = "reqwest")]
2035    #[test]
2036    fn test_builder_eks_with_config() {
2037        let builder = AmazonS3Builder::new()
2038            .with_bucket_name("some-bucket")
2039            .with_config(
2040                AmazonS3ConfigKey::ContainerCredentialsFullUri,
2041                "https://127.0.0.1/eks-credentials",
2042            )
2043            .with_config(
2044                AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
2045                "/tmp/fake-bearer-token",
2046            );
2047
2048        let s3 = builder.build().expect("should build successfully");
2049        let creds = &s3.client.config.credentials;
2050        let debug_str = format!("{creds:?}");
2051        assert!(
2052            debug_str.contains("EKSPodCredentialProvider"),
2053            "expected EKS provider but got: {debug_str}"
2054        );
2055    }
2056
2057    #[cfg(feature = "reqwest")]
2058    #[test]
2059    fn test_builder_web_identity_with_config() {
2060        let builder = AmazonS3Builder::new()
2061            .with_bucket_name("some-bucket")
2062            .with_config(
2063                AmazonS3ConfigKey::WebIdentityTokenFile,
2064                "/tmp/fake-token-file",
2065            )
2066            .with_config(
2067                AmazonS3ConfigKey::RoleArn,
2068                "arn:aws:iam::123456789012:role/test-role",
2069            )
2070            .with_config(AmazonS3ConfigKey::RoleSessionName, "TestSession")
2071            .with_config(
2072                AmazonS3ConfigKey::StsEndpoint,
2073                "https://sts.us-west-2.amazonaws.com",
2074            );
2075
2076        assert_eq!(
2077            builder
2078                .get_config_value(&AmazonS3ConfigKey::WebIdentityTokenFile)
2079                .unwrap(),
2080            "/tmp/fake-token-file"
2081        );
2082        assert_eq!(
2083            builder
2084                .get_config_value(&AmazonS3ConfigKey::RoleArn)
2085                .unwrap(),
2086            "arn:aws:iam::123456789012:role/test-role"
2087        );
2088        assert_eq!(
2089            builder
2090                .get_config_value(&AmazonS3ConfigKey::RoleSessionName)
2091                .unwrap(),
2092            "TestSession"
2093        );
2094        assert_eq!(
2095            builder
2096                .get_config_value(&AmazonS3ConfigKey::StsEndpoint)
2097                .unwrap(),
2098            "https://sts.us-west-2.amazonaws.com"
2099        );
2100
2101        let s3 = builder.build().expect("should build successfully");
2102        let creds = &s3.client.config.credentials;
2103        let debug_str = format!("{creds:?}");
2104        assert!(
2105            debug_str.contains("TokenCredentialProvider"),
2106            "expected TokenCredentialProvider but got: {debug_str}"
2107        );
2108    }
2109}