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