use std::sync::Arc;
use hyper_http_proxy::{Intercept, Proxy, ProxyConnector as HyperProxyConnector};
use hyper_util::client::legacy::connect::HttpConnector;
use tracing::info;
use crate::ZerobusError;
pub(crate) type ProxiedConnector = HyperProxyConnector<HttpConnector>;
pub struct ProxyConnector(ProxiedConnector);
impl ProxyConnector {
#[allow(clippy::result_large_err)]
pub fn new(proxy_uri: &str) -> Result<Self, ZerobusError> {
build_connector(proxy_uri).map(Self)
}
pub(crate) fn into_inner(self) -> ProxiedConnector {
self.0
}
}
#[allow(clippy::result_large_err)]
fn build_connector(proxy_uri: &str) -> Result<ProxiedConnector, ZerobusError> {
let uri = proxy_uri.parse().map_err(|e| {
ZerobusError::InvalidArgument(format!("failed to parse proxy URL '{}': {}", proxy_uri, e))
})?;
let mut proxy = Proxy::new(Intercept::All, uri);
proxy.force_connect();
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
HyperProxyConnector::from_proxy(http_connector, proxy).map_err(|e| {
ZerobusError::ChannelCreationError(format!("failed to build proxy connector: {}", e))
})
}
pub type ConnectorFactory = Arc<dyn Fn(&str) -> Option<ProxyConnector> + Send + Sync>;
const PROXY_ENV_VARS: &[&str] = &[
"grpc_proxy",
"GRPC_PROXY",
"https_proxy",
"HTTPS_PROXY",
"http_proxy",
"HTTP_PROXY",
];
const NO_PROXY_ENV_VARS: &[&str] = &["no_grpc_proxy", "NO_GRPC_PROXY", "no_proxy", "NO_PROXY"];
fn read_first_env(names: &[&str]) -> Option<String> {
for name in names {
if let Ok(val) = std::env::var(name) {
if !val.is_empty() {
return Some(val);
}
}
}
None
}
pub(crate) fn create_proxy_connector() -> Option<ProxiedConnector> {
let proxy_url = read_first_env(PROXY_ENV_VARS)?;
info!("Using HTTP proxy: {}", proxy_url);
match build_connector(&proxy_url) {
Ok(pc) => Some(pc),
Err(e) => {
tracing::warn!("{}", e);
None
}
}
}
pub(crate) fn is_no_proxy(host: &str) -> bool {
let no_proxy = read_first_env(NO_PROXY_ENV_VARS).unwrap_or_default();
host_matches_no_proxy(host, &no_proxy)
}
fn host_matches_no_proxy(host: &str, no_proxy: &str) -> bool {
if no_proxy.is_empty() {
return false;
}
if no_proxy.trim() == "*" {
return true;
}
no_proxy.split(',').any(|entry| {
let entry = entry.trim().trim_start_matches('.');
host == entry || host.ends_with(&format!(".{}", entry))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_proxy_empty_returns_false() {
assert!(!host_matches_no_proxy("example.com", ""));
}
#[test]
fn no_proxy_wildcard_matches_everything() {
assert!(host_matches_no_proxy("anything.com", "*"));
assert!(host_matches_no_proxy("localhost", " * "));
}
#[test]
fn no_proxy_exact_match() {
assert!(host_matches_no_proxy("example.com", "example.com"));
assert!(!host_matches_no_proxy("other.com", "example.com"));
}
#[test]
fn no_proxy_suffix_match() {
assert!(host_matches_no_proxy(
"workspace.cloud.databricks.com",
"databricks.com"
));
assert!(host_matches_no_proxy("foo.example.com", "example.com"));
assert!(!host_matches_no_proxy("notexample.com", "example.com"));
}
#[test]
fn no_proxy_leading_dot_stripped() {
assert!(host_matches_no_proxy("foo.example.com", ".example.com"));
assert!(host_matches_no_proxy("example.com", ".example.com"));
}
#[test]
fn no_proxy_comma_separated() {
let no_proxy = "localhost, 127.0.0.1, .internal.corp";
assert!(host_matches_no_proxy("localhost", no_proxy));
assert!(host_matches_no_proxy("127.0.0.1", no_proxy));
assert!(host_matches_no_proxy("service.internal.corp", no_proxy));
assert!(!host_matches_no_proxy("external.com", no_proxy));
}
#[test]
fn no_proxy_whitespace_handling() {
assert!(host_matches_no_proxy("example.com", " example.com "));
assert!(host_matches_no_proxy(
"example.com",
"other.com , example.com , more.com"
));
}
}