use crate::{mitm::Proxy, ProxyMiddleware};
#[cfg(feature = "rustls_client")]
use crate::mitm::rustls;
#[cfg(feature = "rustls_client")]
use crate::mitm::rustls::{
client::{
danger::{HandshakeSignatureValid, ServerCertVerified},
WebPkiServerVerifier,
},
DigitallySignedStruct, RootCertStore, SignatureScheme,
};
#[cfg(feature = "rcgen_ca")]
use crate::mitm::certificate_authority::RcgenAuthority;
#[cfg(any(feature = "rustls_client", feature = "rcgen_ca"))]
use rustls_pki_types::CertificateDer;
#[cfg(feature = "rustls_client")]
use rustls_pki_types::{IpAddr, Ipv4Addr, Ipv6Addr, ServerName, UnixTime};
use tokio::net::TcpListener;
use tokio::sync::watch;
use tokio_tungstenite::Connector;
use crate::config::NetworkProxyCompression;
use product_os_http_body::BodyBytes;
use product_os_request::ProductOSRequestClient;
use product_os_security::certificates::Certificates;
use product_os_utilities::ProductOSError;
#[cfg(feature = "rustls_client")]
use hyper_rustls::HttpsConnector;
use hyper_util::{
client::legacy::{connect::HttpConnector, Client},
rt::TokioExecutor,
};
use std::fmt::Debug;
use std::{net::SocketAddr, sync::Arc, time::Duration};
use super::ProxyState;
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum AddrOrListener {
Addr(SocketAddr),
Listener(TcpListener),
}
pub struct ProxyBuilder<C, CA, H, W> {
address_or_listener: Result<AddrOrListener, ProductOSError>,
ca: Result<CA, ProductOSError>,
client: Result<Client<C, BodyBytes>, ProductOSError>,
websocket_connector: Option<Connector>,
http_handler: H,
websocket_handler: W,
compression: NetworkProxyCompression,
certificates: Certificates,
custom_requester: Option<ProductOSRequestClient>,
connect_timeout_ms: Option<u64>,
request_timeout_ms: Option<u64>,
#[cfg(feature = "tor")]
tor_client: Option<arti_client::TorClient<tor_rtcompat::PreferredRuntime>>,
#[cfg(feature = "vpn")]
vpn_client: Option<product_os_vpn::ProductOSVPN>,
}
#[allow(dead_code)]
impl<CA, H, W> ProxyBuilder<HttpConnector, CA, H, W> {
pub fn use_http_client(self) -> Self {
let mut http = HttpConnector::new();
if let Some(ms) = self.connect_timeout_ms {
http.set_connect_timeout(Some(Duration::from_millis(ms)));
}
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: Ok(Client::builder(TokioExecutor::new())
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build(http)),
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
}
#[cfg(feature = "rustls_client")]
#[derive(Debug)]
struct CustomCertificateVerifier {
certifier: Arc<WebPkiServerVerifier>,
}
#[cfg(feature = "rustls_client")]
impl CustomCertificateVerifier {
pub fn new() -> Result<Self, ProductOSError> {
let mut root_store = RootCertStore::empty();
root_store.roots = webpki_roots::TLS_SERVER_ROOTS.to_vec();
let certifier_builder = WebPkiServerVerifier::builder(Arc::new(root_store));
let certifier = certifier_builder.build().map_err(|e| {
ProductOSError::GenericError(format!("Failed to build certificate verifier: {e:?}"))
})?;
Ok(Self { certifier })
}
}
#[cfg(feature = "rustls_client")]
impl rustls::client::danger::ServerCertVerifier for CustomCertificateVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
match server_name {
ServerName::DnsName(name) => match name.to_lowercase_owned().as_ref() {
"localhost" => Ok(ServerCertVerified::assertion()),
_ => self.certifier.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
),
},
ServerName::IpAddress(ip) => {
let v4_localhost = Ipv4Addr::from(core::net::Ipv4Addr::LOCALHOST);
let v6_localhost = Ipv6Addr::from(core::net::Ipv6Addr::LOCALHOST);
match ip {
IpAddr::V4(ip) => {
if ip.eq(&v4_localhost) {
Ok(ServerCertVerified::assertion())
} else {
self.certifier.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)
}
}
IpAddr::V6(ip) => {
if ip.eq(&v6_localhost) {
Ok(ServerCertVerified::assertion())
} else {
self.certifier.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)
}
}
}
}
_ => self.certifier.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
),
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.certifier.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
self.certifier.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.certifier.supported_verify_schemes()
}
}
#[cfg(feature = "rustls_client")]
impl<CA, H, W> ProxyBuilder<HttpsConnector<HttpConnector>, CA, H, W> {
#[cfg(feature = "rustls_client")]
pub fn use_rustls_client(
self,
provider: rustls::crypto::CryptoProvider,
) -> Result<Self, ProductOSError> {
match rustls::ClientConfig::builder_with_provider(Arc::new(provider))
.with_safe_default_protocol_versions()
{
Ok(config) => {
let verifier = CustomCertificateVerifier::new()?;
let rustls_config = config
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth();
let mut http = HttpConnector::new();
http.enforce_http(false);
if let Some(ms) = self.connect_timeout_ms {
http.set_connect_timeout(Some(Duration::from_millis(ms)));
}
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(rustls_config.clone())
.https_or_http()
.enable_all_versions()
.wrap_connector(http);
Ok(Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: Ok(Client::builder(TokioExecutor::new())
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build(https)),
websocket_connector: Some(Connector::Rustls(Arc::new(rustls_config))),
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
})
}
Err(e) => Ok(Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: Err(ProductOSError::GenericError(e.to_string())),
websocket_connector: None,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}),
}
}
}
#[cfg(feature = "rcgen_ca")]
impl<C, H, W> ProxyBuilder<C, RcgenAuthority, H, W> {
#[allow(clippy::needless_pass_by_value)]
pub fn use_certificate_authority(self, certificates: Certificates) -> Self {
let private = certificates.private.clone();
let certs = certificates.certificates.clone();
let ca_result = (|| -> Result<RcgenAuthority, ProductOSError> {
let key_pair =
rcgen::KeyPair::try_from(&rustls_pki_types::PrivatePkcs8KeyDer::from(private))
.map_err(|e| {
ProductOSError::GenericError(format!("Error getting private key: {e:?}"))
})?;
let first_cert = certs
.first()
.ok_or_else(|| ProductOSError::GenericError("No certificates provided".into()))?
.to_owned();
let cert_params = rcgen::CertificateParams::from_ca_cert_der(
&rustls_pki_types::CertificateDer::from(first_cert),
)
.map_err(|e| {
ProductOSError::GenericError(format!("Error getting public key params: {e:?}"))
})?;
let cert = cert_params.self_signed(&key_pair).map_err(|e| {
ProductOSError::GenericError(format!("Error self-signing certificate: {e:?}"))
})?;
Ok(RcgenAuthority::new(key_pair, cert, 1_000))
})();
Self {
address_or_listener: self.address_or_listener,
ca: ca_result,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
}
impl<C, CA> ProxyBuilder<C, CA, ProxyMiddleware, ProxyMiddleware> {
pub fn use_http_handler(self, http_handler: ProxyMiddleware) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
#[allow(dead_code)]
pub fn use_websocket_handler(self, websocket_handler: ProxyMiddleware) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
pub fn new() -> Self {
ProxyBuilder::default()
}
}
impl<C, CA, H, W> ProxyBuilder<C, CA, H, W> {
pub fn use_address(self, address: SocketAddr) -> Self {
Self {
address_or_listener: Ok(AddrOrListener::Addr(address)),
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
#[allow(dead_code)]
pub fn use_listener(self, listener: TcpListener) -> Self {
Self {
address_or_listener: Ok(AddrOrListener::Listener(listener)),
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
#[allow(dead_code)]
pub fn use_custom_websocket_connector(self, connector: Connector) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: Some(connector),
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
pub fn use_custom_requester(self, requester: ProductOSRequestClient) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: Some(requester),
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
pub fn use_compression(self, compression: NetworkProxyCompression) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
pub fn use_connect_timeout(self, ms: u64) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: Some(ms),
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
pub fn use_request_timeout(self, ms: u64) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: Some(ms),
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
#[allow(dead_code)]
pub fn use_custom_client(self, client: Client<C, BodyBytes>) -> Self {
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: Ok(client),
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}
}
#[cfg(feature = "tor")]
pub async fn use_tor_client(self) -> Result<Self, ProductOSError> {
let config = arti_client::TorClientConfig::default();
match arti_client::TorClient::builder()
.config(config)
.bootstrap_behavior(arti_client::BootstrapBehavior::OnDemand)
.create_bootstrapped()
.await
{
Ok(client) => Ok(Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: Some(client),
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
}),
Err(e) => Err(ProductOSError::GenericError(e.to_string())),
}
}
#[cfg(feature = "vpn")]
pub fn use_vpn_client(self, vpn_conf: product_os_vpn::VPNConfig) -> Self {
let mut vpn_client = product_os_vpn::ProductOSVPN::new(vpn_conf);
#[allow(deprecated)]
let vpn_result = match vpn_client.connect_sync() {
product_os_vpn::Status::CONNECTED => {
tracing::info!("Successfully connected to VPN");
Some(vpn_client)
}
product_os_vpn::Status::CONNECTING => {
tracing::warn!("VPN still connecting, storing client");
Some(vpn_client)
}
product_os_vpn::Status::FAILED => {
tracing::warn!("Failed to connect to VPN; not storing client");
None
}
product_os_vpn::Status::ERROR(e) => {
tracing::warn!("Error connecting to VPN: {:?}; not storing client", e);
None
}
product_os_vpn::Status::DISCONNECTED => {
tracing::warn!("Disconnected from VPN early; not storing client");
None
}
};
Self {
address_or_listener: self.address_or_listener,
ca: self.ca,
client: self.client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
compression: self.compression,
certificates: self.certificates,
custom_requester: self.custom_requester,
connect_timeout_ms: self.connect_timeout_ms,
request_timeout_ms: self.request_timeout_ms,
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: vpn_result,
}
}
pub fn build(self) -> Result<Proxy<C, CA, H, W>, ProductOSError> {
let address_or_listener = self.address_or_listener?;
let client = self.client?;
let certificate_authority = self.ca?;
let (shutdown_tx, shutdown_rx) = watch::channel(false);
Ok(Proxy {
address_or_listener: std::sync::Mutex::new(Some(address_or_listener)),
ca: Arc::new(certificate_authority),
client,
websocket_connector: self.websocket_connector,
http_handler: self.http_handler,
websocket_handler: self.websocket_handler,
certificates: self.certificates,
custom_requester: self.custom_requester,
compression: self.compression,
request_timeout_ms: self.request_timeout_ms,
server: None,
shutdown_tx,
shutdown_rx,
state: Arc::new(std::sync::atomic::AtomicU8::new(
ProxyState::Initialized.to_u8(),
)),
stopped_notify: Arc::new(tokio::sync::Notify::new()),
#[cfg(feature = "tor")]
tor_client: self.tor_client,
#[cfg(feature = "vpn")]
vpn_client: self.vpn_client,
})
}
}
impl<C, CA> Default for ProxyBuilder<C, CA, ProxyMiddleware, ProxyMiddleware> {
fn default() -> Self {
Self {
address_or_listener: Err(ProductOSError::GenericError(String::from(
"No address or listener defined yet",
))),
ca: Err(ProductOSError::GenericError(String::from(
"No certificate authority defined yet",
))),
client: Err(ProductOSError::GenericError(String::from(
"No client defined yet",
))),
websocket_connector: None,
http_handler: ProxyMiddleware::default(),
websocket_handler: ProxyMiddleware::default(),
certificates: Certificates {
certificates: Vec::new(),
private: Vec::new(),
format: product_os_security::certificates::CertificatesFormat::Pem,
},
custom_requester: None,
compression: NetworkProxyCompression::None,
connect_timeout_ms: None,
request_timeout_ms: None,
#[cfg(feature = "tor")]
tor_client: None,
#[cfg(feature = "vpn")]
vpn_client: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mitm::certificate_authority::RcgenAuthority;
#[test]
fn test_proxy_builder_build_fails_without_address() {
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new();
let result = builder.build();
match result {
Err(err) => assert!(
format!("{err}").contains("address"),
"Error should mention address, got: {err}"
),
Ok(_) => panic!("Expected build to fail without address"),
}
}
#[test]
fn test_proxy_builder_build_fails_without_ca() {
let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new()
.use_address(addr);
let result = builder.build();
assert!(result.is_err());
}
#[test]
fn test_proxy_builder_build_fails_without_client() {
let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
let certs = Certificates::new_ca(
vec![],
Some(vec!["test-ca".to_string()]),
None,
None,
None,
None,
);
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new()
.use_address(addr)
.use_certificate_authority(certs);
let result = builder.build();
match result {
Err(err) => assert!(
format!("{err}").contains("client"),
"Error should mention client, got: {err}"
),
Ok(_) => panic!("Expected build to fail without client"),
}
}
#[test]
fn test_proxy_builder_use_address() {
let addr: SocketAddr = "127.0.0.1:9090".parse().unwrap();
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new()
.use_address(addr);
assert!(builder.address_or_listener.is_ok());
}
#[test]
fn test_proxy_builder_use_compression() {
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new()
.use_compression(NetworkProxyCompression::Gzip);
assert!(matches!(builder.compression, NetworkProxyCompression::Gzip));
}
#[tokio::test]
async fn test_proxy_builder_use_listener() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::new()
.use_listener(listener);
assert!(builder.address_or_listener.is_ok());
}
#[test]
fn test_proxy_builder_default() {
let builder = ProxyBuilder::<
HttpsConnector<HttpConnector>,
RcgenAuthority,
ProxyMiddleware,
ProxyMiddleware,
>::default();
assert!(builder.address_or_listener.is_err());
assert!(builder.ca.is_err());
assert!(builder.client.is_err());
assert!(builder.websocket_connector.is_none());
assert!(builder.custom_requester.is_none());
assert!(matches!(builder.compression, NetworkProxyCompression::None));
}
}