2026-02-09 10:55:46 +00:00
|
|
|
mod common;
|
|
|
|
|
|
|
|
|
|
use common::*;
|
|
|
|
|
use rustproxy::RustProxy;
|
|
|
|
|
use rustproxy_config::RustProxyOptions;
|
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
|
|
|
|
|
|
/// Send a raw HTTP request and return the full response as a string.
|
|
|
|
|
async fn send_http_request(port: u16, host: &str, method: &str, path: &str) -> String {
|
|
|
|
|
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let request = format!(
|
|
|
|
|
"{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
|
|
|
|
method, path, host,
|
|
|
|
|
);
|
|
|
|
|
stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response = Vec::new();
|
|
|
|
|
stream.read_to_end(&mut response).await.unwrap();
|
|
|
|
|
String::from_utf8_lossy(&response).to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extract the body from a raw HTTP response string (after the \r\n\r\n).
|
|
|
|
|
fn extract_body(response: &str) -> &str {
|
|
|
|
|
response.split("\r\n\r\n").nth(1).unwrap_or("")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_basic() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _backend = start_http_echo_backend(backend_port, "main").await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "anyhost.com", "GET", "/hello").await;
|
|
|
|
|
let body = extract_body(&response);
|
|
|
|
|
body.to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(result.contains(r#""method":"GET"#), "Expected GET method, got: {}", result);
|
|
|
|
|
assert!(result.contains(r#""path":"/hello"#), "Expected /hello path, got: {}", result);
|
|
|
|
|
assert!(result.contains(r#""backend":"main"#), "Expected main backend, got: {}", result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_host_routing() {
|
|
|
|
|
let backend1_port = next_port();
|
|
|
|
|
let backend2_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _b1 = start_http_echo_backend(backend1_port, "alpha").await;
|
|
|
|
|
let _b2 = start_http_echo_backend(backend2_port, "beta").await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![
|
|
|
|
|
make_test_route(proxy_port, Some("alpha.example.com"), "127.0.0.1", backend1_port),
|
|
|
|
|
make_test_route(proxy_port, Some("beta.example.com"), "127.0.0.1", backend2_port),
|
|
|
|
|
],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
// Test alpha domain
|
|
|
|
|
let alpha_result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "alpha.example.com", "GET", "/").await;
|
|
|
|
|
extract_body(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(alpha_result.contains(r#""backend":"alpha"#), "Expected alpha backend, got: {}", alpha_result);
|
|
|
|
|
|
|
|
|
|
// Test beta domain
|
|
|
|
|
let beta_result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "beta.example.com", "GET", "/").await;
|
|
|
|
|
extract_body(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(beta_result.contains(r#""backend":"beta"#), "Expected beta backend, got: {}", beta_result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_path_routing() {
|
|
|
|
|
let backend1_port = next_port();
|
|
|
|
|
let backend2_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _b1 = start_http_echo_backend(backend1_port, "api").await;
|
|
|
|
|
let _b2 = start_http_echo_backend(backend2_port, "web").await;
|
|
|
|
|
|
|
|
|
|
let mut api_route = make_test_route(proxy_port, None, "127.0.0.1", backend1_port);
|
|
|
|
|
api_route.route_match.path = Some("/api/**".to_string());
|
|
|
|
|
api_route.priority = Some(10);
|
|
|
|
|
|
|
|
|
|
let web_route = make_test_route(proxy_port, None, "127.0.0.1", backend2_port);
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![api_route, web_route],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
// Test API path
|
|
|
|
|
let api_result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "any.com", "GET", "/api/users").await;
|
|
|
|
|
extract_body(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(api_result.contains(r#""backend":"api"#), "Expected api backend, got: {}", api_result);
|
|
|
|
|
|
|
|
|
|
// Test web path (no /api prefix)
|
|
|
|
|
let web_result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "any.com", "GET", "/index.html").await;
|
|
|
|
|
extract_body(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(web_result.contains(r#""backend":"web"#), "Expected web backend, got: {}", web_result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_cors_preflight() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _backend = start_http_echo_backend(backend_port, "main").await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Send CORS preflight request
|
|
|
|
|
let request = format!(
|
|
|
|
|
"OPTIONS /api/data HTTP/1.1\r\nHost: example.com\r\nOrigin: http://localhost:3000\r\nAccess-Control-Request-Method: POST\r\nConnection: close\r\n\r\n",
|
|
|
|
|
);
|
|
|
|
|
stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response = Vec::new();
|
|
|
|
|
stream.read_to_end(&mut response).await.unwrap();
|
|
|
|
|
String::from_utf8_lossy(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Should get 204 No Content with CORS headers
|
|
|
|
|
assert!(result.contains("204"), "Expected 204 status, got: {}", result);
|
|
|
|
|
assert!(result.to_lowercase().contains("access-control-allow-origin"),
|
|
|
|
|
"Expected CORS header, got: {}", result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_backend_error() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
// Start an HTTP server that returns 500
|
|
|
|
|
let _backend = start_http_server(backend_port, 500, "Internal Error").await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "example.com", "GET", "/fail").await;
|
|
|
|
|
response
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Proxy should relay the 500 from backend
|
|
|
|
|
assert!(result.contains("500"), "Expected 500 status, got: {}", result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_no_route_matched() {
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
// Create a route only for a specific domain
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, Some("known.example.com"), "127.0.0.1", 9999)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "unknown.example.com", "GET", "/").await;
|
|
|
|
|
response
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Should get 502 Bad Gateway (no route matched)
|
|
|
|
|
assert!(result.contains("502"), "Expected 502 status, got: {}", result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_http_forward_backend_unavailable() {
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
let dead_port = next_port(); // No server running here
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, None, "127.0.0.1", dead_port)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "example.com", "GET", "/").await;
|
|
|
|
|
response
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Should get 502 Bad Gateway (backend unavailable)
|
|
|
|
|
assert!(result.contains("502"), "Expected 502 status, got: {}", result);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_https_terminate_http_forward() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
let domain = "httpproxy.example.com";
|
|
|
|
|
|
|
|
|
|
let (cert_pem, key_pem) = generate_self_signed_cert(domain);
|
|
|
|
|
let _backend = start_http_echo_backend(backend_port, "tls-backend").await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_tls_terminate_route(
|
|
|
|
|
proxy_port, domain, "127.0.0.1", backend_port, &cert_pem, &key_pem,
|
|
|
|
|
)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
|
let tls_config = rustls::ClientConfig::builder()
|
|
|
|
|
.dangerous()
|
|
|
|
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
|
|
|
|
.with_no_client_auth();
|
|
|
|
|
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
|
|
|
|
|
|
|
|
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let server_name = rustls::pki_types::ServerName::try_from(domain.to_string()).unwrap();
|
|
|
|
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Send HTTP request through TLS
|
|
|
|
|
let request = format!(
|
|
|
|
|
"GET /api/data HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
|
|
|
|
domain
|
|
|
|
|
);
|
|
|
|
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response = Vec::new();
|
|
|
|
|
tls_stream.read_to_end(&mut response).await.unwrap();
|
|
|
|
|
String::from_utf8_lossy(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let body = extract_body(&result);
|
|
|
|
|
assert!(body.contains(r#""method":"GET"#), "Expected GET, got: {}", body);
|
|
|
|
|
assert!(body.contains(r#""path":"/api/data"#), "Expected /api/data, got: {}", body);
|
|
|
|
|
assert!(body.contains(r#""backend":"tls-backend"#), "Expected tls-backend, got: {}", body);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_websocket_through_proxy() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _backend = start_ws_echo_backend(backend_port).await;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![make_test_route(proxy_port, None, "127.0.0.1", backend_port)],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Send WebSocket upgrade request
|
|
|
|
|
let request = format!(
|
|
|
|
|
"GET /ws HTTP/1.1\r\n\
|
|
|
|
|
Host: example.com\r\n\
|
|
|
|
|
Upgrade: websocket\r\n\
|
|
|
|
|
Connection: Upgrade\r\n\
|
|
|
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
|
|
|
|
|
Sec-WebSocket-Version: 13\r\n\
|
|
|
|
|
\r\n"
|
|
|
|
|
);
|
|
|
|
|
stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Read the 101 response
|
|
|
|
|
let mut response_buf = Vec::with_capacity(4096);
|
|
|
|
|
let mut temp = [0u8; 1];
|
|
|
|
|
loop {
|
|
|
|
|
let n = stream.read(&mut temp).await.unwrap();
|
|
|
|
|
if n == 0 { break; }
|
|
|
|
|
response_buf.push(temp[0]);
|
|
|
|
|
if response_buf.len() >= 4 {
|
|
|
|
|
let len = response_buf.len();
|
|
|
|
|
if response_buf[len-4..] == *b"\r\n\r\n" {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let response_str = String::from_utf8_lossy(&response_buf).to_string();
|
|
|
|
|
assert!(response_str.contains("101"), "Expected 101 Switching Protocols, got: {}", response_str);
|
|
|
|
|
assert!(
|
|
|
|
|
response_str.to_lowercase().contains("upgrade: websocket"),
|
|
|
|
|
"Expected Upgrade header, got: {}",
|
|
|
|
|
response_str
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// After upgrade, send data and verify echo
|
|
|
|
|
let test_data = b"Hello WebSocket!";
|
|
|
|
|
stream.write_all(test_data).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Read echoed data
|
|
|
|
|
let mut echo_buf = vec![0u8; 256];
|
|
|
|
|
let n = stream.read(&mut echo_buf).await.unwrap();
|
|
|
|
|
let echoed = &echo_buf[..n];
|
|
|
|
|
|
|
|
|
|
assert_eq!(echoed, test_data, "Expected echo of sent data");
|
|
|
|
|
|
|
|
|
|
"ok".to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(result, "ok");
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 12:11:49 +00:00
|
|
|
/// Test that terminate-and-reencrypt mode routes HTTP traffic through the
|
|
|
|
|
/// full HTTP proxy with per-request Host-based routing.
|
|
|
|
|
///
|
|
|
|
|
/// This verifies the new behavior: after TLS termination, HTTP data is detected
|
|
|
|
|
/// and routed through HttpProxyService (like nginx) instead of being blindly tunneled.
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_terminate_and_reencrypt_http_routing() {
|
|
|
|
|
let backend1_port = next_port();
|
|
|
|
|
let backend2_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let (cert1, key1) = generate_self_signed_cert("alpha.example.com");
|
|
|
|
|
let (cert2, key2) = generate_self_signed_cert("beta.example.com");
|
|
|
|
|
|
2026-02-16 13:29:45 +00:00
|
|
|
// Generate separate backend certs (backends are independent TLS servers)
|
|
|
|
|
let (backend_cert1, backend_key1) = generate_self_signed_cert("localhost");
|
|
|
|
|
let (backend_cert2, backend_key2) = generate_self_signed_cert("localhost");
|
|
|
|
|
|
|
|
|
|
// Start TLS HTTP echo backends (proxy re-encrypts to these)
|
|
|
|
|
let _b1 = start_tls_http_backend(backend1_port, "alpha", &backend_cert1, &backend_key1).await;
|
|
|
|
|
let _b2 = start_tls_http_backend(backend2_port, "beta", &backend_cert2, &backend_key2).await;
|
|
|
|
|
|
2026-02-16 12:11:49 +00:00
|
|
|
// Create terminate-and-reencrypt routes
|
|
|
|
|
let mut route1 = make_tls_terminate_route(
|
|
|
|
|
proxy_port, "alpha.example.com", "127.0.0.1", backend1_port, &cert1, &key1,
|
|
|
|
|
);
|
|
|
|
|
route1.action.tls.as_mut().unwrap().mode = rustproxy_config::TlsMode::TerminateAndReencrypt;
|
|
|
|
|
|
|
|
|
|
let mut route2 = make_tls_terminate_route(
|
|
|
|
|
proxy_port, "beta.example.com", "127.0.0.1", backend2_port, &cert2, &key2,
|
|
|
|
|
);
|
|
|
|
|
route2.action.tls.as_mut().unwrap().mode = rustproxy_config::TlsMode::TerminateAndReencrypt;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![route1, route2],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
// Test alpha domain - HTTP request through TLS terminate-and-reencrypt
|
|
|
|
|
let alpha_result = with_timeout(async {
|
|
|
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
|
let tls_config = rustls::ClientConfig::builder()
|
|
|
|
|
.dangerous()
|
|
|
|
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
|
|
|
|
.with_no_client_auth();
|
|
|
|
|
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
|
|
|
|
|
|
|
|
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let server_name = rustls::pki_types::ServerName::try_from("alpha.example.com".to_string()).unwrap();
|
|
|
|
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let request = "GET /api/data HTTP/1.1\r\nHost: alpha.example.com\r\nConnection: close\r\n\r\n";
|
|
|
|
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response = Vec::new();
|
|
|
|
|
tls_stream.read_to_end(&mut response).await.unwrap();
|
|
|
|
|
String::from_utf8_lossy(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let alpha_body = extract_body(&alpha_result);
|
|
|
|
|
assert!(
|
|
|
|
|
alpha_body.contains(r#""backend":"alpha"#),
|
|
|
|
|
"Expected alpha backend, got: {}",
|
|
|
|
|
alpha_body
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
alpha_body.contains(r#""method":"GET"#),
|
|
|
|
|
"Expected GET method, got: {}",
|
|
|
|
|
alpha_body
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
alpha_body.contains(r#""path":"/api/data"#),
|
|
|
|
|
"Expected /api/data path, got: {}",
|
|
|
|
|
alpha_body
|
|
|
|
|
);
|
2026-02-16 13:43:22 +00:00
|
|
|
// Verify original Host header is preserved (not replaced with backend IP:port)
|
|
|
|
|
assert!(
|
|
|
|
|
alpha_body.contains(r#""host":"alpha.example.com"#),
|
|
|
|
|
"Expected original Host header alpha.example.com, got: {}",
|
|
|
|
|
alpha_body
|
|
|
|
|
);
|
2026-02-16 12:11:49 +00:00
|
|
|
|
|
|
|
|
// Test beta domain - different host goes to different backend
|
|
|
|
|
let beta_result = with_timeout(async {
|
|
|
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
|
let tls_config = rustls::ClientConfig::builder()
|
|
|
|
|
.dangerous()
|
|
|
|
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
|
|
|
|
.with_no_client_auth();
|
|
|
|
|
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
|
|
|
|
|
|
|
|
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let server_name = rustls::pki_types::ServerName::try_from("beta.example.com".to_string()).unwrap();
|
|
|
|
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let request = "GET /other HTTP/1.1\r\nHost: beta.example.com\r\nConnection: close\r\n\r\n";
|
|
|
|
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response = Vec::new();
|
|
|
|
|
tls_stream.read_to_end(&mut response).await.unwrap();
|
|
|
|
|
String::from_utf8_lossy(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let beta_body = extract_body(&beta_result);
|
|
|
|
|
assert!(
|
|
|
|
|
beta_body.contains(r#""backend":"beta"#),
|
|
|
|
|
"Expected beta backend, got: {}",
|
|
|
|
|
beta_body
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
beta_body.contains(r#""path":"/other"#),
|
|
|
|
|
"Expected /other path, got: {}",
|
|
|
|
|
beta_body
|
|
|
|
|
);
|
2026-02-16 13:43:22 +00:00
|
|
|
// Verify original Host header is preserved for beta too
|
|
|
|
|
assert!(
|
|
|
|
|
beta_body.contains(r#""host":"beta.example.com"#),
|
|
|
|
|
"Expected original Host header beta.example.com, got: {}",
|
|
|
|
|
beta_body
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
2026-02-16 12:11:49 +00:00
|
|
|
|
2026-02-16 13:43:22 +00:00
|
|
|
/// Test that WebSocket upgrade works through terminate-and-reencrypt mode.
|
|
|
|
|
///
|
|
|
|
|
/// Verifies the full chain: client→TLS→proxy terminates→re-encrypts→TLS→backend WebSocket.
|
|
|
|
|
/// The proxy's `handle_websocket_upgrade` checks `upstream.use_tls` and calls
|
|
|
|
|
/// `connect_tls_backend()` when true. This test covers that path.
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_terminate_and_reencrypt_websocket() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
let domain = "ws.example.com";
|
|
|
|
|
|
|
|
|
|
// Frontend cert (client→proxy TLS)
|
|
|
|
|
let (frontend_cert, frontend_key) = generate_self_signed_cert(domain);
|
|
|
|
|
// Backend cert (proxy→backend TLS)
|
|
|
|
|
let (backend_cert, backend_key) = generate_self_signed_cert("localhost");
|
|
|
|
|
|
|
|
|
|
// Start TLS WebSocket echo backend
|
|
|
|
|
let _backend = start_tls_ws_echo_backend(backend_port, &backend_cert, &backend_key).await;
|
|
|
|
|
|
|
|
|
|
// Create terminate-and-reencrypt route
|
|
|
|
|
let mut route = make_tls_terminate_route(
|
|
|
|
|
proxy_port,
|
|
|
|
|
domain,
|
|
|
|
|
"127.0.0.1",
|
|
|
|
|
backend_port,
|
|
|
|
|
&frontend_cert,
|
|
|
|
|
&frontend_key,
|
|
|
|
|
);
|
|
|
|
|
route.action.tls.as_mut().unwrap().mode = rustproxy_config::TlsMode::TerminateAndReencrypt;
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![route],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
let result = with_timeout(
|
|
|
|
|
async {
|
|
|
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
|
let tls_config = rustls::ClientConfig::builder()
|
|
|
|
|
.dangerous()
|
|
|
|
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
|
|
|
|
.with_no_client_auth();
|
|
|
|
|
let connector =
|
|
|
|
|
tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
|
|
|
|
|
|
|
|
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let server_name =
|
|
|
|
|
rustls::pki_types::ServerName::try_from(domain.to_string()).unwrap();
|
|
|
|
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Send WebSocket upgrade request through TLS
|
|
|
|
|
let request = format!(
|
|
|
|
|
"GET /ws HTTP/1.1\r\n\
|
|
|
|
|
Host: {}\r\n\
|
|
|
|
|
Upgrade: websocket\r\n\
|
|
|
|
|
Connection: Upgrade\r\n\
|
|
|
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
|
|
|
|
|
Sec-WebSocket-Version: 13\r\n\
|
|
|
|
|
\r\n",
|
|
|
|
|
domain
|
|
|
|
|
);
|
|
|
|
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Read the 101 response (byte-by-byte until \r\n\r\n)
|
|
|
|
|
let mut response_buf = Vec::with_capacity(4096);
|
|
|
|
|
let mut temp = [0u8; 1];
|
|
|
|
|
loop {
|
|
|
|
|
let n = tls_stream.read(&mut temp).await.unwrap();
|
|
|
|
|
if n == 0 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
response_buf.push(temp[0]);
|
|
|
|
|
if response_buf.len() >= 4 {
|
|
|
|
|
let len = response_buf.len();
|
|
|
|
|
if response_buf[len - 4..] == *b"\r\n\r\n" {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let response_str = String::from_utf8_lossy(&response_buf).to_string();
|
|
|
|
|
assert!(
|
|
|
|
|
response_str.contains("101"),
|
|
|
|
|
"Expected 101 Switching Protocols, got: {}",
|
|
|
|
|
response_str
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
response_str.to_lowercase().contains("upgrade: websocket"),
|
|
|
|
|
"Expected Upgrade header, got: {}",
|
|
|
|
|
response_str
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// After upgrade, send data and verify echo
|
|
|
|
|
let test_data = b"Hello TLS WebSocket!";
|
|
|
|
|
tls_stream.write_all(test_data).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Read echoed data
|
|
|
|
|
let mut echo_buf = vec![0u8; 256];
|
|
|
|
|
let n = tls_stream.read(&mut echo_buf).await.unwrap();
|
|
|
|
|
let echoed = &echo_buf[..n];
|
|
|
|
|
|
|
|
|
|
assert_eq!(echoed, test_data, "Expected echo of sent data");
|
|
|
|
|
|
|
|
|
|
"ok".to_string()
|
|
|
|
|
},
|
|
|
|
|
10,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(result, "ok");
|
2026-02-16 12:11:49 +00:00
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Test that the protocol field on route config is accepted and processed.
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_protocol_field_in_route_config() {
|
|
|
|
|
let backend_port = next_port();
|
|
|
|
|
let proxy_port = next_port();
|
|
|
|
|
|
|
|
|
|
let _backend = start_http_echo_backend(backend_port, "main").await;
|
|
|
|
|
|
|
|
|
|
// Create a route with protocol: "http" - should only match HTTP traffic
|
|
|
|
|
let mut route = make_test_route(proxy_port, None, "127.0.0.1", backend_port);
|
|
|
|
|
route.route_match.protocol = Some("http".to_string());
|
|
|
|
|
|
|
|
|
|
let options = RustProxyOptions {
|
|
|
|
|
routes: vec![route],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut proxy = RustProxy::new(options).unwrap();
|
|
|
|
|
proxy.start().await.unwrap();
|
|
|
|
|
assert!(wait_for_port(proxy_port, 2000).await);
|
|
|
|
|
|
|
|
|
|
// HTTP request should match the route and get proxied
|
|
|
|
|
let result = with_timeout(async {
|
|
|
|
|
let response = send_http_request(proxy_port, "example.com", "GET", "/test").await;
|
|
|
|
|
extract_body(&response).to_string()
|
|
|
|
|
}, 10)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
result.contains(r#""backend":"main"#),
|
|
|
|
|
"Expected main backend, got: {}",
|
|
|
|
|
result
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
result.contains(r#""path":"/test"#),
|
|
|
|
|
"Expected /test path, got: {}",
|
|
|
|
|
result
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
proxy.stop().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 10:55:46 +00:00
|
|
|
/// InsecureVerifier for test TLS client connections.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct InsecureVerifier;
|
|
|
|
|
|
|
|
|
|
impl rustls::client::danger::ServerCertVerifier for InsecureVerifier {
|
|
|
|
|
fn verify_server_cert(
|
|
|
|
|
&self,
|
|
|
|
|
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
|
|
|
|
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
|
|
|
|
_server_name: &rustls::pki_types::ServerName<'_>,
|
|
|
|
|
_ocsp_response: &[u8],
|
|
|
|
|
_now: rustls::pki_types::UnixTime,
|
|
|
|
|
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
|
|
|
|
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn verify_tls12_signature(
|
|
|
|
|
&self,
|
|
|
|
|
_message: &[u8],
|
|
|
|
|
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
|
|
|
_dss: &rustls::DigitallySignedStruct,
|
|
|
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
|
|
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn verify_tls13_signature(
|
|
|
|
|
&self,
|
|
|
|
|
_message: &[u8],
|
|
|
|
|
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
|
|
|
_dss: &rustls::DigitallySignedStruct,
|
|
|
|
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
|
|
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
|
|
|
|
vec![
|
|
|
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
|
|
|
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
|
|
|
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
|
|
|
|
rustls::SignatureScheme::ED25519,
|
|
|
|
|
rustls::SignatureScheme::RSA_PSS_SHA256,
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|