AWS SDK

AWS SDK

rev. 9e1d64f49d144bf9ef92d93ae2361b7075c24670 (ignoring whitespace)

Files changed:

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

@@ -1,1 +68,93 @@
    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         -
//! TLS setup shared by integration-test servers and clients.
           6  +
//! TLS identity and configuration shared by integration tests.
    7      7   
    8         -
use aws_smithy_http_client::tls::{TlsContext, TrustStore};
           8  +
use aws_smithy_http_client::tls::{TlsContext, TlsContextBuilder, TrustStore};
    9      9   
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
   10     10   
use std::fs;
   11     11   
use std::io;
   12     12   
use std::sync::Arc;
   13     13   
use tokio_rustls::{rustls, rustls::ServerConfig, TlsAcceptor};
   14     14   
   15         -
const SERVER_CERT_PATH: &str = "tests/server.pem";
   16         -
const SERVER_KEY_PATH: &str = "tests/server.rsa";
          15  +
/// PEM-backed identity used by integration-test TLS servers.
          16  +
///
          17  +
/// The certificate contains `localhost` and `sdktest.com` as subject
          18  +
/// alternative names.
          19  +
pub(crate) const SERVER_IDENTITY: TestTlsIdentity =
          20  +
    TestTlsIdentity::from_pem("tests/server.pem", "tests/server.rsa");
   17     21   
   18         -
pub(crate) fn server_tls_acceptor(alpn_protocols: &[&[u8]]) -> io::Result<TlsAcceptor> {
   19         -
    // Set the process-wide crypto provider used by the test server.
          22  +
/// Certificate and private-key material for one test TLS identity.
          23  +
#[derive(Clone, Copy, Debug)]
          24  +
pub(crate) struct TestTlsIdentity {
          25  +
    certificate_path: &'static str,
          26  +
    private_key_path: &'static str,
          27  +
}
          28  +
          29  +
impl TestTlsIdentity {
          30  +
    const fn from_pem(certificate_path: &'static str, private_key_path: &'static str) -> Self {
          31  +
        Self {
          32  +
            certificate_path,
          33  +
            private_key_path,
          34  +
        }
          35  +
    }
          36  +
          37  +
    /// Builds a server acceptor using this identity and ALPN protocol list.
          38  +
    pub(crate) fn acceptor(&self, alpn_protocols: &[&[u8]]) -> io::Result<TlsAcceptor> {
          39  +
        // The test server uses one process-wide rustls crypto provider.
   20     40   
        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
   21     41   
   22         -
    let certs = load_certs(SERVER_CERT_PATH)?;
   23         -
    let key = load_private_key(SERVER_KEY_PATH)?;
          42  +
        let certs = load_certs(self.certificate_path)?;
          43  +
        let key = load_private_key(self.private_key_path)?;
   24     44   
        let mut server_config = ServerConfig::builder()
   25     45   
            .with_no_client_auth()
   26     46   
            .with_single_cert(certs, key)
   27     47   
            .map_err(|err| error(err.to_string()))?;
   28     48   
        server_config.alpn_protocols = alpn_protocols
   29     49   
            .iter()
   30     50   
            .map(|protocol| protocol.to_vec())
   31     51   
            .collect();
   32     52   
   33     53   
        Ok(TlsAcceptor::from(Arc::new(server_config)))
   34         -
}
          54  +
    }
          55  +
          56  +
    /// Returns a client TLS context builder that trusts this identity.
          57  +
    pub(crate) fn client_context_builder(&self) -> TlsContextBuilder {
          58  +
        TlsContext::builder().with_trust_store(self.trust_store())
          59  +
    }
   35     60   
   36         -
pub(crate) fn server_tls_context() -> TlsContext {
   37         -
    TlsContext::builder()
   38         -
        .with_trust_store(server_trust_store())
          61  +
    /// Builds a client TLS context that trusts this identity.
          62  +
    pub(crate) fn client_context(&self) -> TlsContext {
          63  +
        self.client_context_builder()
   39     64   
            .build()
   40         -
        .expect("failed to build TlsContext with test server certificate")
   41         -
}
          65  +
            .expect("test TLS identity produces a valid client context")
          66  +
    }
   42     67   
   43         -
/// A [`TrustStore`] containing only the test server certificate.
   44         -
pub(crate) fn server_trust_store() -> TrustStore {
          68  +
    fn trust_store(&self) -> TrustStore {
   45     69   
        let pem_contents =
   46         -
        fs::read(SERVER_CERT_PATH).expect("failed to read server certificate for test TLS context");
          70  +
            fs::read(self.certificate_path).expect("failed to read test TLS identity certificate");
   47     71   
        TrustStore::empty().with_pem_certificate(pem_contents)
          72  +
    }
   48     73   
}
   49     74   
   50     75   
fn error(err: String) -> io::Error {
   51     76   
    io::Error::other(err)
   52     77   
}
   53     78   
   54     79   
fn load_certs(filename: &str) -> io::Result<Vec<CertificateDer<'static>>> {
   55     80   
    let certfile = fs::File::open(filename)
   56     81   
        .map_err(|err| error(format!("failed to open {filename}: {err}")))?;
   57     82   
    let mut reader = io::BufReader::new(certfile);

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

@@ -16,16 +75,108 @@
   36     36   
    SharedHttpClient::new(
   37     37   
        Client::from_partition(pool, partition).expect("declared partition should resolve"),
   38     38   
    )
   39     39   
}
   40     40   
   41     41   
/// Builds an operation connector with the shared test runtime components.
   42     42   
fn connector(client: &SharedHttpClient) -> SharedHttpConnector {
   43     43   
    test_client::connector(client)
   44     44   
}
   45     45   
          46  +
#[tokio::test]
          47  +
async fn custom_dns_resolver_is_used_for_explicit_partition_connections() {
          48  +
    const HOST: &str = "partition-dns.test";
          49  +
          50  +
    let harness = ConnectionTestHarness::builder()
          51  +
        .endpoint(
          52  +
            IP1,
          53  +
            Http1Script::responses([Http1Response::ok().body("custom dns")]),
          54  +
        )
          55  +
        .dns(HOST, [IP1])
          56  +
        .build()
          57  +
        .await
          58  +
        .expect("harness should start");
          59  +
    let partition = PartitionId::from_index(1);
          60  +
    let pool = ConnectionPool::builder()
          61  +
        .partitions([Partition::new(partition, TokioDriverSpawner::current())])
          62  +
        .dns_resolver(harness.dns_resolver())
          63  +
        .build_http()
          64  +
        .expect("valid pool");
          65  +
    let client = shared_client(&pool, partition);
          66  +
    let connector = connector(&client);
          67  +
    let url = format!("http://{HOST}:{}/custom-dns", harness.port());
          68  +
          69  +
    let (status, body) = test_client::get_and_collect(&connector, &url).await;
          70  +
    assert_eq!((status, body.as_slice()), (200, b"custom dns".as_slice()));
          71  +
    assert_eq!(1, harness.dns_lookup_count());
          72  +
          73  +
    drop(connector);
          74  +
    drop(client);
          75  +
    drop(pool);
          76  +
    harness.shutdown().await.expect("clean harness shutdown");
          77  +
}
          78  +
   46     79   
#[tokio::test]
   47     80   
async fn bounded_waiter_proceeds_after_the_active_h1_returns() {
   48     81   
    let body_gate = ManualGate::new();
   49     82   
    let harness = ConnectionTestHarness::builder()
   50     83   
        .endpoint(
   51     84   
            IP1,
   52     85   
            Http1Script::responses([
   53     86   
                Http1Response::ok().body_plan(BodyPlan::split_at_gate(
   54     87   
                    "first-",
   55     88   
                    body_gate.waiter(),

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

@@ -1,1 +114,85 @@
   21     21   
   22     22   
// This inline module form selectively includes only the `client` submodule from
   23     23   
// `tests/common/`. Using `mod common;` (which goes through `common/mod.rs`) would
   24     24   
// also pull in the `h2` and `tls` submodules, generating dead-code warnings under
   25     25   
// `--all-features` since those modules contain items only the h2 and TLS tests use.
   26     26   
mod common {
   27     27   
    pub(crate) mod client;
   28     28   
}
   29     29   
   30     30   
use aws_smithy_async::assert_elapsed;
   31         -
use aws_smithy_http_client::pool::{Client as PoolClient, ConnectionPool};
   32     31   
use aws_smithy_http_client::test_util::wire::connection::{
   33     32   
    BodyPlan, ConnectionCloseReason, ConnectionEvent, ConnectionId, ConnectionScript,
   34     33   
    ConnectionTestHarness, EndpointPlan, HarnessError, Http1Response, Http1Script, ManualGate,
   35     34   
    SocketScript,
   36     35   
};
   37         -
use aws_smithy_http_client::Builder;
   38     36   
use aws_smithy_runtime_api::client::connection::{
   39     37   
    CaptureSmithyConnection, ConnectionMetadata as SmithyConnectionMetadata,
   40     38   
};
   41     39   
use aws_smithy_runtime_api::client::http::{
   42     40   
    HttpConnectorSettings, SharedHttpClient, SharedHttpConnector,
   43     41   
};
   44     42   
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
   45     43   
use aws_smithy_types::body::SdkBody;
   46     44   
use aws_smithy_types::retry::ErrorKind;
   47     45   
use common::client as test_client;
   48         -
use common::client::{BackendConfig, HyperUtilLegacyPool};
          46  +
use common::client::{
          47  +
    BackendConfig, HttpClientBackend, HyperUtilLegacyPool, PartitionedConnectionPool,
          48  +
};
   49     49   
use http_body_util::BodyExt;
   50     50   
use std::collections::HashSet;
   51     51   
use std::net::{IpAddr, Ipv4Addr};
   52     52   
use std::time::Duration;
   53     53   
   54     54   
const IP1: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
   55     55   
   56         -
trait HttpClientBackend {
   57         -
    fn build(&self, config: BackendConfig) -> SharedHttpClient;
   58         -
}
   59         -
   60         -
impl HttpClientBackend for HyperUtilLegacyPool {
   61         -
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
   62         -
        let mut builder = Builder::new();
   63         -
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
   64         -
            builder = builder.pool_idle_timeout(pool_idle_timeout);
   65         -
        }
   66         -
        builder.build_http()
   67         -
    }
   68         -
}
   69         -
   70         -
#[derive(Clone, Copy, Debug)]
   71         -
struct PartitionedConnectionPool;
   72         -
   73         -
impl HttpClientBackend for PartitionedConnectionPool {
   74         -
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
   75         -
        let mut builder = ConnectionPool::builder();
   76         -
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
   77         -
            builder = builder.idle_timeout(pool_idle_timeout);
   78         -
        }
   79         -
        let pool = builder.build_http().expect("valid connection-pool config");
   80         -
        let client = PoolClient::new(&pool).expect("anonymous partition exists");
   81         -
        SharedHttpClient::new(client)
   82         -
    }
   83         -
}
   84         -
   85     56   
fn request_with_body(url: &str, body: &[u8]) -> HttpRequest {
   86     57   
    let mut request = HttpRequest::new(SdkBody::from(body.to_vec()));
   87     58   
    request.set_method("POST").expect("valid HTTP method");
   88     59   
    request.set_uri(url).expect("valid HTTP URI");
   89     60   
    request
   90     61   
        .headers_mut()
   91     62   
        .insert("content-length", body.len().to_string());
   92     63   
    request
   93     64   
}
   94     65   
@@ -230,201 +289,261 @@
  250    221   
                EndpointPlan::queue([
  251    222   
                    Http1Script::responses([Http1Response::ok().body("first")]),
  252    223   
                    Http1Script::responses([Http1Response::ok().body("second")]),
  253    224   
                ]),
  254    225   
            )
  255    226   
            .build()
  256    227   
            .await
  257    228   
            .expect("harness should start");
  258    229   
        let client = backend.build(BackendConfig {
  259    230   
            pool_idle_timeout: Some(idle_timeout),
         231  +
            ..Default::default()
  260    232   
        });
  261    233   
        let connector = test_client::connector(&client);
  262    234   
  263    235   
        let (status, body) =
  264    236   
            test_client::get_and_collect(&connector, &harness.endpoint_url()).await;
  265    237   
        assert_eq!((status, body.as_slice()), (200, b"first".as_slice()));
  266    238   
        let first_connection = http1_request_connection_ids(&harness)[0];
  267    239   
  268    240   
        harness
  269    241   
            .wait_for_event(test_client::WAIT, |event| {
@@ -298,270 +357,330 @@
  318    290   
                        "body",
  319    291   
                    )),
  320    292   
                    Http1Response::ok().body("second"),
  321    293   
                ]),
  322    294   
            )
  323    295   
            .build()
  324    296   
            .await
  325    297   
            .expect("harness should start");
  326    298   
        let client = backend.build(BackendConfig {
  327    299   
            pool_idle_timeout: Some(idle_timeout),
         300  +
            ..Default::default()
  328    301   
        });
  329    302   
        let connector = test_client::connector(&client);
  330    303   
  331    304   
        let first_response = test_client::send_request(
  332    305   
            &connector,
  333    306   
            HttpRequest::get(harness.endpoint_url()).expect("valid HTTP request"),
  334    307   
        )
  335    308   
        .await
  336    309   
        .expect("first request should return response headers");
  337    310   
@@ -2008,1981 +2106,2133 @@
 2028   2001   
    }
 2029   2002   
 2030   2003   
    #[tokio::test]
 2031   2004   
    async fn test_request_is_not_written_before_the_previous_response_completes_with_partitioned_connection_pool(
 2032   2005   
    ) {
 2033   2006   
        request_is_not_written_before_the_previous_response_completes(&PartitionedConnectionPool)
 2034   2007   
            .await;
 2035   2008   
    }
 2036   2009   
}
 2037   2010   
 2038         -
// A DNS resolver can only be installed through `Builder::build_with_resolver`, which is
 2039         -
// available once a TLS provider is selected, so these tests need a TLS feature even though
 2040         -
// they speak plaintext HTTP. `CryptoMode::Ring` requires `rustls-ring` specifically.
 2041         -
#[cfg(feature = "rustls-ring")]
        2011  +
// The legacy backend's custom-resolver constructor requires a TLS provider even though this
        2012  +
// contract speaks plaintext HTTP.
        2013  +
#[cfg(any(
        2014  +
    feature = "rustls-aws-lc",
        2015  +
    feature = "rustls-aws-lc-fips",
        2016  +
    feature = "rustls-ring",
        2017  +
    feature = "s2n-tls"
        2018  +
))]
 2042   2019   
mod dns_resolution {
 2043   2020   
    use super::*;
 2044         -
    use aws_smithy_http_client::tls;
        2021  +
    use aws_smithy_runtime_api::client::dns::SharedDnsResolver;
        2022  +
        2023  +
    /// A configured resolver supplies the address used for an origin connection.
        2024  +
    async fn custom_dns_resolver_is_used_for_origin(backend: &dyn HttpClientBackend) {
        2025  +
        const HOST: &str = "custom-dns.test";
        2026  +
        2027  +
        let harness = ConnectionTestHarness::builder()
        2028  +
            .endpoint(
        2029  +
                IP1,
        2030  +
                Http1Script::responses([Http1Response::ok().body("custom dns")]),
        2031  +
            )
        2032  +
            .dns(HOST, [IP1])
        2033  +
            .build()
        2034  +
            .await
        2035  +
            .expect("harness should start");
        2036  +
        let client = backend.build(BackendConfig {
        2037  +
            dns_resolver: Some(SharedDnsResolver::new(harness.dns_resolver())),
        2038  +
            ..Default::default()
        2039  +
        });
        2040  +
        let connector = test_client::connector(&client);
        2041  +
        let url = format!("http://{HOST}:{}/custom-dns", harness.port());
        2042  +
        2043  +
        let (status, body) = test_client::get_and_collect(&connector, &url).await;
        2044  +
        assert_eq!((status, body.as_slice()), (200, b"custom dns".as_slice()));
        2045  +
        assert_eq!(1, harness.dns_lookup_count());
        2046  +
        assert_eq!(
        2047  +
            vec![(
        2048  +
                "/custom-dns".to_string(),
        2049  +
                Some(format!("{HOST}:{}", harness.port()))
        2050  +
            )],
        2051  +
            harness.http_requests()
        2052  +
        );
        2053  +
        2054  +
        shutdown_harness(harness, connector, client)
        2055  +
            .await
        2056  +
            .expect("clean harness shutdown");
        2057  +
    }
        2058  +
        2059  +
    #[tokio::test]
        2060  +
    async fn test_custom_dns_resolver_is_used_for_origin_with_hyper_util_legacy_pool() {
        2061  +
        custom_dns_resolver_is_used_for_origin(&HyperUtilLegacyPool).await;
        2062  +
    }
        2063  +
        2064  +
    #[tokio::test]
        2065  +
    async fn test_custom_dns_resolver_is_used_for_origin_with_partitioned_connection_pool() {
        2066  +
        custom_dns_resolver_is_used_for_origin(&PartitionedConnectionPool).await;
        2067  +
    }
 2045   2068   
 2046   2069   
    /// A hostname with no DNS entry fails the request at resolution, before any TCP
 2047   2070   
    /// connection is attempted.
 2048         -
    async fn unresolvable_hostname_fails_before_connect() {
        2071  +
    async fn unresolvable_hostname_fails_before_connect(backend: &dyn HttpClientBackend) {
 2049   2072   
        // The endpoint is never connected to. It exists because a harness requires at
 2050   2073   
        // least one endpoint, and it supplies the port used to build the request URL.
 2051   2074   
        let harness = ConnectionTestHarness::builder()
 2052   2075   
            .endpoint(IP1, SocketScript::new().await_client_close())
 2053   2076   
            .build()
 2054   2077   
            .await
 2055   2078   
            .expect("harness should start");
 2056         -
        let client = Builder::new()
 2057         -
            .tls_provider(tls::Provider::Rustls(
 2058         -
                tls::rustls_provider::CryptoMode::Ring,
 2059         -
            ))
 2060         -
            .build_with_resolver(harness.dns_resolver());
        2079  +
        let client = backend.build(BackendConfig {
        2080  +
            dns_resolver: Some(SharedDnsResolver::new(harness.dns_resolver())),
        2081  +
            ..Default::default()
        2082  +
        });
 2061   2083   
        let connector = test_client::connector(&client);
 2062   2084   
 2063   2085   
        let url = format!("http://unknown.test:{}/", harness.port());
 2064   2086   
        let error = test_client::send_request(
 2065   2087   
            &connector,
 2066   2088   
            HttpRequest::get(&url).expect("valid HTTP request"),
 2067   2089   
        )
 2068   2090   
        .await
 2069   2091   
        .expect_err("an unresolvable hostname must fail the request");
 2070   2092   
        assert!(
 2071   2093   
            error.is_io(),
 2072   2094   
            "expected ConnectorError::io for a DNS failure, got {error:?}"
 2073   2095   
        );
 2074   2096   
        let chain = error_chain_display(&error);
 2075   2097   
        assert!(
 2076   2098   
            chain.contains("dns error"),
 2077   2099   
            "the error should identify DNS resolution as the cause, got: {chain}"
 2078   2100   
        );
 2079   2101   
 2080   2102   
        assert_eq!(
 2081   2103   
            harness.tcp_accepted_count(),
 2082   2104   
            0,
 2083   2105   
            "no TCP connection should be attempted when DNS resolution fails"
 2084   2106   
        );
 2085   2107   
        let lookups = harness
 2086   2108   
            .events()
 2087   2109   
            .into_iter()
 2088   2110   
            .filter(
 2089   2111   
                |event| matches!(event, ConnectionEvent::DnsLookup { hostname } if hostname == "unknown.test"),
 2090   2112   
            )
 2091   2113   
            .count();
 2092   2114   
        assert_eq!(
 2093   2115   
            lookups, 1,
 2094   2116   
            "the failed lookup should be recorded exactly once"
 2095   2117   
        );
 2096   2118   
 2097   2119   
        shutdown_harness(harness, connector, client)
 2098   2120   
            .await
 2099   2121   
            .expect("clean harness shutdown");
 2100   2122   
    }
 2101   2123   
 2102   2124   
    #[tokio::test]
 2103   2125   
    async fn test_unresolvable_hostname_fails_before_connect_with_hyper_util_legacy_pool() {
 2104         -
        unresolvable_hostname_fails_before_connect().await;
        2126  +
        unresolvable_hostname_fails_before_connect(&HyperUtilLegacyPool).await;
        2127  +
    }
        2128  +
        2129  +
    #[tokio::test]
        2130  +
    async fn test_unresolvable_hostname_fails_before_connect_with_partitioned_connection_pool() {
        2131  +
        unresolvable_hostname_fails_before_connect(&PartitionedConnectionPool).await;
 2105   2132   
    }
 2106   2133   
}

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

@@ -1,1 +130,81 @@
   11     11   
#![cfg(all(feature = "wire-mock", feature = "rustls-aws-lc"))]
   12     12   
   13     13   
mod common;
   14     14   
   15     15   
use aws_smithy_http_client::pool::{
   16     16   
    Client as PoolClient, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
   17     17   
    TokioDriverSpawner,
   18     18   
};
   19     19   
use aws_smithy_http_client::test_util::wire::connection::{ConnectionCloseReason, ManualGate};
   20     20   
use aws_smithy_http_client::tls;
   21         -
use aws_smithy_http_client::Builder;
   22     21   
use aws_smithy_runtime_api::client::connection::{
   23     22   
    CaptureSmithyConnection, ConnectionMetadata as SmithyConnectionMetadata,
   24     23   
};
   25     24   
use aws_smithy_runtime_api::client::http::{SharedHttpClient, SharedHttpConnector};
   26     25   
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
   27     26   
use bytes::Bytes;
   28     27   
use common::client as test_client;
   29         -
use common::client::{BackendConfig, HyperUtilLegacyPool};
          28  +
use common::client::{
          29  +
    BackendConfig, HttpsClientBackend, HyperUtilLegacyPool, PartitionedConnectionPool,
          30  +
};
   30     31   
use common::h2::{
   31     32   
    H2BodyPlan, H2ConnectionId, H2ConnectionPlan, H2ConnectionScript, H2Event, H2Response,
   32     33   
    H2StreamScript, H2TestServer,
   33     34   
};
   34     35   
use common::tls as test_tls;
   35     36   
use h2::Reason;
   36     37   
use http_body_util::BodyExt;
   37     38   
use std::error::Error;
   38     39   
   39         -
trait HttpsClientBackend {
   40         -
    fn build_https(
   41         -
        &self,
   42         -
        config: BackendConfig,
   43         -
        provider: tls::Provider,
   44         -
        tls_context: tls::TlsContext,
   45         -
    ) -> SharedHttpClient;
   46         -
}
   47         -
   48         -
impl HttpsClientBackend for HyperUtilLegacyPool {
   49         -
    fn build_https(
   50         -
        &self,
   51         -
        config: BackendConfig,
   52         -
        provider: tls::Provider,
   53         -
        tls_context: tls::TlsContext,
   54         -
    ) -> SharedHttpClient {
   55         -
        let mut builder = Builder::new();
   56         -
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
   57         -
            builder = builder.pool_idle_timeout(pool_idle_timeout);
   58         -
        }
   59         -
        builder
   60         -
            .tls_provider(provider)
   61         -
            .tls_context(tls_context)
   62         -
            .build_https()
   63         -
    }
   64         -
}
   65         -
   66         -
#[derive(Clone, Copy, Debug)]
   67         -
struct PartitionedConnectionPool;
   68         -
   69         -
impl HttpsClientBackend for PartitionedConnectionPool {
   70         -
    fn build_https(
   71         -
        &self,
   72         -
        config: BackendConfig,
   73         -
        provider: tls::Provider,
   74         -
        tls_context: tls::TlsContext,
   75         -
    ) -> SharedHttpClient {
   76         -
        let mut builder = ConnectionPool::builder().tls_provider(provider);
   77         -
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
   78         -
            builder = builder.idle_timeout(pool_idle_timeout);
   79         -
        }
   80         -
        let pool = builder
   81         -
            .tls_context(tls_context)
   82         -
            .build_https()
   83         -
            .expect("valid connection-pool config");
   84         -
        let client = PoolClient::new(&pool).expect("anonymous partition exists");
   85         -
        SharedHttpClient::new(client)
   86         -
    }
   87         -
}
   88         -
   89     40   
fn rustls_aws_lc() -> tls::Provider {
   90     41   
    tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)
   91     42   
}
   92     43   
   93     44   
fn h2_client_with_provider(
   94     45   
    backend: &dyn HttpsClientBackend,
   95     46   
    provider: tls::Provider,
   96     47   
) -> SharedHttpClient {
   97     48   
    backend.build_https(
   98     49   
        BackendConfig::default(),
   99     50   
        provider,
  100         -
        test_tls::server_tls_context(),
          51  +
        test_tls::SERVER_IDENTITY.client_context(),
  101     52   
    )
  102     53   
}
  103     54   
  104     55   
fn h2_client(backend: &dyn HttpsClientBackend) -> SharedHttpClient {
  105     56   
    h2_client_with_provider(backend, rustls_aws_lc())
  106     57   
}
  107     58   
  108     59   
fn stream_connection_ids(server: &H2TestServer, path: &str) -> Vec<H2ConnectionId> {
  109     60   
    server
  110     61   
        .events()
@@ -771,722 +833,785 @@
  791    742   
            finish: finished,
  792    743   
            complete: false,
  793    744   
        };
  794    745   
        (finish, SdkBody::from_body_1_x(body))
  795    746   
    }
  796    747   
  797    748   
    fn client_with_idle_timeout(backend: &dyn HttpsClientBackend) -> SharedHttpClient {
  798    749   
        backend.build_https(
  799    750   
            BackendConfig {
  800    751   
                pool_idle_timeout: Some(IDLE_TIMEOUT),
         752  +
                ..Default::default()
  801    753   
            },
  802    754   
            rustls_aws_lc(),
  803         -
            test_tls::server_tls_context(),
         755  +
            test_tls::SERVER_IDENTITY.client_context(),
  804    756   
        )
  805    757   
    }
  806    758   
  807    759   
    /// An idle H2 connection is closed after its configured timeout and then replaced.
  808    760   
    async fn idle_connection_is_evicted_after_timeout(backend: &dyn HttpsClientBackend) {
  809    761   
        let script =
  810    762   
            H2ConnectionScript::new().fallback(H2StreamScript::respond(H2Response::ok("ok")));
  811    763   
        let server = H2TestServer::builder()
  812    764   
            .connections(H2ConnectionPlan::queue([script.clone(), script]))
  813    765   
            .start()
@@ -998,950 +1058,1010 @@
 1018    970   
    use super::*;
 1019    971   
    use std::time::Duration;
 1020    972   
 1021    973   
    fn partitioned_clients(
 1022    974   
        scope: ConnectionReuseScope,
 1023    975   
    ) -> (ConnectionPool, SharedHttpConnector, SharedHttpConnector) {
 1024    976   
        let first = PartitionId::from_index(1);
 1025    977   
        let second = PartitionId::from_index(2);
 1026    978   
        let pool = ConnectionPool::builder()
 1027    979   
            .tls_provider(rustls_aws_lc())
 1028         -
            .tls_context(test_tls::server_tls_context())
         980  +
            .tls_context(test_tls::SERVER_IDENTITY.client_context())
 1029    981   
            .partitions([
 1030    982   
                Partition::new(first, TokioDriverSpawner::current()),
 1031    983   
                Partition::new(second, TokioDriverSpawner::current()),
 1032    984   
            ])
 1033    985   
            .connection_reuse_scope(scope)
 1034    986   
            .max_connections_per_host(1)
 1035    987   
            .build_https()
 1036    988   
            .expect("valid partitioned HTTPS pool");
 1037    989   
        let first_client = SharedHttpClient::new(
 1038    990   
            PoolClient::from_partition(&pool, first).expect("first partition should resolve"),

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

@@ -1,1 +1200,823 @@
    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         -
//! Integration tests for proxy functionality
           6  +
//! Proxy behavior contracts shared by the legacy and partitioned connection pools.
    7      7   
//!
    8         -
//! These tests verify that proxy configuration works end-to-end with real HTTP requests
    9         -
//! using mock proxy servers.
           8  +
//! Each contract is backend-neutral and has an explicit runner for every client
           9  +
//! implementation that must preserve the behavior.
          10  +
   10     11   
#![cfg(feature = "default-client")]
   11     12   
   12         -
use aws_smithy_async::time::SystemTimeSource;
          13  +
mod common {
          14  +
    #[allow(dead_code)]
          15  +
    pub(crate) mod client;
          16  +
    pub(crate) mod proxy;
          17  +
    #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
          18  +
    pub(crate) mod tls;
          19  +
}
          20  +
          21  +
use aws_smithy_http_client::proxy::ProxyConfig;
   13     22   
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
   14     23   
use aws_smithy_http_client::tls;
   15         -
use aws_smithy_http_client::{proxy::ProxyConfig, Connector};
   16         -
use aws_smithy_runtime_api::client::http::{
   17         -
    http_client_fn, HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpConnector,
   18         -
};
          24  +
use aws_smithy_runtime_api::box_error::BoxError;
          25  +
#[cfg(any(
          26  +
    feature = "rustls-aws-lc",
          27  +
    feature = "rustls-aws-lc-fips",
          28  +
    feature = "rustls-ring",
          29  +
    feature = "s2n-tls"
          30  +
))]
          31  +
use aws_smithy_runtime_api::client::dns::SharedDnsResolver;
          32  +
use aws_smithy_runtime_api::client::http::{HttpConnector, SharedHttpClient, SharedHttpConnector};
   19     33   
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
   20         -
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
   21         -
use base64::Engine;
   22         -
use http_1x::{Request, Response, StatusCode};
          34  +
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
          35  +
use common::client::HttpsClientBackend;
          36  +
use common::client::{
          37  +
    self as test_client, BackendConfig, HttpClientBackend, HyperUtilLegacyPool,
          38  +
    PartitionedConnectionPool,
          39  +
};
          40  +
use common::proxy::{basic_authorization, MockHttpServer};
          41  +
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
          42  +
use common::proxy::{MockConnectProxy, MockTlsOrigin};
          43  +
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
          44  +
use common::tls as test_tls;
          45  +
use http_1x::{Response, StatusCode};
   23     46   
use http_body_util::BodyExt;
   24         -
use hyper::body::Incoming;
   25         -
use hyper::service::service_fn;
   26         -
use hyper_util::rt::TokioIo;
   27         -
use std::collections::HashMap;
   28         -
use std::convert::Infallible;
   29         -
use std::net::SocketAddr;
   30         -
use std::sync::{Arc, Mutex};
          47  +
use std::future::Future;
   31     48   
use std::time::Duration;
   32         -
use tokio::net::TcpListener;
   33         -
use tokio::sync::oneshot;
   34         -
   35         -
// ================================================================================================
   36         -
// Test Utilities (Mock Proxy Server)
   37         -
// ================================================================================================
   38         -
   39         -
/// Mock HTTP server that acts as a proxy endpoint for testing
   40         -
#[derive(Debug)]
   41         -
struct MockProxyServer {
   42         -
    conn_count: Arc<()>,
   43         -
    addr: SocketAddr,
   44         -
    shutdown_tx: Option<oneshot::Sender<()>>,
   45         -
    request_log: Arc<Mutex<Vec<RecordedRequest>>>,
   46         -
}
   47         -
   48         -
/// A recorded request received by the mock proxy server
   49         -
#[derive(Debug, Clone)]
   50         -
struct RecordedRequest {
   51         -
    method: String,
   52         -
    uri: String,
   53         -
    headers: HashMap<String, String>,
   54         -
}
   55         -
   56         -
impl MockProxyServer {
   57         -
    /// Create a new mock proxy server with a custom request handler
   58         -
    async fn new<F>(handler: F) -> Self
   59         -
    where
   60         -
        F: Fn(RecordedRequest) -> Response<String> + Send + Sync + 'static,
   61         -
    {
   62         -
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
   63         -
        let addr = listener.local_addr().unwrap();
   64         -
        let (shutdown_tx, shutdown_rx) = oneshot::channel();
   65         -
        let request_log = Arc::new(Mutex::new(Vec::new()));
   66         -
        let request_log_clone = request_log.clone();
   67         -
        let conn_count = Arc::new(());
   68         -
        let server_conn_count = conn_count.clone();
   69         -
   70         -
        let handler = Arc::new(handler);
   71         -
   72         -
        tokio::spawn(async move {
   73         -
            let mut shutdown_rx = shutdown_rx;
   74         -
   75         -
            loop {
   76         -
                tokio::select! {
   77         -
                    result = listener.accept() => {
   78         -
                        match result {
   79         -
                            Ok((stream, _)) => {
   80         -
                                let io = TokioIo::new(stream);
   81         -
                                let handler = handler.clone();
   82         -
                                let request_log = request_log_clone.clone();
   83         -
   84         -
                                let stream_conn_count = server_conn_count.clone();
   85         -
                                tokio::spawn(async move {
   86         -
                                    let _stream_conn_count = stream_conn_count;
   87         -
                                    let service = service_fn(move |req: Request<Incoming>| {
   88         -
                                        let handler = handler.clone();
   89         -
                                        let request_log = request_log.clone();
   90         -
   91         -
                                        async move {
   92         -
                                            // Record the request
   93         -
                                            let recorded = RecordedRequest {
   94         -
                                                method: req.method().to_string(),
   95         -
                                                uri: req.uri().to_string(),
   96         -
                                                headers: req.headers().iter()
   97         -
                                                    .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
   98         -
                                                    .collect(),
   99         -
                                            };
  100         -
  101         -
                                            request_log.lock().unwrap().push(recorded.clone());
  102         -
  103         -
                                            // Call the handler
  104         -
                                            let response = handler(recorded);
  105         -
  106         -
                                            // Convert to hyper response
  107         -
                                            let (parts, body) = response.into_parts();
  108         -
                                            let hyper_response = Response::from_parts(parts, body);
  109         -
  110         -
                                            Ok::<_, Infallible>(hyper_response)
  111         -
                                        }
  112         -
                                    });
  113     49   
  114         -
                                    if let Err(err) = hyper::server::conn::http1::Builder::new()
  115         -
                                        .serve_connection(io, service)
  116         -
                                        .await
  117         -
                                    {
  118         -
                                        eprintln!("Mock proxy server connection error: {}", err);
  119         -
                                    }
  120         -
                                });
  121         -
                            }
  122         -
                            Err(_) => break,
  123         -
                        }
  124         -
                    }
  125         -
                    _ = &mut shutdown_rx => {
  126         -
                        break;
  127         -
                    }
  128         -
                }
  129         -
            }
  130         -
        });
  131         -
  132         -
        Self {
  133         -
            addr,
  134         -
            shutdown_tx: Some(shutdown_tx),
  135         -
            request_log,
  136         -
            conn_count,
  137         -
        }
  138         -
    }
          50  +
struct TestClient {
          51  +
    _client: SharedHttpClient,
          52  +
    connector: SharedHttpConnector,
          53  +
}
  139     54   
  140         -
    /// Return the number of active connections to this server
  141         -
    fn conn_count(&self) -> usize {
  142         -
        // 1 reference for the struct MockProxyServer, 1 reference for the
  143         -
        // socket task.
  144         -
        Arc::strong_count(&self.conn_count)
  145         -
            .checked_sub(2)
  146         -
            .expect("de-count 2 refs")
          55  +
fn http_client(backend: &dyn HttpClientBackend, config: BackendConfig) -> TestClient {
          56  +
    let client = backend.build(config);
          57  +
    let connector = test_client::connector(&client);
          58  +
    TestClient {
          59  +
        _client: client,
          60  +
        connector,
  147     61   
    }
          62  +
}
  148     63   
  149         -
    /// Create a simple mock proxy that returns a fixed response
  150         -
    async fn with_response(status: StatusCode, body: &str) -> Self {
  151         -
        let body = body.to_string();
  152         -
        Self::new(move |_req| {
  153         -
            Response::builder()
  154         -
                .status(status)
  155         -
                .body(body.clone())
  156         -
                .unwrap()
  157         -
        })
  158         -
        .await
          64  +
fn proxy_backend_config(proxy_config: ProxyConfig) -> BackendConfig {
          65  +
    BackendConfig {
          66  +
        proxy_config: Some(proxy_config),
          67  +
        ..Default::default()
  159     68   
    }
          69  +
}
  160     70   
  161         -
    /// Create a mock proxy that validates basic authentication
  162         -
    async fn with_auth_validation(expected_user: &str, expected_pass: &str) -> Self {
  163         -
        let expected_auth = format!(
  164         -
            "Basic {}",
  165         -
            base64::prelude::BASE64_STANDARD.encode(format!("{}:{}", expected_user, expected_pass))
          71  +
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
          72  +
fn https_client(
          73  +
    backend: &dyn HttpsClientBackend,
          74  +
    proxy_config: ProxyConfig,
          75  +
    provider: tls::Provider,
          76  +
    tls_context: tls::TlsContext,
          77  +
) -> TestClient {
          78  +
    let client = backend.build_https(
          79  +
        BackendConfig {
          80  +
            proxy_config: Some(proxy_config),
          81  +
            ..Default::default()
          82  +
        },
          83  +
        provider,
          84  +
        tls_context,
  166     85   
    );
  167         -
  168         -
        Self::new(move |req| {
  169         -
            if let Some(auth_header) = req.headers.get("proxy-authorization") {
  170         -
                if auth_header == &expected_auth {
  171         -
                    Response::builder()
  172         -
                        .status(StatusCode::OK)
  173         -
                        .body("authenticated".to_string())
  174         -
                        .unwrap()
  175         -
                } else {
  176         -
                    Response::builder()
  177         -
                        .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
  178         -
                        .body("invalid credentials".to_string())
  179         -
                        .unwrap()
  180         -
                }
  181         -
            } else {
  182         -
                Response::builder()
  183         -
                    .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
  184         -
                    .header("proxy-authenticate", "Basic realm=\"proxy\"")
  185         -
                    .body("authentication required".to_string())
  186         -
                    .unwrap()
  187         -
            }
  188         -
        })
  189         -
        .await
  190         -
    }
  191         -
  192         -
    /// Get the address this server is listening on
  193         -
    fn addr(&self) -> SocketAddr {
  194         -
        self.addr
          86  +
    let connector = test_client::connector(&client);
          87  +
    TestClient {
          88  +
        _client: client,
          89  +
        connector,
  195     90   
    }
          91  +
}
  196     92   
  197         -
    /// Get all requests received by this server
  198         -
    fn requests(&self) -> Vec<RecordedRequest> {
  199         -
        self.request_log.lock().unwrap().clone()
  200         -
    }
          93  +
async fn send_request(
          94  +
    client: &TestClient,
          95  +
    request: HttpRequest,
          96  +
) -> Result<(StatusCode, String), BoxError> {
          97  +
    let response = client.connector.call(request).await?;
          98  +
    let status = StatusCode::from_u16(response.status().as_u16())?;
          99  +
    let body = response.into_body().collect().await?.to_bytes();
         100  +
    Ok((status, String::from_utf8(body.to_vec())?))
  201    101   
}
  202    102   
  203         -
impl Drop for MockProxyServer {
  204         -
    fn drop(&mut self) {
  205         -
        if let Some(tx) = self.shutdown_tx.take() {
  206         -
            let _ = tx.send(());
  207         -
        }
  208         -
    }
         103  +
async fn get(client: &TestClient, uri: &str) -> Result<(StatusCode, String), BoxError> {
         104  +
    send_request(client, HttpRequest::get(uri)?).await
  209    105   
}
  210    106   
  211         -
/// Utility for running tests with specific environment variables
  212    107   
#[allow(clippy::await_holding_lock)]
  213    108   
async fn with_env_vars<F, Fut, R>(vars: &[(&str, &str)], test: F) -> R
  214    109   
where
  215    110   
    F: FnOnce() -> Fut,
  216         -
    Fut: std::future::Future<Output = R>,
         111  +
    Fut: Future<Output = R>,
  217    112   
{
  218         -
    // Use a static mutex to serialize environment variable tests
  219    113   
    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
  220         -
    let _guard = ENV_MUTEX.lock().unwrap();
  221         -
  222         -
    // Save original environment
  223         -
    let original_vars: Vec<_> = vars
         114  +
    let _guard = ENV_MUTEX.lock().expect("environment lock is not poisoned");
         115  +
    let previous: Vec<_> = vars
  224    116   
        .iter()
  225         -
        .map(|(key, _)| (*key, std::env::var(key)))
         117  +
        .map(|(name, _)| (*name, std::env::var(name)))
  226    118   
        .collect();
  227    119   
  228         -
    // Set test environment variables
  229         -
    for (key, value) in vars {
  230         -
        std::env::set_var(key, value);
         120  +
    for (name, value) in vars {
         121  +
        std::env::set_var(name, value);
  231    122   
    }
  232         -
  233         -
    // Run the test
  234    123   
    let result = test().await;
  235         -
  236         -
    // Restore original environment
  237         -
    for (key, original_value) in original_vars {
  238         -
        match original_value {
  239         -
            Ok(val) => std::env::set_var(key, val),
  240         -
            Err(_) => std::env::remove_var(key),
         124  +
    for (name, value) in previous {
         125  +
        match value {
         126  +
            Ok(value) => std::env::set_var(name, value),
         127  +
            Err(_) => std::env::remove_var(name),
  241    128   
        }
  242    129   
    }
  243         -
  244    130   
    result
  245    131   
}
  246    132   
  247         -
/// Helper function to make HTTP requests through a proxy-configured connector
  248         -
async fn make_http_request_through_proxy(
  249         -
    proxy_config: ProxyConfig,
  250         -
    target_url: &str,
  251         -
) -> Result<(StatusCode, String), Box<dyn std::error::Error + Send + Sync>> {
  252         -
    make_http_request_through_proxy_with_pool_timeout(
  253         -
        proxy_config,
  254         -
        Some(Duration::from_secs(90)),
  255         -
        target_url,
  256         -
    )
  257         -
    .await
  258         -
    .map(|(status, res, _client)| (status, res))
  259         -
}
  260         -
  261         -
/// Helper function to make HTTP requests through a proxy-configured connector
  262         -
async fn make_http_request_through_proxy_with_pool_timeout(
  263         -
    proxy_config: ProxyConfig,
  264         -
    pool_idle_timeout: Option<Duration>,
  265         -
    target_url: &str,
  266         -
) -> Result<(StatusCode, String, SharedHttpConnector), Box<dyn std::error::Error + Send + Sync>> {
  267         -
    // Create an HttpClient using http_client_fn with proxy-configured connector
  268         -
    let http_client = http_client_fn(move |settings, _components| {
  269         -
        let connector = Connector::builder()
  270         -
            .proxy_config(proxy_config.clone())
  271         -
            .pool_idle_timeout(pool_idle_timeout)
  272         -
            .connector_settings(settings.clone())
  273         -
            .build_http();
  274         -
  275         -
        aws_smithy_runtime_api::client::http::SharedHttpConnector::new(connector)
  276         -
    });
  277         -
  278         -
    // Set up runtime components (following smoke_test_client pattern)
  279         -
    let connector_settings = HttpConnectorSettings::builder().build();
  280         -
    let runtime_components = RuntimeComponentsBuilder::for_tests()
  281         -
        .with_time_source(Some(SystemTimeSource::new()))
  282         -
        .build()
  283         -
        .unwrap();
  284         -
  285         -
    // Get the HTTP connector from the client
  286         -
    let http_connector = http_client.http_connector(&connector_settings, &runtime_components);
  287         -
  288         -
    // Create and make the HTTP request
  289         -
    let request = HttpRequest::get(target_url)
  290         -
        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
  291         -
  292         -
    let response = http_connector.call(request).await?;
  293         -
  294         -
    // Extract status and body
  295         -
    let status = response.status();
  296         -
    let body_bytes = response.into_body().collect().await?.to_bytes();
  297         -
    let body_string = String::from_utf8(body_bytes.to_vec())?;
  298         -
  299         -
    Ok((status.into(), body_string, http_connector))
  300         -
}
  301         -
  302         -
// test the pool idle timeout. The test is in this file because it has a convenient
  303         -
// infrastructure for making a server that answers smithy requests.
  304         -
#[tokio::test(start_paused = false)]
  305         -
// can't set start_paused due to <https://github.com/hyperium/hyper/issues/3950>
  306         -
async fn test_http_proxy_connection_pool_timeout() {
  307         -
    const TIMEOUT: Duration = Duration::from_secs(10);
  308         -
  309         -
    // Create a mock proxy server that validates the request was routed through it
  310         -
    let mock_proxy = MockProxyServer::new(|req| {
  311         -
        // Validate that this looks like a proxy request
  312         -
        assert_eq!(req.method, "GET");
  313         -
        // For HTTP proxy, the URI should be the full target URL
  314         -
        assert_eq!(req.uri, "http://aws.amazon.com/api/data");
  315         -
  316         -
        // Return a successful response that we can identify
         133  +
async fn http_forward_proxy_uses_absolute_form(backend: &dyn HttpClientBackend) {
         134  +
    let proxy = MockHttpServer::new(|request| {
         135  +
        assert_eq!("GET", request.method);
         136  +
        assert_eq!("http://api.example.com/v1/data", request.uri);
         137  +
        assert_eq!(
         138  +
            Some(&"api.example.com".to_string()),
         139  +
            request.headers.get("host")
         140  +
        );
  317    141   
        Response::builder()
  318    142   
            .status(StatusCode::OK)
  319         -
            .body("proxied response from mock server".to_string())
  320         -
            .unwrap()
         143  +
            .body("proxied".to_string())
         144  +
            .expect("valid response")
  321    145   
    })
  322    146   
    .await;
  323         -
    // make sure that conn_count for an empty proxy is 0
  324         -
    assert_eq!(mock_proxy.conn_count(), 0);
  325         -
    tracing::info!("Start!");
  326         -
  327         -
    // Configure connector with HTTP proxy
  328         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr())).unwrap();
  329         -
  330         -
    // Make an HTTP request through the proxy - use safe domain
  331         -
    let target_url = "http://aws.amazon.com/api/data";
  332         -
    let start = tokio::time::Instant::now();
  333         -
    let result =
  334         -
        make_http_request_through_proxy_with_pool_timeout(proxy_config, Some(TIMEOUT), target_url)
  335         -
            .await;
  336         -
    // hold _connector to avoid the timer being dropped
  337         -
    let (status, body, _connector) = result.expect("HTTP request through proxy should succeed");
  338         -
    assert_eq!(status, StatusCode::OK);
  339         -
    assert_eq!(body, "proxied response from mock server");
         147  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr())).expect("valid proxy");
         148  +
    let client = http_client(backend, proxy_backend_config(config));
  340    149   
  341         -
    // Verify the mock proxy received the expected request
  342         -
    let requests = mock_proxy.requests();
  343         -
    assert_eq!(requests.len(), 1);
  344         -
    assert_eq!(requests[0].method, "GET");
  345         -
    assert_eq!(requests[0].uri, target_url);
         150  +
    assert_eq!(
         151  +
        (StatusCode::OK, "proxied".to_string()),
         152  +
        get(&client, "http://api.example.com/v1/data")
         153  +
            .await
         154  +
            .expect("proxy request succeeds")
         155  +
    );
         156  +
    assert_eq!(1, proxy.requests().len());
         157  +
}
  346    158   
  347         -
    // after making a request, conn count is 1
  348         -
    assert_eq!(mock_proxy.conn_count(), 1);
         159  +
#[tokio::test]
         160  +
async fn test_http_forward_proxy_uses_absolute_form_with_hyper_util_legacy_pool() {
         161  +
    http_forward_proxy_uses_absolute_form(&HyperUtilLegacyPool).await;
         162  +
}
  349    163   
  350         -
    // sleep 1 second below the idle timeout, conn count is still 1
  351         -
    tokio::time::sleep_until(start + TIMEOUT - Duration::from_secs(1)).await;
  352         -
    assert_eq!(mock_proxy.conn_count(), 1);
         164  +
#[tokio::test]
         165  +
async fn test_http_forward_proxy_uses_absolute_form_with_partitioned_connection_pool() {
         166  +
    http_forward_proxy_uses_absolute_form(&PartitionedConnectionPool).await;
         167  +
}
         168  +
         169  +
#[cfg(any(
         170  +
    feature = "rustls-aws-lc",
         171  +
    feature = "rustls-aws-lc-fips",
         172  +
    feature = "rustls-ring",
         173  +
    feature = "s2n-tls"
         174  +
))]
         175  +
async fn custom_dns_resolver_is_used_for_proxy_connections(backend: &dyn HttpClientBackend) {
         176  +
    const PROXY_HOST: &str = "proxy.test";
         177  +
         178  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "proxied through custom DNS").await;
         179  +
    let resolver = proxy.dns_resolver(PROXY_HOST);
         180  +
    let proxy_config = ProxyConfig::http(format!("http://{PROXY_HOST}:{}", proxy.addr().port()))
         181  +
        .expect("valid proxy URI");
         182  +
    let client = http_client(
         183  +
        backend,
         184  +
        BackendConfig {
         185  +
            dns_resolver: Some(SharedDnsResolver::new(resolver.clone())),
         186  +
            ..proxy_backend_config(proxy_config)
         187  +
        },
         188  +
    );
  353    189   
  354         -
    // sleep 2 more seconds, the connection should be disconnected, conn count should be 0
  355         -
    tokio::time::sleep(Duration::from_secs(3)).await;
  356         -
    assert_eq!(mock_proxy.conn_count(), 0);
         190  +
    assert_eq!(
         191  +
        (StatusCode::OK, "proxied through custom DNS".to_string()),
         192  +
        get(&client, "http://origin.test/custom-dns-proxy")
         193  +
            .await
         194  +
            .expect("proxy request succeeds")
         195  +
    );
         196  +
    assert_eq!(vec![PROXY_HOST.to_string()], resolver.lookups());
         197  +
    assert_eq!(
         198  +
        "http://origin.test/custom-dns-proxy",
         199  +
        proxy.requests()[0].uri
         200  +
    );
  357    201   
}
  358    202   
         203  +
#[cfg(any(
         204  +
    feature = "rustls-aws-lc",
         205  +
    feature = "rustls-aws-lc-fips",
         206  +
    feature = "rustls-ring",
         207  +
    feature = "s2n-tls"
         208  +
))]
  359    209   
#[tokio::test]
  360         -
async fn test_http_proxy_basic_request() {
  361         -
    // Create a mock proxy server that validates the request was routed through it
  362         -
    let mock_proxy = MockProxyServer::new(|req| {
  363         -
        // Validate that this looks like a proxy request
  364         -
        assert_eq!(req.method, "GET");
  365         -
        // For HTTP proxy, the URI should be the full target URL
  366         -
        assert_eq!(req.uri, "http://aws.amazon.com/api/data");
  367         -
  368         -
        // Return a successful response that we can identify
  369         -
        Response::builder()
  370         -
            .status(StatusCode::OK)
  371         -
            .body("proxied response from mock server".to_string())
  372         -
            .unwrap()
  373         -
    })
  374         -
    .await;
  375         -
  376         -
    // Configure connector with HTTP proxy
  377         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr())).unwrap();
         210  +
async fn test_custom_dns_resolver_is_used_for_proxy_connections_with_hyper_util_legacy_pool() {
         211  +
    custom_dns_resolver_is_used_for_proxy_connections(&HyperUtilLegacyPool).await;
         212  +
}
  378    213   
  379         -
    // Make an HTTP request through the proxy - use safe domain
  380         -
    let target_url = "http://aws.amazon.com/api/data";
  381         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
         214  +
#[cfg(any(
         215  +
    feature = "rustls-aws-lc",
         216  +
    feature = "rustls-aws-lc-fips",
         217  +
    feature = "rustls-ring",
         218  +
    feature = "s2n-tls"
         219  +
))]
         220  +
#[tokio::test]
         221  +
async fn test_custom_dns_resolver_is_used_for_proxy_connections_with_partitioned_connection_pool() {
         222  +
    custom_dns_resolver_is_used_for_proxy_connections(&PartitionedConnectionPool).await;
         223  +
}
  382    224   
  383         -
    let (status, body) = result.expect("HTTP request through proxy should succeed");
         225  +
async fn configured_proxy_authentication_is_applied(backend: &dyn HttpClientBackend) {
         226  +
    let proxy = MockHttpServer::with_auth_validation("testuser", "testpass").await;
         227  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
         228  +
        .expect("valid proxy")
         229  +
        .with_basic_auth("testuser", "testpass");
         230  +
    let client = http_client(backend, proxy_backend_config(config));
  384    231   
  385         -
    assert_eq!(status, StatusCode::OK);
  386         -
    assert_eq!(body, "proxied response from mock server");
         232  +
    assert_eq!(
         233  +
        (StatusCode::OK, "authenticated".to_string()),
         234  +
        get(&client, "http://service.test/protected")
         235  +
            .await
         236  +
            .expect("authenticated proxy request succeeds")
         237  +
    );
         238  +
    assert_eq!(
         239  +
        Some(&basic_authorization("testuser", "testpass")),
         240  +
        proxy.requests()[0].headers.get("proxy-authorization")
         241  +
    );
         242  +
}
  387    243   
  388         -
    // Verify the mock proxy received the expected request
  389         -
    let requests = mock_proxy.requests();
  390         -
    assert_eq!(requests.len(), 1);
  391         -
    assert_eq!(requests[0].method, "GET");
  392         -
    assert_eq!(requests[0].uri, target_url);
         244  +
#[tokio::test]
         245  +
async fn test_configured_proxy_authentication_with_hyper_util_legacy_pool() {
         246  +
    configured_proxy_authentication_is_applied(&HyperUtilLegacyPool).await;
  393    247   
}
  394    248   
  395    249   
#[tokio::test]
  396         -
async fn test_proxy_authentication() {
  397         -
    // Create a mock proxy that requires authentication
  398         -
    let mock_proxy = MockProxyServer::with_auth_validation("testuser", "testpass").await;
         250  +
async fn test_configured_proxy_authentication_with_partitioned_connection_pool() {
         251  +
    configured_proxy_authentication_is_applied(&PartitionedConnectionPool).await;
         252  +
}
  399    253   
  400         -
    // Configure connector with authenticated proxy
  401         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr()))
  402         -
        .unwrap()
  403         -
        .with_basic_auth("testuser", "testpass");
         254  +
async fn caller_proxy_authorization_is_preserved(backend: &dyn HttpClientBackend) {
         255  +
    let caller_authorization = basic_authorization("caller", "credentials");
         256  +
    let expected = caller_authorization.clone();
         257  +
    let proxy = MockHttpServer::new(move |request| {
         258  +
        assert_eq!(Some(&expected), request.headers.get("proxy-authorization"));
         259  +
        Response::builder()
         260  +
            .status(StatusCode::OK)
         261  +
            .body("caller authorization".to_string())
         262  +
            .expect("valid response")
         263  +
    })
         264  +
    .await;
         265  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
         266  +
        .expect("valid proxy")
         267  +
        .with_basic_auth("configured", "credentials");
         268  +
    let client = http_client(backend, proxy_backend_config(config));
         269  +
    let mut request = HttpRequest::get("http://service.test/caller-auth").expect("valid request");
         270  +
    request
         271  +
        .headers_mut()
         272  +
        .insert("proxy-authorization", caller_authorization);
  404    273   
  405         -
    // Make request through authenticated proxy - use safe domain
  406         -
    let target_url = "http://aws.amazon.com/protected/resource";
  407         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
         274  +
    assert_eq!(
         275  +
        (StatusCode::OK, "caller authorization".to_string()),
         276  +
        send_request(&client, request)
         277  +
            .await
         278  +
            .expect("caller authorization request succeeds")
         279  +
    );
         280  +
}
  408    281   
  409         -
    let (status, body) = result.expect("Authenticated proxy request should succeed");
         282  +
#[tokio::test]
         283  +
async fn test_caller_proxy_authorization_is_preserved_with_hyper_util_legacy_pool() {
         284  +
    caller_proxy_authorization_is_preserved(&HyperUtilLegacyPool).await;
         285  +
}
  410    286   
  411         -
    assert_eq!(status, StatusCode::OK);
  412         -
    assert_eq!(body, "authenticated");
         287  +
#[tokio::test]
         288  +
async fn test_caller_proxy_authorization_is_preserved_with_partitioned_connection_pool() {
         289  +
    caller_proxy_authorization_is_preserved(&PartitionedConnectionPool).await;
         290  +
}
  413    291   
  414         -
    // Verify the proxy received the request with correct auth
  415         -
    let requests = mock_proxy.requests();
  416         -
    assert_eq!(requests.len(), 1);
         292  +
async fn proxy_url_authentication_is_applied(backend: &dyn HttpClientBackend) {
         293  +
    let proxy = MockHttpServer::with_auth_validation("urluser", "urlpass").await;
         294  +
    let config =
         295  +
        ProxyConfig::http(format!("http://urluser:urlpass@{}", proxy.addr())).expect("valid proxy");
         296  +
    let client = http_client(backend, proxy_backend_config(config));
  417    297   
  418         -
    let expected_auth = format!(
  419         -
        "Basic {}",
  420         -
        base64::prelude::BASE64_STANDARD.encode("testuser:testpass")
         298  +
    assert_eq!(
         299  +
        StatusCode::OK,
         300  +
        get(&client, "http://service.test/url-auth")
         301  +
            .await
         302  +
            .expect("URL-authenticated request succeeds")
         303  +
            .0
  421    304   
    );
  422    305   
    assert_eq!(
  423         -
        requests[0].headers.get("proxy-authorization"),
  424         -
        Some(&expected_auth)
         306  +
        Some(&basic_authorization("urluser", "urlpass")),
         307  +
        proxy.requests()[0].headers.get("proxy-authorization")
  425    308   
    );
  426    309   
}
  427    310   
  428         -
/// Tests URL-embedded proxy authentication (http://user:pass@proxy.com format)
  429         -
/// Verifies that credentials in the proxy URL are properly extracted and used
  430    311   
#[tokio::test]
  431         -
async fn test_proxy_url_embedded_auth() {
  432         -
    let mock_proxy = MockProxyServer::with_auth_validation("urluser", "urlpass").await;
  433         -
  434         -
    // Configure proxy with credentials embedded in URL
  435         -
    let proxy_url = format!("http://urluser:urlpass@{}", mock_proxy.addr());
  436         -
    let proxy_config = ProxyConfig::http(proxy_url).unwrap();
  437         -
  438         -
    // Make request through proxy with URL-embedded auth
  439         -
    let target_url = "http://aws.amazon.com/api/test";
  440         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
         312  +
async fn test_proxy_url_authentication_with_hyper_util_legacy_pool() {
         313  +
    proxy_url_authentication_is_applied(&HyperUtilLegacyPool).await;
         314  +
}
  441    315   
  442         -
    let (status, body) = result.expect("URL-embedded auth proxy request should succeed");
  443         -
    assert_eq!(status, StatusCode::OK);
  444         -
    assert_eq!(body, "authenticated");
         316  +
#[tokio::test]
         317  +
async fn test_proxy_url_authentication_with_partitioned_connection_pool() {
         318  +
    proxy_url_authentication_is_applied(&PartitionedConnectionPool).await;
         319  +
}
  445    320   
  446         -
    // Verify the proxy received the request with correct auth
  447         -
    let requests = mock_proxy.requests();
  448         -
    assert_eq!(requests.len(), 1);
         321  +
async fn proxy_url_authentication_precedes_configured_authentication(
         322  +
    backend: &dyn HttpClientBackend,
         323  +
) {
         324  +
    let proxy = MockHttpServer::with_auth_validation("urluser", "urlpass").await;
         325  +
    let config = ProxyConfig::http(format!("http://urluser:urlpass@{}", proxy.addr()))
         326  +
        .expect("valid proxy")
         327  +
        .with_basic_auth("configured", "credentials");
         328  +
    let client = http_client(backend, proxy_backend_config(config));
  449    329   
  450         -
    let expected_auth = format!(
  451         -
        "Basic {}",
  452         -
        base64::prelude::BASE64_STANDARD.encode("urluser:urlpass")
         330  +
    assert_eq!(
         331  +
        StatusCode::OK,
         332  +
        get(&client, "http://service.test/auth-precedence")
         333  +
            .await
         334  +
            .expect("proxy request succeeds")
         335  +
            .0
  453    336   
    );
  454    337   
    assert_eq!(
  455         -
        requests[0].headers.get("proxy-authorization"),
  456         -
        Some(&expected_auth)
         338  +
        Some(&basic_authorization("urluser", "urlpass")),
         339  +
        proxy.requests()[0].headers.get("proxy-authorization")
  457    340   
    );
  458    341   
}
  459    342   
  460         -
/// Tests authentication precedence: URL-embedded credentials should take precedence over programmatic auth
  461         -
/// Verifies that when both URL auth and with_basic_auth() are provided, URL auth wins
  462    343   
#[tokio::test]
  463         -
async fn test_proxy_auth_precedence() {
  464         -
    let mock_proxy = MockProxyServer::with_auth_validation("urluser", "urlpass").await;
  465         -
  466         -
    // Configure proxy with URL-embedded auth AND programmatic auth
  467         -
    // URL auth should take precedence
  468         -
    let proxy_url = format!("http://urluser:urlpass@{}", mock_proxy.addr());
  469         -
    let proxy_config = ProxyConfig::http(proxy_url)
  470         -
        .unwrap()
  471         -
        .with_basic_auth("programmatic", "auth"); // This should be ignored
  472         -
  473         -
    // Make request - should use URL-embedded auth, not programmatic auth
  474         -
    let target_url = "http://aws.amazon.com/precedence/test";
  475         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
  476         -
  477         -
    let (status, body) = result.expect("Auth precedence test should succeed");
  478         -
    assert_eq!(status, StatusCode::OK);
  479         -
    assert_eq!(body, "authenticated");
  480         -
  481         -
    // Verify the proxy received the request with URL-embedded auth (not programmatic)
  482         -
    let requests = mock_proxy.requests();
  483         -
    assert_eq!(requests.len(), 1);
  484         -
  485         -
    let expected_auth = format!(
  486         -
        "Basic {}",
  487         -
        base64::prelude::BASE64_STANDARD.encode("urluser:urlpass")
  488         -
    );
  489         -
    assert_eq!(
  490         -
        requests[0].headers.get("proxy-authorization"),
  491         -
        Some(&expected_auth)
  492         -
    );
         344  +
async fn test_proxy_url_authentication_precedence_with_hyper_util_legacy_pool() {
         345  +
    proxy_url_authentication_precedes_configured_authentication(&HyperUtilLegacyPool).await;
  493    346   
}
  494    347   
  495    348   
#[tokio::test]
  496         -
async fn test_proxy_from_environment_variables() {
  497         -
    let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "env proxy response").await;
         349  +
async fn test_proxy_url_authentication_precedence_with_partitioned_connection_pool() {
         350  +
    proxy_url_authentication_precedes_configured_authentication(&PartitionedConnectionPool).await;
         351  +
}
  498    352   
         353  +
async fn environment_proxy_is_used(backend: &dyn HttpClientBackend) {
         354  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "environment proxy").await;
         355  +
    let proxy_uri = format!("http://{}", proxy.addr());
  499    356   
    with_env_vars(
  500    357   
        &[
  501         -
            ("HTTP_PROXY", &format!("http://{}", mock_proxy.addr())),
         358  +
            ("HTTP_PROXY", &proxy_uri),
  502    359   
            ("NO_PROXY", "localhost,127.0.0.1"),
  503    360   
        ],
  504    361   
        || async {
  505         -
            // Create connector with environment-based proxy config
  506         -
            let proxy_config = ProxyConfig::from_env();
  507         -
  508         -
            // Make request through environment-configured proxy
  509         -
            let target_url = "http://aws.amazon.com/v1/data";
  510         -
            let result = make_http_request_through_proxy(proxy_config, target_url).await;
  511         -
  512         -
            let (status, body) = result.expect("Environment proxy request should succeed");
  513         -
  514         -
            assert_eq!(status, StatusCode::OK);
  515         -
            assert_eq!(body, "env proxy response");
  516         -
  517         -
            // Verify the proxy received the request
  518         -
            let requests = mock_proxy.requests();
  519         -
            assert_eq!(requests.len(), 1);
  520         -
            assert_eq!(requests[0].uri, target_url);
         362  +
            let client = http_client(backend, proxy_backend_config(ProxyConfig::from_env()));
         363  +
            assert_eq!(
         364  +
                (StatusCode::OK, "environment proxy".to_string()),
         365  +
                get(&client, "http://service.test/environment")
         366  +
                    .await
         367  +
                    .expect("environment proxy request succeeds")
         368  +
            );
  521    369   
        },
  522    370   
    )
  523    371   
    .await;
         372  +
    assert_eq!("http://service.test/environment", proxy.requests()[0].uri);
  524    373   
}
  525    374   
  526         -
/// Tests that NO_PROXY bypass rules work correctly
  527         -
/// Verifies that requests to bypassed hosts do not go through the proxy
  528    375   
#[tokio::test]
  529         -
async fn test_no_proxy_bypass_rules() {
  530         -
    let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "should not reach here").await;
  531         -
  532         -
    // Create a second mock server that will act as the "direct" target
  533         -
    let direct_server = MockProxyServer::with_response(StatusCode::OK, "direct connection").await;
  534         -
  535         -
    // Configure proxy with NO_PROXY rules that include the direct server's address
  536         -
    // Use just the IP address for the NO_PROXY rule
  537         -
    let direct_ip = "127.0.0.1";
  538         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr()))
  539         -
        .unwrap()
  540         -
        .no_proxy(direct_ip);
  541         -
  542         -
    // Make request to the direct server (should bypass proxy due to NO_PROXY rule)
  543         -
    let result = make_http_request_through_proxy(
  544         -
        proxy_config,
  545         -
        &format!("http://{}/test", direct_server.addr()),
  546         -
    )
  547         -
    .await;
         376  +
async fn test_environment_proxy_with_hyper_util_legacy_pool() {
         377  +
    environment_proxy_is_used(&HyperUtilLegacyPool).await;
         378  +
}
  548    379   
  549         -
    let (status, body) = result.expect("Direct connection should succeed");
  550         -
    assert_eq!(status, StatusCode::OK);
  551         -
    assert_eq!(body, "direct connection");
         380  +
#[tokio::test]
         381  +
async fn test_environment_proxy_with_partitioned_connection_pool() {
         382  +
    environment_proxy_is_used(&PartitionedConnectionPool).await;
         383  +
}
  552    384   
  553         -
    // Verify the mock proxy received no requests (bypassed)
  554         -
    let proxy_requests = mock_proxy.requests();
  555         -
    assert_eq!(
  556         -
        proxy_requests.len(),
  557         -
        0,
  558         -
        "Proxy should not have received any requests due to NO_PROXY bypass"
  559         -
    );
         385  +
async fn no_proxy_bypasses_proxy(backend: &dyn HttpClientBackend) {
         386  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
         387  +
    let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
         388  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
         389  +
        .expect("valid proxy")
         390  +
        .no_proxy("127.0.0.1");
         391  +
    let client = http_client(backend, proxy_backend_config(config));
  560    392   
  561         -
    // Verify the direct server received the request
  562         -
    let direct_requests = direct_server.requests();
  563    393   
    assert_eq!(
  564         -
        direct_requests.len(),
  565         -
        1,
  566         -
        "Direct server should have received the request"
         394  +
        (StatusCode::OK, "direct".to_string()),
         395  +
        get(&client, &format!("http://{}/bypass", origin.addr()))
         396  +
            .await
         397  +
            .expect("direct request succeeds")
  567    398   
    );
         399  +
    assert!(proxy.requests().is_empty());
         400  +
    assert_eq!("/bypass", origin.requests()[0].uri);
  568    401   
}
  569    402   
  570         -
/// Tests that disabled proxy configuration results in direct connections
  571         -
/// Verifies that ProxyConfig::disabled() bypasses all proxy logic
  572    403   
#[tokio::test]
  573         -
async fn test_proxy_disabled() {
  574         -
    // Create a direct target server
  575         -
    let direct_server = MockProxyServer::with_response(StatusCode::OK, "direct connection").await;
  576         -
  577         -
    // Create a disabled proxy configuration
  578         -
    let proxy_config = ProxyConfig::disabled();
         404  +
async fn test_no_proxy_bypass_with_hyper_util_legacy_pool() {
         405  +
    no_proxy_bypasses_proxy(&HyperUtilLegacyPool).await;
         406  +
}
  579    407   
  580         -
    // Make request with disabled proxy (should go direct to our mock server)
  581         -
    let result = make_http_request_through_proxy(
  582         -
        proxy_config,
  583         -
        &format!("http://{}/get", direct_server.addr()),
  584         -
    )
  585         -
    .await;
         408  +
#[tokio::test]
         409  +
async fn test_no_proxy_bypass_with_partitioned_connection_pool() {
         410  +
    no_proxy_bypasses_proxy(&PartitionedConnectionPool).await;
         411  +
}
  586    412   
  587         -
    let (status, body) = result.expect("Direct connection should succeed");
  588         -
    assert_eq!(status, StatusCode::OK);
  589         -
    assert_eq!(body, "direct connection");
         413  +
async fn disabled_proxy_uses_direct_origin_form(backend: &dyn HttpClientBackend) {
         414  +
    let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
         415  +
    let client = http_client(backend, proxy_backend_config(ProxyConfig::disabled()));
  590    416   
  591         -
    // Verify the direct server received the request
  592         -
    let requests = direct_server.requests();
  593    417   
    assert_eq!(
  594         -
        requests.len(),
  595         -
        1,
  596         -
        "Direct server should have received the request"
  597         -
    );
  598         -
    assert_eq!(requests[0].method, "GET");
  599         -
    // For direct connections, the URI might be just the path part
  600         -
    assert!(
  601         -
        requests[0].uri == format!("http://{}/get", direct_server.addr())
  602         -
            || requests[0].uri == "/get",
  603         -
        "URI should be either full URL or path, got: {}",
  604         -
        requests[0].uri
         418  +
        (StatusCode::OK, "direct".to_string()),
         419  +
        get(&client, &format!("http://{}/direct", origin.addr()))
         420  +
            .await
         421  +
            .expect("direct request succeeds")
  605    422   
    );
         423  +
    assert_eq!("/direct", origin.requests()[0].uri);
  606    424   
}
  607    425   
  608         -
/// Tests HTTPS-only proxy configuration
  609         -
/// Verifies that HTTP requests bypass HTTPS-only proxies
  610    426   
#[tokio::test]
  611         -
async fn test_https_proxy_configuration() {
  612         -
    let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "https proxy response").await;
  613         -
  614         -
    // Create a direct target server for HTTP requests
  615         -
    let direct_server =
  616         -
        MockProxyServer::with_response(StatusCode::OK, "direct http connection").await;
  617         -
  618         -
    // Configure HTTPS-only proxy
  619         -
    let proxy_config = ProxyConfig::https(format!("http://{}", mock_proxy.addr())).unwrap();
  620         -
  621         -
    // Test: HTTP request should NOT go through HTTPS-only proxy, should go direct
  622         -
    let target_url = format!("http://{}/api", direct_server.addr());
  623         -
    let result = make_http_request_through_proxy(proxy_config.clone(), &target_url).await;
         427  +
async fn test_disabled_proxy_uses_direct_origin_form_with_hyper_util_legacy_pool() {
         428  +
    disabled_proxy_uses_direct_origin_form(&HyperUtilLegacyPool).await;
         429  +
}
  624    430   
  625         -
    // The HTTP request should succeed by going directly to our mock server
  626         -
    let (status, body) = result.expect("HTTP request should succeed via direct connection");
  627         -
    assert_eq!(status, StatusCode::OK);
  628         -
    assert_eq!(body, "direct http connection");
         431  +
#[tokio::test]
         432  +
async fn test_disabled_proxy_uses_direct_origin_form_with_partitioned_connection_pool() {
         433  +
    disabled_proxy_uses_direct_origin_form(&PartitionedConnectionPool).await;
         434  +
}
  629    435   
  630         -
    // Verify the HTTPS-only proxy received no requests
  631         -
    let proxy_requests = mock_proxy.requests();
  632         -
    assert_eq!(
  633         -
        proxy_requests.len(),
  634         -
        0,
  635         -
        "HTTP request should not go through HTTPS-only proxy"
  636         -
    );
         436  +
async fn https_only_proxy_bypasses_http(backend: &dyn HttpClientBackend) {
         437  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
         438  +
    let origin = MockHttpServer::with_response(StatusCode::OK, "direct HTTP").await;
         439  +
    let config =
         440  +
        ProxyConfig::https(format!("http://{}", proxy.addr())).expect("valid proxy configuration");
         441  +
    let client = http_client(backend, proxy_backend_config(config));
  637    442   
  638         -
    // Verify the direct server received the request
  639         -
    let direct_requests = direct_server.requests();
  640    443   
    assert_eq!(
  641         -
        direct_requests.len(),
  642         -
        1,
  643         -
        "Direct server should have received the HTTP request"
         444  +
        (StatusCode::OK, "direct HTTP".to_string()),
         445  +
        get(&client, &format!("http://{}/http-only", origin.addr()))
         446  +
            .await
         447  +
            .expect("direct HTTP request succeeds")
  644    448   
    );
         449  +
    assert!(proxy.requests().is_empty());
  645    450   
}
  646    451   
  647         -
/// Tests all-traffic proxy configuration
  648         -
/// Verifies that both HTTP and HTTPS requests go through all-traffic proxies
  649    452   
#[tokio::test]
  650         -
async fn test_all_traffic_proxy() {
  651         -
    let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "all traffic proxy").await;
  652         -
  653         -
    // Configure proxy for all traffic
  654         -
    let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr())).unwrap();
         453  +
async fn test_https_only_proxy_bypasses_http_with_hyper_util_legacy_pool() {
         454  +
    https_only_proxy_bypasses_http(&HyperUtilLegacyPool).await;
         455  +
}
  655    456   
  656         -
    // HTTP request should go through the proxy
  657         -
    let target_url = "http://aws.amazon.com/api/endpoint";
  658         -
    let result = make_http_request_through_proxy(proxy_config.clone(), target_url).await;
         457  +
#[tokio::test]
         458  +
async fn test_https_only_proxy_bypasses_http_with_partitioned_connection_pool() {
         459  +
    https_only_proxy_bypasses_http(&PartitionedConnectionPool).await;
         460  +
}
  659    461   
  660         -
    let (status, body) = result.expect("HTTP request through all-traffic proxy should succeed");
  661         -
    assert_eq!(status, StatusCode::OK);
  662         -
    assert_eq!(body, "all traffic proxy");
         462  +
async fn all_traffic_proxy_forwards_http(backend: &dyn HttpClientBackend) {
         463  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "all traffic").await;
         464  +
    let config = ProxyConfig::all(format!("http://{}", proxy.addr())).expect("valid proxy");
         465  +
    let client = http_client(backend, proxy_backend_config(config));
  663    466   
  664         -
    // Verify the proxy received the HTTP request
  665         -
    let requests = mock_proxy.requests();
  666    467   
    assert_eq!(
  667         -
        requests.len(),
  668         -
        1,
  669         -
        "Proxy should have received exactly one request"
         468  +
        (StatusCode::OK, "all traffic".to_string()),
         469  +
        get(&client, "http://service.test/all")
         470  +
            .await
         471  +
            .expect("all-traffic proxy request succeeds")
  670    472   
    );
  671         -
    assert_eq!(requests[0].method, "GET");
  672         -
    assert_eq!(requests[0].uri, target_url);
         473  +
    assert_eq!("http://service.test/all", proxy.requests()[0].uri);
  673    474   
}
  674    475   
  675         -
/// Tests proxy connection failure handling
  676         -
/// Verifies that unreachable proxy servers result in appropriate connection errors
  677    476   
#[tokio::test]
  678         -
async fn test_proxy_connection_failure() {
  679         -
    // Configure proxy pointing to non-existent server
  680         -
    let proxy_config = ProxyConfig::http("http://127.0.0.1:1").unwrap(); // Port 1 should be unavailable
         477  +
async fn test_all_traffic_proxy_forwards_http_with_hyper_util_legacy_pool() {
         478  +
    all_traffic_proxy_forwards_http(&HyperUtilLegacyPool).await;
         479  +
}
  681    480   
  682         -
    // Make request through non-existent proxy - use a safe domain that won't cause issues
  683         -
    let target_url = "http://aws.amazon.com/api/test";
  684         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
         481  +
#[tokio::test]
         482  +
async fn test_all_traffic_proxy_forwards_http_with_partitioned_connection_pool() {
         483  +
    all_traffic_proxy_forwards_http(&PartitionedConnectionPool).await;
         484  +
}
  685    485   
  686         -
    // The request should fail with a connection error
         486  +
async fn unreachable_proxy_fails(backend: &dyn HttpClientBackend) {
         487  +
    let config = ProxyConfig::http("http://127.0.0.1:1").expect("valid proxy");
         488  +
    let client = http_client(backend, proxy_backend_config(config));
  687    489   
    assert!(
  688         -
        result.is_err(),
  689         -
        "Request should fail when proxy is unreachable"
         490  +
        get(&client, "http://service.test/unreachable")
         491  +
            .await
         492  +
            .is_err(),
         493  +
        "an unreachable proxy must fail the request"
  690    494   
    );
         495  +
}
  691    496   
  692         -
    let error = result.unwrap_err();
  693         -
    let error_msg = error.to_string().to_lowercase();
  694         -
  695         -
    // Verify it's a connection-related error (not a different kind of error)
  696         -
    assert!(
  697         -
        error_msg.contains("connection")
  698         -
            || error_msg.contains("refused")
  699         -
            || error_msg.contains("unreachable")
  700         -
            || error_msg.contains("timeout")
  701         -
            || error_msg.contains("connect")
  702         -
            || error_msg.contains("io error"), // Include generic IO errors
  703         -
        "Error should be connection-related, got: {}",
  704         -
        error
  705         -
    );
         497  +
#[tokio::test]
         498  +
async fn test_unreachable_proxy_fails_with_hyper_util_legacy_pool() {
         499  +
    unreachable_proxy_fails(&HyperUtilLegacyPool).await;
  706    500   
}
  707    501   
  708         -
/// Tests proxy authentication failure handling
  709         -
/// Verifies that incorrect proxy credentials result in 407 Proxy Authentication Required
  710    502   
#[tokio::test]
  711         -
async fn test_proxy_authentication_failure() {
  712         -
    let mock_proxy = MockProxyServer::with_auth_validation("correct", "password").await;
         503  +
async fn test_unreachable_proxy_fails_with_partitioned_connection_pool() {
         504  +
    unreachable_proxy_fails(&PartitionedConnectionPool).await;
         505  +
}
  713    506   
  714         -
    // Configure proxy with wrong credentials
  715         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr()))
  716         -
        .unwrap()
         507  +
async fn incorrect_proxy_authentication_returns_407(backend: &dyn HttpClientBackend) {
         508  +
    let proxy = MockHttpServer::with_auth_validation("correct", "password").await;
         509  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
         510  +
        .expect("valid proxy")
  717    511   
        .with_basic_auth("wrong", "credentials");
         512  +
    let client = http_client(backend, proxy_backend_config(config));
  718    513   
  719         -
    // Make request with wrong credentials - use safe domain
  720         -
    let target_url = "http://aws.amazon.com/secure/api";
  721         -
    let result = make_http_request_through_proxy(proxy_config, target_url).await;
  722         -
  723         -
    // The request should return 407 Proxy Authentication Required
  724         -
    let (status, _body) = result.expect("Request should complete (even with auth failure)");
  725         -
    assert_eq!(status, StatusCode::PROXY_AUTHENTICATION_REQUIRED);
  726         -
  727         -
    // Verify the proxy received the request (even though auth failed)
  728         -
    let requests = mock_proxy.requests();
  729         -
    assert_eq!(requests.len(), 1, "Proxy should have received the request");
  730         -
  731         -
    // Verify the wrong credentials were sent
  732         -
    let expected_wrong_auth = format!(
  733         -
        "Basic {}",
  734         -
        base64::prelude::BASE64_STANDARD.encode("wrong:credentials")
         514  +
    assert_eq!(
         515  +
        StatusCode::PROXY_AUTHENTICATION_REQUIRED,
         516  +
        get(&client, "http://service.test/denied")
         517  +
            .await
         518  +
            .expect("proxy response is returned")
         519  +
            .0
  735    520   
    );
  736    521   
    assert_eq!(
  737         -
        requests[0].headers.get("proxy-authorization"),
  738         -
        Some(&expected_wrong_auth)
         522  +
        Some(&basic_authorization("wrong", "credentials")),
         523  +
        proxy.requests()[0].headers.get("proxy-authorization")
  739    524   
    );
  740    525   
}
  741    526   
  742         -
/// Tests that ProxyConfig::disabled() overrides environment proxy settings
  743         -
/// Verifies that explicit proxy disabling takes precedence over environment variables
  744    527   
#[tokio::test]
  745         -
async fn test_explicit_proxy_disable_overrides_environment() {
  746         -
    let mock_proxy = MockProxyServer::new(|_req| {
  747         -
        panic!("Request should not reach proxy when explicitly disabled");
  748         -
    })
  749         -
    .await;
  750         -
  751         -
    // Create a direct target server
  752         -
    let direct_server = MockProxyServer::with_response(StatusCode::OK, "direct connection").await;
  753         -
  754         -
    with_env_vars(
  755         -
        &[("HTTP_PROXY", &format!("http://{}", mock_proxy.addr()))],
  756         -
        || async {
  757         -
            // Create connector with explicitly disabled proxy (should override environment)
  758         -
            let proxy_config = ProxyConfig::disabled();
  759         -
  760         -
            // Make request - should go direct despite HTTP_PROXY environment variable
  761         -
            let target_url = format!("http://{}/test", direct_server.addr());
  762         -
            let result = make_http_request_through_proxy(proxy_config, &target_url).await;
  763         -
  764         -
            let (status, body) = result.expect("Direct connection should succeed");
  765         -
            assert_eq!(status, StatusCode::OK);
  766         -
            assert_eq!(body, "direct connection");
         528  +
async fn test_incorrect_proxy_authentication_returns_407_with_hyper_util_legacy_pool() {
         529  +
    incorrect_proxy_authentication_returns_407(&HyperUtilLegacyPool).await;
         530  +
}
  767    531   
  768         -
            // Verify the proxy received no requests (disabled)
  769         -
            let proxy_requests = mock_proxy.requests();
  770         -
            assert_eq!(
  771         -
                proxy_requests.len(),
  772         -
                0,
  773         -
                "Proxy should not receive requests when explicitly disabled"
  774         -
            );
         532  +
#[tokio::test]
         533  +
async fn test_incorrect_proxy_authentication_returns_407_with_partitioned_connection_pool() {
         534  +
    incorrect_proxy_authentication_returns_407(&PartitionedConnectionPool).await;
         535  +
}
  775    536   
  776         -
            // Verify the direct server received the request
  777         -
            let direct_requests = direct_server.requests();
         537  +
async fn disabled_proxy_overrides_environment(backend: &dyn HttpClientBackend) {
         538  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
         539  +
    let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
         540  +
    let proxy_uri = format!("http://{}", proxy.addr());
         541  +
    with_env_vars(&[("HTTP_PROXY", &proxy_uri)], || async {
         542  +
        let client = http_client(backend, proxy_backend_config(ProxyConfig::disabled()));
  778    543   
        assert_eq!(
  779         -
                direct_requests.len(),
  780         -
                1,
  781         -
                "Direct server should have received the request"
         544  +
            (StatusCode::OK, "direct".to_string()),
         545  +
            get(&client, &format!("http://{}/disabled", origin.addr()))
         546  +
                .await
         547  +
                .expect("direct request succeeds")
  782    548   
        );
  783         -
        },
  784         -
    )
         549  +
    })
  785    550   
    .await;
         551  +
    assert!(proxy.requests().is_empty());
         552  +
    assert_eq!("/disabled", origin.requests()[0].uri);
  786    553   
}
  787    554   
  788         -
// ================================================================================================
  789         -
// HTTPS/CONNECT Tunneling Tests
  790         -
// ================================================================================================
  791         -
//
  792         -
// These tests are for HTTPS tunneling through HTTP proxies using the CONNECT method.
  793         -
  794         -
/// Helper function to make HTTPS requests through proxy using TLS providers
  795         -
/// This is similar to make_http_request_through_proxy but uses TLS-enabled connectors
  796         -
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
  797         -
async fn make_https_request_through_proxy(
  798         -
    proxy_config: ProxyConfig,
  799         -
    target_url: &str,
  800         -
    tls_provider: tls::Provider,
  801         -
) -> Result<(StatusCode, String), Box<dyn std::error::Error + Send + Sync>> {
  802         -
    let http_client = http_client_fn(move |settings, _components| {
  803         -
        let connector = Connector::builder()
  804         -
            .proxy_config(proxy_config.clone())
  805         -
            .connector_settings(settings.clone())
  806         -
            .tls_provider(tls_provider.clone())
  807         -
            .build();
  808         -
  809         -
        aws_smithy_runtime_api::client::http::SharedHttpConnector::new(connector)
  810         -
    });
  811         -
  812         -
    let connector_settings = HttpConnectorSettings::builder().build();
  813         -
    let runtime_components = RuntimeComponentsBuilder::for_tests()
  814         -
        .with_time_source(Some(SystemTimeSource::new()))
  815         -
        .build()
  816         -
        .unwrap();
         555  +
#[tokio::test]
         556  +
async fn test_disabled_proxy_overrides_environment_with_hyper_util_legacy_pool() {
         557  +
    disabled_proxy_overrides_environment(&HyperUtilLegacyPool).await;
         558  +
}
  817    559   
  818         -
    let http_connector = http_client.http_connector(&connector_settings, &runtime_components);
         560  +
#[tokio::test]
         561  +
async fn test_disabled_proxy_overrides_environment_with_partitioned_connection_pool() {
         562  +
    disabled_proxy_overrides_environment(&PartitionedConnectionPool).await;
         563  +
}
         564  +
         565  +
async fn idle_proxy_connection_is_evicted(backend: &dyn HttpClientBackend) {
         566  +
    let proxy = MockHttpServer::with_response(StatusCode::OK, "proxied").await;
         567  +
    let config = ProxyConfig::http(format!("http://{}", proxy.addr())).expect("valid proxy");
         568  +
    let client = http_client(
         569  +
        backend,
         570  +
        BackendConfig {
         571  +
            pool_idle_timeout: Some(Duration::from_millis(100)),
         572  +
            ..proxy_backend_config(config)
         573  +
        },
         574  +
    );
  819    575   
  820         -
    let request = HttpRequest::get(target_url)
  821         -
        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
         576  +
    assert_eq!(
         577  +
        StatusCode::OK,
         578  +
        get(&client, "http://service.test/idle")
         579  +
            .await
         580  +
            .expect("proxy request succeeds")
         581  +
            .0
         582  +
    );
         583  +
    assert_eq!(1, proxy.connection_count());
  822    584   
  823         -
    let response = http_connector.call(request).await?;
         585  +
    tokio::time::timeout(Duration::from_secs(2), async {
         586  +
        while proxy.connection_count() != 0 {
         587  +
            tokio::time::sleep(Duration::from_millis(10)).await;
         588  +
        }
         589  +
    })
         590  +
    .await
         591  +
    .expect("idle proxy connection should close");
         592  +
}
  824    593   
  825         -
    let status = response.status();
  826         -
    let body_bytes = response.into_body().collect().await?.to_bytes();
  827         -
    let body_string = String::from_utf8(body_bytes.to_vec())?;
         594  +
#[tokio::test]
         595  +
async fn test_idle_proxy_connection_is_evicted_with_hyper_util_legacy_pool() {
         596  +
    idle_proxy_connection_is_evicted(&HyperUtilLegacyPool).await;
         597  +
}
  828    598   
  829         -
    Ok((status.into(), body_string))
         599  +
#[tokio::test]
         600  +
async fn test_idle_proxy_connection_is_evicted_with_partitioned_connection_pool() {
         601  +
    idle_proxy_connection_is_evicted(&PartitionedConnectionPool).await;
  830    602   
}
  831    603   
  832         -
/// Generic test function for HTTPS CONNECT with authentication
  833         -
/// Tests that HTTPS requests through HTTP proxy use CONNECT method with proper auth headers
  834    604   
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
  835         -
async fn run_https_connect_with_auth_test(tls_provider: tls::Provider, provider_name: &str) {
  836         -
    let mock_proxy = MockProxyServer::new(|req| {
  837         -
        // For HTTPS through HTTP proxy, we should see a CONNECT request
  838         -
        assert_eq!(req.method, "CONNECT");
  839         -
        assert_eq!(req.uri, "secure.aws.amazon.com:443");
  840         -
  841         -
        // Verify authentication header is present
  842         -
        let expected_auth = format!(
  843         -
            "Basic {}",
  844         -
            base64::prelude::BASE64_STANDARD.encode("connectuser:connectpass")
         605  +
async fn https_connect_uses_authority_form_and_authentication(
         606  +
    backend: &dyn HttpsClientBackend,
         607  +
    provider: tls::Provider,
         608  +
) {
         609  +
    let expected = basic_authorization("connectuser", "connectpass");
         610  +
    let expected_for_handler = expected.clone();
         611  +
    let proxy = MockHttpServer::new(move |request| {
         612  +
        assert_eq!("CONNECT", request.method);
         613  +
        assert_eq!("secure.example.com:443", request.uri);
         614  +
        assert_eq!(
         615  +
            Some(&expected_for_handler),
         616  +
            request.headers.get("proxy-authorization")
  845    617   
        );
  846         -
        assert_eq!(req.headers.get("proxy-authorization"), Some(&expected_auth));
  847         -
  848         -
        // Return 400 to avoid dealing with actual TLS tunneling
  849         -
        // The important part is that we got the CONNECT request with correct auth
  850    618   
        Response::builder()
  851    619   
            .status(StatusCode::BAD_REQUEST)
  852         -
            .body("CONNECT tunnel setup failed".to_string())
  853         -
            .unwrap()
         620  +
            .body("tunnel rejected".to_string())
         621  +
            .expect("valid response")
  854    622   
    })
  855    623   
    .await;
  856         -
  857         -
    // Configure proxy with authentication
  858         -
    let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr()))
  859         -
        .unwrap()
         624  +
    let config = ProxyConfig::all(format!("http://{}", proxy.addr()))
         625  +
        .expect("valid proxy")
  860    626   
        .with_basic_auth("connectuser", "connectpass");
         627  +
    let client = https_client(backend, config, provider, tls::TlsContext::default());
  861    628   
  862         -
    // Make HTTPS request - should trigger CONNECT method
  863         -
    let target_url = "https://secure.aws.amazon.com/api/secure";
  864         -
    let result = make_https_request_through_proxy(proxy_config, target_url, tls_provider).await;
  865         -
  866         -
    // We expect this to fail with a connection error since we returned 400
  867         -
    // The important thing is that the CONNECT request was made correctly
  868    629   
    assert!(
  869         -
        result.is_err(),
  870         -
        "CONNECT tunnel should fail with 400 response for {}",
  871         -
        provider_name
  872         -
    );
  873         -
  874         -
    // Verify the proxy received the CONNECT request
  875         -
    let requests = mock_proxy.requests();
  876         -
    assert_eq!(
  877         -
        requests.len(),
  878         -
        1,
  879         -
        "Proxy should have received exactly one CONNECT request for {}",
  880         -
        provider_name
  881         -
    );
  882         -
}
  883         -
  884         -
/// Generic test function for CONNECT without authentication (should get 407)
  885         -
/// Tests that HTTPS requests without auth get proper 407 response
  886         -
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
  887         -
async fn run_https_connect_auth_required_test(tls_provider: tls::Provider, provider_name: &str) {
  888         -
    let mock_proxy = MockProxyServer::new(|req| {
  889         -
        // For HTTPS through HTTP proxy, we should see a CONNECT request
  890         -
        assert_eq!(req.method, "CONNECT");
  891         -
        assert_eq!(req.uri, "secure.aws.amazon.com:443");
  892         -
  893         -
        // No auth header should be present
  894         -
        assert!(!req.headers.contains_key("proxy-authorization"));
  895         -
  896         -
        // Return 407 Proxy Authentication Required
  897         -
        Response::builder()
  898         -
            .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
  899         -
            .body("Proxy authentication required for CONNECT".to_string())
  900         -
            .unwrap()
  901         -
    })
  902         -
    .await;
  903         -
  904         -
    // Configure proxy without authentication
  905         -
    let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr())).unwrap();
  906         -
  907         -
    // Make HTTPS request - should trigger CONNECT method and get 407
  908         -
    let target_url = "https://secure.aws.amazon.com/api/secure";
  909         -
    let result = make_https_request_through_proxy(proxy_config, target_url, tls_provider).await;
  910         -
  911         -
    // We expect this to fail with a connection error since we returned 407
  912         -
    assert!(
  913         -
        result.is_err(),
  914         -
        "CONNECT tunnel should fail with 407 response for {}",
  915         -
        provider_name
  916         -
    );
  917         -
  918         -
    let error_msg = result.unwrap_err().to_string();
  919         -
    let error_msg_lower = error_msg.to_lowercase();
  920         -
  921         -
    // The important thing is that the request failed (which means CONNECT was attempted)
  922         -
    // The specific error message format is less critical for this test
  923         -
    // We accept either specific proxy auth errors OR generic connection errors
  924         -
    // since both indicate the CONNECT tunnel attempt was made
  925         -
    assert!(
  926         -
        error_msg_lower.contains("407")
  927         -
            || error_msg_lower.contains("proxy")
  928         -
            || error_msg_lower.contains("auth")
  929         -
            || error_msg_lower.contains("io error")
  930         -
            || error_msg_lower.contains("connection"),
  931         -
        "Error should be connection-related (indicating CONNECT was attempted) for {}, got: {}",
  932         -
        provider_name,
  933         -
        error_msg
  934         -
    );
  935         -
  936         -
    // Verify the proxy received the CONNECT request
  937         -
    let requests = mock_proxy.requests();
  938         -
    assert_eq!(
  939         -
        requests.len(),
  940         -
        1,
  941         -
        "Proxy should have received exactly one CONNECT request for {}",
  942         -
        provider_name
         630  +
        get(&client, "https://secure.example.com/private")
         631  +
            .await
         632  +
            .is_err(),
         633  +
        "a rejected CONNECT request must fail"
  943    634   
    );
         635  +
    assert_eq!(1, proxy.requests().len());
  944    636   
}
  945    637   
  946         -
/// Tests HTTPS tunneling through HTTP proxy with CONNECT method (rustls provider)
  947         -
/// Verifies that HTTPS requests through HTTP proxy use CONNECT method with authentication
  948    638   
#[cfg(feature = "rustls-ring")]
  949    639   
#[tokio::test]
  950         -
async fn test_https_connect_with_auth_rustls() {
  951         -
    run_https_connect_with_auth_test(
         640  +
async fn test_https_connect_form_and_auth_with_rustls_and_hyper_util_legacy_pool() {
         641  +
    https_connect_uses_authority_form_and_authentication(
         642  +
        &HyperUtilLegacyPool,
  952    643   
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
  953         -
        "rustls",
  954    644   
    )
  955    645   
    .await;
  956    646   
}
  957    647   
  958         -
/// Tests CONNECT method without authentication (should get 407) - rustls provider
  959         -
/// Verifies that HTTPS requests without auth get proper 407 response
  960    648   
#[cfg(feature = "rustls-ring")]
  961    649   
#[tokio::test]
  962         -
async fn test_https_connect_auth_required_rustls() {
  963         -
    run_https_connect_auth_required_test(
         650  +
async fn test_https_connect_form_and_auth_with_rustls_and_partitioned_connection_pool() {
         651  +
    https_connect_uses_authority_form_and_authentication(
         652  +
        &PartitionedConnectionPool,
  964    653   
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
  965         -
        "rustls",
  966    654   
    )
  967    655   
    .await;
  968    656   
}
  969    657   
  970         -
/// Tests HTTPS tunneling through HTTP proxy with CONNECT method (s2n-tls provider)
  971         -
/// Verifies that HTTPS requests through HTTP proxy use CONNECT method with authentication
  972    658   
#[cfg(feature = "s2n-tls")]
  973    659   
#[tokio::test]
  974         -
async fn test_https_connect_with_auth_s2n_tls() {
  975         -
    run_https_connect_with_auth_test(tls::Provider::S2nTls, "s2n-tls").await;
         660  +
async fn test_https_connect_form_and_auth_with_s2n_tls_and_hyper_util_legacy_pool() {
         661  +
    https_connect_uses_authority_form_and_authentication(
         662  +
        &HyperUtilLegacyPool,
         663  +
        tls::Provider::S2nTls,
         664  +
    )
         665  +
    .await;
  976    666   
}
  977    667   
  978         -
/// Tests CONNECT method without authentication (should get 407) - s2n-tls provider
  979         -
/// Verifies that HTTPS requests without auth get proper 407 response
  980    668   
#[cfg(feature = "s2n-tls")]
  981    669   
#[tokio::test]
  982         -
async fn test_https_connect_auth_required_s2n_tls() {
  983         -
    run_https_connect_auth_required_test(tls::Provider::S2nTls, "s2n-tls").await;
         670  +
async fn test_https_connect_form_and_auth_with_s2n_tls_and_partitioned_connection_pool() {
         671  +
    https_connect_uses_authority_form_and_authentication(
         672  +
        &PartitionedConnectionPool,
         673  +
        tls::Provider::S2nTls,
         674  +
    )
         675  +
    .await;
  984    676   
}
  985    677   
  986         -
/// Tests that HTTP requests through proxy use absolute URI form
  987         -
/// Verifies that the full URL (including hostname) is sent to the proxy
  988         -
#[tokio::test]
  989         -
async fn test_http_proxy_absolute_uri_form() {
  990         -
    let target_host = "api.example.com";
  991         -
    let target_path = "/v1/data";
  992         -
    let expected_absolute_uri = format!("http://{}{}", target_host, target_path);
  993         -
  994         -
    // Clone for use in closure
  995         -
    let expected_uri_clone = expected_absolute_uri.clone();
  996         -
    let target_host_clone = target_host.to_string();
  997         -
  998         -
    let mock_proxy = MockProxyServer::new(move |req| {
  999         -
        // For HTTP through proxy, we should see the full absolute URI
 1000         -
        assert_eq!(req.method, "GET");
 1001         -
        assert_eq!(req.uri, expected_uri_clone);
 1002         -
 1003         -
        // Host header should still be present
 1004         -
        assert_eq!(req.headers.get("host"), Some(&target_host_clone));
 1005         -
         678  +
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
         679  +
async fn https_connect_without_authentication_is_rejected(
         680  +
    backend: &dyn HttpsClientBackend,
         681  +
    provider: tls::Provider,
         682  +
) {
         683  +
    let proxy = MockHttpServer::new(|request| {
         684  +
        assert_eq!("CONNECT", request.method);
         685  +
        assert_eq!("secure.example.com:443", request.uri);
         686  +
        assert!(!request.headers.contains_key("proxy-authorization"));
 1006    687   
        Response::builder()
 1007         -
            .status(StatusCode::OK)
 1008         -
            .body("proxied response".to_string())
 1009         -
            .unwrap()
         688  +
            .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
         689  +
            .body("authentication required".to_string())
         690  +
            .expect("valid response")
 1010    691   
    })
 1011    692   
    .await;
         693  +
    let config = ProxyConfig::all(format!("http://{}", proxy.addr())).expect("valid proxy");
         694  +
    let client = https_client(backend, config, provider, tls::TlsContext::default());
 1012    695   
 1013         -
    let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr())).unwrap();
 1014         -
 1015         -
    let result = make_http_request_through_proxy(proxy_config, &expected_absolute_uri).await;
 1016         -
 1017         -
    let (status, body) = result.expect("HTTP request through proxy should succeed");
 1018         -
    assert_eq!(status, StatusCode::OK);
 1019         -
    assert_eq!(body, "proxied response");
 1020         -
 1021         -
    // Verify the proxy received the request with absolute URI
 1022         -
    let requests = mock_proxy.requests();
 1023         -
    assert_eq!(requests.len(), 1);
 1024         -
    assert_eq!(requests[0].uri, expected_absolute_uri);
 1025         -
}
 1026         -
 1027         -
/// Tests that direct HTTP requests (no proxy) use origin form URI
 1028         -
/// Verifies that only the path is sent when connecting directly
 1029         -
#[tokio::test]
 1030         -
async fn test_direct_http_origin_uri_form() {
 1031         -
    let target_path = "/v1/data";
 1032         -
 1033         -
    // Create a direct target server (no proxy)
 1034         -
    let direct_server = MockProxyServer::new(move |req| {
 1035         -
        // For direct connections, we should see only the path (origin form)
 1036         -
        assert_eq!(req.method, "GET");
 1037         -
        // The URI should be just the path part, not the full URL
 1038    696   
    assert!(
 1039         -
            req.uri == target_path || req.uri.ends_with(target_path),
 1040         -
            "Expected origin form URI ending with '{}', got '{}'",
 1041         -
            target_path,
 1042         -
            req.uri
         697  +
        get(&client, "https://secure.example.com/private")
         698  +
            .await
         699  +
            .is_err(),
         700  +
        "a 407 CONNECT response must fail"
 1043    701   
    );
         702  +
    assert_eq!(1, proxy.requests().len());
         703  +
}
 1044    704   
 1045         -
        Response::builder()
 1046         -
            .status(StatusCode::OK)
 1047         -
            .body("direct response".to_string())
 1048         -
            .unwrap()
 1049         -
    })
         705  +
#[cfg(feature = "rustls-ring")]
         706  +
#[tokio::test]
         707  +
async fn test_https_connect_without_auth_with_rustls_and_hyper_util_legacy_pool() {
         708  +
    https_connect_without_authentication_is_rejected(
         709  +
        &HyperUtilLegacyPool,
         710  +
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
         711  +
    )
 1050    712   
    .await;
 1051         -
 1052         -
    // Use disabled proxy to ensure direct connection
 1053         -
    let proxy_config = ProxyConfig::disabled();
 1054         -
 1055         -
    let target_url = format!("http://{}{}", direct_server.addr(), target_path);
 1056         -
    let result = make_http_request_through_proxy(proxy_config, &target_url).await;
 1057         -
 1058         -
    let (status, body) = result.expect("Direct HTTP request should succeed");
 1059         -
    assert_eq!(status, StatusCode::OK);
 1060         -
    assert_eq!(body, "direct response");
 1061         -
 1062         -
    // Verify the server received the request
 1063         -
    let requests = direct_server.requests();
 1064         -
    assert_eq!(requests.len(), 1);
 1065    713   
}
 1066    714   
 1067         -
/// Tests URI form handling with different proxy configurations
 1068         -
/// Verifies that URI form changes based on proxy vs direct connection
         715  +
#[cfg(feature = "rustls-ring")]
 1069    716   
#[tokio::test]
 1070         -
async fn test_uri_form_proxy_vs_direct() {
 1071         -
    let target_host = "test.example.com";
 1072         -
    let target_path = "/api/test";
 1073         -
    let full_url = format!("http://{}{}", target_host, target_path);
 1074         -
 1075         -
    // Test 1: Through proxy - should use absolute form
 1076         -
    {
 1077         -
        // Clone for use in closure
 1078         -
        let target_host_clone = target_host.to_string();
 1079         -
        let target_path_clone = target_path.to_string();
 1080         -
 1081         -
        let mock_proxy = MockProxyServer::new(move |req| {
 1082         -
            // Should receive absolute URI
 1083         -
            assert!(req.uri.starts_with("http://"));
 1084         -
            assert!(req.uri.contains(&target_host_clone));
 1085         -
            assert!(req.uri.contains(&target_path_clone));
 1086         -
 1087         -
            Response::builder()
 1088         -
                .status(StatusCode::OK)
 1089         -
                .body("proxy response".to_string())
 1090         -
                .unwrap()
 1091         -
        })
         717  +
async fn test_https_connect_without_auth_with_rustls_and_partitioned_connection_pool() {
         718  +
    https_connect_without_authentication_is_rejected(
         719  +
        &PartitionedConnectionPool,
         720  +
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
         721  +
    )
 1092    722   
    .await;
         723  +
}
 1093    724   
 1094         -
        let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr())).unwrap();
 1095         -
        let result = make_http_request_through_proxy(proxy_config, &full_url).await;
 1096         -
 1097         -
        assert!(result.is_ok(), "Proxy request should succeed");
 1098         -
        let requests = mock_proxy.requests();
 1099         -
        assert_eq!(requests.len(), 1);
 1100         -
        assert_eq!(requests[0].uri, full_url);
 1101         -
    }
 1102         -
 1103         -
    // Test 2: Direct connection - should use origin form
 1104         -
    {
 1105         -
        let target_path_clone = target_path.to_string();
 1106         -
 1107         -
        let direct_server = MockProxyServer::new(move |req| {
 1108         -
            // Should receive only the path part
 1109         -
            assert!(!req.uri.starts_with("http://"));
 1110         -
            assert!(req.uri == target_path_clone || req.uri.ends_with(&target_path_clone));
 1111         -
 1112         -
            Response::builder()
 1113         -
                .status(StatusCode::OK)
 1114         -
                .body("direct response".to_string())
 1115         -
                .unwrap()
 1116         -
        })
         725  +
#[cfg(feature = "s2n-tls")]
         726  +
#[tokio::test]
         727  +
async fn test_https_connect_without_auth_with_s2n_tls_and_hyper_util_legacy_pool() {
         728  +
    https_connect_without_authentication_is_rejected(&HyperUtilLegacyPool, tls::Provider::S2nTls)
 1117    729   
        .await;
         730  +
}
 1118    731   
 1119         -
        let proxy_config = ProxyConfig::disabled();
 1120         -
        let direct_url = format!("http://{}{}", direct_server.addr(), target_path);
 1121         -
        let result = make_http_request_through_proxy(proxy_config, &direct_url).await;
 1122         -
 1123         -
        assert!(result.is_ok(), "Direct request should succeed");
 1124         -
        let requests = direct_server.requests();
 1125         -
        assert_eq!(requests.len(), 1);
 1126         -
    }
         732  +
#[cfg(feature = "s2n-tls")]
         733  +
#[tokio::test]
         734  +
async fn test_https_connect_without_auth_with_s2n_tls_and_partitioned_connection_pool() {
         735  +
    https_connect_without_authentication_is_rejected(
         736  +
        &PartitionedConnectionPool,
         737  +
        tls::Provider::S2nTls,
         738  +
    )
         739  +
    .await;
 1127    740   
}
 1128    741   
 1129         -
/// Generic test function for CONNECT URI form validation
 1130         -
/// Tests that CONNECT requests use the correct host:port format
 1131    742   
#[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
 1132         -
async fn run_connect_uri_form_test(tls_provider: tls::Provider, provider_name: &str) {
 1133         -
    let target_host = "secure.example.com";
 1134         -
    let target_port = 443;
 1135         -
    let expected_connect_uri = format!("{}:{}", target_host, target_port);
         743  +
async fn tunneled_https_request_uses_origin_form(
         744  +
    backend: &dyn HttpsClientBackend,
         745  +
    provider: tls::Provider,
         746  +
) {
         747  +
    let origin = MockTlsOrigin::new("tunneled response").await;
         748  +
    let authorization = basic_authorization("connectuser", "connectpass");
         749  +
    let proxy = MockConnectProxy::relay_to(origin.addr(), Some(authorization.clone())).await;
         750  +
    let config = ProxyConfig::all(format!("http://{}", proxy.addr()))
         751  +
        .expect("valid proxy")
         752  +
        .with_basic_auth("connectuser", "connectpass");
         753  +
    let client = https_client(
         754  +
        backend,
         755  +
        config,
         756  +
        provider,
         757  +
        test_tls::SERVER_IDENTITY.client_context(),
         758  +
    );
         759  +
    let target = format!("https://localhost:{}/inside?value=1", origin.addr().port());
 1136    760   
 1137         -
    // Clone for use in closure
 1138         -
    let expected_uri_clone = expected_connect_uri.clone();
         761  +
    assert_eq!(
         762  +
        (StatusCode::OK, "tunneled response".to_string()),
         763  +
        get(&client, &target)
         764  +
            .await
         765  +
            .expect("tunneled HTTPS request succeeds")
         766  +
    );
 1139    767   
 1140         -
    let mock_proxy = MockProxyServer::new(move |req| {
 1141         -
        if req.method == "CONNECT" {
 1142         -
            // CONNECT should use host:port format
 1143         -
            assert_eq!(req.uri, expected_uri_clone);
         768  +
    let proxy_requests = proxy.requests();
         769  +
    assert_eq!(1, proxy_requests.len());
         770  +
    assert_eq!("CONNECT", proxy_requests[0].method);
         771  +
    assert_eq!(
         772  +
        format!("localhost:{}", origin.addr().port()),
         773  +
        proxy_requests[0].uri
         774  +
    );
         775  +
    assert_eq!(
         776  +
        Some(&authorization),
         777  +
        proxy_requests[0].headers.get("proxy-authorization")
         778  +
    );
 1144    779   
 1145         -
            // CONNECT requests should not have a Host header in the CONNECT line
 1146         -
            // (the Host header is for the tunneled HTTP request, not the CONNECT)
         780  +
    let origin_requests = origin.requests();
         781  +
    assert_eq!(1, origin_requests.len());
         782  +
    assert_eq!("GET", origin_requests[0].method);
         783  +
    assert_eq!("/inside?value=1", origin_requests[0].uri);
         784  +
    assert!(
         785  +
        !origin_requests[0]
         786  +
            .headers
         787  +
            .contains_key("proxy-authorization"),
         788  +
        "proxy credentials must not cross the CONNECT tunnel"
         789  +
    );
         790  +
}
 1147    791   
 1148         -
            Response::builder()
 1149         -
                .status(StatusCode::OK)
 1150         -
                .body("Connection established".to_string())
 1151         -
                .unwrap()
 1152         -
        } else {
 1153         -
            // This shouldn't happen in our test, but handle it gracefully
 1154         -
            Response::builder()
 1155         -
                .status(StatusCode::BAD_REQUEST)
 1156         -
                .body("Unexpected non-CONNECT request".to_string())
 1157         -
                .unwrap()
 1158         -
        }
 1159         -
    })
         792  +
#[cfg(feature = "rustls-ring")]
         793  +
#[tokio::test]
         794  +
async fn test_tunneled_https_origin_form_with_rustls_and_hyper_util_legacy_pool() {
         795  +
    tunneled_https_request_uses_origin_form(
         796  +
        &HyperUtilLegacyPool,
         797  +
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
         798  +
    )
 1160    799   
    .await;
 1161         -
 1162         -
    let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr())).unwrap();
 1163         -
 1164         -
    // Try to make an HTTPS request - this should trigger CONNECT
 1165         -
    let target_url = format!("https://{}/api/secure", target_host);
 1166         -
 1167         -
    let _result = make_https_request_through_proxy(proxy_config, &target_url, tls_provider).await;
 1168         -
 1169         -
    // The request will likely fail due to our mock setup, but that's OK
 1170         -
    // The important thing is that the CONNECT request was made with correct URI
 1171         -
    let requests = mock_proxy.requests();
 1172         -
    assert_eq!(
 1173         -
        requests.len(),
 1174         -
        1,
 1175         -
        "Should have received exactly one CONNECT request for {}",
 1176         -
        provider_name
 1177         -
    );
 1178         -
    assert_eq!(requests[0].method, "CONNECT");
 1179         -
    assert_eq!(requests[0].uri, expected_connect_uri);
 1180    800   
}
 1181    801   
 1182         -
/// Tests CONNECT method URI form for HTTPS tunneling - rustls provider
 1183         -
/// Verifies that CONNECT requests use the correct host:port format
 1184    802   
#[cfg(feature = "rustls-ring")]
 1185    803   
#[tokio::test]
 1186         -
async fn test_connect_uri_form_rustls() {
 1187         -
    run_connect_uri_form_test(
         804  +
async fn test_tunneled_https_origin_form_with_rustls_and_partitioned_connection_pool() {
         805  +
    tunneled_https_request_uses_origin_form(
         806  +
        &PartitionedConnectionPool,
 1188    807   
        tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
 1189         -
        "rustls",
 1190    808   
    )
 1191    809   
    .await;
 1192    810   
}
 1193    811   
 1194         -
/// Tests CONNECT method URI form for HTTPS tunneling - s2n-tls provider
 1195         -
/// Verifies that CONNECT requests use the correct host:port format
 1196    812   
#[cfg(feature = "s2n-tls")]
 1197    813   
#[tokio::test]
 1198         -
async fn test_connect_uri_form_s2n_tls() {
 1199         -
    run_connect_uri_form_test(tls::Provider::S2nTls, "s2n-tls").await;
         814  +
async fn test_tunneled_https_origin_form_with_s2n_tls_and_hyper_util_legacy_pool() {
         815  +
    tunneled_https_request_uses_origin_form(&HyperUtilLegacyPool, tls::Provider::S2nTls).await;
         816  +
}
         817  +
         818  +
#[cfg(feature = "s2n-tls")]
         819  +
#[tokio::test]
         820  +
async fn test_tunneled_https_origin_form_with_s2n_tls_and_partitioned_connection_pool() {
         821  +
    tunneled_https_request_uses_origin_form(&PartitionedConnectionPool, tls::Provider::S2nTls)
         822  +
        .await;
 1200    823   
}