use headers::Authorization;
use http::uri::InvalidUri;
use hyper_proxy::{Custom, Intercept, Proxy, ProxyConnector};
use no_proxy::NoProxy;
use url::Url;
use vector_config::configurable_component;
use crate::serde::is_default;
fn from_env(key: &str) -> Option<String> {
std::env::var(key.to_lowercase())
.ok()
.or_else(|| std::env::var(key.to_uppercase()).ok())
}
#[derive(serde::Deserialize, serde::Serialize, Clone, Default, Debug, PartialEq, Eq)]
pub struct NoProxyInterceptor(NoProxy);
impl NoProxyInterceptor {
fn intercept(self, expected_scheme: &'static str) -> Intercept {
Intercept::Custom(Custom::from(
move |scheme: Option<&str>, host: Option<&str>, port: Option<u16>| {
if scheme.is_some() && scheme != Some(expected_scheme) {
return false;
}
let matches = host.map_or(false, |host| {
self.0.matches(host)
|| port.map_or(false, |port| {
let url = format!("{host}:{port}");
self.0.matches(&url)
})
});
!matches
},
))
}
}
#[configurable_component]
#[configurable(metadata(docs::advanced))]
#[derive(Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ProxyConfig {
#[serde(
default = "ProxyConfig::default_enabled",
skip_serializing_if = "is_enabled"
)]
pub enabled: bool,
#[configurable(validation(format = "uri"))]
#[configurable(metadata(docs::examples = "http://foo.bar:3128"))]
#[serde(default, skip_serializing_if = "is_default")]
pub http: Option<String>,
#[configurable(validation(format = "uri"))]
#[serde(default, skip_serializing_if = "is_default")]
#[configurable(metadata(docs::examples = "http://foo.bar:3128"))]
pub https: Option<String>,
#[serde(default, skip_serializing_if = "is_default")]
#[configurable(metadata(docs::examples = "localhost"))]
#[configurable(metadata(docs::examples = ".foo.bar"))]
#[configurable(metadata(docs::examples = "*"))]
pub no_proxy: NoProxy,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
enabled: Self::default_enabled(),
http: None,
https: None,
no_proxy: NoProxy::default(),
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)] fn is_enabled(e: &bool) -> bool {
e == &true
}
impl ProxyConfig {
fn default_enabled() -> bool {
true
}
pub fn from_env() -> Self {
Self {
enabled: true,
http: from_env("HTTP_PROXY"),
https: from_env("HTTPS_PROXY"),
no_proxy: from_env("NO_PROXY").map(NoProxy::from).unwrap_or_default(),
}
}
pub fn merge_with_env(global: &Self, component: &Self) -> Self {
Self::from_env().merge(&global.merge(component))
}
fn interceptor(&self) -> NoProxyInterceptor {
NoProxyInterceptor(self.no_proxy.clone())
}
#[must_use]
pub fn merge(&self, other: &Self) -> Self {
let no_proxy = if other.no_proxy.is_empty() {
self.no_proxy.clone()
} else {
other.no_proxy.clone()
};
Self {
enabled: self.enabled && other.enabled,
http: other.http.clone().or_else(|| self.http.clone()),
https: other.https.clone().or_else(|| self.https.clone()),
no_proxy,
}
}
fn build_proxy(
&self,
proxy_scheme: &'static str,
proxy_url: &Option<String>,
) -> Result<Option<Proxy>, InvalidUri> {
proxy_url
.as_ref()
.map(|url| {
url.parse().map(|parsed| {
let mut proxy = Proxy::new(self.interceptor().intercept(proxy_scheme), parsed);
if let Ok(authority) = Url::parse(url) {
if let Some(password) = authority.password() {
let decoded_user = urlencoding::decode(authority.username())
.expect("username must be valid UTF-8.");
let decoded_pw = urlencoding::decode(password)
.expect("Password must be valid UTF-8.");
proxy.set_authorization(Authorization::basic(
&decoded_user,
&decoded_pw,
));
}
}
proxy
})
})
.transpose()
}
fn http_proxy(&self) -> Result<Option<Proxy>, InvalidUri> {
self.build_proxy("http", &self.http)
}
fn https_proxy(&self) -> Result<Option<Proxy>, InvalidUri> {
self.build_proxy("https", &self.https)
}
pub fn configure<C>(&self, connector: &mut ProxyConnector<C>) -> Result<(), InvalidUri> {
if self.enabled {
if let Some(proxy) = self.http_proxy()? {
connector.add_proxy(proxy);
}
if let Some(proxy) = self.https_proxy()? {
connector.add_proxy(proxy);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use base64::prelude::{Engine as _, BASE64_STANDARD};
use env_test_util::TempEnvVar;
use http::{
header::{AUTHORIZATION, PROXY_AUTHORIZATION},
HeaderName, HeaderValue, Uri,
};
use proptest::prelude::*;
const PROXY_HEADERS: [HeaderName; 2] = [AUTHORIZATION, PROXY_AUTHORIZATION];
use super::*;
impl Arbitrary for ProxyConfig {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
(
any::<bool>(),
any::<Option<String>>(),
any::<Option<String>>(),
)
.prop_map(|(enabled, http, https)| Self {
enabled,
http,
https,
no_proxy: Default::default(),
})
.boxed()
}
}
proptest! {
#[test]
fn encodes_and_decodes_through_yaml(config:ProxyConfig) {
let yaml = serde_yaml::to_string(&config).expect("Could not serialize config");
let reloaded: ProxyConfig = serde_yaml::from_str(&yaml)
.expect("Could not deserialize config");
assert_eq!(config, reloaded);
}
}
#[test]
fn merge_simple() {
let first = ProxyConfig::default();
let second = ProxyConfig {
https: Some("https://2.3.4.5:9876".into()),
..Default::default()
};
let result = first.merge(&second);
assert_eq!(result.http, None);
assert_eq!(result.https, Some("https://2.3.4.5:9876".into()));
}
#[test]
fn merge_fill() {
let first = ProxyConfig {
http: Some("http://1.2.3.4:5678".into()),
..Default::default()
};
let second = ProxyConfig {
https: Some("https://2.3.4.5:9876".into()),
..Default::default()
};
let third = ProxyConfig {
no_proxy: NoProxy::from("localhost"),
..Default::default()
};
let result = first.merge(&second).merge(&third);
assert_eq!(result.http, Some("http://1.2.3.4:5678".into()));
assert_eq!(result.https, Some("https://2.3.4.5:9876".into()));
assert!(result.no_proxy.matches("localhost"));
}
#[test]
fn merge_override() {
let first = ProxyConfig {
http: Some("http://1.2.3.4:5678".into()),
no_proxy: NoProxy::from("127.0.0.1,google.com"),
..Default::default()
};
let second = ProxyConfig {
http: Some("http://1.2.3.4:5678".into()),
https: Some("https://2.3.4.5:9876".into()),
no_proxy: NoProxy::from("localhost"),
..Default::default()
};
let result = first.merge(&second);
assert_eq!(result.http, Some("http://1.2.3.4:5678".into()));
assert_eq!(result.https, Some("https://2.3.4.5:9876".into()));
assert!(!result.no_proxy.matches("127.0.0.1"));
assert!(result.no_proxy.matches("localhost"));
}
#[test]
fn with_environment_variables() {
let global_proxy = ProxyConfig {
http: Some("http://1.2.3.4:5678".into()),
..Default::default()
};
let component_proxy = ProxyConfig {
https: Some("https://2.3.4.5:9876".into()),
..Default::default()
};
let _http = TempEnvVar::new("HTTP_PROXY").with("http://remote.proxy");
let _https = TempEnvVar::new("HTTPS_PROXY");
let result = ProxyConfig::merge_with_env(&global_proxy, &component_proxy);
assert_eq!(result.http, Some("http://1.2.3.4:5678".into()));
assert_eq!(result.https, Some("https://2.3.4.5:9876".into()));
let global_proxy = ProxyConfig {
https: Some("https://2.3.4.5:9876".into()),
..Default::default()
};
let component_proxy = ProxyConfig {
enabled: false,
..Default::default()
};
let result = ProxyConfig::merge_with_env(&global_proxy, &component_proxy);
assert!(!result.enabled);
assert_eq!(result.http, Some("http://remote.proxy".into()));
assert_eq!(result.https, Some("https://2.3.4.5:9876".into()));
}
#[test]
fn build_proxy() {
let config = ProxyConfig {
http: Some("http://1.2.3.4:5678".into()),
https: Some("https://2.3.4.5:9876".into()),
..Default::default()
};
let first = config
.http_proxy()
.expect("should not be an error")
.expect("should not be None");
let second = config
.https_proxy()
.expect("should not be an error")
.expect("should not be None");
assert_eq!(
Some(first.uri()),
Uri::try_from("http://1.2.3.4:5678").as_ref().ok()
);
assert_eq!(
Some(second.uri()),
Uri::try_from("https://2.3.4.5:9876").as_ref().ok()
);
}
#[test]
fn build_proxy_with_basic_authorization() {
let config = ProxyConfig {
http: Some("http://user:pass@1.2.3.4:5678".into()),
https: Some("https://user:pass@2.3.4.5:9876".into()),
..Default::default()
};
let first = config
.http_proxy()
.expect("should not be an error")
.expect("should not be None");
let second = config
.https_proxy()
.expect("should not be an error")
.expect("should not be None");
let encoded_header = format!("Basic {}", BASE64_STANDARD.encode("user:pass"));
let expected_header_value = HeaderValue::from_str(encoded_header.as_str());
assert_eq!(
Some(first.uri()),
Uri::try_from("http://user:pass@1.2.3.4:5678").as_ref().ok()
);
for h in &PROXY_HEADERS {
assert_eq!(first.headers().get(h), expected_header_value.as_ref().ok());
}
assert_eq!(
Some(second.uri()),
Uri::try_from("https://user:pass@2.3.4.5:9876")
.as_ref()
.ok()
);
for h in &PROXY_HEADERS {
assert_eq!(second.headers().get(h), expected_header_value.as_ref().ok());
}
}
#[test]
fn build_proxy_with_special_chars_url_encoded() {
let config = ProxyConfig {
http: Some("http://user:P%40ssw0rd@1.2.3.4:5678".into()),
https: Some("https://user:P%40ssw0rd@2.3.4.5:9876".into()),
..Default::default()
};
let first = config
.http_proxy()
.expect("should not be an error")
.expect("should not be None");
let encoded_header = format!("Basic {}", BASE64_STANDARD.encode("user:P@ssw0rd"));
let expected_header_value = HeaderValue::from_str(encoded_header.as_str());
for h in &PROXY_HEADERS {
assert_eq!(first.headers().get(h), expected_header_value.as_ref().ok());
}
}
}