use http_1x::Uri;
use hyper_util::client::proxy::matcher::Matcher;
use std::fmt;
#[derive(Debug, Clone)]
pub struct ProxyConfig {
inner: ProxyConfigInner,
}
#[derive(Debug, Clone)]
enum ProxyConfigInner {
FromEnvironment,
Http {
uri: Uri,
auth: Option<ProxyAuth>,
no_proxy: Option<String>,
},
Https {
uri: Uri,
auth: Option<ProxyAuth>,
no_proxy: Option<String>,
},
All {
uri: Uri,
auth: Option<ProxyAuth>,
no_proxy: Option<String>,
},
Disabled,
}
#[derive(Debug, Clone)]
struct ProxyAuth {
username: String,
password: String,
}
#[derive(Debug)]
pub struct ProxyError {
kind: ErrorKind,
}
#[derive(Debug)]
enum ErrorKind {
InvalidUrl(String),
}
impl From<ErrorKind> for ProxyError {
fn from(value: ErrorKind) -> Self {
Self { kind: value }
}
}
impl fmt::Display for ProxyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ErrorKind::InvalidUrl(url) => write!(f, "invalid proxy URL: {url}"),
}
}
}
impl std::error::Error for ProxyError {}
impl ProxyConfig {
pub fn http<U>(proxy_url: U) -> Result<Self, ProxyError>
where
U: TryInto<Uri>,
U::Error: fmt::Display,
{
let uri = proxy_url
.try_into()
.map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
Self::validate_proxy_uri(&uri)?;
Ok(ProxyConfig {
inner: ProxyConfigInner::Http {
uri,
auth: None,
no_proxy: None,
},
})
}
pub fn https<U>(proxy_url: U) -> Result<Self, ProxyError>
where
U: TryInto<Uri>,
U::Error: fmt::Display,
{
let uri = proxy_url
.try_into()
.map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
Self::validate_proxy_uri(&uri)?;
Ok(ProxyConfig {
inner: ProxyConfigInner::Https {
uri,
auth: None,
no_proxy: None,
},
})
}
pub fn all<U>(proxy_url: U) -> Result<Self, ProxyError>
where
U: TryInto<Uri>,
U::Error: fmt::Display,
{
let uri = proxy_url
.try_into()
.map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
Self::validate_proxy_uri(&uri)?;
Ok(ProxyConfig {
inner: ProxyConfigInner::All {
uri,
auth: None,
no_proxy: None,
},
})
}
pub fn disabled() -> Self {
ProxyConfig {
inner: ProxyConfigInner::Disabled,
}
}
pub fn with_basic_auth<U, P>(mut self, username: U, password: P) -> Self
where
U: Into<String>,
P: Into<String>,
{
let auth = ProxyAuth {
username: username.into(),
password: password.into(),
};
match &mut self.inner {
ProxyConfigInner::Http {
auth: ref mut a, ..
} => *a = Some(auth),
ProxyConfigInner::Https {
auth: ref mut a, ..
} => *a = Some(auth),
ProxyConfigInner::All {
auth: ref mut a, ..
} => *a = Some(auth),
ProxyConfigInner::FromEnvironment | ProxyConfigInner::Disabled => {
}
}
self
}
pub fn no_proxy<S: AsRef<str>>(mut self, rules: S) -> Self {
let rules_str = rules.as_ref().to_string();
match &mut self.inner {
ProxyConfigInner::Http {
no_proxy: ref mut n,
..
} => *n = Some(rules_str),
ProxyConfigInner::Https {
no_proxy: ref mut n,
..
} => *n = Some(rules_str),
ProxyConfigInner::All {
no_proxy: ref mut n,
..
} => *n = Some(rules_str),
ProxyConfigInner::FromEnvironment | ProxyConfigInner::Disabled => {
}
}
self
}
pub fn from_env() -> Self {
ProxyConfig {
inner: ProxyConfigInner::FromEnvironment,
}
}
pub fn is_disabled(&self) -> bool {
matches!(self.inner, ProxyConfigInner::Disabled)
}
pub fn is_from_env(&self) -> bool {
matches!(self.inner, ProxyConfigInner::FromEnvironment)
}
pub(crate) fn into_hyper_util_matcher(self) -> Matcher {
match self.inner {
ProxyConfigInner::FromEnvironment => Matcher::from_env(),
ProxyConfigInner::Http {
uri,
auth,
no_proxy,
} => {
let mut builder = Matcher::builder();
let proxy_url = Self::build_proxy_url(uri, auth);
builder = builder.http(proxy_url);
if let Some(no_proxy_rules) = no_proxy {
builder = builder.no(no_proxy_rules);
}
builder.build()
}
ProxyConfigInner::Https {
uri,
auth,
no_proxy,
} => {
let mut builder = Matcher::builder();
let proxy_url = Self::build_proxy_url(uri, auth);
builder = builder.https(proxy_url);
if let Some(no_proxy_rules) = no_proxy {
builder = builder.no(no_proxy_rules);
}
builder.build()
}
ProxyConfigInner::All {
uri,
auth,
no_proxy,
} => {
let mut builder = Matcher::builder();
let proxy_url = Self::build_proxy_url(uri, auth);
builder = builder.all(proxy_url);
if let Some(no_proxy_rules) = no_proxy {
builder = builder.no(no_proxy_rules);
}
builder.build()
}
ProxyConfigInner::Disabled => {
Matcher::builder().build()
}
}
}
pub(crate) fn requires_tls(&self) -> bool {
match &self.inner {
ProxyConfigInner::Http { uri, .. } => uri.scheme_str() == Some("https"),
ProxyConfigInner::Https { uri, .. } => uri.scheme_str() == Some("https"),
ProxyConfigInner::All { uri, .. } => uri.scheme_str() == Some("https"),
ProxyConfigInner::FromEnvironment => {
Self::env_vars_require_tls()
}
ProxyConfigInner::Disabled => false,
}
}
fn env_vars_require_tls() -> bool {
let proxy_vars = [
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
];
for var in &proxy_vars {
if let Ok(proxy_url) = std::env::var(var) {
if !proxy_url.is_empty() {
if proxy_url.starts_with("https://") {
return true;
}
}
}
}
false
}
fn validate_proxy_uri(uri: &Uri) -> Result<(), ProxyError> {
match uri.scheme_str() {
Some("http") | Some("https") => {}
Some(scheme) => {
return Err(
ErrorKind::InvalidUrl(format!("unsupported proxy scheme: {scheme}")).into(),
);
}
None => {
return Err(ErrorKind::InvalidUrl(
"proxy URL must include scheme (http:// or https://)".to_string(),
)
.into());
}
}
if uri.host().is_none() {
return Err(ErrorKind::InvalidUrl("proxy URL must include host".to_string()).into());
}
Ok(())
}
fn build_proxy_url(uri: Uri, auth: Option<ProxyAuth>) -> String {
let uri_str = uri.to_string();
if let Some(auth) = auth {
if let Some(scheme_end) = uri_str.find("://") {
let scheme = &uri_str[..scheme_end + 3];
let rest = &uri_str[scheme_end + 3..];
if rest.contains('@') {
uri_str
} else {
format!("{}{}:{}@{}", scheme, auth.username, auth.password, rest)
}
} else {
uri_str
}
} else {
uri_str
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_proxy_config_http() {
let config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
assert!(!config.is_disabled());
assert!(!config.is_from_env());
}
#[test]
fn test_proxy_config_https() {
let config = ProxyConfig::https("http://proxy.example.com:8080").unwrap();
assert!(!config.is_disabled());
assert!(!config.is_from_env());
}
#[test]
fn test_proxy_config_all() {
let config = ProxyConfig::all("http://proxy.example.com:8080").unwrap();
assert!(!config.is_disabled());
assert!(!config.is_from_env());
}
#[test]
fn test_proxy_config_disabled() {
let config = ProxyConfig::disabled();
assert!(config.is_disabled());
assert!(!config.is_from_env());
}
#[test]
fn test_proxy_config_with_auth() {
let config = ProxyConfig::http("http://proxy.example.com:8080")
.unwrap()
.with_basic_auth("user", "pass");
assert!(!config.is_disabled());
}
#[test]
fn test_proxy_config_with_no_proxy() {
let config = ProxyConfig::http("http://proxy.example.com:8080")
.unwrap()
.no_proxy("localhost,*.internal");
assert!(!config.is_disabled());
}
#[test]
fn test_proxy_config_invalid_url() {
let result = ProxyConfig::http("not-a-url");
assert!(result.is_err());
}
#[test]
fn test_proxy_config_invalid_scheme() {
let result = ProxyConfig::http("ftp://proxy.example.com:8080");
assert!(result.is_err());
}
#[test]
#[serial_test::serial]
fn test_proxy_config_from_env_with_vars() {
let original_http = env::var("HTTP_PROXY");
env::set_var("HTTP_PROXY", "http://test-proxy:8080");
let config = ProxyConfig::from_env();
assert!(config.is_from_env());
match original_http {
Ok(val) => env::set_var("HTTP_PROXY", val),
Err(_) => env::remove_var("HTTP_PROXY"),
}
}
#[test]
#[serial_test::serial]
fn test_proxy_config_from_env_without_vars() {
let original_vars: Vec<_> = [
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
]
.iter()
.map(|var| (*var, env::var(var)))
.collect();
for (var, _) in &original_vars {
env::remove_var(var);
}
let config = ProxyConfig::from_env();
assert!(config.is_from_env());
for (var, original_value) in original_vars {
match original_value {
Ok(val) => env::set_var(var, val),
Err(_) => env::remove_var(var),
}
}
}
#[test]
#[serial_test::serial]
fn test_auth_cannot_be_added_to_env_config() {
let original_http = env::var("HTTP_PROXY");
env::set_var("HTTP_PROXY", "http://test-proxy:8080");
let config = ProxyConfig::from_env().with_basic_auth("user", "pass");
assert!(config.is_from_env());
match original_http {
Ok(val) => env::set_var("HTTP_PROXY", val),
Err(_) => env::remove_var("HTTP_PROXY"),
}
}
#[test]
#[serial_test::serial]
fn test_no_proxy_cannot_be_added_to_env_config() {
let original_http = env::var("HTTP_PROXY");
env::set_var("HTTP_PROXY", "http://test-proxy:8080");
let config = ProxyConfig::from_env().no_proxy("localhost");
assert!(config.is_from_env());
match original_http {
Ok(val) => env::set_var("HTTP_PROXY", val),
Err(_) => env::remove_var("HTTP_PROXY"),
}
}
#[test]
fn test_build_proxy_url_without_auth() {
let uri = "http://proxy.example.com:8080".parse().unwrap();
let url = ProxyConfig::build_proxy_url(uri, None);
assert_eq!(url, "http://proxy.example.com:8080/");
}
#[test]
fn test_build_proxy_url_with_auth() {
let uri = "http://proxy.example.com:8080".parse().unwrap();
let auth = ProxyAuth {
username: "user".to_string(),
password: "pass".to_string(),
};
let url = ProxyConfig::build_proxy_url(uri, Some(auth));
assert_eq!(url, "http://user:pass@proxy.example.com:8080/");
}
#[test]
fn test_build_proxy_url_with_existing_auth() {
let uri = "http://existing:creds@proxy.example.com:8080"
.parse()
.unwrap();
let auth = ProxyAuth {
username: "user".to_string(),
password: "pass".to_string(),
};
let url = ProxyConfig::build_proxy_url(uri, Some(auth));
assert_eq!(url, "http://existing:creds@proxy.example.com:8080/");
}
#[test]
#[serial_test::serial]
fn test_into_hyper_util_matcher_from_env() {
let original_http = env::var("HTTP_PROXY");
env::set_var("HTTP_PROXY", "http://test-proxy:8080");
let config = ProxyConfig::from_env();
let matcher = config.into_hyper_util_matcher();
let test_uri = "http://example.com".parse().unwrap();
let intercept = matcher.intercept(&test_uri);
assert!(intercept.is_some());
match original_http {
Ok(val) => env::set_var("HTTP_PROXY", val),
Err(_) => env::remove_var("HTTP_PROXY"),
}
}
#[test]
fn test_into_hyper_util_matcher_http() {
let config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
let matcher = config.into_hyper_util_matcher();
let test_uri = "http://example.com".parse().unwrap();
let intercept = matcher.intercept(&test_uri);
assert!(intercept.is_some());
assert!(intercept
.unwrap()
.uri()
.to_string()
.starts_with("http://proxy.example.com:8080"));
let https_uri = "https://example.com".parse().unwrap();
let https_intercept = matcher.intercept(&https_uri);
assert!(https_intercept.is_none());
}
#[test]
fn test_into_hyper_util_matcher_with_auth() {
let config = ProxyConfig::http("http://proxy.example.com:8080")
.unwrap()
.with_basic_auth("user", "pass");
let matcher = config.into_hyper_util_matcher();
let test_uri = "http://example.com".parse().unwrap();
let intercept = matcher.intercept(&test_uri);
assert!(intercept.is_some());
let intercept = intercept.unwrap();
assert!(intercept
.uri()
.to_string()
.contains("proxy.example.com:8080"));
assert!(intercept.basic_auth().is_some());
}
#[test]
fn test_into_hyper_util_matcher_disabled() {
let config = ProxyConfig::disabled();
let matcher = config.into_hyper_util_matcher();
let test_uri = "http://example.com".parse().unwrap();
let intercept = matcher.intercept(&test_uri);
assert!(intercept.is_none());
}
#[test]
#[serial_test::serial]
fn test_requires_tls_detection() {
let http_config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
assert!(!http_config.requires_tls());
let https_config = ProxyConfig::http("https://proxy.example.com:8080").unwrap();
assert!(https_config.requires_tls());
let all_http_config = ProxyConfig::all("http://proxy.example.com:8080").unwrap();
assert!(!all_http_config.requires_tls());
env::set_var("HTTP_PROXY", "https://proxy.example.com:8080");
let env_config = ProxyConfig::from_env();
assert!(env_config.requires_tls()); env::remove_var("HTTP_PROXY");
env::set_var("HTTP_PROXY", "http://proxy.example.com:8080");
let env_config = ProxyConfig::from_env();
assert!(!env_config.requires_tls());
env::remove_var("HTTP_PROXY");
let disabled_config = ProxyConfig::disabled();
assert!(!disabled_config.requires_tls());
}
}