Unverified Commit bc316a0b authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Improve configurability of connectors (#984)

* Make connectors configurable in aws-config & add ca-certs example

* Cleanup cargo.toml

* fix native tls example

* Cleanup comments

* Fix native_tls & cleanup aws-config features

* add test

* add rt-tokio feature to crates to pull in aws-smithy-http
parent 166ffd62
Loading
Loading
Loading
Loading
+9 −6
Original line number Diff line number Diff line
@@ -9,19 +9,18 @@ license = "Apache-2.0"
repository = "https://github.com/awslabs/smithy-rs"

[features]
default-provider = ["profile", "imds", "sts", "http-provider"]
default-provider = ["profile", "imds", "sts", "http-provider", "web-identity-token"]
profile = ["sts", "web-identity-token", "imds", "http-provider"]
imds = ["profile", "aws-smithy-http/rt-tokio", "aws-smithy-http-tower", "aws-smithy-json", "tower", "aws-http"]
imds = ["profile", "aws-smithy-http-tower", "aws-smithy-http", "aws-smithy-json", "aws-http"]
sts = ["aws-sdk-sts", "aws-smithy-http"]
web-identity-token = ["sts", "profile"]
http-provider = ["aws-smithy-json", "aws-smithy-http/rt-tokio", "tower", "tokio/sync"]
tcp-connector = ["tokio/net", "tower"]
http-provider = ["aws-smithy-json", "aws-smithy-http"]

rustls = ["aws-smithy-client/rustls"]
native-tls = ["aws-smithy-client/native-tls"]
rt-tokio = ["aws-smithy-async/rt-tokio"]

default = ["default-provider", "rustls", "rt-tokio", "tcp-connector"]
default = ["default-provider", "rustls", "rt-tokio"]

[dependencies]
aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false, optional = true }
@@ -31,6 +30,7 @@ aws-smithy-types = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-types" }
aws-types = { path = "../../sdk/build/aws-sdk/sdk/aws-types" }
tokio = { version = "1", features = ["sync"] }
tracing = { version = "0.1" }
hyper = { version = "0.14.16", default-features = false }

# imds
aws-http = { path = "../../sdk/build/aws-sdk/sdk/aws-http", optional = true }
@@ -39,7 +39,7 @@ aws-smithy-http-tower = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http-to
aws-smithy-json = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-json", optional = true }
bytes = "1.1.0"
http = "0.2.4"
tower = { version = "0.4.8", optional = true }
tower = { version = "0.4.8" }

[dev-dependencies]
futures-util = "0.3.16"
@@ -59,6 +59,9 @@ serde_json = "1"

aws-smithy-client = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-client", features = ["test-util"] }

# used for a usage example
hyper-rustls = { version = "0.23.0", features = ["webpki-tokio", "http2", "http1"] }

[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu"]
+84 −5
Original line number Diff line number Diff line
@@ -137,6 +137,7 @@ mod loader {

    use crate::default_provider::{app_name, credentials, region, retry_config, timeout_config};
    use crate::meta::region::ProvideRegion;
    use crate::provider_config::ProviderConfig;

    /// Load a cross-service [`Config`](aws_types::config::Config) from the environment
    ///
@@ -152,6 +153,7 @@ mod loader {
        retry_config: Option<RetryConfig>,
        sleep: Option<Arc<dyn AsyncSleep>>,
        timeout_config: Option<TimeoutConfig>,
        provider_config: Option<ProviderConfig>,
    }

    impl ConfigLoader {
@@ -241,6 +243,29 @@ mod loader {
            self
        }

        /// Set configuration for all sub-loaders (credentials, region etc.)
        ///
        /// Update the `ProviderConfig` used for all nested loaders. This can be used to override
        /// the HTTPs` connector used or to stub in an in memory `Env` or `Fs` for testing.
        ///
        /// # Examples
        /// ```no_run
        /// # async fn docs() {
        /// use aws_config::provider_config::ProviderConfig;
        /// let custom_https_connector = hyper_rustls::HttpsConnectorBuilder::new().
        ///     with_webpki_roots()
        ///     .https_only()
        ///     .enable_http1()
        ///     .build();
        /// let provider_config = ProviderConfig::default().with_tcp_connector(custom_https_connector);
        /// let shared_config = aws_config::from_env().configure(provider_config).load().await;
        /// # }
        /// ```
        pub fn configure(mut self, provider_config: ProviderConfig) -> Self {
            self.provider_config = Some(provider_config);
            self
        }

        /// Load the default configuration chain
        ///
        /// If fields have been overridden during builder construction, the override values will be used.
@@ -251,28 +276,42 @@ mod loader {
        /// This means that if you provide a region provider that does not return a region, no region will
        /// be set in the resulting [`Config`](aws_types::config::Config)
        pub async fn load(self) -> aws_types::config::Config {
            let conf = self.provider_config.unwrap_or_default();
            let region = if let Some(provider) = self.region {
                provider.region().await
            } else {
                region::default_provider().region().await
                region::Builder::default()
                    .configure(&conf)
                    .build()
                    .region()
                    .await
            };

            let retry_config = if let Some(retry_config) = self.retry_config {
                retry_config
            } else {
                retry_config::default_provider().retry_config().await
                retry_config::default_provider()
                    .configure(&conf)
                    .retry_config()
                    .await
            };

            let app_name = if self.app_name.is_some() {
                self.app_name
            } else {
                app_name::default_provider().app_name().await
                app_name::default_provider()
                    .configure(&conf)
                    .app_name()
                    .await
            };

            let timeout_config = if let Some(timeout_config) = self.timeout_config {
                timeout_config
            } else {
                timeout_config::default_provider().timeout_config().await
                timeout_config::default_provider()
                    .configure(&conf)
                    .timeout_config()
                    .await
            };

            let sleep_impl = if self.sleep.is_none() {
@@ -293,7 +332,7 @@ mod loader {
            let credentials_provider = if let Some(provider) = self.credentials_provider {
                provider
            } else {
                let mut builder = credentials::DefaultCredentialsChain::builder();
                let mut builder = credentials::DefaultCredentialsChain::builder().configure(conf);
                builder.set_region(region.clone());
                SharedCredentialsProvider::new(builder.build().await)
            };
@@ -309,6 +348,46 @@ mod loader {
            builder.build()
        }
    }

    #[cfg(test)]
    mod test {
        use crate::from_env;
        use crate::provider_config::ProviderConfig;
        use aws_smithy_client::erase::DynConnector;
        use aws_smithy_client::never::NeverConnector;
        use aws_types::credentials::ProvideCredentials;
        use aws_types::os_shim_internal::Env;

        #[tokio::test]
        async fn provider_config_used() {
            let env = Env::from_slice(&[
                ("AWS_MAX_ATTEMPTS", "10"),
                ("AWS_REGION", "us-west-4"),
                ("AWS_ACCESS_KEY_ID", "akid"),
                ("AWS_SECRET_ACCESS_KEY", "secret"),
            ]);
            let loader = from_env()
                .configure(
                    ProviderConfig::empty()
                        .with_env(env)
                        .with_http_connector(DynConnector::new(NeverConnector::new())),
                )
                .load()
                .await;
            assert_eq!(loader.retry_config().unwrap().max_attempts(), 10);
            assert_eq!(loader.region().unwrap().as_ref(), "us-west-4");
            assert_eq!(
                loader
                    .credentials_provider()
                    .unwrap()
                    .provide_credentials()
                    .await
                    .unwrap()
                    .access_key_id(),
                "akid"
            );
        }
    }
}

mod connector {
+27 −14
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
//! Configuration Options for Credential Providers

use crate::connector::default_connector;
use std::error::Error;

use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep};
use aws_smithy_client::erase::DynConnector;
@@ -15,8 +16,9 @@ use aws_types::region::Region;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;

#[cfg(feature = "tcp-connector")]
use aws_smithy_client::erase::boxclone::BoxCloneService;
use http::Uri;
use hyper::client::connect::Connection;
use tokio::io::{AsyncRead, AsyncWrite};

/// Configuration options for Credential Providers
///
@@ -43,8 +45,6 @@ pub(crate) type MakeConnectorFn =
pub(crate) enum HttpConnector {
    Prebuilt(Option<DynConnector>),
    ConnectorFn(Arc<MakeConnectorFn>),
    #[cfg(feature = "tcp-connector")]
    TcpConnector(BoxCloneService<http::Uri, tokio::net::TcpStream, tower::BoxError>),
}

impl Default for HttpConnector {
@@ -266,11 +266,11 @@ impl ProviderConfig {

    /// Override the HTTPS connector for this configuration
    ///
    /// **Warning**: Use of this method will prevent you from taking advantage of the timeout machinery.
    /// Consider `with_tcp_connector`.
    /// **Warning**: Use of this method will prevent you from taking advantage of the HTTP connect timeouts.
    /// Consider [`ProviderConfig::with_tcp_connector`].
    ///
    /// # Stability
    /// This method is expected to change to support HTTP configuration
    /// This method is expected to change to support HTTP configuration.
    pub fn with_http_connector(self, connector: DynConnector) -> Self {
        ProviderConfig {
            connector: HttpConnector::Prebuilt(Some(connector)),
@@ -280,15 +280,28 @@ impl ProviderConfig {

    /// Override the TCP connector for this configuration
    ///
    /// This connector MUST provide an HTTPS encrypted connection.
    ///
    /// # Stability
    /// This method is may to change to support HTTP configuration.
    #[cfg(feature = "tcp-connector")]
    pub fn with_tcp_connector(
        self,
        connector: BoxCloneService<http::Uri, tokio::net::TcpStream, tower::BoxError>,
    ) -> Self {
    /// This method may change to support HTTP configuration.
    pub fn with_tcp_connector<C>(self, connector: C) -> Self
    where
        C: Clone + Send + Sync + 'static,
        C: tower::Service<Uri>,
        C::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
        C::Future: Unpin + Send + 'static,
        C::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
    {
        let connector_fn = move |settings: &HttpSettings, sleep: Option<Arc<dyn AsyncSleep>>| {
            let mut builder = aws_smithy_client::hyper_ext::Adapter::builder()
                .timeout(&settings.timeout_settings);
            if let Some(sleep) = sleep {
                builder = builder.sleep_impl(sleep);
            };
            Some(DynConnector::new(builder.build(connector.clone())))
        };
        ProviderConfig {
            connector: HttpConnector::TcpConnector(connector),
            connector: HttpConnector::ConnectorFn(Arc::new(connector_fn)),
            ..self
        }
    }
+16 −0
Original line number Diff line number Diff line
[package]
name = "custom-root-certs"
version = "0.1.0"
authors = ["rcoh@amazon.com>"]
edition = "2018"

description = "An example demonstrating setting a custom root certificate with rustls"

[dependencies]
aws-config = { path = "../../build/aws-sdk/sdk/aws-config" }
aws-smithy-client = { path = "../../build/aws-sdk/sdk/aws-smithy-client" }
# bringing our own HTTPs so no need for the default features
aws-sdk-s3 = { package = "aws-sdk-s3", path = "../../build/aws-sdk/sdk/s3", default-features = false }
tokio = { version = "1", features = ["full"] }
rustls = "0.20.2"
hyper-rustls = { version = "0.23.0", features = ["http2"] }
+39 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use aws_config::provider_config::ProviderConfig;
use aws_smithy_client::hyper_ext;
use rustls::RootCertStore;

#[tokio::main]
async fn main() {
    // insert your root CAs
    let root_store = RootCertStore::empty();
    let config = rustls::ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(root_store)
        .with_no_client_auth();
    let rustls_connector = hyper_rustls::HttpsConnectorBuilder::new()
        .with_tls_config(config)
        .https_only()
        .enable_http1()
        .enable_http2()
        .build();

    // Currently, aws_config connectors are buildable directly from something that implements `hyper::Connect`.
    // This enables different providers to construct clients with different timeouts.
    let provider_config = ProviderConfig::default().with_tcp_connector(rustls_connector.clone());
    let shared_conf = aws_config::from_env()
        .configure(provider_config)
        .load()
        .await;
    let s3_config = aws_sdk_s3::Config::from(&shared_conf);
    // however, for generated clients, they are constructred from a Hyper adapter directly:
    let s3_client = aws_sdk_s3::Client::from_conf_conn(
        s3_config,
        hyper_ext::Adapter::builder().build(rustls_connector),
    );
    let _ = s3_client.list_buckets().send().await;
}
Loading