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 - |
|
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 - | }
|
139 49 |
|
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")
|
147 - | }
|
50 + | struct TestClient {
|
51 + | _client: SharedHttpClient,
|
52 + | connector: SharedHttpConnector,
|
53 + | }
|
148 54 |
|
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
|
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,
|
159 61 | }
|
62 + | }
|
160 63 |
|
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))
|
166 - | );
|
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
|
64 + | fn proxy_backend_config(proxy_config: ProxyConfig) -> BackendConfig {
|
65 + | BackendConfig {
|
66 + | proxy_config: Some(proxy_config),
|
67 + | ..Default::default()
|
190 68 | }
|
69 + | }
|
191 70 |
|
192 - | /// Get the address this server is listening on
|
193 - | fn addr(&self) -> SocketAddr {
|
194 - | self.addr
|
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,
|
85 + | );
|
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");
|
340 - |
|
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);
|
346 - |
|
347 - | // after making a request, conn count is 1
|
348 - | assert_eq!(mock_proxy.conn_count(), 1);
|
349 - |
|
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);
|
353 - |
|
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);
|
147 + | let config = ProxyConfig::http(format!("http://{}", proxy.addr())).expect("valid proxy");
|
148 + | let client = http_client(backend, proxy_backend_config(config));
|
149 + |
|
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());
|
357 157 | }
|
358 158 |
|
359 159 | #[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();
|
378 - |
|
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;
|
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 + | }
|
382 163 |
|
383 - | let (status, body) = result.expect("HTTP request through proxy should succeed");
|
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 + | );
|
384 189 |
|
385 - | assert_eq!(status, StatusCode::OK);
|
386 - | assert_eq!(body, "proxied response from mock server");
|
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 + | );
|
201 + | }
|
387 202 |
|
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);
|
203 + | #[cfg(any(
|
204 + | feature = "rustls-aws-lc",
|
205 + | feature = "rustls-aws-lc-fips",
|
206 + | feature = "rustls-ring",
|
207 + | feature = "s2n-tls"
|
208 + | ))]
|
209 + | #[tokio::test]
|
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;
|
393 212 | }
|
394 213 |
|
214 + | #[cfg(any(
|
215 + | feature = "rustls-aws-lc",
|
216 + | feature = "rustls-aws-lc-fips",
|
217 + | feature = "rustls-ring",
|
218 + | feature = "s2n-tls"
|
219 + | ))]
|
395 220 | #[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;
|
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 + | }
|
399 224 |
|
400 - | // Configure connector with authenticated proxy
|
401 - | let proxy_config = ProxyConfig::http(format!("http://{}", mock_proxy.addr()))
|
402 - | .unwrap()
|
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")
|
403 229 | .with_basic_auth("testuser", "testpass");
|
230 + | let client = http_client(backend, proxy_backend_config(config));
|
404 231 |
|
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;
|
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 + | }
|
408 243 |
|
409 - | let (status, body) = result.expect("Authenticated proxy request should succeed");
|
244 + | #[tokio::test]
|
245 + | async fn test_configured_proxy_authentication_with_hyper_util_legacy_pool() {
|
246 + | configured_proxy_authentication_is_applied(&HyperUtilLegacyPool).await;
|
247 + | }
|
410 248 |
|
411 - | assert_eq!(status, StatusCode::OK);
|
412 - | assert_eq!(body, "authenticated");
|
249 + | #[tokio::test]
|
250 + | async fn test_configured_proxy_authentication_with_partitioned_connection_pool() {
|
251 + | configured_proxy_authentication_is_applied(&PartitionedConnectionPool).await;
|
252 + | }
|
413 253 |
|
414 - | // Verify the proxy received the request with correct auth
|
415 - | let requests = mock_proxy.requests();
|
416 - | assert_eq!(requests.len(), 1);
|
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);
|
417 273 |
|
418 - | let expected_auth = format!(
|
419 - | "Basic {}",
|
420 - | base64::prelude::BASE64_STANDARD.encode("testuser:testpass")
|
421 - | );
|
422 274 | assert_eq!(
|
423 - | requests[0].headers.get("proxy-authorization"),
|
424 - | Some(&expected_auth)
|
275 + | (StatusCode::OK, "caller authorization".to_string()),
|
276 + | send_request(&client, request)
|
277 + | .await
|
278 + | .expect("caller authorization request succeeds")
|
425 279 | );
|
426 280 | }
|
427 281 |
|
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 282 | #[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;
|
283 + | async fn test_caller_proxy_authorization_is_preserved_with_hyper_util_legacy_pool() {
|
284 + | caller_proxy_authorization_is_preserved(&HyperUtilLegacyPool).await;
|
285 + | }
|
441 286 |
|
442 - | let (status, body) = result.expect("URL-embedded auth proxy request should succeed");
|
443 - | assert_eq!(status, StatusCode::OK);
|
444 - | 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 + | }
|
445 291 |
|
446 - | // Verify the proxy received the request with correct auth
|
447 - | let requests = mock_proxy.requests();
|
448 - | 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));
|
449 297 |
|
450 - | let expected_auth = format!(
|
451 - | "Basic {}",
|
452 - | base64::prelude::BASE64_STANDARD.encode("urluser:urlpass")
|
298 + | assert_eq!(
|
299 + | StatusCode::OK,
|
300 + | get(&client, "http://service.test/url-auth")
|
301 + | .await
|
302 + | .expect("URL-authenticated request succeeds")
|
303 + | .0
|
453 304 | );
|
454 305 | assert_eq!(
|
455 - | requests[0].headers.get("proxy-authorization"),
|
456 - | Some(&expected_auth)
|
306 + | Some(&basic_authorization("urluser", "urlpass")),
|
307 + | proxy.requests()[0].headers.get("proxy-authorization")
|
457 308 | );
|
458 309 | }
|
459 310 |
|
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 311 | #[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")
|
312 + | async fn test_proxy_url_authentication_with_hyper_util_legacy_pool() {
|
313 + | proxy_url_authentication_is_applied(&HyperUtilLegacyPool).await;
|
314 + | }
|
315 + |
|
316 + | #[tokio::test]
|
317 + | async fn test_proxy_url_authentication_with_partitioned_connection_pool() {
|
318 + | proxy_url_authentication_is_applied(&PartitionedConnectionPool).await;
|
319 + | }
|
320 + |
|
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));
|
329 + |
|
330 + | assert_eq!(
|
331 + | StatusCode::OK,
|
332 + | get(&client, "http://service.test/auth-precedence")
|
333 + | .await
|
334 + | .expect("proxy request succeeds")
|
335 + | .0
|
488 336 | );
|
489 337 | assert_eq!(
|
490 - | requests[0].headers.get("proxy-authorization"),
|
491 - | Some(&expected_auth)
|
338 + | Some(&basic_authorization("urluser", "urlpass")),
|
339 + | proxy.requests()[0].headers.get("proxy-authorization")
|
492 340 | );
|
493 341 | }
|
494 342 |
|
495 343 | #[tokio::test]
|
496 - | async fn test_proxy_from_environment_variables() {
|
497 - | let mock_proxy = MockProxyServer::with_response(StatusCode::OK, "env proxy response").await;
|
344 + | async fn test_proxy_url_authentication_precedence_with_hyper_util_legacy_pool() {
|
345 + | proxy_url_authentication_precedes_configured_authentication(&HyperUtilLegacyPool).await;
|
346 + | }
|
347 + |
|
348 + | #[tokio::test]
|
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");
|
767 - |
|
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 - | );
|
775 - |
|
776 - | // Verify the direct server received the request
|
777 - | let direct_requests = direct_server.requests();
|
778 - | assert_eq!(
|
779 - | direct_requests.len(),
|
780 - | 1,
|
781 - | "Direct server should have received the request"
|
782 - | );
|
783 - | },
|
784 - | )
|
785 - | .await;
|
786 - | }
|
787 - |
|
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();
|
817 - |
|
818 - | let http_connector = http_client.http_connector(&connector_settings, &runtime_components);
|
819 - |
|
820 - | let request = HttpRequest::get(target_url)
|
821 - | .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
|
822 - |
|
823 - | let response = http_connector.call(request).await?;
|
824 - |
|
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())?;
|
828 - |
|
829 - | Ok((status.into(), body_string))
|
528 + | async fn test_incorrect_proxy_authentication_returns_407_with_hyper_util_legacy_pool() {
|
529 + | incorrect_proxy_authentication_returns_407(&HyperUtilLegacyPool).await;
|
830 530 | }
|
831 531 |
|
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 - | #[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")
|
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 + | }
|
536 + |
|
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()));
|
543 + | assert_eq!(
|
544 + | (StatusCode::OK, "direct".to_string()),
|
545 + | get(&client, &format!("http://{}/disabled", origin.addr()))
|
546 + | .await
|
547 + | .expect("direct request succeeds")
|
845 548 | );
|
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 - | Response::builder()
|
851 - | .status(StatusCode::BAD_REQUEST)
|
852 - | .body("CONNECT tunnel setup failed".to_string())
|
853 - | .unwrap()
|
854 549 | })
|
855 550 | .await;
|
551 + | assert!(proxy.requests().is_empty());
|
552 + | assert_eq!("/disabled", origin.requests()[0].uri);
|
553 + | }
|
856 554 |
|
857 - | // Configure proxy with authentication
|
858 - | let proxy_config = ProxyConfig::all(format!("http://{}", mock_proxy.addr()))
|
859 - | .unwrap()
|
860 - | .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;
|
555 + | #[tokio::test]
|
556 + | async fn test_disabled_proxy_overrides_environment_with_hyper_util_legacy_pool() {
|
557 + | disabled_proxy_overrides_environment(&HyperUtilLegacyPool).await;
|
558 + | }
|
865 559 |
|
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
|
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 + | },
|
872 574 | );
|
873 575 |
|
874 - | // Verify the proxy received the CONNECT request
|
875 - | let requests = mock_proxy.requests();
|
876 576 | assert_eq!(
|
877 - | requests.len(),
|
878 - | 1,
|
879 - | "Proxy should have received exactly one CONNECT request for {}",
|
880 - | provider_name
|
577 + | StatusCode::OK,
|
578 + | get(&client, "http://service.test/idle")
|
579 + | .await
|
580 + | .expect("proxy request succeeds")
|
581 + | .0
|
881 582 | );
|
583 + | assert_eq!(1, proxy.connection_count());
|
584 + |
|
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");
|
882 592 | }
|
883 593 |
|
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");
|
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 + | }
|
892 598 |
|
893 - | // No auth header should be present
|
894 - | assert!(!req.headers.contains_key("proxy-authorization"));
|
599 + | #[tokio::test]
|
600 + | async fn test_idle_proxy_connection_is_evicted_with_partitioned_connection_pool() {
|
601 + | idle_proxy_connection_is_evicted(&PartitionedConnectionPool).await;
|
602 + | }
|
895 603 |
|
896 - | // Return 407 Proxy Authentication Required
|
604 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
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")
|
617 + | );
|
897 618 | Response::builder()
|
898 - | .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
|
899 - | .body("Proxy authentication required for CONNECT".to_string())
|
900 - | .unwrap()
|
619 + | .status(StatusCode::BAD_REQUEST)
|
620 + | .body("tunnel rejected".to_string())
|
621 + | .expect("valid response")
|
901 622 | })
|
902 623 | .await;
|
624 + | let config = ProxyConfig::all(format!("http://{}", proxy.addr()))
|
625 + | .expect("valid proxy")
|
626 + | .with_basic_auth("connectuser", "connectpass");
|
627 + | let client = https_client(backend, config, provider, tls::TlsContext::default());
|
903 628 |
|
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 629 | 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);
|
696 + | assert!(
|
697 + | get(&client, "https://secure.example.com/private")
|
698 + | .await
|
699 + | .is_err(),
|
700 + | "a 407 CONNECT response must fail"
|
701 + | );
|
702 + | assert_eq!(1, proxy.requests().len());
|
1025 703 | }
|
1026 704 |
|
1027 - | /// Tests that direct HTTP requests (no proxy) use origin form URI
|
1028 - | /// Verifies that only the path is sent when connecting directly
|
705 + | #[cfg(feature = "rustls-ring")]
|
1029 706 | #[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 - | 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
|
1043 - | );
|
1044 - |
|
1045 - | Response::builder()
|
1046 - | .status(StatusCode::OK)
|
1047 - | .body("direct response".to_string())
|
1048 - | .unwrap()
|
1049 - | })
|
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 - | })
|
1092 - | .await;
|
1093 - |
|
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));
|
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 + | )
|
722 + | .await;
|
723 + | }
|
1111 724 |
|
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;
|
1118 - |
|
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 - | }
|
1127 730 | }
|
1128 731 |
|
1129 - | /// Generic test function for CONNECT URI form validation
|
1130 - | /// Tests that CONNECT requests use the correct host:port format
|
1131 - | #[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);
|
1136 - |
|
1137 - | // Clone for use in closure
|
1138 - | let expected_uri_clone = expected_connect_uri.clone();
|
1139 - |
|
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);
|
1144 - |
|
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)
|
1147 - |
|
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 - | })
|
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 + | )
|
1160 739 | .await;
|
740 + | }
|
1161 741 |
|
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);
|
742 + | #[cfg(any(feature = "rustls-ring", feature = "s2n-tls"))]
|
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());
|
1166 760 |
|
1167 - | let _result = make_https_request_through_proxy(proxy_config, &target_url, tls_provider).await;
|
761 + | assert_eq!(
|
762 + | (StatusCode::OK, "tunneled response".to_string()),
|
763 + | get(&client, &target)
|
764 + | .await
|
765 + | .expect("tunneled HTTPS request succeeds")
|
766 + | );
|
1168 767 |
|
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();
|
768 + | let proxy_requests = proxy.requests();
|
769 + | assert_eq!(1, proxy_requests.len());
|
770 + | assert_eq!("CONNECT", proxy_requests[0].method);
|
1172 771 | assert_eq!(
|
1173 - | requests.len(),
|
1174 - | 1,
|
1175 - | "Should have received exactly one CONNECT request for {}",
|
1176 - | provider_name
|
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 + | );
|
779 + |
|
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"
|
1177 789 | );
|
1178 - | assert_eq!(requests[0].method, "CONNECT");
|
1179 - | assert_eq!(requests[0].uri, expected_connect_uri);
|
1180 790 | }
|
1181 791 |
|
1182 - | /// Tests CONNECT method URI form for HTTPS tunneling - rustls provider
|
1183 - | /// Verifies that CONNECT requests use the correct host:port format
|
1184 792 | #[cfg(feature = "rustls-ring")]
|
1185 793 | #[tokio::test]
|
1186 - | async fn test_connect_uri_form_rustls() {
|
1187 - | run_connect_uri_form_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,
|
1188 797 | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
1189 - | "rustls",
|
1190 798 | )
|
1191 799 | .await;
|
1192 800 | }
|
1193 801 |
|
1194 - | /// Tests CONNECT method URI form for HTTPS tunneling - s2n-tls provider
|
1195 - | /// Verifies that CONNECT requests use the correct host:port format
|
802 + | #[cfg(feature = "rustls-ring")]
|
803 + | #[tokio::test]
|
804 + | async fn test_tunneled_https_origin_form_with_rustls_and_partitioned_connection_pool() {
|
805 + | tunneled_https_request_uses_origin_form(
|
806 + | &PartitionedConnectionPool,
|
807 + | tls::Provider::rustls(tls::rustls_provider::CryptoMode::Ring),
|
808 + | )
|
809 + | .await;
|
810 + | }
|
811 + |
|
812 + | #[cfg(feature = "s2n-tls")]
|
813 + | #[tokio::test]
|
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 + |
|
1196 818 | #[cfg(feature = "s2n-tls")]
|
1197 819 | #[tokio::test]
|
1198 - | async fn test_connect_uri_form_s2n_tls() {
|
1199 - | run_connect_uri_form_test(tls::Provider::S2nTls, "s2n-tls").await;
|
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 | }
|