From bbad5fda4ba94908b6efbdc0faae483c1c5d6a18 Mon Sep 17 00:00:00 2001 From: William Howard Date: Mon, 27 Nov 2023 08:38:11 +0000 Subject: [PATCH 1/2] Add HTTP POST, PUT and HEAD support. Initial tests --- src/lib.rs | 5 +- tests/integration_test.rs | 146 ++++++++++++++++++++++++++++++++------ 2 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 36f95ae..f894aef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -309,7 +309,10 @@ pub async fn run_server(config_path: String) { .expect("TLS config error"); let app = Router::new() - .route("/*path", get(proxy_handler)) + .route( + "/*path", + get(proxy_handler).post(proxy_handler).put(proxy_handler), + ) .layer(Extension(proxy_config)) .layer(Extension(proxy_state)); diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9af7561..2d61d36 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,5 +1,5 @@ use reqwest::header::HOST; -use reqwest::{Error, Response}; +use reqwest::{Error, Method, Response}; use std::net::TcpListener; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Mutex; @@ -25,6 +25,11 @@ impl MockBackend { let template = ResponseTemplate::new(200).set_body_raw(resp_body, "text/plain"); Mock::given(method("GET")) + .and(path("/test")) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("POST")) .and(path("/test")) .respond_with(template) .mount(&mock_server) @@ -94,6 +99,7 @@ async fn http_request( url: &str, host_header: Option<&str>, no_proxy: Option, + method: Option, ) -> Result { let mut client_builder = reqwest::Client::builder().danger_accept_invalid_certs(true); if protocol == "http1" { @@ -103,21 +109,28 @@ async fn http_request( } let client = client_builder.build().unwrap(); + let client_method; + if method == Some(Method::POST) { + client_method = client.post(url); + } else if method == Some(Method::PUT) { + client_method = client.put(url); + } else { + client_method = client.get(url); + } if host_header.is_some() { - client - .get(url) + client_method .header(HOST, host_header.unwrap()) .send() .await } else if no_proxy.is_some() && no_proxy.unwrap() { - client.get(url).header("x-no-proxy", "true").send().await + client_method.header("x-no-proxy", "true").send().await } else { - client.get(url).send().await + client_method.send().await } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http1_test() { +async fn http1_get() { MOCK_BACKEND .lock() .unwrap() @@ -134,6 +147,7 @@ async fn http1_test() { "https://localhost:4000/test", Some("test.home"), None, + None, ) .await; @@ -144,7 +158,7 @@ async fn http1_test() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http1_no_host_header_test() { +async fn http1_get_no_host_header() { MOCK_BACKEND .lock() .unwrap() @@ -156,7 +170,7 @@ async fn http1_no_host_header_test() { thread::sleep(time::Duration::from_millis(1000)); // Send a request to the proxy without a host header - let resp = http_request("http1", "https://localhost:4000/test", None, None).await; + let resp = http_request("http1", "https://localhost:4000/test", None, None, None).await; // In this case the proxy should respond with a 404 assert_response(resp, 404, "Host header not defined").await; @@ -165,7 +179,7 @@ async fn http1_no_host_header_test() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http1_no_proxy_header_status() { +async fn http1_get_no_proxy_header_status() { MOCK_BACKEND .lock() .unwrap() @@ -177,7 +191,14 @@ async fn http1_no_proxy_header_status() { thread::sleep(time::Duration::from_millis(1000)); // Send an internal /status request - let resp = http_request("http1", "https://localhost:4000/status", None, Some(true)).await; + let resp = http_request( + "http1", + "https://localhost:4000/status", + None, + Some(true), + None, + ) + .await; // In this case the proxy should respond with a 200 assert_response(resp, 200, "The proxy is running").await; @@ -186,7 +207,7 @@ async fn http1_no_proxy_header_status() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http1_no_proxy_header_metrics() { +async fn http1_get_no_proxy_header_metrics() { MOCK_BACKEND .lock() .unwrap() @@ -203,11 +224,19 @@ async fn http1_no_proxy_header_metrics() { "https://localhost:4000/test", Some("test.home"), None, + None, ) .await; // Send an internal /metrics request - let resp = http_request("http1", "https://localhost:4000/metrics", None, Some(true)).await; + let resp = http_request( + "http1", + "https://localhost:4000/metrics", + None, + Some(true), + None, + ) + .await; let response = resp.unwrap(); let status = response.status(); let body = response.bytes().await.unwrap(); @@ -218,7 +247,35 @@ async fn http1_no_proxy_header_metrics() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http2_test() { +async fn http1_post() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http1", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::POST), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, "This is the mock backend!").await; + + finish(proxy_parent); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn http2_get() { MOCK_BACKEND .lock() .unwrap() @@ -235,6 +292,7 @@ async fn http2_test() { "https://localhost:4000/test", Some("test.home"), None, + None, ) .await; @@ -245,7 +303,7 @@ async fn http2_test() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http2_no_host_header_test() { +async fn http2_get_no_host_header() { MOCK_BACKEND .lock() .unwrap() @@ -257,7 +315,7 @@ async fn http2_no_host_header_test() { thread::sleep(time::Duration::from_millis(1000)); // Send a request to the proxy without a host header - let resp = http_request("http2", "https://localhost:4000/test", None, None).await; + let resp = http_request("http2", "https://localhost:4000/test", None, None, None).await; // In this case the proxy should respond with a 404 assert_response(resp, 404, "Host header not defined").await; @@ -266,7 +324,7 @@ async fn http2_no_host_header_test() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http2_no_proxy_header_status() { +async fn http2_get_no_proxy_header_status() { MOCK_BACKEND .lock() .unwrap() @@ -278,7 +336,14 @@ async fn http2_no_proxy_header_status() { thread::sleep(time::Duration::from_millis(1000)); // Send an internal /status request - let resp = http_request("http2", "https://localhost:4000/status", None, Some(true)).await; + let resp = http_request( + "http2", + "https://localhost:4000/status", + None, + Some(true), + None, + ) + .await; // In this case the proxy should respond with a 200 assert_response(resp, 200, "The proxy is running").await; @@ -287,7 +352,7 @@ async fn http2_no_proxy_header_status() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn http2_no_proxy_header_metrics() { +async fn http2_get_no_proxy_header_metrics() { MOCK_BACKEND .lock() .unwrap() @@ -304,11 +369,19 @@ async fn http2_no_proxy_header_metrics() { "https://localhost:4000/test", Some("test.home"), None, + None, ) .await; // Send an internal /metrics request - let resp = http_request("http2", "https://localhost:4000/metrics", None, Some(true)).await; + let resp = http_request( + "http2", + "https://localhost:4000/metrics", + None, + Some(true), + None, + ) + .await; let response = resp.unwrap(); let status = response.status(); let body = response.bytes().await.unwrap(); @@ -319,7 +392,35 @@ async fn http2_no_proxy_header_metrics() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_load_balancing_round_robin() { +async fn http2_post() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http2", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::POST), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, "This is the mock backend!").await; + + finish(proxy_parent); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn load_balancing_round_robin() { MOCK_BACKEND .lock() .unwrap() @@ -341,6 +442,7 @@ async fn test_load_balancing_round_robin() { "https://localhost:4000/test", Some("test-lb.home"), None, + None, ) .await; @@ -352,6 +454,7 @@ async fn test_load_balancing_round_robin() { "https://localhost:4000/test", Some("test-lb.home"), None, + None, ) .await; assert_response(resp, 200, "This is the mock backend 2!").await; @@ -360,7 +463,7 @@ async fn test_load_balancing_round_robin() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_proxied_backend_timeout() { +async fn proxied_backend_timeout() { let proxy_parent = start_proxy(); // Sleep this thread while the server starts up @@ -372,6 +475,7 @@ async fn test_proxied_backend_timeout() { "https://localhost:4000/delay", Some("test.home"), None, + None, ) .await; // In this case the proxy should respond with a 504 From 761d8249273062404ce424cf48b20203832b18de Mon Sep 17 00:00:00 2001 From: William Howard Date: Mon, 27 Nov 2023 22:09:24 +0000 Subject: [PATCH 2/2] Additional tests --- src/lib.rs | 4 +- tests/integration_test.rs | 163 ++++++++++++++++++++++++++++++++++---- 2 files changed, 148 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f894aef..26559df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -211,7 +211,7 @@ async fn proxy_handler( // A non internal request, but the host header has not been defined (_, _, false, false) => { - info!("Host header not defined"); + debug!("Host header not defined"); *response.body_mut() = Body::from("Host header not defined"); *response.status_mut() = StatusCode::NOT_FOUND; } @@ -256,7 +256,7 @@ async fn proxy_handler( } response = proxy_config.client.make_request(req).await; - info!( + debug!( "Proxied response from: {} | Status: {}", uri, response.status() diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2d61d36..ba2e40d 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -22,16 +22,25 @@ impl MockBackend { if self.mock_server.is_none() { let listener = TcpListener::bind(address).unwrap(); let mock_server = MockServer::builder().listener(listener).start().await; - let template = ResponseTemplate::new(200).set_body_raw(resp_body, "text/plain"); Mock::given(method("GET")) .and(path("/test")) - .respond_with(template.clone()) + .respond_with(ResponseTemplate::new(200).set_body_raw(resp_body, "text/plain")) + .mount(&mock_server) + .await; + Mock::given(method("HEAD")) + .and(path("/test")) + .respond_with(ResponseTemplate::new(200)) .mount(&mock_server) .await; Mock::given(method("POST")) .and(path("/test")) - .respond_with(template) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; + Mock::given(method("PUT")) + .and(path("/test")) + .respond_with(ResponseTemplate::new(200)) .mount(&mock_server) .await; Mock::given(method("GET")) @@ -86,12 +95,18 @@ fn start_proxy() -> bool { parent } -async fn assert_response(resp: Result, expected_status: u16, expected_body: &str) { +async fn assert_response( + resp: Result, + expected_status: u16, + expected_body: Option<&str>, +) { let response = resp.unwrap(); let status = response.status(); let body = response.bytes().await.unwrap(); assert_eq!(status, expected_status); - assert_eq!(body, expected_body); + if let Some(expected_body) = expected_body { + assert_eq!(body, expected_body); + } } async fn http_request( @@ -110,7 +125,9 @@ async fn http_request( let client = client_builder.build().unwrap(); let client_method; - if method == Some(Method::POST) { + if method == Some(Method::HEAD) { + client_method = client.head(url); + } else if method == Some(Method::POST) { client_method = client.post(url); } else if method == Some(Method::PUT) { client_method = client.put(url); @@ -152,7 +169,7 @@ async fn http1_get() { .await; // In this case the response should be a 200 from the mock backend - assert_response(resp, 200, "This is the mock backend!").await; + assert_response(resp, 200, Some("This is the mock backend!")).await; finish(proxy_parent); } @@ -173,7 +190,7 @@ async fn http1_get_no_host_header() { let resp = http_request("http1", "https://localhost:4000/test", None, None, None).await; // In this case the proxy should respond with a 404 - assert_response(resp, 404, "Host header not defined").await; + assert_response(resp, 404, Some("Host header not defined")).await; finish(proxy_parent); } @@ -201,7 +218,7 @@ async fn http1_get_no_proxy_header_status() { .await; // In this case the proxy should respond with a 200 - assert_response(resp, 200, "The proxy is running").await; + assert_response(resp, 200, Some("The proxy is running")).await; finish(proxy_parent); } @@ -246,6 +263,34 @@ async fn http1_get_no_proxy_header_metrics() { finish(proxy_parent); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn http1_head() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http1", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::HEAD), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, None).await; + + finish(proxy_parent); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn http1_post() { MOCK_BACKEND @@ -269,7 +314,35 @@ async fn http1_post() { .await; // In this case the response should be a 200 from the mock backend - assert_response(resp, 200, "This is the mock backend!").await; + assert_response(resp, 200, None).await; + + finish(proxy_parent); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn http1_put() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http1", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::PUT), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, None).await; finish(proxy_parent); } @@ -297,7 +370,7 @@ async fn http2_get() { .await; // In this case the response should be a 200 from the mock backend - assert_response(resp, 200, "This is the mock backend!").await; + assert_response(resp, 200, Some("This is the mock backend!")).await; finish(proxy_parent); } @@ -318,7 +391,7 @@ async fn http2_get_no_host_header() { let resp = http_request("http2", "https://localhost:4000/test", None, None, None).await; // In this case the proxy should respond with a 404 - assert_response(resp, 404, "Host header not defined").await; + assert_response(resp, 404, Some("Host header not defined")).await; finish(proxy_parent); } @@ -346,7 +419,7 @@ async fn http2_get_no_proxy_header_status() { .await; // In this case the proxy should respond with a 200 - assert_response(resp, 200, "The proxy is running").await; + assert_response(resp, 200, Some("The proxy is running")).await; finish(proxy_parent); } @@ -391,6 +464,34 @@ async fn http2_get_no_proxy_header_metrics() { finish(proxy_parent); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn http2_head() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http2", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::HEAD), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, None).await; + + finish(proxy_parent); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn http2_post() { MOCK_BACKEND @@ -414,7 +515,35 @@ async fn http2_post() { .await; // In this case the response should be a 200 from the mock backend - assert_response(resp, 200, "This is the mock backend!").await; + assert_response(resp, 200, None).await; + + finish(proxy_parent); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn http2_put() { + MOCK_BACKEND + .lock() + .unwrap() + .init("127.0.0.1:8000", "This is the mock backend!") + .await; + let proxy_parent = start_proxy(); + + // Sleep this thread while the server starts up + thread::sleep(time::Duration::from_millis(1000)); + + // Send a request to the proxy, which should be forwarded to the mock server + let resp = http_request( + "http2", + "https://localhost:4000/test", + Some("test.home"), + None, + Some(Method::PUT), + ) + .await; + + // In this case the response should be a 200 from the mock backend + assert_response(resp, 200, None).await; finish(proxy_parent); } @@ -446,7 +575,7 @@ async fn load_balancing_round_robin() { ) .await; - assert_response(resp, 200, "This is the mock backend!").await; + assert_response(resp, 200, Some("This is the mock backend!")).await; // Response from the second mock backend let resp = http_request( @@ -457,7 +586,7 @@ async fn load_balancing_round_robin() { None, ) .await; - assert_response(resp, 200, "This is the mock backend 2!").await; + assert_response(resp, 200, Some("This is the mock backend 2!")).await; finish(proxy_parent); } @@ -479,7 +608,7 @@ async fn proxied_backend_timeout() { ) .await; // In this case the proxy should respond with a 504 - assert_response(resp, 504, "Request timeout").await; + assert_response(resp, 504, Some("Request timeout")).await; finish(proxy_parent); }