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(); } /// 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(); // Start plain HTTP echo backends (proxy terminates client TLS, connects plain to backend) let _b1 = start_http_echo_backend(backend1_port, "alpha").await; let _b2 = start_http_echo_backend(backend2_port, "beta").await; let (cert1, key1) = generate_self_signed_cert("alpha.example.com"); let (cert2, key2) = generate_self_signed_cert("beta.example.com"); // 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 ); // 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 ); 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(); } /// 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 { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &rustls::pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &rustls::pki_types::CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { vec![ rustls::SignatureScheme::RSA_PKCS1_SHA256, rustls::SignatureScheme::ECDSA_NISTP256_SHA256, rustls::SignatureScheme::ECDSA_NISTP384_SHA384, rustls::SignatureScheme::ED25519, rustls::SignatureScheme::RSA_PSS_SHA256, ] } }