AWS SDK

AWS SDK

rev. 8158c52aaf18ca396e77f487169df4a7c09c250d

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/examples/custom-dns.rs

@@ -1,1 +26,35 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
use aws_smithy_http_client::{
           7  +
    pool::{Client as PoolClient, ConnectionPool},
    7      8   
    tls::{self, rustls_provider::CryptoMode},
    8      9   
    Builder,
    9     10   
};
   10     11   
use aws_smithy_runtime_api::client::dns::{DnsFuture, ResolveDns};
   11     12   
use std::net::{IpAddr, Ipv4Addr};
   12     13   
   13     14   
#[derive(Debug, Clone)]
   14     15   
struct StaticResolver;
   15     16   
   16     17   
impl ResolveDns for StaticResolver {
   17     18   
    fn resolve_dns<'a>(&'a self, _name: &'a str) -> DnsFuture<'a> {
   18     19   
        DnsFuture::ready(Ok(vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]))
   19     20   
    }
   20     21   
}
   21     22   
   22     23   
fn main() {
   23         -
    let _client = Builder::new()
          24  +
    let _legacy_client = Builder::new()
   24     25   
        .tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
   25     26   
        .build_with_resolver(StaticResolver);
          27  +
          28  +
    let pool = ConnectionPool::builder()
          29  +
        .dns_resolver(StaticResolver)
          30  +
        .tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
          31  +
        .build_https()
          32  +
        .expect("valid connection pool");
          33  +
    let _partitioned_client =
          34  +
        PoolClient::new(&pool).expect("pool contains the anonymous partition");
   26     35   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client.rs

@@ -1,1 +53,55 @@
    6      6   
mod dns;
    7      7   
pub mod pool;
    8      8   
/// Proxy configuration
    9      9   
pub mod proxy;
   10     10   
mod timeout;
   11     11   
/// TLS connector(s)
   12     12   
pub mod tls;
   13     13   
   14     14   
pub(crate) mod connect;
   15     15   
          16  +
use self::connect::ConnectPath;
   16     17   
use crate::cfg::cfg_tls;
   17     18   
use crate::tls::TlsContext;
   18     19   
use aws_smithy_async::future::timeout::TimedOutError;
   19     20   
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
   20     21   
use aws_smithy_runtime_api::box_error::BoxError;
   21     22   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   22     23   
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
   23     24   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
          25  +
use aws_smithy_runtime_api::client::dns::ResolveDnsError;
   24     26   
use aws_smithy_runtime_api::client::http::{
   25     27   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpClient,
   26     28   
    SharedHttpConnector,
   27     29   
};
   28     30   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   29     31   
use aws_smithy_runtime_api::client::result::ConnectorError;
   30     32   
use aws_smithy_runtime_api::client::runtime_components::{
   31     33   
    RuntimeComponents, RuntimeComponentsBuilder,
   32     34   
};
   33     35   
use aws_smithy_runtime_api::shared::IntoShared;
@@ -489,491 +550,553 @@
  509    511   
    }
  510    512   
}
  511    513   
  512    514   
/// Extract a smithy connection from a hyper CaptureConnection
  513    515   
fn extract_smithy_connection(capture_conn: &CaptureConnection) -> Option<ConnectionMetadata> {
  514    516   
    let capture_conn = capture_conn.clone();
  515    517   
    if let Some(conn) = capture_conn.clone().connection_metadata().as_ref() {
  516    518   
        let mut extensions = Extensions::new();
  517    519   
        conn.get_extras(&mut extensions);
  518    520   
        let http_info = extensions.get::<HttpInfo>();
         521  +
        let connect_path = ConnectPath::from_connected(conn, &extensions);
  519    522   
        let mut builder = ConnectionMetadata::builder()
  520         -
            .proxied(conn.is_proxied())
         523  +
            .proxied(connect_path.is_proxied())
  521    524   
            .poison_fn(move || match capture_conn.connection_metadata().as_ref() {
  522    525   
                Some(conn) => conn.poison(),
  523    526   
                None => tracing::trace!("no connection existed to poison"),
  524    527   
            });
  525    528   
  526    529   
        builder
  527    530   
            .set_local_addr(http_info.map(|info| info.local_addr()))
  528    531   
            .set_remote_addr(http_info.map(|info| info.remote_addr()));
  529    532   
  530    533   
        let smithy_connection = builder.build();
@@ -610,613 +669,676 @@
  630    633   
fn downcast_error(err: BoxError) -> ConnectorError {
  631    634   
    // is a `TimedOutError` (from aws_smithy_async::timeout) in the chain? if it is, this is a timeout
  632    635   
    if find_source::<TimedOutError>(err.as_ref()).is_some() {
  633    636   
        return ConnectorError::timeout(err);
  634    637   
    }
  635    638   
    // is the top of chain error actually already a `ConnectorError`? return that directly
  636    639   
    let err = match err.downcast::<ConnectorError>() {
  637    640   
        Ok(connector_error) => return *connector_error,
  638    641   
        Err(box_error) => box_error,
  639    642   
    };
         643  +
    // DNS resolution is part of connection establishment and is retryable as I/O.
         644  +
    if find_source::<ResolveDnsError>(err.as_ref()).is_some() {
         645  +
        return ConnectorError::io(err);
         646  +
    }
  640    647   
    // generally, the top of chain will probably be a hyper error. Go through a set of hyper specific
  641    648   
    // error classifications
  642    649   
    let err = match find_source::<hyper::Error>(err.as_ref()) {
  643    650   
        Some(hyper_error) => return to_connector_error(hyper_error)(err),
  644    651   
        None => match find_source::<hyper_util::client::legacy::Error>(err.as_ref()) {
  645    652   
            Some(hyper_util_err) => {
  646    653   
                if hyper_util_err.is_connect()
  647    654   
                    || find_source::<std::io::Error>(hyper_util_err).is_some()
  648    655   
                {
  649    656   
                    return ConnectorError::io(err);

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/connect.rs

@@ -1,1 +164,249 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
use crate::client::connect;
    6      6   
use crate::proxy;
    7      7   
use aws_smithy_runtime_api::box_error::BoxError;
    8         -
use http_1x::Uri;
           8  +
use http_1x::{Extensions, HeaderMap, HeaderValue, Uri};
    9      9   
use hyper::rt::{Read, ReadBufCursor, Write};
   10     10   
use hyper_util::client::legacy::connect::{Connected, Connection};
          11  +
use hyper_util::client::proxy::matcher::Matcher;
   11     12   
use pin_project_lite::pin_project;
          13  +
use std::fmt;
   12     14   
use std::future::Future;
   13     15   
use std::io;
   14     16   
use std::io::IoSlice;
   15     17   
use std::pin::Pin;
          18  +
use std::sync::Arc;
   16     19   
use std::task::{Context, Poll};
   17     20   
   18     21   
pub(crate) trait AsyncConn:
   19     22   
    Read + Write + Connection + Send + Sync + Unpin + 'static
   20     23   
{
   21     24   
}
   22     25   
   23     26   
impl<T: Read + Write + Connection + Send + Sync + Unpin + 'static> AsyncConn for T {}
   24     27   
   25     28   
pub(crate) type BoxConn = Box<dyn AsyncConn>;
   26     29   
   27     30   
// Future for connecting
   28     31   
pub(crate) type Connecting = Pin<Box<dyn Future<Output = Result<Conn, BoxError>> + Send>>;
   29     32   
          33  +
/// Authorization selected for cleartext HTTP requests sent through a proxy.
          34  +
#[derive(Clone)]
          35  +
pub(crate) struct ProxyAuthorization(HeaderValue);
          36  +
          37  +
impl fmt::Debug for ProxyAuthorization {
          38  +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          39  +
        f.write_str("ProxyAuthorization(** redacted **)")
          40  +
    }
          41  +
}
          42  +
          43  +
impl ProxyAuthorization {
          44  +
    pub(crate) fn new(value: HeaderValue) -> Self {
          45  +
        Self(value)
          46  +
    }
          47  +
          48  +
    /// Applies the selected authorization unless the caller supplied one.
          49  +
    pub(crate) fn apply(&self, headers: &mut HeaderMap) {
          50  +
        headers
          51  +
            .entry(http_1x::header::PROXY_AUTHORIZATION)
          52  +
            .or_insert_with(|| self.0.clone());
          53  +
    }
          54  +
}
          55  +
          56  +
/// How an established transport reaches its origin.
          57  +
#[derive(Clone, Debug)]
          58  +
pub(crate) enum ConnectPath {
          59  +
    /// The transport reaches the origin without a proxy.
          60  +
    Direct,
          61  +
    /// HTTP/1 requests are sent to a forward proxy in absolute form.
          62  +
    ForwardProxy {
          63  +
        authorization: Option<ProxyAuthorization>,
          64  +
    },
          65  +
    /// The transport reaches the origin through an established CONNECT tunnel.
          66  +
    #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
          67  +
    ProxyTunnel,
          68  +
}
          69  +
          70  +
impl ConnectPath {
          71  +
    pub(crate) fn forward_proxy(authorization: Option<HeaderValue>) -> Self {
          72  +
        Self::ForwardProxy {
          73  +
            authorization: authorization.map(ProxyAuthorization::new),
          74  +
        }
          75  +
    }
          76  +
          77  +
    /// Recovers the full path when available and preserves custom connector behavior.
          78  +
    pub(crate) fn from_connected(connected: &Connected, extras: &Extensions) -> Self {
          79  +
        extras.get::<Self>().cloned().unwrap_or_else(|| {
          80  +
            if connected.is_proxied() {
          81  +
                Self::forward_proxy(None)
          82  +
            } else {
          83  +
                Self::Direct
          84  +
            }
          85  +
        })
          86  +
    }
          87  +
          88  +
    /// Returns whether a proxy participates in the established transport.
          89  +
    pub(crate) fn is_proxied(&self) -> bool {
          90  +
        !matches!(self, Self::Direct)
          91  +
    }
          92  +
          93  +
    /// Returns whether HTTP/1 requests require an absolute-form target.
          94  +
    pub(crate) fn uses_absolute_form(&self) -> bool {
          95  +
        matches!(self, Self::ForwardProxy { .. })
          96  +
    }
          97  +
          98  +
    /// Applies forwarding credentials to an HTTP/1 request.
          99  +
    pub(crate) fn apply_proxy_authorization(&self, headers: &mut HeaderMap) {
         100  +
        if let Self::ForwardProxy {
         101  +
            authorization: Some(authorization),
         102  +
        } = self
         103  +
        {
         104  +
            authorization.apply(headers);
         105  +
        }
         106  +
    }
         107  +
}
         108  +
   30    109   
pin_project! {
   31    110   
    pub(crate) struct Conn {
   32    111   
        #[pin]
   33    112   
        pub(super)inner: BoxConn,
   34         -
        pub(super) is_proxy: bool,
         113  +
        pub(super) connect_path: ConnectPath,
   35    114   
    }
   36    115   
}
   37    116   
   38    117   
impl Connection for Conn {
   39    118   
    fn connected(&self) -> Connected {
   40         -
        self.inner.connected().proxy(self.is_proxy)
         119  +
        self.inner
         120  +
            .connected()
         121  +
            .proxy(self.connect_path.uses_absolute_form())
         122  +
            .extra(self.connect_path.clone())
   41    123   
    }
   42    124   
}
   43    125   
   44    126   
impl Read for Conn {
   45    127   
    fn poll_read(
   46    128   
        self: Pin<&mut Self>,
   47    129   
        cx: &mut Context<'_>,
   48    130   
        buf: ReadBufCursor<'_>,
   49    131   
    ) -> Poll<io::Result<()>> {
   50    132   
        let this = self.project();
   51    133   
        Read::poll_read(this.inner, cx, buf)
   52    134   
    }
   53    135   
}
   54    136   
   55    137   
impl Write for Conn {
   56    138   
    fn poll_write(
   57    139   
        self: Pin<&mut Self>,
   58    140   
        cx: &mut Context<'_>,
   59    141   
        buf: &[u8],
   60    142   
    ) -> Poll<Result<usize, io::Error>> {
   61    143   
        let this = self.project();
   62    144   
        Write::poll_write(this.inner, cx, buf)
   63    145   
    }
   64    146   
   65    147   
    fn poll_write_vectored(
   66    148   
        self: Pin<&mut Self>,
   67    149   
        cx: &mut Context<'_>,
   68    150   
        bufs: &[IoSlice<'_>],
   69    151   
    ) -> Poll<Result<usize, io::Error>> {
   70    152   
        let this = self.project();
   71    153   
        Write::poll_write_vectored(this.inner, cx, bufs)
   72    154   
    }
   73    155   
   74    156   
    fn is_write_vectored(&self) -> bool {
   75    157   
        self.inner.is_write_vectored()
   76    158   
    }
   77    159   
   78    160   
    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
   79    161   
        let this = self.project();
   80    162   
        Write::poll_flush(this.inner, cx)
   81    163   
    }
   82    164   
   83    165   
    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
   84    166   
        let this = self.project();
   85    167   
        Write::poll_shutdown(this.inner, cx)
   86    168   
    }
   87    169   
}
   88    170   
   89    171   
/// HTTP-only proxy connector for handling HTTP requests through HTTP proxies
   90    172   
///
   91    173   
/// This connector handles the HTTP proxy logic when no TLS provider is selected,
   92    174   
/// including request URL modification and proxy authentication.
   93    175   
#[derive(Debug, Clone)]
   94    176   
pub(crate) struct HttpProxyConnector<C> {
   95    177   
    inner: C,
   96         -
    proxy_config: proxy::ProxyConfig,
         178  +
    proxy_matcher: Option<Arc<Matcher>>,
   97    179   
}
   98    180   
   99    181   
impl<C> HttpProxyConnector<C> {
  100    182   
    pub(crate) fn new(inner: C, proxy_config: proxy::ProxyConfig) -> Self {
         183  +
        let proxy_matcher = if proxy_config.is_disabled() {
         184  +
            None
         185  +
        } else {
         186  +
            Some(Arc::new(proxy_config.into_hyper_util_matcher()))
         187  +
        };
  101    188   
        Self {
  102    189   
            inner,
  103         -
            proxy_config,
         190  +
            proxy_matcher,
  104    191   
        }
  105    192   
    }
  106    193   
}
  107    194   
  108    195   
impl<C> tower::Service<Uri> for HttpProxyConnector<C>
  109    196   
where
  110    197   
    C: tower::Service<Uri> + Clone + Send + 'static,
  111    198   
    C::Response: hyper::rt::Read
  112    199   
        + hyper::rt::Write
  113    200   
        + hyper_util::client::legacy::connect::Connection
  114    201   
        + Send
  115    202   
        + Sync
  116    203   
        + Unpin
  117    204   
        + 'static,
  118    205   
    C::Future: Send + 'static,
  119    206   
    C::Error: Into<BoxError>,
  120    207   
{
  121    208   
    type Response = connect::Conn;
  122    209   
    type Error = BoxError;
  123    210   
    type Future = connect::Connecting;
  124    211   
  125    212   
    fn poll_ready(
  126    213   
        &mut self,
  127    214   
        cx: &mut std::task::Context<'_>,
  128    215   
    ) -> std::task::Poll<Result<(), Self::Error>> {
  129    216   
        self.inner.poll_ready(cx).map_err(Into::into)
  130    217   
    }
  131    218   
  132    219   
    fn call(&mut self, dst: Uri) -> Self::Future {
  133         -
        // Check if this request should be proxied
  134         -
        let proxy_intercept = if !self.proxy_config.is_disabled() {
  135         -
            let matcher = self.proxy_config.clone().into_hyper_util_matcher();
  136         -
            matcher.intercept(&dst)
  137         -
        } else {
  138         -
            None
  139         -
        };
         220  +
        let proxy_intercept = self
         221  +
            .proxy_matcher
         222  +
            .as_ref()
         223  +
            .and_then(|matcher| matcher.intercept(&dst));
  140    224   
  141    225   
        if let Some(intercept) = proxy_intercept {
  142    226   
            // HTTP through proxy: Connect to proxy server
  143    227   
            let proxy_uri = intercept.uri().clone();
         228  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  144    229   
            let fut = self.inner.call(proxy_uri);
  145    230   
            Box::pin(async move {
  146    231   
                let conn = fut.await.map_err(Into::into)?;
  147    232   
                Ok(connect::Conn {
  148    233   
                    inner: Box::new(conn),
  149         -
                    is_proxy: true,
         234  +
                    connect_path,
  150    235   
                })
  151    236   
            })
  152    237   
        } else {
  153    238   
            // Direct connection
  154    239   
            let fut = self.inner.call(dst);
  155    240   
            Box::pin(async move {
  156    241   
                let conn = fut.await.map_err(Into::into)?;
  157    242   
                Ok(connect::Conn {
  158    243   
                    inner: Box::new(conn),
  159         -
                    is_proxy: false,
         244  +
                    connect_path: ConnectPath::Direct,
  160    245   
                })
  161    246   
            })
  162    247   
        }
  163    248   
    }
  164    249   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/builder.rs

@@ -1,1 +60,67 @@
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Construction and validation for [`ConnectionPool`].
    7      7   
    8      8   
use super::establish::{self, TransportFactory};
    9      9   
use super::maintenance::MaintenanceConfig;
   10     10   
use super::registry::{AdmissionPolicy, PartitionRegistry, PartitionRegistryError};
   11     11   
use super::{ConnectionPool, ConnectionReuseScope, Partition, PoolConfig, PoolInner};
          12  +
use crate::client::connect::HttpProxyConnector;
          13  +
use crate::client::dns::HyperUtilResolver;
   12     14   
use crate::client::{TlsProviderSelected, TlsUnset};
          15  +
use crate::proxy::ProxyConfig;
   13     16   
use crate::sync::Arc;
   14     17   
use crate::tls::{self, TlsContext};
   15     18   
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
   16     19   
use aws_smithy_async::time::{SharedTimeSource, TimeSource};
   17     20   
#[cfg(any(
   18     21   
    all(test, feature = "rt-tokio"),
   19     22   
    all(feature = "test-util", aws_sdk_unstable)
   20     23   
))]
   21     24   
use aws_smithy_runtime_api::box_error::BoxError;
          25  +
use aws_smithy_runtime_api::client::dns::{ResolveDns, SharedDnsResolver};
          26  +
use aws_smithy_runtime_api::shared::IntoShared;
   22     27   
#[cfg(any(
   23     28   
    all(test, feature = "rt-tokio"),
   24     29   
    all(feature = "test-util", aws_sdk_unstable)
   25     30   
))]
   26     31   
use http_1x::Uri;
   27     32   
use hyper_util::client::legacy::connect::dns::GaiResolver;
          33  +
use hyper_util::client::legacy::connect::dns::Name;
   28     34   
use hyper_util::client::legacy::connect::HttpConnector;
   29     35   
use std::error::Error;
   30     36   
use std::fmt;
          37  +
use std::net::SocketAddr;
   31     38   
use std::num::NonZeroUsize;
   32     39   
use std::sync::atomic::AtomicU64;
   33     40   
use std::time::Duration;
   34     41   
#[cfg(any(
   35     42   
    all(test, feature = "rt-tokio"),
   36     43   
    all(feature = "test-util", aws_sdk_unstable)
   37     44   
))]
   38     45   
use tower::Service;
   39     46   
   40     47   
/// Default duration for retaining an idle reusable connection.
@@ -63,70 +153,184 @@
   83     90   
    /// Whether newly created TCP sockets disable Nagle's algorithm.
   84     91   
    tcp_nodelay: bool,
   85     92   
    /// TCP keepalive setting, including explicit disablement.
   86     93   
    tcp_keepalive: TriStateOption<Duration>,
   87     94   
    /// Optional live-connection limit per scheme, host, and port.
   88     95   
    max_connections_per_host: Option<usize>,
   89     96   
    /// Partitions allowed to reuse each other's connections.
   90     97   
    reuse_scope: ConnectionReuseScope,
   91     98   
    /// Complete explicit partition set, or `None` for anonymous topology.
   92     99   
    partitions: Option<Vec<Partition>>,
         100  +
    /// Pool-wide proxy selection, authentication, and bypass policy.
         101  +
    proxy_config: ProxyConfig,
         102  +
    /// Caller-supplied DNS resolver, or `None` for the client default.
         103  +
    dns_resolver: Option<SharedDnsResolver>,
   93    104   
    /// TLS typestate carried into protocol-specific terminal builders.
   94    105   
    tls: Tls,
   95    106   
}
   96    107   
   97    108   
impl Default for Builder<TlsUnset> {
   98    109   
    fn default() -> Self {
   99    110   
        Self {
  100    111   
            idle_timeout: TriStateOption::NotSet,
  101    112   
            time_source: None,
  102    113   
            sleep_impl: None,
  103    114   
            tcp_nodelay: true,
  104    115   
            tcp_keepalive: TriStateOption::NotSet,
  105    116   
            max_connections_per_host: None,
  106    117   
            reuse_scope: ConnectionReuseScope::default(),
  107    118   
            partitions: None,
         119  +
            proxy_config: ProxyConfig::disabled(),
         120  +
            dns_resolver: None,
  108    121   
            tls: TlsUnset {},
  109    122   
        }
  110    123   
    }
  111    124   
}
  112    125   
  113    126   
impl<Tls: fmt::Debug> fmt::Debug for Builder<Tls> {
  114    127   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  115    128   
        f.debug_struct("Builder")
  116    129   
            .field("idle_timeout", &self.idle_timeout)
  117    130   
            .field("time_source", &self.time_source)
  118    131   
            .field("sleep_impl", &self.sleep_impl)
  119    132   
            .field("tcp_nodelay", &self.tcp_nodelay)
  120    133   
            .field("tcp_keepalive", &self.tcp_keepalive)
  121    134   
            .field("max_connections_per_host", &self.max_connections_per_host)
  122    135   
            .field("reuse_scope", &self.reuse_scope)
  123    136   
            .field("partitions", &self.partitions.as_ref().map(Vec::len))
         137  +
            .field(
         138  +
                "proxy_config",
         139  +
                &if self.proxy_config.is_disabled() {
         140  +
                    "disabled"
         141  +
                } else if self.proxy_config.is_from_env() {
         142  +
                    "environment"
         143  +
                } else {
         144  +
                    "configured"
         145  +
                },
         146  +
            )
         147  +
            .field(
         148  +
                "dns_resolver",
         149  +
                &if self.dns_resolver.is_some() {
         150  +
                    "custom"
         151  +
                } else {
         152  +
                    "default"
         153  +
                },
         154  +
            )
  124    155   
            .field("tls", &self.tls)
  125    156   
            .finish()
  126    157   
    }
  127    158   
}
  128    159   
  129    160   
impl<Tls> Builder<Tls> {
  130    161   
    /// Sets how long a reusable idle connection remains in the pool.
  131    162   
    ///
  132    163   
    /// Passing `None` disables idle expiration. When unset, the pool uses a
  133    164   
    /// 90-second timeout.
@@ -212,243 +425,542 @@
  232    263   
    pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
  233    264   
        self.partitions = Some(partitions.into_iter().collect());
  234    265   
        self
  235    266   
    }
  236    267   
  237    268   
    /// Mutably declares or clears the explicit partition set.
  238    269   
    pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
  239    270   
        self.partitions = partitions;
  240    271   
        self
  241    272   
    }
         273  +
         274  +
    /// Configures proxy selection for every connection established by this pool.
         275  +
    ///
         276  +
    /// The configuration applies across every partition. Proxying is disabled
         277  +
    /// unless this method is called.
         278  +
    pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
         279  +
        self.proxy_config = config;
         280  +
        self
         281  +
    }
         282  +
         283  +
    /// Mutably configures pool-wide proxy selection.
         284  +
    ///
         285  +
    /// Passing `None` disables proxying.
         286  +
    pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
         287  +
        self.proxy_config = config.unwrap_or_else(ProxyConfig::disabled);
         288  +
        self
         289  +
    }
         290  +
         291  +
    /// Configures the DNS resolver used by connections established by this pool.
         292  +
    ///
         293  +
    /// The resolver applies across every partition. When unset, the client uses
         294  +
    /// its default resolver, which currently delegates to the operating system.
         295  +
    pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
         296  +
        self.dns_resolver = Some(resolver.into_shared());
         297  +
        self
         298  +
    }
         299  +
         300  +
    /// Mutably configures the pool-wide DNS resolver.
         301  +
    ///
         302  +
    /// Passing `None` restores the client default.
         303  +
    pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
         304  +
        self.dns_resolver = resolver;
         305  +
        self
         306  +
    }
  242    307   
}
  243    308   
  244    309   
impl Builder<TlsUnset> {
  245    310   
    /// Selects the TLS provider used by HTTPS pool connections.
  246    311   
    pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
  247    312   
        Builder {
  248    313   
            idle_timeout: self.idle_timeout,
  249    314   
            time_source: self.time_source,
  250    315   
            sleep_impl: self.sleep_impl,
  251    316   
            tcp_nodelay: self.tcp_nodelay,
  252    317   
            tcp_keepalive: self.tcp_keepalive,
  253    318   
            max_connections_per_host: self.max_connections_per_host,
  254    319   
            reuse_scope: self.reuse_scope,
  255    320   
            partitions: self.partitions,
         321  +
            proxy_config: self.proxy_config,
         322  +
            dns_resolver: self.dns_resolver,
  256    323   
            tls: TlsProviderSelected {
  257    324   
                provider,
  258    325   
                context: TlsContext::default(),
  259    326   
            },
  260    327   
        }
  261    328   
    }
  262    329   
}
  263    330   
  264    331   
crate::cfg::cfg_tls! {
  265    332   
    impl Builder<TlsProviderSelected> {
  266    333   
        /// Sets provider-specific TLS configuration for HTTPS connections.
  267    334   
        pub fn tls_context(mut self, context: TlsContext) -> Self {
  268    335   
            self.tls.context = context;
  269    336   
            self
  270    337   
        }
  271    338   
  272    339   
        /// Mutably sets provider-specific TLS configuration.
  273    340   
        pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
  274    341   
            self.tls.context = context;
  275    342   
            self
  276    343   
        }
  277    344   
  278    345   
        /// Builds a pool whose connector performs TLS and ALPN negotiation.
  279         -
        ///
  280         -
        /// This builder currently creates direct connections. Proxy routing
  281         -
        /// remains available through the existing `Connector` API.
  282    346   
        pub fn build_https(self) -> Result<ConnectionPool, BuildError> {
         347  +
            let resolver = self.dns_resolver.clone();
         348  +
            match resolver {
         349  +
                Some(resolver) => {
         350  +
                    self.build_https_with_resolver(HyperUtilResolver { resolver })
         351  +
                }
         352  +
                None => self.build_https_with_resolver(GaiResolver::new()),
         353  +
            }
         354  +
        }
         355  +
         356  +
        /// Builds an HTTPS transport around one concrete Hyper resolver.
         357  +
        fn build_https_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
         358  +
        where
         359  +
            R: tower::Service<Name> + Clone + Send + Sync + 'static,
         360  +
            R::Response: Iterator<Item = SocketAddr>,
         361  +
            R::Future: Send,
         362  +
            R::Error: Into<Box<dyn Error + Send + Sync>>,
         363  +
        {
  283    364   
            validate_default_connector_interfaces(self.partitions.as_deref())?;
  284    365   
            let provider = self.tls.provider.clone();
  285    366   
            let context = self.tls.context.clone();
         367  +
            let proxy_config = self.proxy_config.clone();
  286    368   
            match provider {
  287    369   
                #[cfg(feature = "__rustls")]
  288    370   
                tls::Provider::Rustls(crypto_mode) => {
  289    371   
                    let tcp_nodelay = self.tcp_nodelay;
  290    372   
                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
  291    373   
                    let transport = establish::from_cached_interface_connector(
  292    374   
                        move |interface, alpn_protocols| {
  293    375   
                            let mut connector =
  294         -
                                HttpConnector::new_with_resolver(GaiResolver::new());
         376  +
                                HttpConnector::new_with_resolver(resolver.clone());
  295    377   
                            connector.set_nodelay(tcp_nodelay);
  296    378   
                            connector.set_keepalive(tcp_keepalive);
  297    379   
                            set_default_connector_interface(&mut connector, interface);
  298    380   
                            tls::rustls_provider::build_connector::wrap_connector_with_alpn(
  299    381   
                                connector,
  300    382   
                                crypto_mode.clone(),
  301    383   
                                &context,
  302         -
                                crate::proxy::ProxyConfig::disabled(),
         384  +
                                proxy_config.clone(),
  303    385   
                                alpn_protocols,
  304    386   
                            )
  305    387   
                        },
  306    388   
                        true,
  307    389   
                    );
  308    390   
                    self.build_with_transport(transport)
  309    391   
                }
  310    392   
                #[cfg(feature = "s2n-tls")]
  311    393   
                tls::Provider::S2nTls => {
  312    394   
                    let tcp_nodelay = self.tcp_nodelay;
  313    395   
                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
  314    396   
                    let transport = establish::from_cached_interface_connector(
  315    397   
                        move |interface, _alpn_protocols| {
  316    398   
                            let mut connector =
  317         -
                                HttpConnector::new_with_resolver(GaiResolver::new());
         399  +
                                HttpConnector::new_with_resolver(resolver.clone());
  318    400   
                            connector.set_nodelay(tcp_nodelay);
  319    401   
                            connector.set_keepalive(tcp_keepalive);
  320    402   
                            set_default_connector_interface(&mut connector, interface);
  321    403   
                            tls::s2n_tls_provider::build_connector::wrap_connector(
  322    404   
                                connector,
  323    405   
                                &context,
  324         -
                                crate::proxy::ProxyConfig::disabled(),
         406  +
                                proxy_config.clone(),
  325    407   
                            )
  326    408   
                        },
  327    409   
                        false,
  328    410   
                    );
  329    411   
                    self.build_with_transport(transport)
  330    412   
                }
  331    413   
            }
  332    414   
        }
  333    415   
    }
  334    416   
}
  335    417   
  336    418   
impl Builder<TlsUnset> {
  337    419   
    /// Builds a pool for cleartext HTTP connections.
  338         -
    #[doc(hidden)]
  339    420   
    pub fn build_http(self) -> Result<ConnectionPool, BuildError> {
         421  +
        let resolver = self.dns_resolver.clone();
         422  +
        match resolver {
         423  +
            Some(resolver) => self.build_http_with_resolver(HyperUtilResolver { resolver }),
         424  +
            None => self.build_http_with_resolver(GaiResolver::new()),
         425  +
        }
         426  +
    }
         427  +
         428  +
    /// Builds a cleartext transport around one concrete Hyper resolver.
         429  +
    fn build_http_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
         430  +
    where
         431  +
        R: tower::Service<Name> + Clone + Send + Sync + 'static,
         432  +
        R::Response: Iterator<Item = SocketAddr>,
         433  +
        R::Future: Send,
         434  +
        R::Error: Into<Box<dyn Error + Send + Sync>>,
         435  +
    {
  340    436   
        validate_default_connector_interfaces(self.partitions.as_deref())?;
  341         -
        let mut connector = HttpConnector::new_with_resolver(GaiResolver::new());
  342         -
        connector.set_nodelay(self.tcp_nodelay);
  343         -
        connector.set_keepalive(self.tcp_keepalive.clone().resolve(None));
  344         -
        let transport = establish::from_interface_connector(move |interface| {
  345         -
            let mut connector = connector.clone();
  346         -
            set_default_connector_interface(&mut connector, interface);
  347         -
            connector
  348         -
        });
         437  +
        let tcp_nodelay = self.tcp_nodelay;
         438  +
        let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
         439  +
        let proxy_config = self.proxy_config.clone();
         440  +
        let transport = if proxy_config.is_disabled() {
         441  +
            establish::from_interface_connector(move |interface| {
         442  +
                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
         443  +
                connector.set_nodelay(tcp_nodelay);
         444  +
                connector.set_keepalive(tcp_keepalive);
         445  +
                set_default_connector_interface(&mut connector, interface);
         446  +
                connector
         447  +
            })
         448  +
        } else {
         449  +
            establish::from_interface_connector(move |interface| {
         450  +
                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
         451  +
                connector.set_nodelay(tcp_nodelay);
         452  +
                connector.set_keepalive(tcp_keepalive);
         453  +
                set_default_connector_interface(&mut connector, interface);
         454  +
                HttpProxyConnector::new(connector, proxy_config.clone())
         455  +
            })
         456  +
        };
  349    457   
        self.build_with_transport(transport)
  350    458   
    }
  351    459   
  352    460   
    /// Builds a pool around a test transport connector.
  353    461   
    #[cfg(all(feature = "test-util", aws_sdk_unstable))]
  354    462   
    #[doc(hidden)]
  355    463   
    pub fn build_http_with_tcp_connector<C, IO>(
  356    464   
        self,
  357    465   
        connector: C,
  358    466   
    ) -> Result<ConnectionPool, BuildError>
  359    467   
    where
  360    468   
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  361    469   
        C::Error: Into<BoxError>,
  362    470   
        C::Future: Send + 'static,
  363    471   
        IO: hyper::rt::Read
  364    472   
            + hyper::rt::Write
  365    473   
            + hyper_util::client::legacy::connect::Connection
  366    474   
            + Send
  367    475   
            + Sync
  368    476   
            + Unpin
  369    477   
            + 'static,
  370    478   
    {
  371    479   
        self.build_with_transport(establish::from_connector(connector))
  372    480   
    }
  373    481   
  374    482   
    #[cfg(all(test, feature = "rt-tokio"))]
  375    483   
    pub(super) fn build_with_connector<C, IO>(
  376    484   
        self,
  377    485   
        connector: C,
  378    486   
    ) -> Result<ConnectionPool, BuildError>
  379    487   
    where
  380    488   
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  381    489   
        C::Error: Into<BoxError>,
  382    490   
        C::Future: Send + 'static,
  383    491   
        IO: hyper::rt::Read
  384    492   
            + hyper::rt::Write
  385    493   
            + hyper_util::client::legacy::connect::Connection
  386    494   
            + Send
  387    495   
            + Sync
  388    496   
            + Unpin
  389    497   
            + 'static,
  390    498   
    {
  391    499   
        self.build_with_transport(establish::from_connector(connector))
  392    500   
    }
  393    501   
}
  394    502   
  395    503   
impl<Tls> Builder<Tls> {
         504  +
    /// Builds a pool with a transport supplied by a pool-internal test.
         505  +
    #[cfg(test)]
         506  +
    pub(super) fn build_with_transport_for_test(
         507  +
        self,
         508  +
        transport: std::sync::Arc<dyn TransportFactory>,
         509  +
    ) -> Result<ConnectionPool, BuildError> {
         510  +
        self.build_with_transport(transport)
         511  +
    }
         512  +
  396    513   
    /// Validates pool policy and installs the type-erased transport factory.
  397    514   
    fn build_with_transport(
  398    515   
        self,
  399    516   
        transport: std::sync::Arc<dyn TransportFactory>,
  400    517   
    ) -> Result<ConnectionPool, BuildError> {
  401    518   
        let max_connections_per_host = match self.max_connections_per_host {
  402    519   
            Some(0) => {
  403    520   
                return Err(BuildError::new(
  404    521   
                    "max_connections_per_host must be greater than zero",
  405    522   
                ))
@@ -509,626 +666,839 @@
  529    646   
    target_os = "fuchsia",
  530    647   
    target_os = "illumos",
  531    648   
    target_os = "ios",
  532    649   
    target_os = "linux",
  533    650   
    target_os = "macos",
  534    651   
    target_os = "solaris",
  535    652   
    target_os = "tvos",
  536    653   
    target_os = "visionos",
  537    654   
    target_os = "watchos",
  538    655   
))]
  539         -
fn set_default_connector_interface(
  540         -
    connector: &mut HttpConnector<GaiResolver>,
  541         -
    interface: Option<&str>,
  542         -
) {
         656  +
fn set_default_connector_interface<R>(connector: &mut HttpConnector<R>, interface: Option<&str>) {
  543    657   
    if let Some(interface) = interface {
  544    658   
        connector.set_interface(interface);
  545    659   
    }
  546    660   
}
  547    661   
  548    662   
/// Leaves the default connector unchanged on unsupported targets.
  549    663   
#[cfg(not(any(
  550    664   
    target_os = "android",
  551    665   
    target_os = "fuchsia",
  552    666   
    target_os = "illumos",
  553    667   
    target_os = "ios",
  554    668   
    target_os = "linux",
  555    669   
    target_os = "macos",
  556    670   
    target_os = "solaris",
  557    671   
    target_os = "tvos",
  558    672   
    target_os = "visionos",
  559    673   
    target_os = "watchos",
  560    674   
)))]
  561         -
fn set_default_connector_interface(
  562         -
    _connector: &mut HttpConnector<GaiResolver>,
  563         -
    _interface: Option<&str>,
  564         -
) {
         675  +
fn set_default_connector_interface<R>(_connector: &mut HttpConnector<R>, _interface: Option<&str>) {
  565    676   
}
  566    677   
  567    678   
#[cfg(all(test, not(smithy_http_client_loom)))]
  568    679   
mod tests {
  569    680   
    use super::*;
  570    681   
    use crate::client::pool::{DriverSpawner, PartitionId};
  571    682   
    use std::future::Future;
         683  +
    use std::net::IpAddr;
  572    684   
    use std::pin::Pin;
  573    685   
  574    686   
    #[derive(Debug)]
  575    687   
    struct PendingSleep;
  576    688   
  577    689   
    impl AsyncSleep for PendingSleep {
  578    690   
        fn sleep(&self, _duration: Duration) -> aws_smithy_async::rt::sleep::Sleep {
  579    691   
            aws_smithy_async::rt::sleep::Sleep::new(std::future::pending())
  580    692   
        }
  581    693   
    }
  582    694   
  583    695   
    #[derive(Debug)]
  584    696   
    struct TestSpawner;
  585    697   
  586    698   
    impl DriverSpawner for TestSpawner {
  587    699   
        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
  588    700   
            drop(driver);
  589    701   
        }
  590    702   
    }
  591    703   
         704  +
    #[derive(Debug)]
         705  +
    struct EmptyDnsResolver;
         706  +
         707  +
    impl ResolveDns for EmptyDnsResolver {
         708  +
        fn resolve_dns<'a>(
         709  +
            &'a self,
         710  +
            _name: &'a str,
         711  +
        ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
         712  +
            aws_smithy_runtime_api::client::dns::DnsFuture::ready(Ok(Vec::<IpAddr>::new()))
         713  +
        }
         714  +
    }
         715  +
  592    716   
    fn partition(index: usize) -> Partition {
  593    717   
        Partition::new(PartitionId::from_index(index), TestSpawner)
  594    718   
    }
  595    719   
  596    720   
    #[test]
  597    721   
    fn builder_preserves_unset_disabled_and_configured_settings() {
  598    722   
        let mut builder = Builder::default();
  599    723   
        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
  600    724   
        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
  601    725   
        assert!(builder.tcp_nodelay);
  602    726   
        assert_eq!(None, builder.max_connections_per_host);
  603    727   
        assert!(builder.partitions.is_none());
         728  +
        assert!(builder.proxy_config.is_disabled());
         729  +
        assert!(builder.dns_resolver.is_none());
  604    730   
  605    731   
        builder.set_idle_timeout(Some(None));
  606    732   
        builder.set_tcp_keepalive(Some(None));
  607    733   
        assert_eq!(TriStateOption::ExplicitlyUnset, builder.idle_timeout);
  608    734   
        assert_eq!(TriStateOption::ExplicitlyUnset, builder.tcp_keepalive);
  609    735   
  610    736   
        builder.set_idle_timeout(Some(Some(Duration::from_secs(5))));
  611    737   
        builder.set_tcp_keepalive(Some(Some(Duration::from_secs(10))));
  612    738   
        builder.set_tcp_nodelay(false);
  613    739   
        builder.set_max_connections_per_host(Some(3));
  614    740   
        builder.set_partitions(Some(vec![partition(7)]));
  615    741   
        assert_eq!(
  616    742   
            TriStateOption::Set(Duration::from_secs(5)),
  617    743   
            builder.idle_timeout
  618    744   
        );
  619    745   
        assert_eq!(
  620    746   
            TriStateOption::Set(Duration::from_secs(10)),
  621    747   
            builder.tcp_keepalive
  622    748   
        );
  623    749   
        assert!(!builder.tcp_nodelay);
  624    750   
        assert_eq!(Some(3), builder.max_connections_per_host);
  625    751   
        assert_eq!(1, builder.partitions.as_ref().unwrap().len());
  626    752   
  627    753   
        builder.set_idle_timeout(None);
  628    754   
        builder.set_tcp_keepalive(None);
  629    755   
        builder.set_max_connections_per_host(None);
  630    756   
        builder.set_partitions(None);
  631    757   
        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
  632    758   
        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
  633    759   
        assert_eq!(None, builder.max_connections_per_host);
  634    760   
        assert!(builder.partitions.is_none());
  635    761   
    }
  636    762   
         763  +
    #[test]
         764  +
    fn proxy_and_dns_settings_set_reset_and_redact() {
         765  +
        let mut builder = Builder::default();
         766  +
        builder.set_proxy_config(Some(
         767  +
            ProxyConfig::http("http://proxy.test:8080")
         768  +
                .unwrap()
         769  +
                .with_basic_auth("secret-user", "secret-password"),
         770  +
        ));
         771  +
        builder.set_dns_resolver(Some(SharedDnsResolver::new(EmptyDnsResolver)));
         772  +
         773  +
        assert!(!builder.proxy_config.is_disabled());
         774  +
        assert!(builder.dns_resolver.is_some());
         775  +
        let debug = format!("{builder:?}");
         776  +
        assert!(debug.contains("configured"));
         777  +
        assert!(debug.contains("custom"));
         778  +
        assert!(!debug.contains("secret-user"));
         779  +
        assert!(!debug.contains("secret-password"));
         780  +
         781  +
        builder.set_proxy_config(None);
         782  +
        builder.set_dns_resolver(None);
         783  +
        assert!(builder.proxy_config.is_disabled());
         784  +
        assert!(builder.dns_resolver.is_none());
         785  +
    }
         786  +
         787  +
    #[cfg(feature = "rustls-ring")]
         788  +
    #[test]
         789  +
    fn tls_selection_preserves_proxy_and_dns_settings() {
         790  +
        let builder = Builder::default()
         791  +
            .proxy_config(
         792  +
                ProxyConfig::http("http://proxy.test:8080")
         793  +
                    .unwrap()
         794  +
                    .with_basic_auth("secret-user", "secret-password"),
         795  +
            )
         796  +
            .dns_resolver(EmptyDnsResolver)
         797  +
            .tls_provider(tls::Provider::rustls(
         798  +
                tls::rustls_provider::CryptoMode::Ring,
         799  +
            ));
         800  +
         801  +
        assert!(!builder.proxy_config.is_disabled());
         802  +
        assert!(builder.dns_resolver.is_some());
         803  +
        let debug = format!("{builder:?}");
         804  +
        assert!(debug.contains("configured"));
         805  +
        assert!(debug.contains("custom"));
         806  +
        assert!(!debug.contains("secret-user"));
         807  +
        assert!(!debug.contains("secret-password"));
         808  +
    }
         809  +
  637    810   
    #[test]
  638    811   
    fn terminal_build_resolves_the_default_idle_timeout() {
  639    812   
        let pool = Builder::default()
  640    813   
            .sleep_impl(PendingSleep)
  641    814   
            .build_http()
  642    815   
            .unwrap();
  643    816   
  644    817   
        assert_eq!(
  645    818   
            Some(Duration::from_secs(90)),
  646    819   
            pool.inner.config.idle_timeout

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/client.rs

@@ -1,1 +51,55 @@
    5      5   
    6      6   
//! Partition-bound handles for Smithy HTTP operations.
    7      7   
    8      8   
use super::partition::PartitionId;
    9      9   
use super::registry::PartitionState;
   10     10   
use super::ConnectionPool;
   11     11   
use crate::client::downcast_error;
   12     12   
use crate::client::timeout::{self, TimeoutKind};
   13     13   
use crate::sync::Arc;
   14     14   
use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep};
          15  +
use aws_smithy_runtime_api::box_error::BoxError;
   15     16   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
   16     17   
use aws_smithy_runtime_api::client::http::{
   17     18   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
   18     19   
};
   19     20   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   20     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   21         -
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
          22  +
use aws_smithy_runtime_api::client::runtime_components::{
          23  +
    RuntimeComponents, RuntimeComponentsBuilder,
          24  +
};
          25  +
use aws_smithy_types::config_bag::ConfigBag;
   22     26   
use std::borrow::Cow;
   23     27   
use std::error::Error;
   24     28   
use std::fmt;
   25     29   
use std::time::Duration;
   26     30   
   27     31   
/// Smithy HTTP client bound to one connection-pool partition.
   28     32   
///
   29     33   
/// Construction resolves the partition once. Every request uses that partition
   30     34   
/// for establishment placement and local reuse, while the configured reuse
   31     35   
/// scope may permit dispatch through a peer partition's connection. Cloning a
@@ -56,60 +115,130 @@
   76     80   
impl fmt::Debug for Client {
   77     81   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
   78     82   
        f.debug_struct("Client")
   79     83   
            .field("partition", &self.partition.id())
   80     84   
            .field("pool", &self.pool)
   81     85   
            .finish_non_exhaustive()
   82     86   
    }
   83     87   
}
   84     88   
   85     89   
impl HttpClient for Client {
          90  +
    fn validate_base_client_config(
          91  +
        &self,
          92  +
        _: &RuntimeComponentsBuilder,
          93  +
        _: &ConfigBag,
          94  +
    ) -> Result<(), BoxError> {
          95  +
        self.pool
          96  +
            .inner
          97  +
            .transport
          98  +
            .initialize_for_partition(&self.partition)
          99  +
    }
         100  +
   86    101   
    fn http_connector(
   87    102   
        &self,
   88    103   
        settings: &HttpConnectorSettings,
   89    104   
        components: &RuntimeComponents,
   90    105   
    ) -> SharedHttpConnector {
   91    106   
        let connect_timeout = settings.connect_timeout();
   92    107   
        let read_timeout = settings.read_timeout();
   93    108   
        let sleep = components.sleep_impl().or_else(default_async_sleep);
   94    109   
   95    110   
        SharedHttpConnector::new(PoolConnector {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/connection.rs

@@ -1,1 +47,48 @@
    8      8   
//! [`ConnectionState`] serializes dispatch commitment with logical close.
    9      9   
//! [`DispatchGuard`] accounts for accepted requests.
   10     10   
//! [`PhysicalConnectionGuard`] follows root I/O until the client releases its
   11     11   
//! physical connection handle, including transfer through a protocol upgrade.
   12     12   
//! The operating system may continue TCP teardown afterward. Logical close
   13     13   
//! returns bounded capacity without waiting for either lifetime to finish.
   14     14   
   15     15   
use super::admission::CapacityLease;
   16     16   
use super::origin::OriginKey;
   17     17   
use super::partition::PartitionId;
          18  +
use crate::client::connect::ConnectPath;
   18     19   
use crate::sync::{Arc, Mutex};
   19     20   
pub use aws_smithy_runtime_api::client::connection::ConnectionId;
   20     21   
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
   21     22   
use http_1x::Extensions;
   22     23   
use hyper::rt::{Read, ReadBufCursor, Write};
   23     24   
use hyper_util::client::legacy::connect::{Connected, Connection, HttpInfo};
   24     25   
use pin_project_lite::pin_project;
   25     26   
use std::fmt;
   26     27   
use std::io::{self, IoSlice};
   27     28   
use std::net::SocketAddr;
@@ -51,52 +200,202 @@
   71     72   
    /// Canonical origin this connection may serve.
   72     73   
    origin: OriginKey,
   73     74   
    /// Partition that retains the transport and protocol driver.
   74     75   
    owner_partition: PartitionId,
   75     76   
    /// Protocol selected by configuration or ALPN.
   76     77   
    protocol: NegotiatedProtocol,
   77     78   
    /// Local socket address reported by the connector, when available.
   78     79   
    local_addr: Option<SocketAddr>,
   79     80   
    /// Remote socket address reported by the connector, when available.
   80     81   
    remote_addr: Option<SocketAddr>,
   81         -
    /// Whether the transport reaches the origin through a proxy.
   82         -
    proxied: bool,
          82  +
    /// How the established transport reaches its origin.
          83  +
    connect_path: ConnectPath,
   83     84   
    /// Connector metadata copied into every response on this connection.
   84     85   
    connected: Connected,
   85     86   
}
   86     87   
   87     88   
impl ConnectionInfo {
   88     89   
    /// Captures immutable facts after transport negotiation selects a protocol.
   89     90   
    pub(super) fn new(
   90     91   
        id: ConnectionId,
   91     92   
        origin: OriginKey,
   92     93   
        owner_partition: PartitionId,
   93     94   
        protocol: NegotiatedProtocol,
   94     95   
        connected: Connected,
   95     96   
    ) -> Arc<Self> {
   96     97   
        let mut extras = Extensions::new();
   97     98   
        connected.get_extras(&mut extras);
   98     99   
        let http_info = extras.get::<HttpInfo>();
         100  +
        let connect_path = ConnectPath::from_connected(&connected, &extras);
   99    101   
        Arc::new(Self {
  100    102   
            id,
  101    103   
            origin,
  102    104   
            owner_partition,
  103    105   
            protocol,
  104    106   
            local_addr: http_info.map(HttpInfo::local_addr),
  105    107   
            remote_addr: http_info.map(HttpInfo::remote_addr),
  106         -
            proxied: connected.is_proxied(),
         108  +
            connect_path,
  107    109   
            connected,
  108    110   
        })
  109    111   
    }
  110    112   
  111    113   
    /// Returns the pool-assigned physical connection identity.
  112    114   
    pub(super) fn id(&self) -> ConnectionId {
  113    115   
        self.id
  114    116   
    }
  115    117   
  116    118   
    /// Returns the canonical origin this connection may serve.
  117    119   
    pub(super) fn origin(&self) -> &OriginKey {
  118    120   
        &self.origin
  119    121   
    }
  120    122   
  121    123   
    /// Returns the partition that owns this connection's I/O and driver.
  122    124   
    pub(super) fn owner_partition(&self) -> PartitionId {
  123    125   
        self.owner_partition
  124    126   
    }
  125    127   
  126    128   
    /// Returns the established HTTP protocol.
  127    129   
    pub(super) fn protocol(&self) -> NegotiatedProtocol {
  128    130   
        self.protocol
  129    131   
    }
  130    132   
  131    133   
    /// Returns the connector-reported local socket address.
  132    134   
    #[cfg(test)]
  133    135   
    pub(super) fn local_addr(&self) -> Option<SocketAddr> {
  134    136   
        self.local_addr
  135    137   
    }
  136    138   
  137    139   
    /// Returns the connector-reported remote socket address.
  138    140   
    #[cfg(test)]
  139    141   
    pub(super) fn remote_addr(&self) -> Option<SocketAddr> {
  140    142   
        self.remote_addr
  141    143   
    }
  142    144   
  143         -
    /// Returns whether the origin is reached through a proxy.
  144         -
    pub(super) fn is_proxied(&self) -> bool {
  145         -
        self.proxied
         145  +
    /// Returns how this connection reaches its origin.
         146  +
    pub(super) fn connect_path(&self) -> &ConnectPath {
         147  +
        &self.connect_path
  146    148   
    }
  147    149   
  148    150   
    /// Copies connector-provided values into a response extension map.
  149    151   
    pub(super) fn apply_connector_extras(&self, extensions: &mut Extensions) {
  150    152   
        self.connected.get_extras(extensions);
  151    153   
    }
  152    154   
  153    155   
    /// Builds Smithy metadata with close authority for this H1 record.
  154    156   
    pub(super) fn metadata(&self, close: super::cell::h1::H1CloseHandle) -> ConnectionMetadata {
  155    157   
        let mut builder = ConnectionMetadata::builder()
  156         -
            .proxied(self.proxied)
         158  +
            .proxied(self.connect_path.is_proxied())
  157    159   
            .connection_id(self.id)
  158    160   
            .poison_fn(move || {
  159    161   
                close.close(CloseReason::Poisoned);
  160    162   
            });
  161    163   
        builder
  162    164   
            .set_local_addr(self.local_addr)
  163    165   
            .set_remote_addr(self.remote_addr);
  164    166   
        builder.build()
  165    167   
    }
  166    168   
  167    169   
    /// Builds Smithy metadata with close authority for this H2 generation.
  168    170   
    pub(super) fn h2_metadata(&self, close: super::cell::h2::H2CloseHandle) -> ConnectionMetadata {
  169    171   
        let mut builder = ConnectionMetadata::builder()
  170         -
            .proxied(self.proxied)
         172  +
            .proxied(self.connect_path.is_proxied())
  171    173   
            .connection_id(self.id)
  172    174   
            .poison_fn(move || {
  173    175   
                close.close(CloseReason::Poisoned);
  174    176   
            });
  175    177   
        builder
  176    178   
            .set_local_addr(self.local_addr)
  177    179   
            .set_remote_addr(self.remote_addr);
  178    180   
        builder.build()
  179    181   
    }
  180    182   
@@ -724,726 +784,786 @@
  744    746   
                .extra(ConnectorMarker("connector-extra")),
  745    747   
        );
  746    748   
        let (connection, _physical) = ConnectionState::unbounded(info);
  747    749   
  748    750   
        assert_eq!(ConnectionId::new(7), connection.info().id());
  749    751   
        assert_eq!(&origin, connection.info().origin());
  750    752   
        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
  751    753   
        assert_eq!(NegotiatedProtocol::Http1, connection.info().protocol());
  752    754   
        assert_eq!(None, connection.info().local_addr());
  753    755   
        assert_eq!(None, connection.info().remote_addr());
  754         -
        assert!(connection.info().is_proxied());
         756  +
        assert!(connection.info().connect_path().is_proxied());
  755    757   
        let mut extensions = Extensions::new();
  756    758   
        connection.info().apply_connector_extras(&mut extensions);
  757    759   
        assert_eq!(
  758    760   
            Some(&ConnectorMarker("connector-extra")),
  759    761   
            extensions.get::<ConnectorMarker>()
  760    762   
        );
  761    763   
    }
  762    764   
  763    765   
    #[test]
  764    766   
    fn dropping_connection_io_completes_physical_connection_ownership() {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/dispatch/h1.rs

@@ -44,44 +104,106 @@
   64     64   
        let metadata =
   65     65   
            captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
   66     66   
        return Err(ConnectorError::user(
   67     67   
            "an HTTP/2 request cannot use an HTTP/1 connection".into(),
   68     68   
        )
   69     69   
        .with_connection(metadata));
   70     70   
    }
   71     71   
   72     72   
    add_host_header(&mut request, &context.absolute_uri)
   73     73   
        .map_err(|error| ConnectorError::user(error.into()))?;
   74         -
    rewrite_h1_request_target(&mut request, connection.info().is_proxied());
          74  +
    let connect_path = connection.info().connect_path();
          75  +
    connect_path.apply_proxy_authorization(request.headers_mut());
          76  +
    rewrite_h1_request_target(&mut request, connect_path.uses_absolute_form());
   75     77   
   76     78   
    // Commit against logical close before transferring the request to
   77     79   
    // Hyper. A close that wins this race leaves the request untouched.
   78     80   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
   79     81   
        tracing::trace!(
   80     82   
            connection_id = %connection.id(),
   81     83   
            request_partition = ?context.partition.id(),
   82     84   
            connection_partition = ?connection.owner_partition(),
   83     85   
            origin_scheme = %connection.info().origin().scheme(),
   84     86   
            origin_host = connection.info().origin().host(),
@@ -396,398 +455,458 @@
  416    418   
        return;
  417    419   
    }
  418    420   
    let mut parts = Parts::default();
  419    421   
    parts.path_and_query = request.uri().path_and_query().cloned();
  420    422   
    *request.uri_mut() = Uri::from_parts(parts).expect("request path and query are a valid URI");
  421    423   
}
  422    424   
  423    425   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  424    426   
mod tests {
  425    427   
    use super::*;
         428  +
    use crate::client::connect::ConnectPath;
  426    429   
    use crate::client::pool::cell::h1::H1Sender;
  427    430   
    use crate::client::pool::cell::OriginCell;
  428    431   
    use crate::client::pool::connection::ConnectionInfo;
  429    432   
    use crate::client::pool::dispatch::RequestOptions;
  430    433   
    use crate::client::pool::registry::PartitionState;
  431    434   
    use crate::client::pool::{
  432    435   
        Client, ConnectionId, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
  433    436   
        TokioDriverSpawner,
  434    437   
    };
  435    438   
    use crate::client::timeout::test::{NeverConnects, NeverReplies};
@@ -799,802 +858,887 @@
  819    822   
            add_host_header(&mut request, &absolute_uri).unwrap();
  820    823   
  821    824   
            assert_eq!(
  822    825   
                expected,
  823    826   
                request.headers()[http_1x::header::HOST].to_str().unwrap(),
  824    827   
                "{name}"
  825    828   
            );
  826    829   
        }
  827    830   
    }
  828    831   
         832  +
    #[test]
         833  +
    fn forward_proxy_authorization_preserves_a_caller_value() {
         834  +
        let connect_path =
         835  +
            ConnectPath::forward_proxy(Some(http_1x::HeaderValue::from_static("Basic connector")));
         836  +
        let caller_value = http_1x::HeaderValue::from_static("Basic caller");
         837  +
         838  +
        let mut request = Request::get("http://example.com/")
         839  +
            .body(SdkBody::empty())
         840  +
            .unwrap();
         841  +
        connect_path.apply_proxy_authorization(request.headers_mut());
         842  +
        assert_eq!(
         843  +
            request.headers()[http_1x::header::PROXY_AUTHORIZATION],
         844  +
            "Basic connector"
         845  +
        );
         846  +
         847  +
        let mut request = Request::get("http://example.com/")
         848  +
            .header(http_1x::header::PROXY_AUTHORIZATION, caller_value.clone())
         849  +
            .body(SdkBody::empty())
         850  +
            .unwrap();
         851  +
        connect_path.apply_proxy_authorization(request.headers_mut());
         852  +
        assert_eq!(
         853  +
            request.headers().get(http_1x::header::PROXY_AUTHORIZATION),
         854  +
            Some(&caller_value)
         855  +
        );
         856  +
    }
         857  +
  829    858   
    #[test]
  830    859   
    fn h1_request_target_uses_the_required_wire_form() {
  831    860   
        let cases = [
  832    861   
            (
  833    862   
                "CONNECT",
  834    863   
                Method::CONNECT,
  835    864   
                "http://example.com:8443/ignored",
  836    865   
                false,
  837    866   
                "example.com:8443",
  838    867   
            ),

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/establish/transport.rs

@@ -1,1 +46,47 @@
    5      5   
    6      6   
//! Transport connection policy below HTTP protocol establishment.
    7      7   
//!
    8      8   
//! This module owns the boundary from a pool connection attempt to connected
    9      9   
//! I/O with a negotiated protocol. [`TransportConnectContext`] keeps placement,
   10     10   
//! timeout, and ALPN inputs together. A factory may delegate DNS, proxy, TLS,
   11     11   
//! and ALPN to an injected connector, but it must report whether an HTTP/1-only
   12     12   
//! attempt is enforceable before admission may reclaim an idle H2 connection.
   13     13   
//!
   14     14   
//! Cached factories retain configured connector services, not connections or
   15         -
//! DNS results. Network I/O begins only when [`TransportFactory::connect`] is
   16         -
//! called.
          15  +
//! DNS results. Base client validation initializes those services for the
          16  +
//! selected partition without network I/O. Network I/O begins only when
          17  +
//! [`TransportFactory::connect`] is called.
   17     18   
   18     19   
use super::super::admission::ProtocolRequirement;
   19     20   
use super::super::registry::PartitionState;
   20     21   
use crate::client::connect::{AsyncConn, BoxConn};
   21     22   
use crate::client::timeout::{self, TimeoutKind};
   22     23   
use aws_smithy_async::rt::sleep::SharedAsyncSleep;
   23     24   
use aws_smithy_runtime_api::box_error::BoxError;
   24     25   
use http_1x::Uri;
   25     26   
use std::future::{poll_fn, Future};
   26     27   
use std::pin::Pin;
@@ -74,75 +133,144 @@
   94     95   
type TransportFuture = Pin<Box<dyn Future<Output = Result<BoxConn, BoxError>> + Send + 'static>>;
   95     96   
   96     97   
/// Type-erased transport construction below HTTP protocol establishment.
   97     98   
///
   98     99   
/// A pool retains one factory. The concrete connector remains responsible for
   99    100   
/// the transport stages it implements and for returning connector metadata.
  100    101   
/// Protocol establishment consumes the resulting [`BoxConn`].
  101    102   
pub(in crate::client::pool) trait TransportFactory:
  102    103   
    Send + Sync + 'static
  103    104   
{
         105  +
    /// Initializes connector services used by one partition.
         106  +
    ///
         107  +
    /// Initialization must be idempotent and must not perform DNS resolution
         108  +
    /// or network I/O. Factories without retained initialization state may use
         109  +
    /// the default no-op implementation.
         110  +
    fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         111  +
        let _ = partition;
         112  +
        Ok(())
         113  +
    }
         114  +
  104    115   
    /// Returns whether an H1-required attempt is guaranteed to negotiate H1.
  105    116   
    fn can_guarantee_http1(&self) -> bool;
  106    117   
  107    118   
    /// Creates one partition-bound transport.
  108    119   
    ///
  109    120   
    /// Connector readiness completes before the connection timeout starts.
  110    121   
    fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture;
  111    122   
}
  112    123   
  113    124   
/// Connector factory whose concrete service is selected by interface.
@@ -180,191 +0,569 @@
  200    211   
    C::Error: Into<BoxError>,
  201    212   
    C::Future: Send + 'static,
  202    213   
    IO: AsyncConn,
  203    214   
{
  204    215   
    StdArc::new(ServiceTransportFactory {
  205    216   
        connector_for_interface,
  206    217   
        can_guarantee_http1,
  207    218   
    })
  208    219   
}
  209    220   
         221  +
/// Identifies one configured connector service in the TLS transport cache.
         222  +
///
         223  +
/// Partition identity is deliberately absent. Partitions with equal interface
         224  +
/// bindings use the same connector configuration and therefore share entries.
         225  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         226  +
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
         227  +
struct ConnectorCacheKey {
         228  +
    /// Network-interface binding applied before connect.
         229  +
    interface: Option<StdArc<str>>,
         230  +
    /// Protocol offer installed in the TLS connector.
         231  +
    alpn_protocols: AlpnProtocols,
         232  +
}
         233  +
         234  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         235  +
impl ConnectorCacheKey {
         236  +
    /// Derives the connector configuration used by one partition and offer.
         237  +
    fn for_partition(partition: &PartitionState, alpn_protocols: AlpnProtocols) -> Self {
         238  +
        Self {
         239  +
            interface: partition.interface().cloned(),
         240  +
            alpn_protocols,
         241  +
        }
         242  +
    }
         243  +
         244  +
    /// Returns the borrowed interface name expected by connector builders.
         245  +
    fn interface(&self) -> Option<&str> {
         246  +
        self.interface.as_deref()
         247  +
    }
         248  +
}
         249  +
         250  +
/// Retains configured TLS connector services by placement and ALPN offer.
         251  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         252  +
struct CachedTransportFactory<F, C> {
         253  +
    /// Constructs a connector service for one exact cache key.
         254  +
    factory: F,
         255  +
    /// Whether every H1-required connection is guaranteed to negotiate H1.
         256  +
    can_guarantee_http1: bool,
         257  +
    /// Connector services constructed during client validation or first use.
         258  +
    connectors: crate::sync::Mutex<std::collections::HashMap<ConnectorCacheKey, C>>,
         259  +
}
         260  +
         261  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         262  +
impl<F, C> CachedTransportFactory<F, C>
         263  +
where
         264  +
    F: Fn(Option<&str>, AlpnProtocols) -> C,
         265  +
    C: Clone,
         266  +
{
         267  +
    /// Returns the retained connector for one partition and protocol offer.
         268  +
    fn connector(&self, partition: &PartitionState, alpn_protocols: AlpnProtocols) -> C {
         269  +
        let key = ConnectorCacheKey::for_partition(partition, alpn_protocols);
         270  +
        match self.connectors.lock().entry(key) {
         271  +
            std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
         272  +
            std::collections::hash_map::Entry::Vacant(entry) => {
         273  +
                let connector = (self.factory)(entry.key().interface(), entry.key().alpn_protocols);
         274  +
                entry.insert(connector).clone()
         275  +
            }
         276  +
        }
         277  +
    }
         278  +
         279  +
    /// Materializes every connector variant this partition may select.
         280  +
    fn initialize_connectors_for_partition(&self, partition: &PartitionState) {
         281  +
        drop(self.connector(partition, HTTP_ALPN_PROTOCOLS));
         282  +
        drop(self.connector(partition, HTTP1_ALPN_PROTOCOLS));
         283  +
    }
         284  +
}
         285  +
         286  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         287  +
impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
         288  +
where
         289  +
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
         290  +
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
         291  +
    C::Error: Into<BoxError>,
         292  +
    C::Future: Send + 'static,
         293  +
    IO: AsyncConn,
         294  +
{
         295  +
    fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         296  +
        self.initialize_connectors_for_partition(partition);
         297  +
        Ok(())
         298  +
    }
         299  +
         300  +
    fn can_guarantee_http1(&self) -> bool {
         301  +
        self.can_guarantee_http1
         302  +
    }
         303  +
         304  +
    fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
         305  +
        let TransportConnectContext {
         306  +
            partition,
         307  +
            uri,
         308  +
            timeout,
         309  +
            alpn_protocols,
         310  +
        } = context;
         311  +
        let mut connector = self.connector(partition, alpn_protocols);
         312  +
        Box::pin(async move {
         313  +
            poll_fn(|cx| connector.poll_ready(cx))
         314  +
                .await
         315  +
                .map_err(Into::into)?;
         316  +
            let connect = connector.call(uri);
         317  +
            let io = timeout::maybe_timeout_future(
         318  +
                connect,
         319  +
                timeout.as_ref().map(|timeout| timeout.duration),
         320  +
                timeout.as_ref().map(|timeout| &timeout.sleep),
         321  +
                TimeoutKind::Connect,
         322  +
            )
         323  +
            .await?;
         324  +
            Ok(Box::new(io) as BoxConn)
         325  +
        })
         326  +
    }
         327  +
}
         328  +
  210    329   
/// Erases and caches configured TLS connector services.
  211    330   
///
  212         -
/// Interface placement is immutable for a partition. The first request for
  213         -
/// each `(interface, ALPN offer)` constructs its connector while holding the
  214         -
/// cache lock; later requests clone that retained connector. Provider
         331  +
/// Interface placement is immutable for a partition. Client validation
         332  +
/// constructs both ALPN variants for the selected partition while holding the
         333  +
/// cache lock; later requests clone the retained connector. Provider
  215    334   
/// configuration and certificate loading therefore occur at most once for
  216    335   
/// each placement and offer rather than once per connection.
  217    336   
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
  218    337   
pub(in crate::client::pool) fn from_cached_interface_connector<F, C, IO>(
  219    338   
    connector_for_interface: F,
  220    339   
    can_guarantee_http1: bool,
  221    340   
) -> StdArc<dyn TransportFactory>
  222    341   
where
  223    342   
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  224    343   
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  225    344   
    C::Error: Into<BoxError>,
  226    345   
    C::Future: Send + 'static,
  227    346   
    IO: AsyncConn,
  228    347   
{
  229         -
    struct CachedTransportFactory<F, C> {
  230         -
        factory: F,
  231         -
        can_guarantee_http1: bool,
  232         -
        connectors:
  233         -
            crate::sync::Mutex<std::collections::HashMap<(Option<String>, AlpnProtocols), C>>,
  234         -
    }
  235         -
  236         -
    impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
  237         -
    where
  238         -
        F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  239         -
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  240         -
        C::Error: Into<BoxError>,
  241         -
        C::Future: Send + 'static,
  242         -
        IO: AsyncConn,
  243         -
    {
  244         -
        fn can_guarantee_http1(&self) -> bool {
  245         -
            self.can_guarantee_http1
  246         -
        }
  247         -
  248         -
        fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
  249         -
            let TransportConnectContext {
  250         -
                partition,
  251         -
                uri,
  252         -
                timeout,
  253         -
                alpn_protocols,
  254         -
            } = context;
  255         -
            let interface = partition.interface().map(|value| value.to_string());
  256         -
            let mut connector = self
  257         -
                .connectors
  258         -
                .lock()
  259         -
                .entry((interface.clone(), alpn_protocols))
  260         -
                .or_insert_with(|| (self.factory)(interface.as_deref(), alpn_protocols))
  261         -
                .clone();
  262         -
            Box::pin(async move {
  263         -
                poll_fn(|cx| connector.poll_ready(cx))
  264         -
                    .await
  265         -
                    .map_err(Into::into)?;
  266         -
                let connect = connector.call(uri);
  267         -
                let io = timeout::maybe_timeout_future(
  268         -
                    connect,
  269         -
                    timeout.as_ref().map(|timeout| timeout.duration),
  270         -
                    timeout.as_ref().map(|timeout| &timeout.sleep),
  271         -
                    TimeoutKind::Connect,
  272         -
                )
  273         -
                .await?;
  274         -
                Ok(Box::new(io) as BoxConn)
  275         -
            })
  276         -
        }
  277         -
    }
  278         -
  279    348   
    StdArc::new(CachedTransportFactory {
  280    349   
        factory: connector_for_interface,
  281    350   
        can_guarantee_http1,
  282    351   
        connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
  283    352   
    })
  284    353   
}
  285    354   
  286    355   
/// Selects the ALPN offer that preserves the request's wire semantics.
  287    356   
fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
  288    357   
    match requirement {
  289    358   
        ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
  290    359   
        ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
  291    360   
    }
  292    361   
}
         362  +
         363  +
#[cfg(all(
         364  +
    test,
         365  +
    any(feature = "__rustls", feature = "s2n-tls"),
         366  +
    any(
         367  +
        target_os = "android",
         368  +
        target_os = "fuchsia",
         369  +
        target_os = "illumos",
         370  +
        target_os = "ios",
         371  +
        target_os = "linux",
         372  +
        target_os = "macos",
         373  +
        target_os = "solaris",
         374  +
        target_os = "tvos",
         375  +
        target_os = "visionos",
         376  +
        target_os = "watchos",
         377  +
    )
         378  +
))]
         379  +
mod tests {
         380  +
    use super::*;
         381  +
    use crate::client::pool::maintenance::MaintenanceConfig;
         382  +
    use crate::client::pool::partition::{
         383  +
        ConnectionReuseScope, DriverSpawner, Partition, PartitionId,
         384  +
    };
         385  +
    use crate::client::pool::registry::PartitionRegistry;
         386  +
    use aws_smithy_runtime_api::client::http::HttpClient;
         387  +
    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
         388  +
    use aws_smithy_types::config_bag::ConfigBag;
         389  +
         390  +
    use std::sync::{Arc, Mutex};
         391  +
         392  +
    #[derive(Debug)]
         393  +
    struct TestSpawner;
         394  +
         395  +
    impl DriverSpawner for TestSpawner {
         396  +
        fn spawn(&self, _: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {}
         397  +
    }
         398  +
         399  +
    struct InitializationRecorder {
         400  +
        partitions: Arc<Mutex<Vec<PartitionId>>>,
         401  +
    }
         402  +
         403  +
    impl TransportFactory for InitializationRecorder {
         404  +
        fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         405  +
            self.partitions
         406  +
                .lock()
         407  +
                .expect("initialization log is not poisoned")
         408  +
                .push(partition.id());
         409  +
            Ok(())
         410  +
        }
         411  +
         412  +
        fn can_guarantee_http1(&self) -> bool {
         413  +
            true
         414  +
        }
         415  +
         416  +
        fn connect(&self, _: TransportConnectContext<'_>) -> TransportFuture {
         417  +
            Box::pin(async { panic!("client validation must not start a connection") })
         418  +
        }
         419  +
    }
         420  +
         421  +
    type Construction = (Option<String>, AlpnProtocols);
         422  +
         423  +
    struct CachedFactoryFixture<F> {
         424  +
        factory: CachedTransportFactory<F, usize>,
         425  +
        constructions: Arc<Mutex<Vec<Construction>>>,
         426  +
    }
         427  +
         428  +
    fn recording_factory() -> CachedFactoryFixture<impl Fn(Option<&str>, AlpnProtocols) -> usize> {
         429  +
        let constructions = Arc::new(Mutex::new(Vec::new()));
         430  +
        let observed = constructions.clone();
         431  +
        let factory = move |interface: Option<&str>, alpn_protocols: AlpnProtocols| {
         432  +
            let mut constructions = observed.lock().expect("construction log is not poisoned");
         433  +
            constructions.push((interface.map(str::to_owned), alpn_protocols));
         434  +
            constructions.len()
         435  +
        };
         436  +
        CachedFactoryFixture {
         437  +
            factory: CachedTransportFactory {
         438  +
                factory,
         439  +
                can_guarantee_http1: true,
         440  +
                connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
         441  +
            },
         442  +
            constructions,
         443  +
        }
         444  +
    }
         445  +
         446  +
    fn registry(partitions: Option<Vec<Partition>>) -> PartitionRegistry {
         447  +
        PartitionRegistry::new(
         448  +
            partitions,
         449  +
            ConnectionReuseScope::Partition,
         450  +
            None,
         451  +
            MaintenanceConfig::default(),
         452  +
        )
         453  +
        .expect("valid partition registry")
         454  +
    }
         455  +
         456  +
    #[test]
         457  +
    fn client_validation_initializes_its_selected_partition() {
         458  +
        let selected = PartitionId::from_index(7);
         459  +
        let initialized = Arc::new(Mutex::new(Vec::new()));
         460  +
        let pool = crate::client::pool::builder::Builder::default()
         461  +
            .partitions([Partition::new(selected, TestSpawner)])
         462  +
            .build_with_transport_for_test(Arc::new(InitializationRecorder {
         463  +
                partitions: initialized.clone(),
         464  +
            }))
         465  +
            .expect("valid test pool");
         466  +
        let client = crate::client::pool::Client::from_partition(&pool, selected)
         467  +
            .expect("selected partition exists");
         468  +
         469  +
        client
         470  +
            .validate_base_client_config(&RuntimeComponentsBuilder::for_tests(), &ConfigBag::base())
         471  +
            .expect("transport initialization succeeds");
         472  +
         473  +
        assert_eq!(
         474  +
            vec![selected],
         475  +
            *initialized
         476  +
                .lock()
         477  +
                .expect("initialization log is not poisoned")
         478  +
        );
         479  +
    }
         480  +
         481  +
    #[test]
         482  +
    fn initialization_constructs_each_alpn_variant_once() {
         483  +
        let partition = registry(None)
         484  +
            .partition(PartitionId::ANONYMOUS)
         485  +
            .expect("anonymous partition exists");
         486  +
        let fixture = recording_factory();
         487  +
         488  +
        fixture
         489  +
            .factory
         490  +
            .initialize_connectors_for_partition(&partition);
         491  +
        fixture
         492  +
            .factory
         493  +
            .initialize_connectors_for_partition(&partition);
         494  +
         495  +
        let constructions = fixture
         496  +
            .constructions
         497  +
            .lock()
         498  +
            .expect("construction log is not poisoned");
         499  +
        assert_eq!(2, constructions.len());
         500  +
        assert_eq!(None, constructions[0].0);
         501  +
        assert_eq!(HTTP_ALPN_PROTOCOLS, constructions[0].1);
         502  +
        assert_eq!(HTTP1_ALPN_PROTOCOLS, constructions[1].1);
         503  +
        assert_eq!(2, fixture.factory.connectors.lock().len());
         504  +
    }
         505  +
         506  +
    #[test]
         507  +
    fn equal_interface_bindings_share_connector_entries() {
         508  +
        let first = PartitionId::from_index(1);
         509  +
        let second = PartitionId::from_index(2);
         510  +
        let registry = registry(Some(vec![
         511  +
            Partition::new(first, TestSpawner).interface("interface-a"),
         512  +
            Partition::new(second, TestSpawner).interface("interface-a"),
         513  +
        ]));
         514  +
        let fixture = recording_factory();
         515  +
         516  +
        fixture.factory.initialize_connectors_for_partition(
         517  +
            &registry.partition(first).expect("first partition exists"),
         518  +
        );
         519  +
        fixture.factory.initialize_connectors_for_partition(
         520  +
            &registry.partition(second).expect("second partition exists"),
         521  +
        );
         522  +
         523  +
        let constructions = fixture
         524  +
            .constructions
         525  +
            .lock()
         526  +
            .expect("construction log is not poisoned");
         527  +
        assert_eq!(
         528  +
            [
         529  +
                (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
         530  +
                (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
         531  +
            ],
         532  +
            constructions.as_slice()
         533  +
        );
         534  +
        assert_eq!(2, fixture.factory.connectors.lock().len());
         535  +
    }
         536  +
         537  +
    #[test]
         538  +
    fn distinct_interface_bindings_use_distinct_connector_entries() {
         539  +
        let first = PartitionId::from_index(1);
         540  +
        let second = PartitionId::from_index(2);
         541  +
        let registry = registry(Some(vec![
         542  +
            Partition::new(first, TestSpawner).interface("interface-a"),
         543  +
            Partition::new(second, TestSpawner).interface("interface-b"),
         544  +
        ]));
         545  +
        let fixture = recording_factory();
         546  +
         547  +
        fixture.factory.initialize_connectors_for_partition(
         548  +
            &registry.partition(first).expect("first partition exists"),
         549  +
        );
         550  +
        fixture.factory.initialize_connectors_for_partition(
         551  +
            &registry.partition(second).expect("second partition exists"),
         552  +
        );
         553  +
         554  +
        let constructions = fixture
         555  +
            .constructions
         556  +
            .lock()
         557  +
            .expect("construction log is not poisoned");
         558  +
        assert_eq!(
         559  +
            [
         560  +
                (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
         561  +
                (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
         562  +
                (Some("interface-b".to_string()), HTTP_ALPN_PROTOCOLS),
         563  +
                (Some("interface-b".to_string()), HTTP1_ALPN_PROTOCOLS),
         564  +
            ],
         565  +
            constructions.as_slice()
         566  +
        );
         567  +
        assert_eq!(4, fixture.factory.connectors.lock().len());
         568  +
    }
         569  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/tls/rustls_provider.rs

@@ -318,318 +378,378 @@
  338    338   
            .collect();
  339    339   
        conn.enforce_http(false);
  340    340   
        let https_connector =
  341    341   
            hyper_rustls::HttpsConnector::from((conn, client_config.clone()));
  342    342   
  343    343   
        super::connect::RustTlsConnector::new(https_connector, client_config, proxy_config)
  344    344   
    }
  345    345   
}
  346    346   
  347    347   
pub(crate) mod connect {
  348         -
    use crate::client::connect::{Conn, Connecting};
         348  +
    use crate::client::connect::{Conn, ConnectPath, Connecting};
  349    349   
    use crate::client::proxy::ProxyConfig;
  350    350   
    use aws_smithy_runtime_api::box_error::BoxError;
  351    351   
    use http_1x::uri::Scheme;
  352    352   
    use http_1x::Uri;
  353    353   
    use hyper::rt::{Read, ReadBufCursor, Write};
  354    354   
    use hyper_rustls::MaybeHttpsStream;
  355    355   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
  356    356   
    use hyper_util::client::proxy::matcher::Matcher;
  357    357   
    use hyper_util::rt::TokioIo;
  358    358   
    use pin_project_lite::pin_project;
@@ -422,422 +554,555 @@
  442    442   
        R::Response: Iterator<Item = std::net::SocketAddr>,
  443    443   
        R::Future: Send,
  444    444   
        R::Error: Into<Box<dyn Error + Send + Sync>>,
  445    445   
    {
  446    446   
        fn handle_direct_connection(&mut self, dst: Uri) -> Connecting {
  447    447   
            let fut = self.https.call(dst);
  448    448   
            Box::pin(async move {
  449    449   
                let conn = fut.await?;
  450    450   
                Ok(Conn {
  451    451   
                    inner: Box::new(conn),
  452         -
                    is_proxy: false,
         452  +
                    connect_path: ConnectPath::Direct,
  453    453   
                })
  454    454   
            })
  455    455   
        }
  456    456   
  457    457   
        fn handle_http_through_proxy(
  458    458   
            &mut self,
  459    459   
            _dst: Uri,
  460    460   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  461    461   
        ) -> Connecting {
  462    462   
            // For HTTP through proxy, connect to the proxy and let it handle the request
  463    463   
            let proxy_uri = intercept.uri().clone();
         464  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  464    465   
            let fut = self.https.call(proxy_uri);
  465    466   
            Box::pin(async move {
  466    467   
                let conn = fut.await?;
  467    468   
                Ok(Conn {
  468    469   
                    inner: Box::new(conn),
  469         -
                    is_proxy: true,
         470  +
                    connect_path,
  470    471   
                })
  471    472   
            })
  472    473   
        }
  473    474   
  474    475   
        fn handle_https_through_proxy(
  475    476   
            &mut self,
  476    477   
            dst: Uri,
  477    478   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  478    479   
        ) -> Connecting {
  479    480   
            use rustls_pki_types::ServerName as RustlsServerName;
  480    481   
            // For HTTPS through HTTP proxy, we need to:
  481    482   
            // 1. Establish CONNECT tunnel using the HTTPS connector
  482    483   
            // 2. Perform manual TLS handshake over the tunneled stream
  483    484   
  484    485   
            let tunnel = hyper_util::client::legacy::connect::proxy::Tunnel::new(
  485    486   
                intercept.uri().clone(),
  486    487   
                self.https.clone(),
  487    488   
            );
  488    489   
  489    490   
            // Configure tunnel with authentication if present
  490    491   
            let mut tunnel = if let Some(auth) = intercept.basic_auth() {
  491    492   
                tunnel.with_auth(auth.clone())
  492    493   
            } else {
  493    494   
                tunnel
  494    495   
            };
  495    496   
  496    497   
            let tls_config = self.tls_config.clone();
  497    498   
            let dst_clone = dst.clone();
  498    499   
  499    500   
            Box::pin(async move {
  500    501   
                // Establish CONNECT tunnel
  501    502   
                tracing::trace!("tunneling HTTPS over proxy");
  502    503   
                let tunneled = tunnel
  503    504   
                    .call(dst_clone.clone())
  504    505   
                    .await
  505    506   
                    .map_err(|e| BoxError::from(format!("CONNECT tunnel failed: {e}")))?;
  506    507   
  507    508   
                // Stage 2: Manual TLS handshake over tunneled stream
  508    509   
                let host = dst_clone
  509    510   
                    .host()
  510    511   
                    .ok_or("missing host in URI for TLS handshake")?;
  511    512   
  512    513   
                let server_name = RustlsServerName::try_from(host.to_owned()).map_err(|e| {
  513    514   
                    BoxError::from(format!("invalid server name for TLS handshake: {e}"))
  514    515   
                })?;
  515    516   
  516    517   
                let tls_connector = tokio_rustls::TlsConnector::from(tls_config)
  517    518   
                    .connect(server_name, TokioIo::new(tunneled))
  518    519   
                    .await?;
  519    520   
  520    521   
                Ok(Conn {
  521    522   
                    inner: Box::new(RustTlsConn {
  522    523   
                        inner: TokioIo::new(tls_connector),
  523    524   
                    }),
  524         -
                    is_proxy: true,
         525  +
                    connect_path: ConnectPath::ProxyTunnel,
  525    526   
                })
  526    527   
            })
  527    528   
        }
  528    529   
    }
  529    530   
  530    531   
    pin_project! {
  531    532   
        pub(crate) struct RustTlsConn<T> {
  532    533   
            #[pin] pub(super) inner: TokioIo<TlsStream<T>>
  533    534   
        }
  534    535   
    }

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/tls/s2n_tls_provider.rs

@@ -209,209 +269,269 @@
  229    229   
            assert!(!matches_host_name("baz1.example.com", "baz*.example.com"));
  230    230   
            assert!(!matches_host_name(
  231    231   
                "foo.bar.example.com",
  232    232   
                "foo.*.example.com"
  233    233   
            ));
  234    234   
        }
  235    235   
    }
  236    236   
}
  237    237   
  238    238   
pub(crate) mod connect {
  239         -
    use crate::client::connect::{Conn, Connecting};
         239  +
    use crate::client::connect::{Conn, ConnectPath, Connecting};
  240    240   
    use crate::client::proxy::ProxyConfig;
  241    241   
    use aws_smithy_runtime_api::box_error::BoxError;
  242    242   
    use http_1x::uri::Scheme;
  243    243   
    use http_1x::Uri;
  244    244   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
  245    245   
    use hyper_util::client::proxy::matcher::Matcher;
  246    246   
    use hyper_util::rt::TokioIo;
  247    247   
    use std::error::Error;
  248    248   
    use std::sync::Arc;
  249    249   
    use std::{
@@ -307,307 +437,438 @@
  327    327   
        R::Response: Iterator<Item = std::net::SocketAddr>,
  328    328   
        R::Future: Send,
  329    329   
        R::Error: Into<Box<dyn Error + Send + Sync>>,
  330    330   
    {
  331    331   
        fn handle_direct_connection(&mut self, dst: Uri) -> Connecting {
  332    332   
            let fut = self.https.call(dst);
  333    333   
            Box::pin(async move {
  334    334   
                let conn = fut.await?;
  335    335   
                Ok(Conn {
  336    336   
                    inner: Box::new(conn),
  337         -
                    is_proxy: false,
         337  +
                    connect_path: ConnectPath::Direct,
  338    338   
                })
  339    339   
            })
  340    340   
        }
  341    341   
  342    342   
        fn handle_http_through_proxy(
  343    343   
            &mut self,
  344    344   
            _dst: Uri,
  345    345   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  346    346   
        ) -> Connecting {
  347    347   
            // For HTTP through proxy, connect to the proxy and let it handle the request
  348    348   
            let proxy_uri = intercept.uri().clone();
         349  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  349    350   
            let fut = self.https.call(proxy_uri);
  350    351   
            Box::pin(async move {
  351    352   
                let conn = fut.await?;
  352    353   
                Ok(Conn {
  353    354   
                    inner: Box::new(conn),
  354         -
                    is_proxy: true,
         355  +
                    connect_path,
  355    356   
                })
  356    357   
            })
  357    358   
        }
  358    359   
  359    360   
        fn handle_https_through_proxy(
  360    361   
            &mut self,
  361    362   
            dst: Uri,
  362    363   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  363    364   
        ) -> Connecting {
  364    365   
            // For HTTPS through HTTP proxy, we need to:
  365    366   
            // 1. Establish CONNECT tunnel using the HTTPS connector
  366    367   
            // 2. Perform manual TLS handshake over the tunneled stream
  367    368   
  368    369   
            let tunnel = hyper_util::client::legacy::connect::proxy::Tunnel::new(
  369    370   
                intercept.uri().clone(),
  370    371   
                self.https.clone(),
  371    372   
            );
  372    373   
  373    374   
            // Configure tunnel with authentication if present
  374    375   
            let mut tunnel = if let Some(auth) = intercept.basic_auth() {
  375    376   
                tunnel.with_auth(auth.clone())
  376    377   
            } else {
  377    378   
                tunnel
  378    379   
            };
  379    380   
  380    381   
            let tls_config = self.tls_config.clone();
  381    382   
            let dst_clone = dst.clone();
  382    383   
  383    384   
            Box::pin(async move {
  384    385   
                // Stage 1: Establish CONNECT tunnel
  385    386   
                tracing::trace!("tunneling HTTPS over proxy using s2n-tls");
  386    387   
                let tunneled = tunnel
  387    388   
                    .call(dst_clone.clone())
  388    389   
                    .await
  389    390   
                    .map_err(|e| BoxError::from(format!("CONNECT tunnel failed: {e}")))?;
  390    391   
  391    392   
                // Stage 2: Manual TLS handshake over tunneled stream
  392    393   
                let host = dst_clone
  393    394   
                    .host()
  394    395   
                    .ok_or("missing host in URI for TLS handshake")?;
  395    396   
  396    397   
                // s2n-tls uses string server names (simpler than rustls ServerName)
  397    398   
                let tls_connector = s2n_tls_tokio::TlsConnector::new(tls_config);
  398    399   
                let tls_stream = tls_connector
  399    400   
                    .connect(host, TokioIo::new(tunneled))
  400    401   
                    .await
  401    402   
                    .map_err(|e| BoxError::from(format!("s2n-tls handshake failed: {e}")))?;
  402    403   
  403    404   
                Ok(Conn {
  404    405   
                    inner: Box::new(S2nTlsConn {
  405    406   
                        inner: TokioIo::new(tls_stream),
  406    407   
                    }),
  407         -
                    is_proxy: true,
         408  +
                    connect_path: ConnectPath::ProxyTunnel,
  408    409   
                })
  409    410   
            })
  410    411   
        }
  411    412   
    }
  412    413   
  413    414   
    // Simple wrapper that implements Connection for s2n-tls streams
  414    415   
    struct S2nTlsConn<T>
  415    416   
    where
  416    417   
        T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
  417    418   
    {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/common/client.rs

@@ -1,1 +64,259 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Client configuration and request helpers shared by connection behavior tests.
    7      7   
    8      8   
use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
    9      9   
use aws_smithy_async::time::SystemTimeSource;
          10  +
use aws_smithy_http_client::pool::{Client as PoolClient, ConnectionPool};
          11  +
use aws_smithy_http_client::proxy::ProxyConfig;
          12  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
          13  +
use aws_smithy_http_client::tls;
          14  +
use aws_smithy_http_client::{Builder, Connector};
          15  +
use aws_smithy_runtime_api::client::dns::SharedDnsResolver;
   10     16   
use aws_smithy_runtime_api::client::http::{
   11         -
    HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient, SharedHttpConnector,
          17  +
    http_client_fn, HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient,
          18  +
    SharedHttpConnector,
   12     19   
};
   13     20   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   14     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   15     22   
use aws_smithy_runtime_api::client::runtime_components::{
   16     23   
    RuntimeComponents, RuntimeComponentsBuilder,
   17     24   
};
   18     25   
use http_body_util::BodyExt;
   19     26   
use std::time::Duration;
   20     27   
   21     28   
/// Default timeout for test waits and deadline assertions.
   22     29   
pub(crate) const WAIT: Duration = Duration::from_secs(5);
   23     30   
   24         -
#[derive(Clone, Copy, Debug, Default)]
          31  +
/// Settings applied uniformly by backend-neutral connection contracts.
          32  +
#[derive(Clone, Debug, Default)]
   25     33   
pub(crate) struct BackendConfig {
   26     34   
    pub(crate) pool_idle_timeout: Option<Duration>,
          35  +
    pub(crate) proxy_config: Option<ProxyConfig>,
          36  +
    pub(crate) dns_resolver: Option<SharedDnsResolver>,
   27     37   
}
   28     38   
   29     39   
/// Hyper 1.x through `hyper_util::client::legacy::Client`.
   30     40   
///
   31     41   
/// "Legacy" is Hyper Util's module name and does not refer to smithy-rs's `hyper-014` feature.
   32     42   
#[derive(Clone, Copy, Debug)]
   33     43   
pub(crate) struct HyperUtilLegacyPool;
   34     44   
          45  +
/// The partition-aware Smithy connection pool.
          46  +
#[derive(Clone, Copy, Debug)]
          47  +
pub(crate) struct PartitionedConnectionPool;
          48  +
          49  +
/// Builds a cleartext client for one connection-pool implementation.
          50  +
#[allow(dead_code)]
          51  +
pub(crate) trait HttpClientBackend {
          52  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient;
          53  +
}
          54  +
          55  +
impl HttpClientBackend for HyperUtilLegacyPool {
          56  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
          57  +
        let BackendConfig {
          58  +
            pool_idle_timeout,
          59  +
            proxy_config,
          60  +
            dns_resolver,
          61  +
        } = config;
          62  +
        if proxy_config.is_none() && dns_resolver.is_none() {
          63  +
            let mut builder = Builder::new();
          64  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
          65  +
                builder = builder.pool_idle_timeout(pool_idle_timeout);
          66  +
            }
          67  +
            return builder.build_http();
          68  +
        }
          69  +
          70  +
        http_client_fn(move |settings, _| {
          71  +
            let mut builder = Connector::builder().connector_settings(settings.clone());
          72  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
          73  +
                builder = builder.pool_idle_timeout(Some(pool_idle_timeout));
          74  +
            }
          75  +
            if let Some(proxy_config) = &proxy_config {
          76  +
                builder = builder.proxy_config(proxy_config.clone());
          77  +
            }
          78  +
            if let Some(resolver) = &dns_resolver {
          79  +
                #[cfg(any(
          80  +
                    feature = "rustls-aws-lc",
          81  +
                    feature = "rustls-aws-lc-fips",
          82  +
                    feature = "rustls-ring",
          83  +
                    feature = "s2n-tls"
          84  +
                ))]
          85  +
                return SharedHttpConnector::new(
          86  +
                    builder
          87  +
                        .tls_provider(custom_dns_test_provider())
          88  +
                        .build_with_resolver(resolver.clone()),
          89  +
                );
          90  +
                #[cfg(not(any(
          91  +
                    feature = "rustls-aws-lc",
          92  +
                    feature = "rustls-aws-lc-fips",
          93  +
                    feature = "rustls-ring",
          94  +
                    feature = "s2n-tls"
          95  +
                )))]
          96  +
                {
          97  +
                    let _ = resolver;
          98  +
                    panic!("the legacy custom-DNS test backend requires an enabled TLS provider");
          99  +
                }
         100  +
            }
         101  +
            SharedHttpConnector::new(builder.build_http())
         102  +
        })
         103  +
    }
         104  +
}
         105  +
         106  +
impl HttpClientBackend for PartitionedConnectionPool {
         107  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
         108  +
        let mut builder = ConnectionPool::builder();
         109  +
        if let Some(dns_resolver) = config.dns_resolver {
         110  +
            builder = builder.dns_resolver(dns_resolver);
         111  +
        }
         112  +
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
         113  +
            builder = builder.idle_timeout(pool_idle_timeout);
         114  +
        }
         115  +
        if let Some(proxy_config) = config.proxy_config {
         116  +
            builder = builder.proxy_config(proxy_config);
         117  +
        }
         118  +
        let pool = builder.build_http().expect("valid connection-pool config");
         119  +
        SharedHttpClient::new(PoolClient::new(&pool).expect("anonymous partition exists"))
         120  +
    }
         121  +
}
         122  +
         123  +
#[cfg(any(
         124  +
    feature = "rustls-aws-lc",
         125  +
    feature = "rustls-aws-lc-fips",
         126  +
    feature = "rustls-ring",
         127  +
    feature = "s2n-tls"
         128  +
))]
         129  +
fn custom_dns_test_provider() -> tls::Provider {
         130  +
    #[cfg(feature = "rustls-aws-lc")]
         131  +
    {
         132  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)
         133  +
    }
         134  +
    #[cfg(all(not(feature = "rustls-aws-lc"), feature = "rustls-aws-lc-fips"))]
         135  +
    {
         136  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLcFips)
         137  +
    }
         138  +
    #[cfg(all(
         139  +
        not(feature = "rustls-aws-lc"),
         140  +
        not(feature = "rustls-aws-lc-fips"),
         141  +
        feature = "rustls-ring"
         142  +
    ))]
         143  +
    {
         144  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::Ring)
         145  +
    }
         146  +
    #[cfg(all(
         147  +
        not(feature = "rustls-aws-lc"),
         148  +
        not(feature = "rustls-aws-lc-fips"),
         149  +
        not(feature = "rustls-ring"),
         150  +
        feature = "s2n-tls"
         151  +
    ))]
         152  +
    {
         153  +
        tls::Provider::S2nTls
         154  +
    }
         155  +
}
         156  +
         157  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         158  +
/// Builds a TLS client for one connection-pool implementation.
         159  +
#[allow(dead_code)]
         160  +
pub(crate) trait HttpsClientBackend {
         161  +
    fn build_https(
         162  +
        &self,
         163  +
        config: BackendConfig,
         164  +
        provider: tls::Provider,
         165  +
        tls_context: tls::TlsContext,
         166  +
    ) -> SharedHttpClient;
         167  +
}
         168  +
         169  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         170  +
impl HttpsClientBackend for HyperUtilLegacyPool {
         171  +
    fn build_https(
         172  +
        &self,
         173  +
        config: BackendConfig,
         174  +
        provider: tls::Provider,
         175  +
        tls_context: tls::TlsContext,
         176  +
    ) -> SharedHttpClient {
         177  +
        let Some(proxy_config) = config.proxy_config else {
         178  +
            let mut builder = Builder::new();
         179  +
            if let Some(pool_idle_timeout) = config.pool_idle_timeout {
         180  +
                builder = builder.pool_idle_timeout(pool_idle_timeout);
         181  +
            }
         182  +
            return builder
         183  +
                .tls_provider(provider)
         184  +
                .tls_context(tls_context)
         185  +
                .build_https();
         186  +
        };
         187  +
         188  +
        let pool_idle_timeout = config.pool_idle_timeout;
         189  +
        http_client_fn(move |settings, _| {
         190  +
            let mut builder = Connector::builder()
         191  +
                .connector_settings(settings.clone())
         192  +
                .proxy_config(proxy_config.clone());
         193  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
         194  +
                builder = builder.pool_idle_timeout(Some(pool_idle_timeout));
         195  +
            }
         196  +
            SharedHttpConnector::new(
         197  +
                builder
         198  +
                    .tls_provider(provider.clone())
         199  +
                    .tls_context(tls_context.clone())
         200  +
                    .build(),
         201  +
            )
         202  +
        })
         203  +
    }
         204  +
}
         205  +
         206  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         207  +
impl HttpsClientBackend for PartitionedConnectionPool {
         208  +
    fn build_https(
         209  +
        &self,
         210  +
        config: BackendConfig,
         211  +
        provider: tls::Provider,
         212  +
        tls_context: tls::TlsContext,
         213  +
    ) -> SharedHttpClient {
         214  +
        let mut builder = ConnectionPool::builder();
         215  +
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
         216  +
            builder = builder.idle_timeout(pool_idle_timeout);
         217  +
        }
         218  +
        if let Some(proxy_config) = config.proxy_config {
         219  +
            builder = builder.proxy_config(proxy_config);
         220  +
        }
         221  +
        let pool = builder
         222  +
            .tls_provider(provider)
         223  +
            .tls_context(tls_context)
         224  +
            .build_https()
         225  +
            .expect("valid connection-pool config");
         226  +
        SharedHttpClient::new(PoolClient::new(&pool).expect("anonymous partition exists"))
         227  +
    }
         228  +
}
         229  +
   35    230   
pub(crate) fn runtime_components() -> RuntimeComponents {
   36    231   
    RuntimeComponentsBuilder::for_tests()
   37    232   
        .with_time_source(Some(SystemTimeSource::new()))
   38    233   
        .with_sleep_impl(Some(SharedAsyncSleep::new(TokioSleep::new())))
   39    234   
        .build()
   40    235   
        .expect("valid runtime components")
   41    236   
}
   42    237   
   43    238   
pub(crate) fn connector_with_settings(
   44    239   
    client: &SharedHttpClient,