#[cfg(all(target_os = "macos", feature = "system-proxy"))]
mod mac;
#[cfg(unix)]
mod uds;
#[cfg(all(windows, feature = "system-proxy"))]
mod win;
pub(crate) mod matcher;
use std::hash::{Hash, Hasher};
#[cfg(unix)]
use std::{path::Path, sync::Arc};
use http::{HeaderMap, Uri, header::HeaderValue};
use crate::{IntoUri, ext::UriExt};
#[derive(Clone, Debug)]
pub struct Proxy {
extra: Extra,
scheme: ProxyScheme,
no_proxy: Option<NoProxy>,
}
#[derive(Clone, Debug, Default)]
pub struct NoProxy {
inner: String,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, PartialEq, Eq)]
pub(crate) enum Intercepted {
Proxy(matcher::Intercept),
#[cfg(unix)]
Unix(Arc<Path>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct Matcher {
inner: Box<matcher::Matcher>,
}
#[derive(Clone, Debug)]
enum ProxyScheme {
All(Uri),
Http(Uri),
Https(Uri),
#[cfg(unix)]
Unix(Arc<Path>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct Extra {
auth: Option<HeaderValue>,
misc: Option<HeaderMap>,
}
impl Proxy {
pub fn http<U: IntoUri>(uri: U) -> crate::Result<Proxy> {
uri.into_uri().map(ProxyScheme::Http).map(Proxy::new)
}
pub fn https<U: IntoUri>(uri: U) -> crate::Result<Proxy> {
uri.into_uri().map(ProxyScheme::Https).map(Proxy::new)
}
pub fn all<U: IntoUri>(uri: U) -> crate::Result<Proxy> {
uri.into_uri().map(ProxyScheme::All).map(Proxy::new)
}
#[cfg(unix)]
pub fn unix<P: uds::IntoUnixSocket>(unix: P) -> crate::Result<Proxy> {
Ok(Proxy::new(ProxyScheme::Unix(unix.unix_socket())))
}
fn new(scheme: ProxyScheme) -> Proxy {
Proxy {
extra: Extra {
auth: None,
misc: None,
},
scheme,
no_proxy: None,
}
}
pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
match self.scheme {
ProxyScheme::All(ref mut uri)
| ProxyScheme::Http(ref mut uri)
| ProxyScheme::Https(ref mut uri) => {
let header = crate::util::basic_auth(username, Some(password));
uri.set_userinfo(username, Some(password));
self.extra.auth = Some(header);
}
#[cfg(unix)]
ProxyScheme::Unix(_) => {
}
}
self
}
pub fn custom_http_auth(mut self, header_value: HeaderValue) -> Proxy {
self.extra.auth = Some(header_value);
self
}
pub fn custom_http_headers(mut self, headers: HeaderMap) -> Proxy {
match self.scheme {
ProxyScheme::All(_) | ProxyScheme::Http(_) | ProxyScheme::Https(_) => {
self.extra.misc = Some(headers);
}
#[cfg(unix)]
ProxyScheme::Unix(_) => {
}
}
self
}
pub fn no_proxy(mut self, no_proxy: Option<NoProxy>) -> Proxy {
self.no_proxy = no_proxy;
self
}
pub(crate) fn into_matcher(self) -> Matcher {
let Proxy {
scheme,
extra,
no_proxy,
} = self;
let no_proxy = no_proxy.as_ref().map_or("", |n| n.inner.as_ref());
let inner = match scheme {
ProxyScheme::All(uri) => matcher::Matcher::builder()
.all(uri.to_string())
.no(no_proxy)
.build(extra),
ProxyScheme::Http(uri) => matcher::Matcher::builder()
.http(uri.to_string())
.no(no_proxy)
.build(extra),
ProxyScheme::Https(uri) => matcher::Matcher::builder()
.https(uri.to_string())
.no(no_proxy)
.build(extra),
#[cfg(unix)]
ProxyScheme::Unix(unix) => matcher::Matcher::builder()
.unix(unix)
.no(no_proxy)
.build(extra),
};
Matcher {
inner: Box::new(inner),
}
}
}
impl NoProxy {
pub fn from_env() -> Option<NoProxy> {
let raw = std::env::var("NO_PROXY")
.or_else(|_| std::env::var("no_proxy"))
.ok()?;
Some(Self::from_string(&raw).unwrap_or_default())
}
pub fn from_string(no_proxy_list: &str) -> Option<Self> {
Some(NoProxy {
inner: no_proxy_list.into(),
})
}
}
impl Matcher {
pub(crate) fn system() -> Self {
Self {
inner: Box::new(matcher::Matcher::from_system()),
}
}
#[inline]
pub(crate) fn intercept(&self, dst: &Uri) -> Option<Intercepted> {
self.inner.intercept(dst)
}
}
impl Hash for Extra {
fn hash<H: Hasher>(&self, state: &mut H) {
self.auth.hash(state);
if let Some(ref misc) = self.misc {
for (k, v) in misc.iter() {
k.as_str().hash(state);
v.as_bytes().hash(state);
}
} else {
1u8.hash(state);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uri(s: &str) -> Uri {
s.parse().unwrap()
}
fn intercept(p: &Matcher, s: &Uri) -> matcher::Intercept {
match p.intercept(s).unwrap() {
Intercepted::Proxy(proxy) => proxy,
_ => {
unreachable!("intercepted_port should only be called with a Proxy matcher")
}
}
}
fn intercepted_uri(p: &Matcher, s: &str) -> Uri {
match p.intercept(&s.parse().unwrap()).unwrap() {
Intercepted::Proxy(proxy) => proxy.uri().clone(),
_ => {
unreachable!("intercepted_uri should only be called with a Proxy matcher")
}
}
}
#[test]
fn test_http() {
let target = "http://example.domain/";
let p = Proxy::http(target).unwrap().into_matcher();
let http = "http://hyper.rs";
let other = "https://hyper.rs";
assert_eq!(intercepted_uri(&p, http), target);
assert!(p.intercept(&uri(other)).is_none());
}
#[test]
fn test_https() {
let target = "http://example.domain/";
let p = Proxy::https(target).unwrap().into_matcher();
let http = "http://hyper.rs";
let other = "https://hyper.rs";
assert!(p.intercept(&uri(http)).is_none());
assert_eq!(intercepted_uri(&p, other), target);
}
#[test]
fn test_all() {
let target = "http://example.domain/";
let p = Proxy::all(target).unwrap().into_matcher();
let http = "http://hyper.rs";
let https = "https://hyper.rs";
assert_eq!(intercepted_uri(&p, http), target);
assert_eq!(intercepted_uri(&p, https), target);
}
#[test]
fn test_standard_with_custom_auth_header() {
let target = "http://example.domain/";
let p = Proxy::all(target)
.unwrap()
.custom_http_auth(http::HeaderValue::from_static("testme"))
.into_matcher();
let got = intercept(&p, &uri("http://anywhere.local"));
let auth = got.basic_auth().unwrap();
assert_eq!(auth, "testme");
}
#[test]
fn test_maybe_has_http_auth() {
let uri = uri("http://example.domain/");
let m = Proxy::all("https://letme:in@yo.local")
.unwrap()
.into_matcher();
let got = intercept(&m, &uri);
assert!(got.basic_auth().is_some(), "https forwards");
let m = Proxy::all("http://letme:in@yo.local")
.unwrap()
.into_matcher();
let got = intercept(&m, &uri);
assert!(got.basic_auth().is_some(), "http forwards");
}
#[test]
fn test_maybe_has_http_custom_headers() {
let uri = uri("http://example.domain/");
let mut headers = HeaderMap::new();
headers.insert("x-custom-header", HeaderValue::from_static("custom-value"));
let m = Proxy::all("https://yo.local")
.unwrap()
.custom_http_headers(headers.clone())
.into_matcher();
match m.intercept(&uri).unwrap() {
Intercepted::Proxy(proxy) => {
let got_headers = proxy.custom_headers().unwrap();
assert_eq!(got_headers, &headers, "https forwards");
}
_ => {
unreachable!("Expected a Proxy Intercepted");
}
}
let m = Proxy::all("http://yo.local")
.unwrap()
.custom_http_headers(headers.clone())
.into_matcher();
match m.intercept(&uri).unwrap() {
Intercepted::Proxy(proxy) => {
let got_headers = proxy.custom_headers().unwrap();
assert_eq!(got_headers, &headers, "http forwards");
}
_ => {
unreachable!("Expected a Proxy Intercepted");
}
}
}
fn test_socks_proxy_default_port(uri: &str, url2: &str, port: u16) {
let m = Proxy::all(uri).unwrap().into_matcher();
let http = "http://hyper.rs";
let https = "https://hyper.rs";
assert_eq!(intercepted_uri(&m, http).port_u16(), Some(1080));
assert_eq!(intercepted_uri(&m, https).port_u16(), Some(1080));
let m = Proxy::all(url2).unwrap().into_matcher();
assert_eq!(intercepted_uri(&m, http).port_u16(), Some(port));
assert_eq!(intercepted_uri(&m, https).port_u16(), Some(port));
}
#[test]
fn test_socks4_proxy_default_port() {
test_socks_proxy_default_port("socks4://example.com", "socks4://example.com:1234", 1234);
test_socks_proxy_default_port("socks4a://example.com", "socks4a://example.com:1234", 1234);
}
#[test]
fn test_socks5_proxy_default_port() {
test_socks_proxy_default_port("socks5://example.com", "socks5://example.com:1234", 1234);
test_socks_proxy_default_port("socks5h://example.com", "socks5h://example.com:1234", 1234);
}
}