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 + | #[cfg(all(
|
17 + | feature = "wire-mock",
|
18 + | any(feature = "rustls-ring", feature = "s2n-tls")
|
19 + | ))]
|
20 + | #[allow(dead_code)]
|
21 + | pub(crate) mod h2;
|
22 + | pub(crate) mod proxy;
|
23 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
24 + | pub(crate) mod tls;
|
25 + | }
|
26 + |
|
27 + | use aws_smithy_http_client::proxy::ProxyConfig;
|
13 28 | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
14 29 | 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,
|
30 + | use aws_smithy_runtime_api::box_error::BoxError;
|
31 + | #[cfg(any(
|
32 + | feature = "rustls-aws-lc",
|
33 + | feature = "rustls-aws-lc-fips",
|
34 + | feature = "rustls-ring",
|
35 + | feature = "s2n-tls"
|
36 + | ))]
|
37 + | use aws_smithy_runtime_api::client::dns::{
|
38 + | DnsFuture, ResolveDns, ResolveDnsError, SharedDnsResolver,
|
18 39 | };
|
40 + | use aws_smithy_runtime_api::client::http::{HttpConnector, SharedHttpClient, SharedHttpConnector};
|
19 41 | 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};
|
42 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
43 + | use common::client::HttpsClientBackend;
|
44 + | use common::client::{
|
45 + | self as test_client, BackendConfig, HttpClientBackend, HyperUtilLegacyPool,
|
46 + | PartitionedConnectionPool,
|
47 + | };
|
48 + | #[cfg(all(
|
49 + | feature = "wire-mock",
|
50 + | any(feature = "rustls-ring", feature = "s2n-tls")
|
51 + | ))]
|
52 + | use common::h2::{
|
53 + | H2ConnectionPlan, H2ConnectionScript, H2Event, H2Response, H2StreamScript, H2TestServer,
|
54 + | };
|
55 + | use common::proxy::{basic_authorization, MockHttpServer};
|
56 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
57 + | use common::proxy::{MockConnectProxy, MockTlsOrigin};
|
58 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
59 + | use common::tls as test_tls;
|
60 + | use http_1x::{Response, StatusCode};
|
23 61 | 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};
|
62 + | use std::future::Future;
|
31 63 | 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 64 |
|
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 - | }
|
65 + | struct TestClient {
|
66 + | _client: SharedHttpClient,
|
67 + | connector: SharedHttpConnector,
|
68 + | }
|
69 + |
|
70 + | fn http_client(backend: &dyn HttpClientBackend, config: BackendConfig) -> TestClient {
|
71 + | let client = backend.build(config);
|
72 + | let connector = test_client::connector(&client);
|
73 + | TestClient {
|
74 + | _client: client,
|
75 + | connector,
|
129 76 | }
|
130 - | });
|
77 + | }
|
131 78 |
|
132 - | Self {
|
133 - | addr,
|
134 - | shutdown_tx: Some(shutdown_tx),
|
135 - | request_log,
|
136 - | conn_count,
|
79 + | fn proxy_backend_config(proxy_config: ProxyConfig) -> BackendConfig {
|
80 + | BackendConfig {
|
81 + | proxy_config: Some(proxy_config),
|
82 + | ..Default::default()
|
137 83 | }
|
84 + | }
|
85 + |
|
86 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
87 + | fn https_client(
|
88 + | backend: &dyn HttpsClientBackend,
|
89 + | config: BackendConfig,
|
90 + | provider: tls::Provider,
|
91 + | tls_context: tls::TlsContext,
|
92 + | ) -> TestClient {
|
93 + | let client = backend.build_https(config, provider, tls_context);
|
94 + | let connector = test_client::connector(&client);
|
95 + | TestClient {
|
96 + | _client: client,
|
97 + | connector,
|
138 98 | }
|
99 + | }
|
139 100 |
|
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")
|
101 + | async fn send_request(
|
102 + | client: &TestClient,
|
103 + | request: HttpRequest,
|
104 + | ) -> Result<(StatusCode, String), BoxError> {
|
105 + | let response = client.connector.call(request).await?;
|
106 + | let status = StatusCode::from_u16(response.status().as_u16())?;
|
107 + | let body = response.into_body().collect().await?.to_bytes();
|
108 + | Ok((status, String::from_utf8(body.to_vec())?))
|
109 + | }
|
110 + |
|
111 + | async fn get(client: &TestClient, uri: &str) -> Result<(StatusCode, String), BoxError> {
|
112 + | send_request(client, HttpRequest::get(uri)?).await
|
113 + | }
|
114 + |
|
115 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
116 + | #[derive(Clone, Debug)]
|
117 + | struct FailingDns;
|
118 + |
|
119 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
120 + | impl ResolveDns for FailingDns {
|
121 + | fn resolve_dns<'a>(&'a self, name: &'a str) -> DnsFuture<'a> {
|
122 + | DnsFuture::ready(Err(ResolveDnsError::new(std::io::Error::other(format!(
|
123 + | "intentional DNS failure for {name}"
|
124 + | )))))
|
147 125 | }
|
126 + | }
|
148 127 |
|
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 - | })
|
128 + | #[cfg(all(
|
129 + | feature = "wire-mock",
|
130 + | any(feature = "rustls-ring", feature = "s2n-tls")
|
131 + | ))]
|
132 + | async fn h2_forward_proxy_applies_configured_authentication(
|
133 + | backend: &dyn HttpsClientBackend,
|
134 + | provider: tls::Provider,
|
135 + | ) {
|
136 + | let server = H2TestServer::builder()
|
137 + | .connections(H2ConnectionPlan::queue([H2ConnectionScript::new().route(
|
138 + | "/protected",
|
139 + | H2StreamScript::respond(H2Response::ok("authenticated over H2")),
|
140 + | )]))
|
141 + | .start()
|
158 142 | .await
|
159 - | }
|
143 + | .expect("H2 forward proxy should start");
|
144 + | let config = ProxyConfig::http(server.url("/"))
|
145 + | .expect("valid proxy URI")
|
146 + | .with_basic_auth("h2user", "h2pass");
|
147 + | let client = https_client(
|
148 + | backend,
|
149 + | proxy_backend_config(config),
|
150 + | provider,
|
151 + | test_tls::SERVER_IDENTITY.client_context(),
|
152 + | );
|
160 153 |
|
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))
|
154 + | assert_eq!(
|
155 + | (StatusCode::OK, "authenticated over H2".to_string()),
|
156 + | get(&client, "http://origin.test/protected")
|
157 + | .await
|
158 + | .expect("H2 forward-proxy request should succeed")
|
166 159 | );
|
160 + | let expected = basic_authorization("h2user", "h2pass");
|
161 + | let authorization = server.events().into_iter().find_map(|event| match event {
|
162 + | H2Event::StreamAccepted {
|
163 + | path,
|
164 + | proxy_authorization,
|
165 + | ..
|
166 + | } if path == "/protected" => proxy_authorization,
|
167 + | _ => None,
|
168 + | });
|
169 + | assert_eq!(Some(expected), authorization);
|
167 170 |
|
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 - | })
|
171 + | drop(client);
|
172 + | server
|
173 + | .shutdown()
|
189 174 | .await
|
190 - | }
|
175 + | .expect("clean H2 forward-proxy shutdown");
|
176 + | }
|
191 177 |
|
192 - | /// Get the address this server is listening on
|
193 - | fn addr(&self) -> SocketAddr {
|
194 - | self.addr
|
195 - | }
|
178 + | #[cfg(all(feature = "wire-mock", feature = "rustls-ring"))]
|
179 + | #[tokio::test]
|
180 + | async fn test_h2_forward_proxy_auth_with_rustls_and_hyper_util_legacy_pool() {
|
181 + | h2_forward_proxy_applies_configured_authentication(
|
182 + | &HyperUtilLegacyPool,
|
183 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
184 + | )
|
185 + | .await;
|
186 + | }
|
196 187 |
|
197 - | /// Get all requests received by this server
|
198 - | fn requests(&self) -> Vec<RecordedRequest> {
|
199 - | self.request_log.lock().unwrap().clone()
|
200 - | }
|
188 + | #[cfg(all(feature = "wire-mock", feature = "rustls-ring"))]
|
189 + | #[tokio::test]
|
190 + | async fn test_h2_forward_proxy_auth_with_rustls_and_partitioned_connection_pool() {
|
191 + | h2_forward_proxy_applies_configured_authentication(
|
192 + | &PartitionedConnectionPool,
|
193 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
194 + | )
|
195 + | .await;
|
201 196 | }
|
202 197 |
|
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 - | }
|
198 + | #[cfg(all(feature = "wire-mock", feature = "s2n-tls"))]
|
199 + | #[tokio::test]
|
200 + | async fn test_h2_forward_proxy_auth_with_s2n_tls_and_hyper_util_legacy_pool() {
|
201 + | h2_forward_proxy_applies_configured_authentication(&HyperUtilLegacyPool, tls::Provider::S2nTls)
|
202 + | .await;
|
203 + | }
|
204 + |
|
205 + | #[cfg(all(feature = "wire-mock", feature = "s2n-tls"))]
|
206 + | #[tokio::test]
|
207 + | async fn test_h2_forward_proxy_auth_with_s2n_tls_and_partitioned_connection_pool() {
|
208 + | h2_forward_proxy_applies_configured_authentication(
|
209 + | &PartitionedConnectionPool,
|
210 + | tls::Provider::S2nTls,
|
211 + | )
|
212 + | .await;
|
213 + | }
|
214 + |
|
215 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
216 + | async fn https_proxy_dns_failure_remains_retryable_io(
|
217 + | backend: &dyn HttpsClientBackend,
|
218 + | provider: tls::Provider,
|
219 + | ) {
|
220 + | let client = https_client(
|
221 + | backend,
|
222 + | BackendConfig {
|
223 + | proxy_config: Some(
|
224 + | ProxyConfig::all("http://proxy.invalid:8080").expect("valid proxy URI"),
|
225 + | ),
|
226 + | dns_resolver: Some(SharedDnsResolver::new(FailingDns)),
|
227 + | ..Default::default()
|
228 + | },
|
229 + | provider,
|
230 + | tls::TlsContext::default(),
|
231 + | );
|
232 + |
|
233 + | let error = test_client::send_request(
|
234 + | &client.connector,
|
235 + | HttpRequest::get("https://origin.test/protected").expect("valid request"),
|
236 + | )
|
237 + | .await
|
238 + | .expect_err("proxy DNS failure must fail the request");
|
239 + | assert!(
|
240 + | error.is_io(),
|
241 + | "HTTPS proxy DNS failure should remain retryable I/O, got {error:?}"
|
242 + | );
|
243 + | }
|
244 + |
|
245 + | #[cfg(feature = "rustls-ring")]
|
246 + | #[tokio::test]
|
247 + | async fn test_https_proxy_dns_failure_with_rustls_and_hyper_util_legacy_pool() {
|
248 + | https_proxy_dns_failure_remains_retryable_io(
|
249 + | &HyperUtilLegacyPool,
|
250 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
251 + | )
|
252 + | .await;
|
253 + | }
|
254 + |
|
255 + | #[cfg(feature = "rustls-ring")]
|
256 + | #[tokio::test]
|
257 + | async fn test_https_proxy_dns_failure_with_rustls_and_partitioned_connection_pool() {
|
258 + | https_proxy_dns_failure_remains_retryable_io(
|
259 + | &PartitionedConnectionPool,
|
260 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
261 + | )
|
262 + | .await;
|
263 + | }
|
264 + |
|
265 + | #[cfg(feature = "s2n-tls")]
|
266 + | #[tokio::test]
|
267 + | async fn test_https_proxy_dns_failure_with_s2n_tls_and_hyper_util_legacy_pool() {
|
268 + | https_proxy_dns_failure_remains_retryable_io(&HyperUtilLegacyPool, tls::Provider::S2nTls).await;
|
269 + | }
|
270 + |
|
271 + | #[cfg(feature = "s2n-tls")]
|
272 + | #[tokio::test]
|
273 + | async fn test_https_proxy_dns_failure_with_s2n_tls_and_partitioned_connection_pool() {
|
274 + | https_proxy_dns_failure_remains_retryable_io(&PartitionedConnectionPool, tls::Provider::S2nTls)
|
275 + | .await;
|
209 276 | }
|
210 277 |
|
211 - | /// Utility for running tests with specific environment variables
|
212 278 | #[allow(clippy::await_holding_lock)]
|
213 279 | async fn with_env_vars<F, Fut, R>(vars: &[(&str, &str)], test: F) -> R
|
214 280 | where
|
215 281 | F: FnOnce() -> Fut,
|
216 - | Fut: std::future::Future<Output = R>,
|
282 + | Fut: Future<Output = R>,
|
217 283 | {
|
218 - | // Use a static mutex to serialize environment variable tests
|
219 284 | 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
|
285 + | let _guard = ENV_MUTEX.lock().expect("environment lock is not poisoned");
|
286 + | let previous: Vec<_> = vars
|
224 287 | .iter()
|
225 - | .map(|(key, _)| (*key, std::env::var(key)))
|
288 + | .map(|(name, _)| (*name, std::env::var(name)))
|
226 289 | .collect();
|
227 290 |
|
228 - | // Set test environment variables
|
229 - | for (key, value) in vars {
|
230 - | std::env::set_var(key, value);
|
291 + | for (name, value) in vars {
|
292 + | std::env::set_var(name, value);
|
231 293 | }
|
232 - |
|
233 - | // Run the test
|
234 294 | 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),
|
295 + | for (name, value) in previous {
|
296 + | match value {
|
297 + | Ok(value) => std::env::set_var(name, value),
|
298 + | Err(_) => std::env::remove_var(name),
|
241 299 | }
|
242 300 | }
|
243 - |
|
244 301 | result
|
245 302 | }
|
246 303 |
|
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
|
304 + | async fn http_forward_proxy_uses_absolute_form(backend: &dyn HttpClientBackend) {
|
305 + | let proxy = MockHttpServer::new(|request| {
|
306 + | assert_eq!("GET", request.method);
|
307 + | assert_eq!("http://api.example.com/v1/data", request.uri);
|
308 + | assert_eq!(
|
309 + | Some(&"api.example.com".to_string()),
|
310 + | request.headers.get("host")
|
311 + | );
|
317 312 | Response::builder()
|
318 313 | .status(StatusCode::OK)
|
319 - | .body("proxied response from mock server".to_string())
|
320 - | .unwrap()
|
314 + | .body("proxied".to_string())
|
315 + | .expect("valid response")
|
321 316 | })
|
322 317 | .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");
|
318 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr())).expect("valid proxy");
|
319 + | let client = http_client(backend, proxy_backend_config(config));
|
340 320 |
|
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);
|
321 + | assert_eq!(
|
322 + | (StatusCode::OK, "proxied".to_string()),
|
323 + | get(&client, "http://api.example.com/v1/data")
|
324 + | .await
|
325 + | .expect("proxy request succeeds")
|
326 + | );
|
327 + | assert_eq!(1, proxy.requests().len());
|
328 + | }
|
346 329 |
|
347 - | // after making a request, conn count is 1
|
348 - | assert_eq!(mock_proxy.conn_count(), 1);
|
330 + | #[tokio::test]
|
331 + | async fn test_http_forward_proxy_uses_absolute_form_with_hyper_util_legacy_pool() {
|
332 + | http_forward_proxy_uses_absolute_form(&HyperUtilLegacyPool).await;
|
333 + | }
|
349 334 |
|
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);
|
335 + | #[tokio::test]
|
336 + | async fn test_http_forward_proxy_uses_absolute_form_with_partitioned_connection_pool() {
|
337 + | http_forward_proxy_uses_absolute_form(&PartitionedConnectionPool).await;
|
338 + | }
|
339 + |
|
340 + | #[cfg(any(
|
341 + | feature = "rustls-aws-lc",
|
342 + | feature = "rustls-aws-lc-fips",
|
343 + | feature = "rustls-ring",
|
344 + | feature = "s2n-tls"
|
345 + | ))]
|
346 + | async fn custom_dns_resolver_is_used_for_proxy_connections(backend: &dyn HttpClientBackend) {
|
347 + | const PROXY_HOST: &str = "proxy.test";
|
348 + |
|
349 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "proxied through custom DNS").await;
|
350 + | let resolver = proxy.dns_resolver(PROXY_HOST);
|
351 + | let proxy_config = ProxyConfig::http(format!("http://{PROXY_HOST}:{}", proxy.addr().port()))
|
352 + | .expect("valid proxy URI");
|
353 + | let client = http_client(
|
354 + | backend,
|
355 + | BackendConfig {
|
356 + | dns_resolver: Some(SharedDnsResolver::new(resolver.clone())),
|
357 + | ..proxy_backend_config(proxy_config)
|
358 + | },
|
359 + | );
|
353 360 |
|
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);
|
361 + | assert_eq!(
|
362 + | (StatusCode::OK, "proxied through custom DNS".to_string()),
|
363 + | get(&client, "http://origin.test/custom-dns-proxy")
|
364 + | .await
|
365 + | .expect("proxy request succeeds")
|
366 + | );
|
367 + | assert_eq!(vec![PROXY_HOST.to_string()], resolver.lookups());
|
368 + | assert_eq!(
|
369 + | "http://origin.test/custom-dns-proxy",
|
370 + | proxy.requests()[0].uri
|
371 + | );
|
357 372 | }
|
358 373 |
|
374 + | #[cfg(any(
|
375 + | feature = "rustls-aws-lc",
|
376 + | feature = "rustls-aws-lc-fips",
|
377 + | feature = "rustls-ring",
|
378 + | feature = "s2n-tls"
|
379 + | ))]
|
359 380 | #[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();
|
381 + | async fn test_custom_dns_resolver_is_used_for_proxy_connections_with_hyper_util_legacy_pool() {
|
382 + | custom_dns_resolver_is_used_for_proxy_connections(&HyperUtilLegacyPool).await;
|
383 + | }
|
378 384 |
|
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;
|
385 + | #[cfg(any(
|
386 + | feature = "rustls-aws-lc",
|
387 + | feature = "rustls-aws-lc-fips",
|
388 + | feature = "rustls-ring",
|
389 + | feature = "s2n-tls"
|
390 + | ))]
|
391 + | #[tokio::test]
|
392 + | async fn test_custom_dns_resolver_is_used_for_proxy_connections_with_partitioned_connection_pool() {
|
393 + | custom_dns_resolver_is_used_for_proxy_connections(&PartitionedConnectionPool).await;
|
394 + | }
|
382 395 |
|
383 - | let (status, body) = result.expect("HTTP request through proxy should succeed");
|
396 + | async fn configured_proxy_authentication_is_applied(backend: &dyn HttpClientBackend) {
|
397 + | let proxy = MockHttpServer::with_auth_validation("testuser", "testpass").await;
|
398 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
|
399 + | .expect("valid proxy")
|
400 + | .with_basic_auth("testuser", "testpass");
|
401 + | let client = http_client(backend, proxy_backend_config(config));
|
384 402 |
|
385 - | assert_eq!(status, StatusCode::OK);
|
386 - | assert_eq!(body, "proxied response from mock server");
|
403 + | assert_eq!(
|
404 + | (StatusCode::OK, "authenticated".to_string()),
|
405 + | get(&client, "http://service.test/protected")
|
406 + | .await
|
407 + | .expect("authenticated proxy request succeeds")
|
408 + | );
|
409 + | assert_eq!(
|
410 + | Some(&basic_authorization("testuser", "testpass")),
|
411 + | proxy.requests()[0].headers.get("proxy-authorization")
|
412 + | );
|
413 + | }
|
387 414 |
|
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);
|
415 + | #[tokio::test]
|
416 + | async fn test_configured_proxy_authentication_with_hyper_util_legacy_pool() {
|
417 + | configured_proxy_authentication_is_applied(&HyperUtilLegacyPool).await;
|
393 418 | }
|
394 419 |
|
395 420 | #[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;
|
421 + | async fn test_configured_proxy_authentication_with_partitioned_connection_pool() {
|
422 + | configured_proxy_authentication_is_applied(&PartitionedConnectionPool).await;
|
423 + | }
|
399 424 |
|
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");
|
425 + | async fn caller_proxy_authorization_is_preserved(backend: &dyn HttpClientBackend) {
|
426 + | let caller_authorization = basic_authorization("caller", "credentials");
|
427 + | let expected = caller_authorization.clone();
|
428 + | let proxy = MockHttpServer::new(move |request| {
|
429 + | assert_eq!(Some(&expected), request.headers.get("proxy-authorization"));
|
430 + | Response::builder()
|
431 + | .status(StatusCode::OK)
|
432 + | .body("caller authorization".to_string())
|
433 + | .expect("valid response")
|
434 + | })
|
435 + | .await;
|
436 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
|
437 + | .expect("valid proxy")
|
438 + | .with_basic_auth("configured", "credentials");
|
439 + | let client = http_client(backend, proxy_backend_config(config));
|
440 + | let mut request = HttpRequest::get("http://service.test/caller-auth").expect("valid request");
|
441 + | request
|
442 + | .headers_mut()
|
443 + | .insert("proxy-authorization", caller_authorization);
|
404 444 |
|
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;
|
445 + | assert_eq!(
|
446 + | (StatusCode::OK, "caller authorization".to_string()),
|
447 + | send_request(&client, request)
|
448 + | .await
|
449 + | .expect("caller authorization request succeeds")
|
450 + | );
|
451 + | }
|
408 452 |
|
409 - | let (status, body) = result.expect("Authenticated proxy request should succeed");
|
453 + | #[tokio::test]
|
454 + | async fn test_caller_proxy_authorization_is_preserved_with_hyper_util_legacy_pool() {
|
455 + | caller_proxy_authorization_is_preserved(&HyperUtilLegacyPool).await;
|
456 + | }
|
410 457 |
|
411 - | assert_eq!(status, StatusCode::OK);
|
412 - | assert_eq!(body, "authenticated");
|
458 + | #[tokio::test]
|
459 + | async fn test_caller_proxy_authorization_is_preserved_with_partitioned_connection_pool() {
|
460 + | caller_proxy_authorization_is_preserved(&PartitionedConnectionPool).await;
|
461 + | }
|
413 462 |
|
414 - | // Verify the proxy received the request with correct auth
|
415 - | let requests = mock_proxy.requests();
|
416 - | assert_eq!(requests.len(), 1);
|
463 + | async fn proxy_url_authentication_is_applied(backend: &dyn HttpClientBackend) {
|
464 + | let proxy = MockHttpServer::with_auth_validation("urluser", "urlpass").await;
|
465 + | let config =
|
466 + | ProxyConfig::http(format!("http://urluser:urlpass@{}", proxy.addr())).expect("valid proxy");
|
467 + | let client = http_client(backend, proxy_backend_config(config));
|
417 468 |
|
418 - | let expected_auth = format!(
|
419 - | "Basic {}",
|
420 - | base64::prelude::BASE64_STANDARD.encode("testuser:testpass")
|
469 + | assert_eq!(
|
470 + | StatusCode::OK,
|
471 + | get(&client, "http://service.test/url-auth")
|
472 + | .await
|
473 + | .expect("URL-authenticated request succeeds")
|
474 + | .0
|
421 475 | );
|
422 476 | assert_eq!(
|
423 - | requests[0].headers.get("proxy-authorization"),
|
424 - | Some(&expected_auth)
|
477 + | Some(&basic_authorization("urluser", "urlpass")),
|
478 + | proxy.requests()[0].headers.get("proxy-authorization")
|
425 479 | );
|
426 480 | }
|
427 481 |
|
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 482 | #[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;
|
483 + | async fn test_proxy_url_authentication_with_hyper_util_legacy_pool() {
|
484 + | proxy_url_authentication_is_applied(&HyperUtilLegacyPool).await;
|
485 + | }
|
441 486 |
|
442 - | let (status, body) = result.expect("URL-embedded auth proxy request should succeed");
|
443 - | assert_eq!(status, StatusCode::OK);
|
444 - | assert_eq!(body, "authenticated");
|
487 + | #[tokio::test]
|
488 + | async fn test_proxy_url_authentication_with_partitioned_connection_pool() {
|
489 + | proxy_url_authentication_is_applied(&PartitionedConnectionPool).await;
|
490 + | }
|
445 491 |
|
446 - | // Verify the proxy received the request with correct auth
|
447 - | let requests = mock_proxy.requests();
|
448 - | assert_eq!(requests.len(), 1);
|
492 + | async fn proxy_url_authentication_precedes_configured_authentication(
|
493 + | backend: &dyn HttpClientBackend,
|
494 + | ) {
|
495 + | let proxy = MockHttpServer::with_auth_validation("urluser", "urlpass").await;
|
496 + | let config = ProxyConfig::http(format!("http://urluser:urlpass@{}", proxy.addr()))
|
497 + | .expect("valid proxy")
|
498 + | .with_basic_auth("configured", "credentials");
|
499 + | let client = http_client(backend, proxy_backend_config(config));
|
449 500 |
|
450 - | let expected_auth = format!(
|
451 - | "Basic {}",
|
452 - | base64::prelude::BASE64_STANDARD.encode("urluser:urlpass")
|
501 + | assert_eq!(
|
502 + | StatusCode::OK,
|
503 + | get(&client, "http://service.test/auth-precedence")
|
504 + | .await
|
505 + | .expect("proxy request succeeds")
|
506 + | .0
|
453 507 | );
|
454 508 | assert_eq!(
|
455 - | requests[0].headers.get("proxy-authorization"),
|
456 - | Some(&expected_auth)
|
509 + | Some(&basic_authorization("urluser", "urlpass")),
|
510 + | proxy.requests()[0].headers.get("proxy-authorization")
|
457 511 | );
|
458 512 | }
|
459 513 |
|
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 514 | #[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 - | );
|
515 + | async fn test_proxy_url_authentication_precedence_with_hyper_util_legacy_pool() {
|
516 + | proxy_url_authentication_precedes_configured_authentication(&HyperUtilLegacyPool).await;
|
493 517 | }
|
494 518 |
|
495 519 | #[tokio::test]
|
496 - | async fn test_proxy_from_environment_variables() {
|
497 - | let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "env proxy response").await;
|
520 + | async fn test_proxy_url_authentication_precedence_with_partitioned_connection_pool() {
|
521 + | proxy_url_authentication_precedes_configured_authentication(&PartitionedConnectionPool).await;
|
522 + | }
|
498 523 |
|
524 + | async fn environment_proxy_is_used(backend: &dyn HttpClientBackend) {
|
525 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "environment proxy").await;
|
526 + | let proxy_uri = format!("http://{}", proxy.addr());
|
499 527 | with_env_vars(
|
500 528 | &[
|
501 - | ("HTTP_PROXY", &format!("http://{}", mock_proxy.addr())),
|
529 + | ("HTTP_PROXY", &proxy_uri),
|
502 530 | ("NO_PROXY", "localhost,127.0.0.1"),
|
503 531 | ],
|
504 532 | || 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);
|
533 + | let client = http_client(backend, proxy_backend_config(ProxyConfig::from_env()));
|
534 + | assert_eq!(
|
535 + | (StatusCode::OK, "environment proxy".to_string()),
|
536 + | get(&client, "http://service.test/environment")
|
537 + | .await
|
538 + | .expect("environment proxy request succeeds")
|
539 + | );
|
521 540 | },
|
522 541 | )
|
523 542 | .await;
|
543 + | assert_eq!("http://service.test/environment", proxy.requests()[0].uri);
|
524 544 | }
|
525 545 |
|
526 - | /// Tests that NO_PROXY bypass rules work correctly
|
527 - | /// Verifies that requests to bypassed hosts do not go through the proxy
|
528 546 | #[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;
|
547 + | async fn test_environment_proxy_with_hyper_util_legacy_pool() {
|
548 + | environment_proxy_is_used(&HyperUtilLegacyPool).await;
|
549 + | }
|
548 550 |
|
549 - | let (status, body) = result.expect("Direct connection should succeed");
|
550 - | assert_eq!(status, StatusCode::OK);
|
551 - | assert_eq!(body, "direct connection");
|
551 + | #[tokio::test]
|
552 + | async fn test_environment_proxy_with_partitioned_connection_pool() {
|
553 + | environment_proxy_is_used(&PartitionedConnectionPool).await;
|
554 + | }
|
552 555 |
|
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 - | );
|
556 + | async fn no_proxy_bypasses_proxy(backend: &dyn HttpClientBackend) {
|
557 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
|
558 + | let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
|
559 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
|
560 + | .expect("valid proxy")
|
561 + | .no_proxy("127.0.0.1");
|
562 + | let client = http_client(backend, proxy_backend_config(config));
|
560 563 |
|
561 - | // Verify the direct server received the request
|
562 - | let direct_requests = direct_server.requests();
|
563 564 | assert_eq!(
|
564 - | direct_requests.len(),
|
565 - | 1,
|
566 - | "Direct server should have received the request"
|
565 + | (StatusCode::OK, "direct".to_string()),
|
566 + | get(&client, &format!("http://{}/bypass", origin.addr()))
|
567 + | .await
|
568 + | .expect("direct request succeeds")
|
567 569 | );
|
570 + | assert!(proxy.requests().is_empty());
|
571 + | assert_eq!("/bypass", origin.requests()[0].uri);
|
568 572 | }
|
569 573 |
|
570 - | /// Tests that disabled proxy configuration results in direct connections
|
571 - | /// Verifies that ProxyConfig::disabled() bypasses all proxy logic
|
572 574 | #[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();
|
575 + | async fn test_no_proxy_bypass_with_hyper_util_legacy_pool() {
|
576 + | no_proxy_bypasses_proxy(&HyperUtilLegacyPool).await;
|
577 + | }
|
579 578 |
|
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;
|
579 + | #[tokio::test]
|
580 + | async fn test_no_proxy_bypass_with_partitioned_connection_pool() {
|
581 + | no_proxy_bypasses_proxy(&PartitionedConnectionPool).await;
|
582 + | }
|
586 583 |
|
587 - | let (status, body) = result.expect("Direct connection should succeed");
|
588 - | assert_eq!(status, StatusCode::OK);
|
589 - | assert_eq!(body, "direct connection");
|
584 + | async fn disabled_proxy_uses_direct_origin_form(backend: &dyn HttpClientBackend) {
|
585 + | let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
|
586 + | let client = http_client(backend, proxy_backend_config(ProxyConfig::disabled()));
|
590 587 |
|
591 - | // Verify the direct server received the request
|
592 - | let requests = direct_server.requests();
|
593 588 | 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
|
589 + | (StatusCode::OK, "direct".to_string()),
|
590 + | get(&client, &format!("http://{}/direct", origin.addr()))
|
591 + | .await
|
592 + | .expect("direct request succeeds")
|
605 593 | );
|
594 + | assert_eq!("/direct", origin.requests()[0].uri);
|
606 595 | }
|
607 596 |
|
608 - | /// Tests HTTPS-only proxy configuration
|
609 - | /// Verifies that HTTP requests bypass HTTPS-only proxies
|
610 597 | #[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;
|
598 + | async fn test_disabled_proxy_uses_direct_origin_form_with_hyper_util_legacy_pool() {
|
599 + | disabled_proxy_uses_direct_origin_form(&HyperUtilLegacyPool).await;
|
600 + | }
|
624 601 |
|
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");
|
602 + | #[tokio::test]
|
603 + | async fn test_disabled_proxy_uses_direct_origin_form_with_partitioned_connection_pool() {
|
604 + | disabled_proxy_uses_direct_origin_form(&PartitionedConnectionPool).await;
|
605 + | }
|
629 606 |
|
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 - | );
|
607 + | async fn https_only_proxy_bypasses_http(backend: &dyn HttpClientBackend) {
|
608 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
|
609 + | let origin = MockHttpServer::with_response(StatusCode::OK, "direct HTTP").await;
|
610 + | let config =
|
611 + | ProxyConfig::https(format!("http://{}", proxy.addr())).expect("valid proxy configuration");
|
612 + | let client = http_client(backend, proxy_backend_config(config));
|
637 613 |
|
638 - | // Verify the direct server received the request
|
639 - | let direct_requests = direct_server.requests();
|
640 614 | assert_eq!(
|
641 - | direct_requests.len(),
|
642 - | 1,
|
643 - | "Direct server should have received the HTTP request"
|
615 + | (StatusCode::OK, "direct HTTP".to_string()),
|
616 + | get(&client, &format!("http://{}/http-only", origin.addr()))
|
617 + | .await
|
618 + | .expect("direct HTTP request succeeds")
|
644 619 | );
|
620 + | assert!(proxy.requests().is_empty());
|
645 621 | }
|
646 622 |
|
647 - | /// Tests all-traffic proxy configuration
|
648 - | /// Verifies that both HTTP and HTTPS requests go through all-traffic proxies
|
649 623 | #[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();
|
624 + | async fn test_https_only_proxy_bypasses_http_with_hyper_util_legacy_pool() {
|
625 + | https_only_proxy_bypasses_http(&HyperUtilLegacyPool).await;
|
626 + | }
|
655 627 |
|
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;
|
628 + | #[tokio::test]
|
629 + | async fn test_https_only_proxy_bypasses_http_with_partitioned_connection_pool() {
|
630 + | https_only_proxy_bypasses_http(&PartitionedConnectionPool).await;
|
631 + | }
|
659 632 |
|
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");
|
633 + | async fn all_traffic_proxy_forwards_http(backend: &dyn HttpClientBackend) {
|
634 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "all traffic").await;
|
635 + | let config = ProxyConfig::all(format!("http://{}", proxy.addr())).expect("valid proxy");
|
636 + | let client = http_client(backend, proxy_backend_config(config));
|
663 637 |
|
664 - | // Verify the proxy received the HTTP request
|
665 - | let requests = mock_proxy.requests();
|
666 638 | assert_eq!(
|
667 - | requests.len(),
|
668 - | 1,
|
669 - | "Proxy should have received exactly one request"
|
639 + | (StatusCode::OK, "all traffic".to_string()),
|
640 + | get(&client, "http://service.test/all")
|
641 + | .await
|
642 + | .expect("all-traffic proxy request succeeds")
|
670 643 | );
|
671 - | assert_eq!(requests[0].method, "GET");
|
672 - | assert_eq!(requests[0].uri, target_url);
|
644 + | assert_eq!("http://service.test/all", proxy.requests()[0].uri);
|
673 645 | }
|
674 646 |
|
675 - | /// Tests proxy connection failure handling
|
676 - | /// Verifies that unreachable proxy servers result in appropriate connection errors
|
677 647 | #[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
|
648 + | async fn test_all_traffic_proxy_forwards_http_with_hyper_util_legacy_pool() {
|
649 + | all_traffic_proxy_forwards_http(&HyperUtilLegacyPool).await;
|
650 + | }
|
681 651 |
|
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;
|
652 + | #[tokio::test]
|
653 + | async fn test_all_traffic_proxy_forwards_http_with_partitioned_connection_pool() {
|
654 + | all_traffic_proxy_forwards_http(&PartitionedConnectionPool).await;
|
655 + | }
|
685 656 |
|
686 - | // The request should fail with a connection error
|
657 + | async fn unreachable_proxy_fails(backend: &dyn HttpClientBackend) {
|
658 + | let config = ProxyConfig::http("http://127.0.0.1:1").expect("valid proxy");
|
659 + | let client = http_client(backend, proxy_backend_config(config));
|
687 660 | assert!(
|
688 - | result.is_err(),
|
689 - | "Request should fail when proxy is unreachable"
|
661 + | get(&client, "http://service.test/unreachable")
|
662 + | .await
|
663 + | .is_err(),
|
664 + | "an unreachable proxy must fail the request"
|
690 665 | );
|
666 + | }
|
691 667 |
|
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 - | );
|
668 + | #[tokio::test]
|
669 + | async fn test_unreachable_proxy_fails_with_hyper_util_legacy_pool() {
|
670 + | unreachable_proxy_fails(&HyperUtilLegacyPool).await;
|
706 671 | }
|
707 672 |
|
708 - | /// Tests proxy authentication failure handling
|
709 - | /// Verifies that incorrect proxy credentials result in 407 Proxy Authentication Required
|
710 673 | #[tokio::test]
|
711 - | async fn test_proxy_authentication_failure() {
|
712 - | let mock_proxy = MockProxyServer::with_auth_validation("correct", "password").await;
|
674 + | async fn test_unreachable_proxy_fails_with_partitioned_connection_pool() {
|
675 + | unreachable_proxy_fails(&PartitionedConnectionPool).await;
|
676 + | }
|
713 677 |
|
714 - | // Configure proxy with wrong credentials
|
715 - | let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr()))
|
716 - | .unwrap()
|
678 + | async fn incorrect_proxy_authentication_returns_407(backend: &dyn HttpClientBackend) {
|
679 + | let proxy = MockHttpServer::with_auth_validation("correct", "password").await;
|
680 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr()))
|
681 + | .expect("valid proxy")
|
717 682 | .with_basic_auth("wrong", "credentials");
|
683 + | let client = http_client(backend, proxy_backend_config(config));
|
718 684 |
|
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")
|
685 + | assert_eq!(
|
686 + | StatusCode::PROXY_AUTHENTICATION_REQUIRED,
|
687 + | get(&client, "http://service.test/denied")
|
688 + | .await
|
689 + | .expect("proxy response is returned")
|
690 + | .0
|
735 691 | );
|
736 692 | assert_eq!(
|
737 - | requests[0].headers.get("proxy-authorization"),
|
738 - | Some(&expected_wrong_auth)
|
693 + | Some(&basic_authorization("wrong", "credentials")),
|
694 + | proxy.requests()[0].headers.get("proxy-authorization")
|
739 695 | );
|
740 696 | }
|
741 697 |
|
742 - | /// Tests that ProxyConfig::disabled() overrides environment proxy settings
|
743 - | /// Verifies that explicit proxy disabling takes precedence over environment variables
|
744 698 | #[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");
|
699 + | async fn test_incorrect_proxy_authentication_returns_407_with_hyper_util_legacy_pool() {
|
700 + | incorrect_proxy_authentication_returns_407(&HyperUtilLegacyPool).await;
|
701 + | }
|
767 702 |
|
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 - | );
|
703 + | #[tokio::test]
|
704 + | async fn test_incorrect_proxy_authentication_returns_407_with_partitioned_connection_pool() {
|
705 + | incorrect_proxy_authentication_returns_407(&PartitionedConnectionPool).await;
|
706 + | }
|
775 707 |
|
776 - | // Verify the direct server received the request
|
777 - | let direct_requests = direct_server.requests();
|
708 + | async fn disabled_proxy_overrides_environment(backend: &dyn HttpClientBackend) {
|
709 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "unexpected proxy").await;
|
710 + | let origin = MockHttpServer::with_response(StatusCode::OK, "direct").await;
|
711 + | let proxy_uri = format!("http://{}", proxy.addr());
|
712 + | with_env_vars(&[("HTTP_PROXY", &proxy_uri)], || async {
|
713 + | let client = http_client(backend, proxy_backend_config(ProxyConfig::disabled()));
|
778 714 | assert_eq!(
|
779 - | direct_requests.len(),
|
780 - | 1,
|
781 - | "Direct server should have received the request"
|
715 + | (StatusCode::OK, "direct".to_string()),
|
716 + | get(&client, &format!("http://{}/disabled", origin.addr()))
|
717 + | .await
|
718 + | .expect("direct request succeeds")
|
782 719 | );
|
783 - | },
|
784 - | )
|
720 + | })
|
785 721 | .await;
|
722 + | assert!(proxy.requests().is_empty());
|
723 + | assert_eq!("/disabled", origin.requests()[0].uri);
|
786 724 | }
|
787 725 |
|
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();
|
726 + | #[tokio::test]
|
727 + | async fn test_disabled_proxy_overrides_environment_with_hyper_util_legacy_pool() {
|
728 + | disabled_proxy_overrides_environment(&HyperUtilLegacyPool).await;
|
729 + | }
|
817 730 |
|
818 - | let http_connector = http_client.http_connector(&connector_settings, &runtime_components);
|
731 + | #[tokio::test]
|
732 + | async fn test_disabled_proxy_overrides_environment_with_partitioned_connection_pool() {
|
733 + | disabled_proxy_overrides_environment(&PartitionedConnectionPool).await;
|
734 + | }
|
735 + |
|
736 + | async fn idle_proxy_connection_is_evicted(backend: &dyn HttpClientBackend) {
|
737 + | let proxy = MockHttpServer::with_response(StatusCode::OK, "proxied").await;
|
738 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr())).expect("valid proxy");
|
739 + | let client = http_client(
|
740 + | backend,
|
741 + | BackendConfig {
|
742 + | pool_idle_timeout: Some(Duration::from_millis(100)),
|
743 + | ..proxy_backend_config(config)
|
744 + | },
|
745 + | );
|
819 746 |
|
820 - | let request = HttpRequest::get(target_url)
|
821 - | .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
|
747 + | assert_eq!(
|
748 + | StatusCode::OK,
|
749 + | get(&client, "http://service.test/idle")
|
750 + | .await
|
751 + | .expect("proxy request succeeds")
|
752 + | .0
|
753 + | );
|
754 + | assert_eq!(1, proxy.connection_count());
|
822 755 |
|
823 - | let response = http_connector.call(request).await?;
|
756 + | tokio::time::timeout(Duration::from_secs(2), async {
|
757 + | while proxy.connection_count() != 0 {
|
758 + | tokio::time::sleep(Duration::from_millis(10)).await;
|
759 + | }
|
760 + | })
|
761 + | .await
|
762 + | .expect("idle proxy connection should close");
|
763 + | }
|
824 764 |
|
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())?;
|
765 + | #[tokio::test]
|
766 + | async fn test_idle_proxy_connection_is_evicted_with_hyper_util_legacy_pool() {
|
767 + | idle_proxy_connection_is_evicted(&HyperUtilLegacyPool).await;
|
768 + | }
|
828 769 |
|
829 - | Ok((status.into(), body_string))
|
770 + | #[tokio::test]
|
771 + | async fn test_idle_proxy_connection_is_evicted_with_partitioned_connection_pool() {
|
772 + | idle_proxy_connection_is_evicted(&PartitionedConnectionPool).await;
|
830 773 | }
|
831 774 |
|
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 775 | #[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")
|
776 + | async fn https_connect_uses_authority_form_and_authentication(
|
777 + | backend: &dyn HttpsClientBackend,
|
778 + | provider: tls::Provider,
|
779 + | ) {
|
780 + | let expected = basic_authorization("connectuser", "connectpass");
|
781 + | let expected_for_handler = expected.clone();
|
782 + | let proxy = MockHttpServer::new(move |request| {
|
783 + | assert_eq!("CONNECT", request.method);
|
784 + | assert_eq!("secure.example.com:443", request.uri);
|
785 + | assert_eq!(
|
786 + | Some(&expected_for_handler),
|
787 + | request.headers.get("proxy-authorization")
|
845 788 | );
|
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 789 | Response::builder()
|
851 790 | .status(StatusCode::BAD_REQUEST)
|
852 - | .body("CONNECT tunnel setup failed".to_string())
|
853 - | .unwrap()
|
791 + | .body("tunnel rejected".to_string())
|
792 + | .expect("valid response")
|
854 793 | })
|
855 794 | .await;
|
856 - |
|
857 - | // Configure proxy with authentication
|
858 - | let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr()))
|
859 - | .unwrap()
|
795 + | let config = ProxyConfig::all(format!("http://{}", proxy.addr()))
|
796 + | .expect("valid proxy")
|
860 797 | .with_basic_auth("connectuser", "connectpass");
|
861 - |
|
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 - | 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
|
798 + | let client = https_client(
|
799 + | backend,
|
800 + | proxy_backend_config(config),
|
801 + | provider,
|
802 + | tls::TlsContext::default(),
|
916 803 | );
|
917 804 |
|
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 805 | 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
|
806 + | get(&client, "https://secure.example.com/private")
|
807 + | .await
|
808 + | .is_err(),
|
809 + | "a rejected CONNECT request must fail"
|
943 810 | );
|
811 + | assert_eq!(1, proxy.requests().len());
|
944 812 | }
|
945 813 |
|
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 814 | #[cfg(feature = "rustls-ring")]
|
949 815 | #[tokio::test]
|
950 - | async fn test_https_connect_with_auth_rustls() {
|
951 - | run_https_connect_with_auth_test(
|
816 + | async fn test_https_connect_form_and_auth_with_rustls_and_hyper_util_legacy_pool() {
|
817 + | https_connect_uses_authority_form_and_authentication(
|
818 + | &HyperUtilLegacyPool,
|
952 819 | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
953 - | "rustls",
|
954 820 | )
|
955 821 | .await;
|
956 822 | }
|
957 823 |
|
958 - | /// Tests CONNECT method without authentication (should get 407) - rustls provider
|
959 - | /// Verifies that HTTPS requests without auth get proper 407 response
|
960 824 | #[cfg(feature = "rustls-ring")]
|
961 825 | #[tokio::test]
|
962 - | async fn test_https_connect_auth_required_rustls() {
|
963 - | run_https_connect_auth_required_test(
|
826 + | async fn test_https_connect_form_and_auth_with_rustls_and_partitioned_connection_pool() {
|
827 + | https_connect_uses_authority_form_and_authentication(
|
828 + | &PartitionedConnectionPool,
|
964 829 | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
965 - | "rustls",
|
966 830 | )
|
967 831 | .await;
|
968 832 | }
|
969 833 |
|
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 834 | #[cfg(feature = "s2n-tls")]
|
973 835 | #[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;
|
836 + | async fn test_https_connect_form_and_auth_with_s2n_tls_and_hyper_util_legacy_pool() {
|
837 + | https_connect_uses_authority_form_and_authentication(
|
838 + | &HyperUtilLegacyPool,
|
839 + | tls::Provider::S2nTls,
|
840 + | )
|
841 + | .await;
|
976 842 | }
|
977 843 |
|
978 - | /// Tests CONNECT method without authentication (should get 407) - s2n-tls provider
|
979 - | /// Verifies that HTTPS requests without auth get proper 407 response
|
980 844 | #[cfg(feature = "s2n-tls")]
|
981 845 | #[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;
|
846 + | async fn test_https_connect_form_and_auth_with_s2n_tls_and_partitioned_connection_pool() {
|
847 + | https_connect_uses_authority_form_and_authentication(
|
848 + | &PartitionedConnectionPool,
|
849 + | tls::Provider::S2nTls,
|
850 + | )
|
851 + | .await;
|
984 852 | }
|
985 853 |
|
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 - |
|
854 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
855 + | async fn https_connect_without_authentication_is_rejected(
|
856 + | backend: &dyn HttpsClientBackend,
|
857 + | provider: tls::Provider,
|
858 + | ) {
|
859 + | let proxy = MockHttpServer::new(|request| {
|
860 + | assert_eq!("CONNECT", request.method);
|
861 + | assert_eq!("secure.example.com:443", request.uri);
|
862 + | assert!(!request.headers.contains_key("proxy-authorization"));
|
1006 863 | Response::builder()
|
1007 - | .status(StatusCode::OK)
|
1008 - | .body("proxied response".to_string())
|
1009 - | .unwrap()
|
864 + | .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
|
865 + | .body("authentication required".to_string())
|
866 + | .expect("valid response")
|
1010 867 | })
|
1011 868 | .await;
|
869 + | let config = ProxyConfig::all(format!("http://{}", proxy.addr())).expect("valid proxy");
|
870 + | let client = https_client(
|
871 + | backend,
|
872 + | proxy_backend_config(config),
|
873 + | provider,
|
874 + | tls::TlsContext::default(),
|
875 + | );
|
1012 876 |
|
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 877 | 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
|
878 + | get(&client, "https://secure.example.com/private")
|
879 + | .await
|
880 + | .is_err(),
|
881 + | "a 407 CONNECT response must fail"
|
1043 882 | );
|
883 + | assert_eq!(1, proxy.requests().len());
|
884 + | }
|
1044 885 |
|
1045 - | Response::builder()
|
1046 - | .status(StatusCode::OK)
|
1047 - | .body("direct response".to_string())
|
1048 - | .unwrap()
|
1049 - | })
|
886 + | #[cfg(feature = "rustls-ring")]
|
887 + | #[tokio::test]
|
888 + | async fn test_https_connect_without_auth_with_rustls_and_hyper_util_legacy_pool() {
|
889 + | https_connect_without_authentication_is_rejected(
|
890 + | &HyperUtilLegacyPool,
|
891 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
892 + | )
|
1050 893 | .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 894 | }
|
1066 895 |
|
1067 - | /// Tests URI form handling with different proxy configurations
|
1068 - | /// Verifies that URI form changes based on proxy vs direct connection
|
896 + | #[cfg(feature = "rustls-ring")]
|
1069 897 | #[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 - | })
|
898 + | async fn test_https_connect_without_auth_with_rustls_and_partitioned_connection_pool() {
|
899 + | https_connect_without_authentication_is_rejected(
|
900 + | &PartitionedConnectionPool,
|
901 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
902 + | )
|
1092 903 | .await;
|
904 + | }
|
1093 905 |
|
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 - | })
|
906 + | #[cfg(feature = "s2n-tls")]
|
907 + | #[tokio::test]
|
908 + | async fn test_https_connect_without_auth_with_s2n_tls_and_hyper_util_legacy_pool() {
|
909 + | https_connect_without_authentication_is_rejected(&HyperUtilLegacyPool, tls::Provider::S2nTls)
|
1117 910 | .await;
|
911 + | }
|
1118 912 |
|
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 - | }
|
913 + | #[cfg(feature = "s2n-tls")]
|
914 + | #[tokio::test]
|
915 + | async fn test_https_connect_without_auth_with_s2n_tls_and_partitioned_connection_pool() {
|
916 + | https_connect_without_authentication_is_rejected(
|
917 + | &PartitionedConnectionPool,
|
918 + | tls::Provider::S2nTls,
|
919 + | )
|
920 + | .await;
|
1127 921 | }
|
1128 922 |
|
1129 - | /// Generic test function for CONNECT URI form validation
|
1130 - | /// Tests that CONNECT requests use the correct host:port format
|
1131 923 | #[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);
|
924 + | async fn tunneled_https_request_uses_origin_form(
|
925 + | backend: &dyn HttpsClientBackend,
|
926 + | provider: tls::Provider,
|
927 + | ) {
|
928 + | let origin = MockTlsOrigin::new("tunneled response").await;
|
929 + | let authorization = basic_authorization("connectuser", "connectpass");
|
930 + | let proxy = MockConnectProxy::relay_to(origin.addr(), Some(authorization.clone())).await;
|
931 + | let config = ProxyConfig::all(format!("http://{}", proxy.addr()))
|
932 + | .expect("valid proxy")
|
933 + | .with_basic_auth("connectuser", "connectpass");
|
934 + | let client = https_client(
|
935 + | backend,
|
936 + | proxy_backend_config(config),
|
937 + | provider,
|
938 + | test_tls::SERVER_IDENTITY.client_context(),
|
939 + | );
|
940 + | let target = format!("https://localhost:{}/inside?value=1", origin.addr().port());
|
1136 941 |
|
1137 - | // Clone for use in closure
|
1138 - | let expected_uri_clone = expected_connect_uri.clone();
|
942 + | assert_eq!(
|
943 + | (StatusCode::OK, "tunneled response".to_string()),
|
944 + | get(&client, &target)
|
945 + | .await
|
946 + | .expect("tunneled HTTPS request succeeds")
|
947 + | );
|
1139 948 |
|
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);
|
949 + | let proxy_requests = proxy.requests();
|
950 + | assert_eq!(1, proxy_requests.len());
|
951 + | assert_eq!("CONNECT", proxy_requests[0].method);
|
952 + | assert_eq!(
|
953 + | format!("localhost:{}", origin.addr().port()),
|
954 + | proxy_requests[0].uri
|
955 + | );
|
956 + | assert_eq!(
|
957 + | Some(&authorization),
|
958 + | proxy_requests[0].headers.get("proxy-authorization")
|
959 + | );
|
1144 960 |
|
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)
|
961 + | let origin_requests = origin.requests();
|
962 + | assert_eq!(1, origin_requests.len());
|
963 + | assert_eq!("GET", origin_requests[0].method);
|
964 + | assert_eq!("/inside?value=1", origin_requests[0].uri);
|
965 + | assert!(
|
966 + | !origin_requests[0]
|
967 + | .headers
|
968 + | .contains_key("proxy-authorization"),
|
969 + | "proxy credentials must not cross the CONNECT tunnel"
|
970 + | );
|
971 + | }
|
1147 972 |
|
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 - | })
|
973 + | #[cfg(feature = "rustls-ring")]
|
974 + | #[tokio::test]
|
975 + | async fn test_tunneled_https_origin_form_with_rustls_and_hyper_util_legacy_pool() {
|
976 + | tunneled_https_request_uses_origin_form(
|
977 + | &HyperUtilLegacyPool,
|
978 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
979 + | )
|
1160 980 | .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 981 | }
|
1181 982 |
|
1182 - | /// Tests CONNECT method URI form for HTTPS tunneling - rustls provider
|
1183 - | /// Verifies that CONNECT requests use the correct host:port format
|
1184 983 | #[cfg(feature = "rustls-ring")]
|
1185 984 | #[tokio::test]
|
1186 - | async fn test_connect_uri_form_rustls() {
|
1187 - | run_connect_uri_form_test(
|
985 + | async fn test_tunneled_https_origin_form_with_rustls_and_partitioned_connection_pool() {
|
986 + | tunneled_https_request_uses_origin_form(
|
987 + | &PartitionedConnectionPool,
|
1188 988 | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
1189 - | "rustls",
|
1190 989 | )
|
1191 990 | .await;
|
1192 991 | }
|
1193 992 |
|
1194 - | /// Tests CONNECT method URI form for HTTPS tunneling - s2n-tls provider
|
1195 - | /// Verifies that CONNECT requests use the correct host:port format
|
1196 993 | #[cfg(feature = "s2n-tls")]
|
1197 994 | #[tokio::test]
|
1198 - | async fn test_connect_uri_form_s2n_tls() {
|
1199 - | run_connect_uri_form_test(tls::Provider::S2nTls, "s2n-tls").await;
|
995 + | async fn test_tunneled_https_origin_form_with_s2n_tls_and_hyper_util_legacy_pool() {
|
996 + | tunneled_https_request_uses_origin_form(&HyperUtilLegacyPool, tls::Provider::S2nTls).await;
|
997 + | }
|
998 + |
|
999 + | #[cfg(feature = "s2n-tls")]
|
1000 + | #[tokio::test]
|
1001 + | async fn test_tunneled_https_origin_form_with_s2n_tls_and_partitioned_connection_pool() {
|
1002 + | tunneled_https_request_uses_origin_form(&PartitionedConnectionPool, tls::Provider::S2nTls)
|
1003 + | .await;
|
1200 1004 | }
|