#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
use std::sync::Arc;
use async_trait::async_trait;
#[cfg(feature = "rustls_client")]
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use parking_lot::Mutex;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
use product_os_server::ServerConfig;
use product_os_capabilities::{RegistryService, Service};
#[cfg(feature = "rcgen_ca")]
use crate::mitm::certificate_authority::RcgenAuthority;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
use crate::proxy_middleware::DefaultAsyncMiddleware;
use product_os_security::certificates::{Certificates, CertificatesFormat};
use product_os_security::RandomGeneratorTemplate;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
use product_os_utilities::ProductOSError;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
type ProxyClient = HttpsConnector<HttpConnector>;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
type ProxyCa = RcgenAuthority;
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
type ProxyClient = HttpConnector;
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
type ProxyCa = ();
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
mod ca;
mod config;
mod error;
mod mitm;
mod proxy_middleware;
pub use crate::config::{
NetworkProxy, NetworkProxyCSPManipulators, NetworkProxyCertificateAuthority,
NetworkProxyCertificateAuthorityFiles, NetworkProxyCertificateAuthorityManaged,
NetworkProxyCertificateAuthorityTrust, NetworkProxyCompression, NetworkProxyHeaderManipulators,
NetworkProxyManipulatorAction, NetworkProxyNetwork, NetworkProxyOnMissingTrust,
NetworkProxyTrustTarget, NetworkProxyTunnel, TunnelType,
};
pub use crate::error::ProxyError;
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
pub use crate::ca::{
active_https_self_test, active_platform_https_self_test, build_setup_prompt,
default_browser_trust_targets, ensure_ca_trust, load_certificate_authority, manual_trust_steps,
needs_reinstall, probe_connect_host, probe_trust_report, run_cert_guide_wizard,
run_post_startup_active_probe, run_startup_trust_guide, session_trust_targets,
strict_all_trusted, system_trust_install_command, trust_status_str, wait_for_proxy_ready,
CaServeMiddleware, CaSetupPrompt, CaTrustReport, CaTrustSetup, EnsureTrustOptions,
EnsureTrustResult, LoadedCa, TargetInstallResult, TargetTrustStatus, TrustInstaller,
TrustProbe, TrustStatus, CA_PEM_PATH, CA_SETUP_PATH,
};
pub use crate::mitm::ProxyState;
pub use crate::proxy_middleware::ProxyMiddleware;
pub struct Proxy {
key: String,
config: crate::config::NetworkProxy,
vpn_config: Option<product_os_vpn::VPN>,
middleware: Option<ProxyMiddleware>,
#[allow(dead_code, clippy::struct_field_names)]
underlying_proxy:
Option<Arc<mitm::Proxy<ProxyClient, ProxyCa, ProxyMiddleware, ProxyMiddleware>>>,
server_task: Option<tokio::task::JoinHandle<Result<(), String>>>,
}
impl Proxy {
#[must_use]
pub fn new(mut config: crate::config::NetworkProxy) -> Self {
config.apply_timeout_defaults();
let mut random_generator = product_os_security::RandomGenerator::new(None);
let key = random_generator.get_random_string(10);
if config.network.port == 0 {
config.network.port = u16::try_from(random_generator.get_random_usize(1025, 65535))
.expect("random port must fit in u16");
}
if config.network.insecure_port == 0 {
loop {
let insecure_port = u16::try_from(random_generator.get_random_usize(1025, 65535))
.expect("random port must fit in u16");
if insecure_port != config.network.port {
config.network.insecure_port = insecure_port;
break;
}
}
}
Self {
key,
config,
vpn_config: None,
middleware: None,
underlying_proxy: None,
server_task: None,
}
}
pub fn set_middleware(&mut self, middleware: Option<ProxyMiddleware>) {
self.middleware = middleware;
}
pub fn set_vpn_config(&mut self, vpn_config: Option<product_os_vpn::VPN>) {
self.vpn_config = vpn_config;
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[allow(clippy::unused_async, clippy::too_many_lines)]
pub async fn run(&mut self) -> Result<(), ProductOSError> {
let mut config = product_os_server::ServerConfig::new();
config.network = product_os_server::Network {
protocol: if self.config.network.secure {
"https".to_string()
} else {
"http".to_string()
},
secure: self.config.network.secure,
host: self.config.network.host.clone(),
port: self.config.network.port,
listen_all_interfaces: self.config.network.listen_all_interfaces,
allow_insecure: self.config.network.allow_insecure,
insecure_port: self.config.network.insecure_port,
insecure_use_different_port: false,
insecure_force_secure: false,
connect_info: false,
};
let cert_config = self
.config
.certificate_authority
.clone()
.ok_or_else(|| ProductOSError::from(ProxyError::NoCertificateAuthority))?;
let loaded_ca = crate::ca::load_certificate_authority(&cert_config)?;
if let Some(trust) = &cert_config.trust {
if let Some(managed_config) = loaded_ca.to_managed_config() {
if let Ok(managed) =
product_os_security::certificates::ManagedCa::load_or_create(&managed_config)
{
if let Err(e) = crate::ca::run_startup_trust_guide(
&managed,
trust,
&self.config.network.host,
self.config.network.port,
)
.await
{
tracing::warn!("startup CA trust guide failed: {e:?}");
}
}
}
}
let post_startup_trust = cert_config.trust.clone();
let post_startup_cert_paths = (loaded_ca.cert_path.clone(), loaded_ca.key_path.clone());
let post_startup_host = self.config.network.host.clone();
let post_startup_port = self.config.network.port;
let certificates = loaded_ca.certificates.clone();
let socket_address = ServerConfig::get_socket_address(
self.config.network.host.as_str(),
self.config.network.port,
self.config.network.listen_all_interfaces,
);
let base_middleware = self
.middleware
.clone()
.unwrap_or_else(|| ProxyMiddleware::new(Arc::new(DefaultAsyncMiddleware::new())));
let serve_endpoint = cert_config
.managed
.as_ref()
.is_some_and(|m| m.serve_cert_endpoint);
let http_handler = if serve_endpoint {
let inner = base_middleware.middleware_handler();
ProxyMiddleware::new(Arc::new(crate::ca::CaServeMiddleware::new(
inner,
loaded_ca.cert_pem.clone(),
self.config.network.host.clone(),
self.config.network.port,
)))
} else {
base_middleware
};
let mut proxy_builder = mitm::Proxy::https_builder()
.use_address(socket_address)
.use_certificate_authority(certificates.clone())
.use_compression(self.config.compression)
.use_rustls_client({
match mitm::rustls::crypto::ring::default_provider().install_default() {
Ok(()) => mitm::rustls::crypto::ring::default_provider(),
Err(existing) => existing.as_ref().clone(),
}
})?
.use_http_handler(http_handler);
if self.config.connect_timeout > 0 {
proxy_builder = proxy_builder.use_connect_timeout(self.config.connect_timeout);
}
if self.config.request_timeout > 0 {
proxy_builder = proxy_builder.use_request_timeout(self.config.request_timeout);
}
if self.config.custom_requester {
proxy_builder = proxy_builder
.use_custom_requester(product_os_request::ProductOSRequestClient::new());
}
if self.config.enable_tunnel {
match self.config.tunnel_settings.tunnel_type {
crate::config::TunnelType::Tor => {
#[cfg(feature = "tor")]
{
tracing::info!("Enabling Tor");
proxy_builder = proxy_builder.use_tor_client().await.map_err(|e| {
ProductOSError::from(ProxyError::TorBootstrapFailed(format!("{e:?}")))
})?;
}
#[cfg(not(feature = "tor"))]
{
return Err(ProductOSError::from(ProxyError::TorFeatureDisabled));
}
}
crate::config::TunnelType::Vpn => {
#[cfg(feature = "vpn")]
{
match &self.vpn_config {
None => return Err(ProductOSError::from(ProxyError::VpnConfigMissing)),
Some(vpn_conf) => {
proxy_builder = proxy_builder.use_vpn_client(vpn_conf.clone());
}
}
}
#[cfg(not(feature = "vpn"))]
{
return Err(ProductOSError::from(ProxyError::VpnFeatureDisabled));
}
}
}
}
let proxy = proxy_builder
.build()
.map_err(|e| ProductOSError::from(ProxyError::BuildFailed(format!("{e:?}"))))?;
self.underlying_proxy = Some(Arc::new(proxy));
let port = self.config.network.port;
if let Some(proxy) = &self.underlying_proxy {
let proxy_clone = proxy.clone();
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
tracing::info!("Starting SSL Proxy server on port: {}", port);
let handle = tokio::spawn(async move {
let result = proxy_clone.start_with_signal(Some(started_tx)).await;
match result {
Ok(()) => {
tracing::info!("SSL Proxy server stopped cleanly on port: {}", port);
Ok(())
}
Err(e) => {
let message = e.to_string();
tracing::error!(
"SSL Proxy server exited with error on port {}: {}",
port,
message
);
Err(message)
}
}
});
match started_rx.await {
Ok(Ok(())) => {
tracing::info!("SSL Proxy server started successfully on port: {}", port);
self.server_task = Some(handle);
}
Ok(Err(message)) => {
let _ = handle.await;
self.underlying_proxy = None;
return Err(ProductOSError::from(ProxyError::StartupFailed(message)));
}
Err(_) => {
let task_result = handle.await.ok().and_then(Result::err);
self.underlying_proxy = None;
let message = task_result.unwrap_or_else(|| {
"proxy startup task ended before signaling readiness".to_string()
});
return Err(ProductOSError::from(ProxyError::StartupFailed(message)));
}
}
if let Some(trust) = post_startup_trust {
if trust.active_https_probe {
if let (Some(cert_path), Some(key_path)) = post_startup_cert_paths {
let managed_config = crate::ca::LoadedCa {
cert_path: Some(cert_path),
key_path: Some(key_path),
certificates: certificates.clone(),
cert_pem: Vec::new(),
}
.to_managed_config();
tokio::spawn(async move {
if let Some(managed_config) = managed_config {
if let Ok(managed) =
product_os_security::certificates::ManagedCa::load_or_create(
&managed_config,
)
{
let _ = crate::ca::run_post_startup_active_probe(
&managed,
&trust,
&post_startup_host,
post_startup_port,
)
.await;
}
}
});
}
}
}
}
Ok(())
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
pub async fn halt(&mut self) -> Result<(), ProductOSError> {
match &self.underlying_proxy {
None => Err(ProductOSError::from(ProxyError::NoUnderlyingProxy)),
Some(proxy) => {
let proxy_clone = proxy.clone();
let port = self.config.network.port;
tracing::info!("Stopping SSL Proxy server on port: {}", port);
proxy_clone.stop().await?;
if let Some(handle) = self.server_task.take() {
match handle.await {
Ok(Ok(())) => {}
Ok(Err(message)) => {
self.underlying_proxy = None;
return Err(ProductOSError::from(ProxyError::StartupFailed(message)));
}
Err(err) => {
self.underlying_proxy = None;
return Err(ProductOSError::from(ProxyError::StartupFailed(
err.to_string(),
)));
}
}
}
self.underlying_proxy = None;
Ok(())
}
}
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
pub async fn rerun(&mut self) -> Result<(), ProductOSError> {
self.halt().await?;
self.run().await
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[must_use]
pub fn get_state(&self) -> ProxyState {
match &self.underlying_proxy {
None => ProxyState::Uninitialized,
Some(proxy) => proxy.get_state(),
}
}
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
#[must_use]
pub fn get_state(&self) -> ProxyState {
ProxyState::Uninitialized
}
#[must_use]
pub fn get_config(&self) -> &crate::config::NetworkProxy {
&self.config
}
#[must_use]
pub fn get_port(&self) -> u16 {
self.config.network.port
}
#[cfg(test)]
#[must_use]
pub fn test_middleware(&self) -> &Option<ProxyMiddleware> {
&self.middleware
}
#[cfg(test)]
#[must_use]
pub fn test_vpn_config(&self) -> &Option<product_os_vpn::VPN> {
&self.vpn_config
}
#[must_use]
pub fn generate_certificate_authority(cert_path: Option<(String, String)>) -> Certificates {
match cert_path {
None => Certificates::new_ca_with_profile(
&product_os_security::certificates::CaProfile::development(),
Some(CertificatesFormat::Pkcs8),
),
Some((cert_file, key_file)) => {
Certificates::new_ca_from_file(cert_file, key_file, None)
}
}
}
pub fn load_or_create_managed_certificate_authority(
storage_path: Option<std::path::PathBuf>,
) -> Result<LoadedCa, ProductOSError> {
let ca_config = crate::config::NetworkProxyCertificateAuthority {
managed: Some(crate::config::NetworkProxyCertificateAuthorityManaged {
enabled: true,
storage_path: storage_path.map(|path| path.display().to_string()),
..Default::default()
}),
..Default::default()
};
crate::ca::load_certificate_authority(&ca_config)
}
}
#[async_trait]
impl Service for Proxy {
async fn register(&self, _service: Arc<dyn Service>) -> RegistryService {
tracing::warn!("Proxy requires mutable registration via register_mut");
RegistryService {
identifier: "Proxy".to_string(),
key: self.key.clone(),
kind: "Proxy".to_string(),
active: false,
enabled: false,
created_at: Default::default(),
updated_at: Default::default(),
service: None,
service_mut: None,
}
}
async fn register_mut(&self, service: Arc<Mutex<dyn Service>>) -> RegistryService {
RegistryService {
identifier: "Proxy".to_string(),
key: self.key.clone(),
kind: "Proxy".to_string(),
active: false,
enabled: false,
created_at: Default::default(),
updated_at: Default::default(),
service: None,
service_mut: Some(service),
}
}
fn identifier(&self) -> String {
"Proxy".to_string()
}
fn key(&self) -> String {
self.key.clone()
}
fn is_enabled(&self) -> bool {
self.config.enable
}
fn is_active(&self) -> bool {
matches!(self.get_state(), ProxyState::Started)
}
async fn status(&self) -> Result<(), ()> {
if matches!(self.get_state(), ProxyState::Started) {
Ok(())
} else {
Err(())
}
}
async fn init_service(&self) -> Result<(), ()> {
Ok(())
}
async fn start(&self) -> Result<(), ()> {
Err(())
}
async fn stop(&self) -> Result<(), ()> {
Err(())
}
async fn restart(&self) -> Result<(), ()> {
Err(())
}
async fn init_service_mut(&mut self) -> Result<(), ()> {
Ok(())
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
async fn start_mut(&mut self) -> Result<(), ()> {
self.run().await.map_err(|e| {
tracing::error!("Failed to start proxy: {:?}", e);
})
}
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
async fn start_mut(&mut self) -> Result<(), ()> {
tracing::error!("Proxy requires rustls_client and rcgen_ca features to run");
Err(())
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
async fn stop_mut(&mut self) -> Result<(), ()> {
match self.halt().await {
Ok(()) => Ok(()),
Err(_) => Err(()),
}
}
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
async fn stop_mut(&mut self) -> Result<(), ()> {
Err(())
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
async fn restart_mut(&mut self) -> Result<(), ()> {
match self.rerun().await {
Ok(()) => Ok(()),
Err(_) => Err(()),
}
}
#[cfg(not(all(feature = "rustls_client", feature = "rcgen_ca")))]
async fn restart_mut(&mut self) -> Result<(), ()> {
Err(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{
NetworkProxy, NetworkProxyCertificateAuthority, NetworkProxyCertificateAuthorityManaged,
NetworkProxyCompression, NetworkProxyNetwork, NetworkProxyTunnel, TunnelType,
};
use tempfile::TempDir;
fn create_test_config() -> NetworkProxy {
NetworkProxy {
enable: true,
network: NetworkProxyNetwork {
secure: true,
host: "127.0.0.1".to_string(),
port: 0, listen_all_interfaces: false,
allow_insecure: false,
insecure_port: 0,
},
certificate_authority: None, compression: NetworkProxyCompression::None,
custom_requester: false,
enable_tunnel: false,
..Default::default()
}
}
fn create_managed_test_config() -> (NetworkProxy, TempDir) {
let dir = tempfile::tempdir().expect("temp dir");
let mut config = create_test_config();
config.certificate_authority = Some(NetworkProxyCertificateAuthority {
managed: Some(NetworkProxyCertificateAuthorityManaged {
enabled: true,
storage_path: Some(dir.path().display().to_string()),
..Default::default()
}),
..Default::default()
});
(config, dir)
}
#[test]
fn test_proxy_new_creates_instance() {
let config = create_test_config();
let proxy = Proxy::new(config);
let key = proxy.key();
assert!(!key.is_empty());
assert_eq!(key.len(), 10);
}
#[test]
fn test_proxy_new_assigns_random_port_when_zero() {
let config = create_test_config();
let proxy = Proxy::new(config);
let port = proxy.get_port();
assert!(port >= 1025);
assert_ne!(port, proxy.get_config().network.insecure_port);
}
#[test]
fn test_proxy_new_preserves_explicit_port() {
let mut config = create_test_config();
config.network.port = 8080;
let proxy = Proxy::new(config);
assert_eq!(proxy.get_port(), 8080);
}
#[test]
fn test_proxy_get_config_returns_reference() {
let config = create_test_config();
let proxy = Proxy::new(config);
let retrieved_config = proxy.get_config();
assert_eq!(retrieved_config.network.host, "127.0.0.1");
assert!(retrieved_config.network.secure);
}
#[test]
fn test_proxy_get_state_uninitialized() {
let config = create_test_config();
let proxy = Proxy::new(config);
assert_eq!(proxy.get_state(), ProxyState::Uninitialized);
}
#[test]
fn test_proxy_set_middleware() {
let config = create_test_config();
let mut proxy = Proxy::new(config);
let middleware = ProxyMiddleware::new(Arc::new(DefaultAsyncMiddleware::new()));
proxy.set_middleware(Some(middleware));
assert!(proxy.test_middleware().is_some());
}
#[test]
fn test_proxy_set_middleware_none() {
let config = create_test_config();
let mut proxy = Proxy::new(config);
proxy.set_middleware(None);
assert!(proxy.test_middleware().is_none());
}
#[test]
fn test_proxy_set_vpn_config() {
let config = create_test_config();
let mut proxy = Proxy::new(config);
proxy.set_vpn_config(None);
assert!(proxy.test_vpn_config().is_none());
}
#[tokio::test]
async fn test_proxy_halt_without_underlying_proxy_returns_error() {
let config = create_test_config();
let mut proxy = Proxy::new(config);
let result = proxy.halt().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_proxy_rerun_without_underlying_proxy_returns_error() {
let config = create_test_config();
let mut proxy = Proxy::new(config);
let result = proxy.rerun().await;
assert!(result.is_err());
}
#[test]
fn test_proxy_generate_certificate_authority_with_none() {
let certs = Proxy::generate_certificate_authority(None);
assert!(!certs.certificates.is_empty());
}
#[test]
fn test_proxy_middleware_new() {
let middleware_handler = Arc::new(DefaultAsyncMiddleware::new());
let _middleware = ProxyMiddleware::new(middleware_handler);
}
#[test]
fn test_proxy_middleware_default() {
let _middleware = ProxyMiddleware::default();
}
#[test]
fn test_proxy_state_enum_equality() {
assert_eq!(ProxyState::Uninitialized, ProxyState::Uninitialized);
assert_eq!(ProxyState::Initialized, ProxyState::Initialized);
assert_eq!(ProxyState::Started, ProxyState::Started);
assert_eq!(ProxyState::StopRequested, ProxyState::StopRequested);
assert_eq!(ProxyState::Stopped, ProxyState::Stopped);
assert_ne!(ProxyState::Uninitialized, ProxyState::Initialized);
}
#[test]
fn test_proxy_state_enum_clone() {
let state = ProxyState::Started;
let cloned = state;
assert_eq!(state, cloned);
}
#[test]
fn test_proxy_service_trait_identifier() {
let config = create_test_config();
let proxy = Proxy::new(config);
assert_eq!(proxy.identifier(), "Proxy");
}
#[test]
fn test_proxy_service_trait_key() {
let config = create_test_config();
let proxy = Proxy::new(config);
let key = proxy.key();
assert_eq!(key.len(), 10);
}
#[tokio::test]
async fn test_proxy_service_trait_status_requires_running_proxy() {
let config = create_test_config();
let proxy = Proxy::new(config);
let result = proxy.status().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_proxy_service_trait_register_mut() {
let config = create_test_config();
let proxy = Proxy::new(config);
let proxy_arc = Arc::new(Mutex::new(proxy));
let config2 = create_test_config();
let proxy2 = Proxy::new(config2);
let registry = proxy2.register_mut(proxy_arc).await;
assert_eq!(registry.identifier, "Proxy");
assert_eq!(registry.kind, "Proxy");
assert!(!registry.active);
assert!(!registry.enabled);
}
#[test]
fn test_proxy_service_trait_is_enabled_respects_config() {
let mut config = create_test_config();
config.enable = false;
let proxy = Proxy::new(config);
assert!(!proxy.is_enabled());
let proxy = Proxy::new(create_test_config());
assert!(proxy.is_enabled());
}
#[test]
fn test_default_async_middleware_new() {
let _middleware = DefaultAsyncMiddleware::new();
}
#[test]
fn test_proxy_state_debug() {
let state = ProxyState::Started;
let debug_str = format!("{state:?}");
assert!(debug_str.contains("Started"));
}
#[test]
fn test_proxy_state_partial_eq() {
assert!(ProxyState::Started == ProxyState::Started);
assert!(ProxyState::Started != ProxyState::Stopped);
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[tokio::test]
async fn test_proxy_integration_forward_http_request() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::net::TcpStream;
let proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy_port = proxy_listener.local_addr().unwrap().port();
let certs = Proxy::generate_certificate_authority(None);
let proxy = mitm::Proxy::https_builder()
.use_listener(proxy_listener)
.use_certificate_authority(certs)
.use_rustls_client({
match mitm::rustls::crypto::ring::default_provider().install_default() {
Ok(()) => mitm::rustls::crypto::ring::default_provider(),
Err(existing) => existing.as_ref().clone(),
}
})
.expect("rustls client config")
.use_http_handler(ProxyMiddleware::default())
.build()
.unwrap();
let proxy = Arc::new(proxy);
let proxy_clone = proxy.clone();
tokio::spawn(async move {
let _ = proxy_clone.start().await;
});
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
let upstream_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let upstream_port = upstream_listener.local_addr().unwrap().port();
let upstream_handle = tokio::spawn(async move {
let (mut stream, _) = upstream_listener.accept().await.unwrap();
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
let _ = stream.write_all(response).await;
let _ = stream.flush().await;
});
let mut client = TcpStream::connect((std::net::IpAddr::from([127, 0, 0, 1]), proxy_port))
.await
.unwrap();
let request = format!(
"GET http://127.0.0.1:{upstream_port}/ HTTP/1.1\r\nHost: 127.0.0.1:{upstream_port}\r\n\r\n"
);
client.write_all(request.as_bytes()).await.unwrap();
client.flush().await.unwrap();
let mut buf = vec![0u8; 1024];
let n = client.read(&mut buf).await.unwrap();
let response = String::from_utf8_lossy(&buf[..n]);
assert!(
response.contains("200 OK"),
"expected 200 OK in response, got: {response}"
);
let _ = proxy.stop().await;
let _ = upstream_handle.await;
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[tokio::test]
async fn test_proxy_run_waits_for_real_startup() {
let (config, _dir) = create_managed_test_config();
let mut proxy = Proxy::new(config);
proxy.run().await.expect("proxy run");
assert_eq!(proxy.get_state(), ProxyState::Started);
assert!(proxy.status().await.is_ok());
wait_for_proxy_ready("127.0.0.1", proxy.get_port())
.await
.expect("proxy ready");
proxy.halt().await.expect("proxy halt");
assert_eq!(proxy.get_state(), ProxyState::Uninitialized);
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[tokio::test]
async fn test_proxy_run_surfaces_bind_failure() {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let (mut config, _dir) = create_managed_test_config();
config.network.port = port;
config.network.insecure_port = port + 1;
let mut proxy = Proxy::new(config);
let err = proxy.run().await.expect_err("bind should fail");
assert!(format!("{err}").contains("proxy startup failed"));
assert_eq!(proxy.get_state(), ProxyState::Uninitialized);
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca"))]
#[tokio::test]
async fn test_proxy_rerun_performs_real_restart() {
let (config, _dir) = create_managed_test_config();
let mut proxy = Proxy::new(config);
proxy.run().await.expect("first start");
let port = proxy.get_port();
proxy.rerun().await.expect("restart");
assert_eq!(proxy.get_port(), port);
assert_eq!(proxy.get_state(), ProxyState::Started);
wait_for_proxy_ready("127.0.0.1", port)
.await
.expect("proxy ready after rerun");
proxy.halt().await.expect("halt after rerun");
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca", not(feature = "vpn")))]
#[tokio::test]
async fn test_proxy_run_rejects_vpn_tunnel_when_feature_disabled() {
let (mut config, _dir) = create_managed_test_config();
config.enable_tunnel = true;
config.tunnel_settings = NetworkProxyTunnel {
tunnel_type: TunnelType::Vpn,
..Default::default()
};
let mut proxy = Proxy::new(config);
let err = proxy
.run()
.await
.expect_err("vpn feature should be required");
assert!(format!("{err}").contains("'vpn' feature"));
}
#[cfg(all(feature = "rustls_client", feature = "rcgen_ca", feature = "vpn"))]
#[tokio::test]
async fn test_proxy_run_rejects_missing_vpn_config() {
let (mut config, _dir) = create_managed_test_config();
config.enable_tunnel = true;
config.tunnel_settings = NetworkProxyTunnel {
tunnel_type: TunnelType::Vpn,
..Default::default()
};
let mut proxy = Proxy::new(config);
let err = proxy
.run()
.await
.expect_err("vpn config should be required");
assert!(format!("{err}").contains("no VPN config"));
}
#[test]
fn test_load_or_create_managed_certificate_authority_returns_paths() {
let dir = tempfile::tempdir().expect("temp dir");
let loaded =
Proxy::load_or_create_managed_certificate_authority(Some(dir.path().to_path_buf()))
.expect("managed CA");
assert!(loaded.cert_path.is_some());
assert!(loaded.key_path.is_some());
}
}