1752 lines
58 KiB
Rust
1752 lines
58 KiB
Rust
use std::fmt;
|
|
#[cfg(feature = "socks")]
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
use crate::into_url::{IntoUrl, IntoUrlSealed};
|
|
use crate::Url;
|
|
use http::{header::HeaderValue, Uri};
|
|
use ipnet::IpNet;
|
|
use once_cell::sync::Lazy;
|
|
use percent_encoding::percent_decode;
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::error::Error;
|
|
use std::net::IpAddr;
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::enums::HKEY_CURRENT_USER;
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::RegKey;
|
|
|
|
/// Configuration of a proxy that a `Client` should pass requests to.
|
|
///
|
|
/// A `Proxy` has a couple pieces to it:
|
|
///
|
|
/// - a URL of how to talk to the proxy
|
|
/// - rules on what `Client` requests should be directed to the proxy
|
|
///
|
|
/// For instance, let's look at `Proxy::http`:
|
|
///
|
|
/// ```rust
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let proxy = reqwest::Proxy::http("https://secure.example")?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// This proxy will intercept all HTTP requests, and make use of the proxy
|
|
/// at `https://secure.example`. A request to `http://hyper.rs` will talk
|
|
/// to your proxy. A request to `https://hyper.rs` will not.
|
|
///
|
|
/// Multiple `Proxy` rules can be configured for a `Client`. The `Client` will
|
|
/// check each `Proxy` in the order it was added. This could mean that a
|
|
/// `Proxy` added first with eager intercept rules, such as `Proxy::all`,
|
|
/// would prevent a `Proxy` later in the list from ever working, so take care.
|
|
///
|
|
/// By enabling the `"socks"` feature it is possible to use a socks proxy:
|
|
/// ```rust
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let proxy = reqwest::Proxy::http("socks5://192.168.1.1:9000")?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[derive(Clone)]
|
|
pub struct Proxy {
|
|
intercept: Intercept,
|
|
no_proxy: Option<NoProxy>,
|
|
}
|
|
|
|
/// Represents a possible matching entry for an IP address
|
|
#[derive(Clone, Debug)]
|
|
enum Ip {
|
|
Address(IpAddr),
|
|
Network(IpNet),
|
|
}
|
|
|
|
/// A wrapper around a list of IP cidr blocks or addresses with a [IpMatcher::contains] method for
|
|
/// checking if an IP address is contained within the matcher
|
|
#[derive(Clone, Debug, Default)]
|
|
struct IpMatcher(Vec<Ip>);
|
|
|
|
/// A wrapper around a list of domains with a [DomainMatcher::contains] method for checking if a
|
|
/// domain is contained within the matcher
|
|
#[derive(Clone, Debug, Default)]
|
|
struct DomainMatcher(Vec<String>);
|
|
|
|
/// A configuration for filtering out requests that shouldn't be proxied
|
|
#[derive(Clone, Debug, Default)]
|
|
struct NoProxy {
|
|
ips: IpMatcher,
|
|
domains: DomainMatcher,
|
|
}
|
|
|
|
/// A particular scheme used for proxying requests.
|
|
///
|
|
/// For example, HTTP vs SOCKS5
|
|
#[derive(Clone)]
|
|
pub enum ProxyScheme {
|
|
Http {
|
|
auth: Option<HeaderValue>,
|
|
host: http::uri::Authority,
|
|
},
|
|
Https {
|
|
auth: Option<HeaderValue>,
|
|
host: http::uri::Authority,
|
|
},
|
|
#[cfg(feature = "socks")]
|
|
Socks5 {
|
|
addr: SocketAddr,
|
|
auth: Option<(String, String)>,
|
|
remote_dns: bool,
|
|
},
|
|
}
|
|
|
|
impl ProxyScheme {
|
|
fn maybe_http_auth(&self) -> Option<&HeaderValue> {
|
|
match self {
|
|
ProxyScheme::Http { auth, .. } | ProxyScheme::Https { auth, .. } => auth.as_ref(),
|
|
#[cfg(feature = "socks")]
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait used for converting into a proxy scheme. This trait supports
|
|
/// parsing from a URL-like type, whilst also supporting proxy schemes
|
|
/// built directly using the factory methods.
|
|
pub trait IntoProxyScheme {
|
|
fn into_proxy_scheme(self) -> crate::Result<ProxyScheme>;
|
|
}
|
|
|
|
impl<S: IntoUrl> IntoProxyScheme for S {
|
|
fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
|
|
// validate the URL
|
|
let url = match self.as_str().into_url() {
|
|
Ok(ok) => ok,
|
|
Err(e) => {
|
|
let mut presumed_to_have_scheme = true;
|
|
let mut source = e.source();
|
|
while let Some(err) = source {
|
|
if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
|
|
match parse_error {
|
|
url::ParseError::RelativeUrlWithoutBase => {
|
|
presumed_to_have_scheme = false;
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
} else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() {
|
|
presumed_to_have_scheme = false;
|
|
break;
|
|
}
|
|
source = err.source();
|
|
}
|
|
if !presumed_to_have_scheme {
|
|
// the issue could have been caused by a missing scheme, so we try adding http://
|
|
let try_this = format!("http://{}", self.as_str());
|
|
try_this.into_url().map_err(|_| {
|
|
// return the original error
|
|
crate::error::builder(e)
|
|
})?
|
|
} else {
|
|
return Err(crate::error::builder(e));
|
|
}
|
|
}
|
|
};
|
|
ProxyScheme::parse(url)
|
|
}
|
|
}
|
|
|
|
// These bounds are accidentally leaked by the blanket impl of IntoProxyScheme
|
|
// for all types that implement IntoUrl. So, this function exists to detect
|
|
// if we were to break those bounds for a user.
|
|
fn _implied_bounds() {
|
|
fn prox<T: IntoProxyScheme>(_t: T) {}
|
|
|
|
fn url<T: IntoUrl>(t: T) {
|
|
prox(t);
|
|
}
|
|
}
|
|
|
|
impl IntoProxyScheme for ProxyScheme {
|
|
fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
impl Proxy {
|
|
/// Proxy all HTTP traffic to the passed URL.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let client = reqwest::Client::builder()
|
|
/// .proxy(reqwest::Proxy::http("https://my.prox")?)
|
|
/// .build()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn http<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
|
|
Ok(Proxy::new(Intercept::Http(
|
|
proxy_scheme.into_proxy_scheme()?,
|
|
)))
|
|
}
|
|
|
|
/// Proxy all HTTPS traffic to the passed URL.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let client = reqwest::Client::builder()
|
|
/// .proxy(reqwest::Proxy::https("https://example.prox:4545")?)
|
|
/// .build()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn https<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
|
|
Ok(Proxy::new(Intercept::Https(
|
|
proxy_scheme.into_proxy_scheme()?,
|
|
)))
|
|
}
|
|
|
|
/// Proxy **all** traffic to the passed URL.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let client = reqwest::Client::builder()
|
|
/// .proxy(reqwest::Proxy::all("http://pro.xy")?)
|
|
/// .build()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn all<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
|
|
Ok(Proxy::new(Intercept::All(
|
|
proxy_scheme.into_proxy_scheme()?,
|
|
)))
|
|
}
|
|
|
|
/// Provide a custom function to determine what traffic to proxy to where.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let target = reqwest::Url::parse("https://my.prox")?;
|
|
/// let client = reqwest::Client::builder()
|
|
/// .proxy(reqwest::Proxy::custom(move |url| {
|
|
/// if url.host_str() == Some("hyper.rs") {
|
|
/// Some(target.clone())
|
|
/// } else {
|
|
/// None
|
|
/// }
|
|
/// }))
|
|
/// .build()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn custom<F, U: IntoProxyScheme>(fun: F) -> Proxy
|
|
where
|
|
F: Fn(&Url) -> Option<U> + Send + Sync + 'static,
|
|
{
|
|
Proxy::new(Intercept::Custom(Custom {
|
|
auth: None,
|
|
func: Arc::new(move |url| fun(url).map(IntoProxyScheme::into_proxy_scheme)),
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn system() -> Proxy {
|
|
let mut proxy = if cfg!(feature = "__internal_proxy_sys_no_cache") {
|
|
Proxy::new(Intercept::System(Arc::new(get_sys_proxies(
|
|
get_from_registry(),
|
|
))))
|
|
} else {
|
|
Proxy::new(Intercept::System(SYS_PROXIES.clone()))
|
|
};
|
|
proxy.no_proxy = NoProxy::new();
|
|
proxy
|
|
}
|
|
|
|
fn new(intercept: Intercept) -> Proxy {
|
|
Proxy {
|
|
intercept,
|
|
no_proxy: None,
|
|
}
|
|
}
|
|
|
|
/// Set the `Proxy-Authorization` header using Basic auth.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # fn run() -> Result<(), Box<std::error::Error>> {
|
|
/// let proxy = reqwest::Proxy::https("http://localhost:1234")?
|
|
/// .basic_auth("Aladdin", "open sesame");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
|
|
self.intercept.set_basic_auth(username, password);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn maybe_has_http_auth(&self) -> bool {
|
|
match &self.intercept {
|
|
Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().is_some(),
|
|
// Custom *may* match 'http', so assume so.
|
|
Intercept::Custom(_) => true,
|
|
Intercept::System(system) => system
|
|
.get("http")
|
|
.and_then(|s| s.maybe_http_auth())
|
|
.is_some(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn http_basic_auth<D: Dst>(&self, uri: &D) -> Option<HeaderValue> {
|
|
match &self.intercept {
|
|
Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().cloned(),
|
|
Intercept::System(system) => system
|
|
.get("http")
|
|
.and_then(|s| s.maybe_http_auth().cloned()),
|
|
Intercept::Custom(custom) => {
|
|
custom.call(uri).and_then(|s| s.maybe_http_auth().cloned())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn intercept<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
|
|
match self.intercept {
|
|
Intercept::All(ref u) => Some(u.clone()),
|
|
Intercept::Http(ref u) => {
|
|
if uri.scheme() == "http" {
|
|
Some(u.clone())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Intercept::Https(ref u) => {
|
|
if uri.scheme() == "https" {
|
|
Some(u.clone())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Intercept::System(ref map) => {
|
|
let in_no_proxy = self
|
|
.no_proxy
|
|
.as_ref()
|
|
.map_or(false, |np| np.contains(uri.host()));
|
|
if in_no_proxy {
|
|
None
|
|
} else {
|
|
map.get(uri.scheme()).cloned()
|
|
}
|
|
}
|
|
Intercept::Custom(ref custom) => custom.call(uri),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_match<D: Dst>(&self, uri: &D) -> bool {
|
|
match self.intercept {
|
|
Intercept::All(_) => true,
|
|
Intercept::Http(_) => uri.scheme() == "http",
|
|
Intercept::Https(_) => uri.scheme() == "https",
|
|
Intercept::System(ref map) => map.contains_key(uri.scheme()),
|
|
Intercept::Custom(ref custom) => custom.call(uri).is_some(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Proxy {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.debug_tuple("Proxy")
|
|
.field(&self.intercept)
|
|
.field(&self.no_proxy)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl NoProxy {
|
|
/// Returns a new no-proxy configuration based on environment variables (or `None` if no variables are set)
|
|
///
|
|
/// The rules are as follows:
|
|
/// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked
|
|
/// * If neither environment variable is set, `None` is returned
|
|
/// * Entries are expected to be comma-separated (whitespace between entries is ignored)
|
|
/// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
|
|
/// for example "`192.168.1.0/24`").
|
|
/// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
|
|
/// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
|
|
/// and `.google.com` are equivalent) and would match both that domain AND all subdomains.
|
|
///
|
|
/// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match
|
|
/// (and therefore would bypass the proxy):
|
|
/// * `http://google.com/`
|
|
/// * `http://www.google.com/`
|
|
/// * `http://192.168.1.42/`
|
|
///
|
|
/// The URL `http://notgoogle.com/` would not match.
|
|
///
|
|
fn new() -> Option<Self> {
|
|
let raw = env::var("NO_PROXY")
|
|
.or_else(|_| env::var("no_proxy"))
|
|
.unwrap_or_default();
|
|
if raw.is_empty() {
|
|
return None;
|
|
}
|
|
let mut ips = Vec::new();
|
|
let mut domains = Vec::new();
|
|
let parts = raw.split(',').map(str::trim);
|
|
for part in parts {
|
|
match part.parse::<IpNet>() {
|
|
// If we can parse an IP net or address, then use it, otherwise, assume it is a domain
|
|
Ok(ip) => ips.push(Ip::Network(ip)),
|
|
Err(_) => match part.parse::<IpAddr>() {
|
|
Ok(addr) => ips.push(Ip::Address(addr)),
|
|
Err(_) => domains.push(part.to_owned()),
|
|
},
|
|
}
|
|
}
|
|
Some(NoProxy {
|
|
ips: IpMatcher(ips),
|
|
domains: DomainMatcher(domains),
|
|
})
|
|
}
|
|
|
|
fn contains(&self, host: &str) -> bool {
|
|
// According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off
|
|
// the end in order to parse correctly
|
|
let host = if host.starts_with('[') {
|
|
let x: &[_] = &['[', ']'];
|
|
host.trim_matches(x)
|
|
} else {
|
|
host
|
|
};
|
|
match host.parse::<IpAddr>() {
|
|
// If we can parse an IP addr, then use it, otherwise, assume it is a domain
|
|
Ok(ip) => self.ips.contains(ip),
|
|
Err(_) => self.domains.contains(host),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IpMatcher {
|
|
fn contains(&self, addr: IpAddr) -> bool {
|
|
for ip in self.0.iter() {
|
|
match ip {
|
|
Ip::Address(address) => {
|
|
if &addr == address {
|
|
return true;
|
|
}
|
|
}
|
|
Ip::Network(net) => {
|
|
if net.contains(&addr) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
impl DomainMatcher {
|
|
// The following links may be useful to understand the origin of these rules:
|
|
// * https://curl.se/libcurl/c/CURLOPT_NOPROXY.html
|
|
// * https://github.com/curl/curl/issues/1208
|
|
fn contains(&self, domain: &str) -> bool {
|
|
let domain_len = domain.len();
|
|
for d in self.0.iter() {
|
|
if d == domain || (d.starts_with('.') && &d[1..] == domain) {
|
|
return true;
|
|
} else if domain.ends_with(d) {
|
|
if d.starts_with('.') {
|
|
// If the first character of d is a dot, that means the first character of domain
|
|
// must also be a dot, so we are looking at a subdomain of d and that matches
|
|
return true;
|
|
} else if domain.as_bytes()[domain_len - d.len() - 1] == b'.' {
|
|
// Given that d is a prefix of domain, if the prior character in domain is a dot
|
|
// then that means we must be matching a subdomain of d, and that matches
|
|
return true;
|
|
}
|
|
} else if d == "*" {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
impl ProxyScheme {
|
|
// To start conservative, keep builders private for now.
|
|
|
|
/// Proxy traffic via the specified URL over HTTP
|
|
fn http(host: &str) -> crate::Result<Self> {
|
|
Ok(ProxyScheme::Http {
|
|
auth: None,
|
|
host: host.parse().map_err(crate::error::builder)?,
|
|
})
|
|
}
|
|
|
|
/// Proxy traffic via the specified URL over HTTPS
|
|
fn https(host: &str) -> crate::Result<Self> {
|
|
Ok(ProxyScheme::Https {
|
|
auth: None,
|
|
host: host.parse().map_err(crate::error::builder)?,
|
|
})
|
|
}
|
|
|
|
/// Proxy traffic via the specified socket address over SOCKS5
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Current SOCKS5 support is provided via blocking IO.
|
|
#[cfg(feature = "socks")]
|
|
fn socks5(addr: SocketAddr) -> crate::Result<Self> {
|
|
Ok(ProxyScheme::Socks5 {
|
|
addr,
|
|
auth: None,
|
|
remote_dns: false,
|
|
})
|
|
}
|
|
|
|
/// Proxy traffic via the specified socket address over SOCKS5H
|
|
///
|
|
/// This differs from SOCKS5 in that DNS resolution is also performed via the proxy.
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Current SOCKS5 support is provided via blocking IO.
|
|
#[cfg(feature = "socks")]
|
|
fn socks5h(addr: SocketAddr) -> crate::Result<Self> {
|
|
Ok(ProxyScheme::Socks5 {
|
|
addr,
|
|
auth: None,
|
|
remote_dns: true,
|
|
})
|
|
}
|
|
|
|
/// Use a username and password when connecting to the proxy server
|
|
fn with_basic_auth<T: Into<String>, U: Into<String>>(
|
|
mut self,
|
|
username: T,
|
|
password: U,
|
|
) -> Self {
|
|
self.set_basic_auth(username, password);
|
|
self
|
|
}
|
|
|
|
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
|
|
match *self {
|
|
ProxyScheme::Http { ref mut auth, .. } => {
|
|
let header = encode_basic_auth(&username.into(), &password.into());
|
|
*auth = Some(header);
|
|
}
|
|
ProxyScheme::Https { ref mut auth, .. } => {
|
|
let header = encode_basic_auth(&username.into(), &password.into());
|
|
*auth = Some(header);
|
|
}
|
|
#[cfg(feature = "socks")]
|
|
ProxyScheme::Socks5 { ref mut auth, .. } => {
|
|
*auth = Some((username.into(), password.into()));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self {
|
|
match self {
|
|
ProxyScheme::Http { ref mut auth, .. } => {
|
|
if auth.is_none() {
|
|
*auth = update.clone();
|
|
}
|
|
}
|
|
ProxyScheme::Https { ref mut auth, .. } => {
|
|
if auth.is_none() {
|
|
*auth = update.clone();
|
|
}
|
|
}
|
|
#[cfg(feature = "socks")]
|
|
ProxyScheme::Socks5 { .. } => {}
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
/// Convert a URL into a proxy scheme
|
|
///
|
|
/// Supported schemes: HTTP, HTTPS, (SOCKS5, SOCKS5H if `socks` feature is enabled).
|
|
// Private for now...
|
|
fn parse(url: Url) -> crate::Result<Self> {
|
|
use url::Position;
|
|
|
|
// Resolve URL to a host and port
|
|
#[cfg(feature = "socks")]
|
|
let to_addr = || {
|
|
let addrs = url
|
|
.socket_addrs(|| match url.scheme() {
|
|
"socks5" | "socks5h" => Some(1080),
|
|
_ => None,
|
|
})
|
|
.map_err(crate::error::builder)?;
|
|
addrs
|
|
.into_iter()
|
|
.next()
|
|
.ok_or_else(|| crate::error::builder("unknown proxy scheme"))
|
|
};
|
|
|
|
let mut scheme = match url.scheme() {
|
|
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
|
|
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
|
|
#[cfg(feature = "socks")]
|
|
"socks5" => Self::socks5(to_addr()?)?,
|
|
#[cfg(feature = "socks")]
|
|
"socks5h" => Self::socks5h(to_addr()?)?,
|
|
_ => return Err(crate::error::builder("unknown proxy scheme")),
|
|
};
|
|
|
|
if let Some(pwd) = url.password() {
|
|
let decoded_username = percent_decode(url.username().as_bytes()).decode_utf8_lossy();
|
|
let decoded_password = percent_decode(pwd.as_bytes()).decode_utf8_lossy();
|
|
scheme = scheme.with_basic_auth(decoded_username, decoded_password);
|
|
}
|
|
|
|
Ok(scheme)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn scheme(&self) -> &str {
|
|
match self {
|
|
ProxyScheme::Http { .. } => "http",
|
|
ProxyScheme::Https { .. } => "https",
|
|
#[cfg(feature = "socks")]
|
|
ProxyScheme::Socks5 { .. } => "socks5",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn host(&self) -> &str {
|
|
match self {
|
|
ProxyScheme::Http { host, .. } => host.as_str(),
|
|
ProxyScheme::Https { host, .. } => host.as_str(),
|
|
#[cfg(feature = "socks")]
|
|
ProxyScheme::Socks5 { .. } => panic!("socks5"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for ProxyScheme {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
ProxyScheme::Http { auth: _auth, host } => write!(f, "http://{}", host),
|
|
ProxyScheme::Https { auth: _auth, host } => write!(f, "https://{}", host),
|
|
#[cfg(feature = "socks")]
|
|
ProxyScheme::Socks5 {
|
|
addr,
|
|
auth: _auth,
|
|
remote_dns,
|
|
} => {
|
|
let h = if *remote_dns { "h" } else { "" };
|
|
write!(f, "socks5{}://{}", h, addr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type SystemProxyMap = HashMap<String, ProxyScheme>;
|
|
type RegistryProxyValues = (u32, String);
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum Intercept {
|
|
All(ProxyScheme),
|
|
Http(ProxyScheme),
|
|
Https(ProxyScheme),
|
|
System(Arc<SystemProxyMap>),
|
|
Custom(Custom),
|
|
}
|
|
|
|
impl Intercept {
|
|
fn set_basic_auth(&mut self, username: &str, password: &str) {
|
|
match self {
|
|
Intercept::All(ref mut s)
|
|
| Intercept::Http(ref mut s)
|
|
| Intercept::Https(ref mut s) => s.set_basic_auth(username, password),
|
|
Intercept::System(_) => unimplemented!(),
|
|
Intercept::Custom(ref mut custom) => {
|
|
let header = encode_basic_auth(username, password);
|
|
custom.auth = Some(header);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Custom {
|
|
// This auth only applies if the returned ProxyScheme doesn't have an auth...
|
|
auth: Option<HeaderValue>,
|
|
func: Arc<dyn Fn(&Url) -> Option<crate::Result<ProxyScheme>> + Send + Sync + 'static>,
|
|
}
|
|
|
|
impl Custom {
|
|
fn call<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
|
|
let url = format!(
|
|
"{}://{}{}{}",
|
|
uri.scheme(),
|
|
uri.host(),
|
|
uri.port().map(|_| ":").unwrap_or(""),
|
|
uri.port().map(|p| p.to_string()).unwrap_or_default()
|
|
)
|
|
.parse()
|
|
.expect("should be valid Url");
|
|
|
|
(self.func)(&url)
|
|
.and_then(|result| result.ok())
|
|
.map(|scheme| scheme.if_no_auth(&self.auth))
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Custom {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.write_str("_")
|
|
}
|
|
}
|
|
|
|
pub(crate) fn encode_basic_auth(username: &str, password: &str) -> HeaderValue {
|
|
let val = format!("{}:{}", username, password);
|
|
let mut header = format!("Basic {}", base64::encode(&val))
|
|
.parse::<HeaderValue>()
|
|
.expect("base64 is always valid HeaderValue");
|
|
header.set_sensitive(true);
|
|
header
|
|
}
|
|
|
|
/// A helper trait to allow testing `Proxy::intercept` without having to
|
|
/// construct `hyper::client::connect::Destination`s.
|
|
pub(crate) trait Dst {
|
|
fn scheme(&self) -> &str;
|
|
fn host(&self) -> &str;
|
|
fn port(&self) -> Option<u16>;
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
impl Dst for Uri {
|
|
fn scheme(&self) -> &str {
|
|
self.scheme().expect("Uri should have a scheme").as_str()
|
|
}
|
|
|
|
fn host(&self) -> &str {
|
|
Uri::host(self).expect("<Uri as Dst>::host should have a str")
|
|
}
|
|
|
|
fn port(&self) -> Option<u16> {
|
|
self.port().map(|p| p.as_u16())
|
|
}
|
|
}
|
|
|
|
static SYS_PROXIES: Lazy<Arc<SystemProxyMap>> =
|
|
Lazy::new(|| Arc::new(get_sys_proxies(get_from_registry())));
|
|
|
|
/// Get system proxies information.
|
|
///
|
|
/// It can only support Linux, Unix like, and windows system. Note that it will always
|
|
/// return a HashMap, even if something runs into error when find registry information in
|
|
/// Windows system. Note that invalid proxy url in the system setting will be ignored.
|
|
///
|
|
/// Returns:
|
|
/// System proxies information as a hashmap like
|
|
/// {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")}
|
|
fn get_sys_proxies(
|
|
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))] registry_values: Option<
|
|
RegistryProxyValues,
|
|
>,
|
|
) -> SystemProxyMap {
|
|
let proxies = get_from_environment();
|
|
|
|
// TODO: move the following #[cfg] to `if expression` when attributes on `if` expressions allowed
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if proxies.is_empty() {
|
|
// don't care errors if can't get proxies from registry, just return an empty HashMap.
|
|
if let Some(registry_values) = registry_values {
|
|
return parse_registry_values(registry_values);
|
|
}
|
|
}
|
|
}
|
|
proxies
|
|
}
|
|
|
|
fn insert_proxy(proxies: &mut SystemProxyMap, scheme: impl Into<String>, addr: String) -> bool {
|
|
if addr.trim().is_empty() {
|
|
// do not accept empty or whitespace proxy address
|
|
false
|
|
} else if let Ok(valid_addr) = addr.into_proxy_scheme() {
|
|
proxies.insert(scheme.into(), valid_addr);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn get_from_environment() -> SystemProxyMap {
|
|
let mut proxies = HashMap::new();
|
|
|
|
if is_cgi() {
|
|
if log::log_enabled!(log::Level::Warn) && env::var_os("HTTP_PROXY").is_some() {
|
|
log::warn!("HTTP_PROXY environment variable ignored in CGI");
|
|
}
|
|
} else if !insert_from_env(&mut proxies, "http", "HTTP_PROXY") {
|
|
insert_from_env(&mut proxies, "http", "http_proxy");
|
|
}
|
|
|
|
if !insert_from_env(&mut proxies, "https", "HTTPS_PROXY") {
|
|
insert_from_env(&mut proxies, "https", "https_proxy");
|
|
}
|
|
|
|
proxies
|
|
}
|
|
|
|
fn insert_from_env(proxies: &mut SystemProxyMap, scheme: &str, var: &str) -> bool {
|
|
if let Ok(val) = env::var(var) {
|
|
insert_proxy(proxies, scheme, val)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Check if we are being executed in a CGI context.
|
|
///
|
|
/// If so, a malicious client can send the `Proxy:` header, and it will
|
|
/// be in the `HTTP_PROXY` env var. So we don't use it :)
|
|
fn is_cgi() -> bool {
|
|
env::var_os("REQUEST_METHOD").is_some()
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn get_from_registry_impl() -> Result<RegistryProxyValues, Box<dyn Error>> {
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
let internet_setting: RegKey =
|
|
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?;
|
|
// ensure the proxy is enable, if the value doesn't exist, an error will returned.
|
|
let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?;
|
|
let proxy_server: String = internet_setting.get_value("ProxyServer")?;
|
|
|
|
Ok((proxy_enable, proxy_server))
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn get_from_registry() -> Option<RegistryProxyValues> {
|
|
get_from_registry_impl().ok()
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn get_from_registry() -> Option<RegistryProxyValues> {
|
|
None
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn parse_registry_values_impl(
|
|
registry_values: RegistryProxyValues,
|
|
) -> Result<SystemProxyMap, Box<dyn Error>> {
|
|
let (proxy_enable, proxy_server) = registry_values;
|
|
|
|
if proxy_enable == 0 {
|
|
return Ok(HashMap::new());
|
|
}
|
|
|
|
let mut proxies = HashMap::new();
|
|
if proxy_server.contains("=") {
|
|
// per-protocol settings.
|
|
for p in proxy_server.split(";") {
|
|
let protocol_parts: Vec<&str> = p.split("=").collect();
|
|
match protocol_parts.as_slice() {
|
|
[protocol, address] => {
|
|
// If address doesn't specify an explicit protocol as protocol://address
|
|
// then default to HTTP
|
|
let address = if extract_type_prefix(*address).is_some() {
|
|
String::from(*address)
|
|
} else {
|
|
format!("http://{}", address)
|
|
};
|
|
|
|
insert_proxy(&mut proxies, *protocol, address);
|
|
}
|
|
_ => {
|
|
// Contains invalid protocol setting, just break the loop
|
|
// And make proxies to be empty.
|
|
proxies.clear();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if let Some(scheme) = extract_type_prefix(&proxy_server) {
|
|
// Explicit protocol has been specified
|
|
insert_proxy(&mut proxies, scheme, proxy_server.to_owned());
|
|
} else {
|
|
// No explicit protocol has been specified, default to HTTP
|
|
insert_proxy(&mut proxies, "http", format!("http://{}", proxy_server));
|
|
insert_proxy(&mut proxies, "https", format!("http://{}", proxy_server));
|
|
}
|
|
}
|
|
Ok(proxies)
|
|
}
|
|
|
|
/// Extract the protocol from the given address, if present
|
|
/// For example, "https://example.com" will return Some("https")
|
|
#[cfg(target_os = "windows")]
|
|
fn extract_type_prefix(address: &str) -> Option<&str> {
|
|
if let Some(indice) = address.find("://") {
|
|
if indice == 0 {
|
|
None
|
|
} else {
|
|
let prefix = &address[..indice];
|
|
let contains_banned = prefix.contains(|c| c == ':' || c == '/');
|
|
|
|
if !contains_banned {
|
|
Some(prefix)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn parse_registry_values(registry_values: RegistryProxyValues) -> SystemProxyMap {
|
|
parse_registry_values_impl(registry_values).unwrap_or(HashMap::new())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use once_cell::sync::Lazy;
|
|
use std::sync::Mutex;
|
|
|
|
impl Dst for Url {
|
|
fn scheme(&self) -> &str {
|
|
Url::scheme(self)
|
|
}
|
|
|
|
fn host(&self) -> &str {
|
|
Url::host_str(self).expect("<Url as Dst>::host should have a str")
|
|
}
|
|
|
|
fn port(&self) -> Option<u16> {
|
|
Url::port(self)
|
|
}
|
|
}
|
|
|
|
fn url(s: &str) -> Url {
|
|
s.parse().unwrap()
|
|
}
|
|
|
|
fn intercepted_uri(p: &Proxy, s: &str) -> Uri {
|
|
let (scheme, host) = match p.intercept(&url(s)).unwrap() {
|
|
ProxyScheme::Http { host, .. } => ("http", host),
|
|
ProxyScheme::Https { host, .. } => ("https", host),
|
|
#[cfg(feature = "socks")]
|
|
_ => panic!("intercepted as socks"),
|
|
};
|
|
http::Uri::builder()
|
|
.scheme(scheme)
|
|
.authority(host)
|
|
.path_and_query("/")
|
|
.build()
|
|
.expect("intercepted_uri")
|
|
}
|
|
|
|
#[test]
|
|
fn test_http() {
|
|
let target = "http://example.domain/";
|
|
let p = Proxy::http(target).unwrap();
|
|
|
|
let http = "http://hyper.rs";
|
|
let other = "https://hyper.rs";
|
|
|
|
assert_eq!(intercepted_uri(&p, http), target);
|
|
assert!(p.intercept(&url(other)).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_https() {
|
|
let target = "http://example.domain/";
|
|
let p = Proxy::https(target).unwrap();
|
|
|
|
let http = "http://hyper.rs";
|
|
let other = "https://hyper.rs";
|
|
|
|
assert!(p.intercept(&url(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();
|
|
|
|
let http = "http://hyper.rs";
|
|
let https = "https://hyper.rs";
|
|
let other = "x-youve-never-heard-of-me-mr-proxy://hyper.rs";
|
|
|
|
assert_eq!(intercepted_uri(&p, http), target);
|
|
assert_eq!(intercepted_uri(&p, https), target);
|
|
assert_eq!(intercepted_uri(&p, other), target);
|
|
}
|
|
|
|
#[test]
|
|
fn test_custom() {
|
|
let target1 = "http://example.domain/";
|
|
let target2 = "https://example.domain/";
|
|
let p = Proxy::custom(move |url| {
|
|
if url.host_str() == Some("hyper.rs") {
|
|
target1.parse().ok()
|
|
} else if url.scheme() == "http" {
|
|
target2.parse().ok()
|
|
} else {
|
|
None::<Url>
|
|
}
|
|
});
|
|
|
|
let http = "http://seanmonstar.com";
|
|
let https = "https://hyper.rs";
|
|
let other = "x-youve-never-heard-of-me-mr-proxy://seanmonstar.com";
|
|
|
|
assert_eq!(intercepted_uri(&p, http), target2);
|
|
assert_eq!(intercepted_uri(&p, https), target1);
|
|
assert!(p.intercept(&url(other)).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_proxy_scheme_parse() {
|
|
let ps = "http://foo:bar@localhost:1239".into_proxy_scheme().unwrap();
|
|
|
|
match ps {
|
|
ProxyScheme::Http { auth, host } => {
|
|
assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
|
|
assert_eq!(host, "localhost:1239");
|
|
}
|
|
other => panic!("unexpected: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_proxy_scheme_ip_address_default_http() {
|
|
let ps = "192.168.1.1:8888".into_proxy_scheme().unwrap();
|
|
|
|
match ps {
|
|
ProxyScheme::Http { auth, host } => {
|
|
assert!(auth.is_none());
|
|
assert_eq!(host, "192.168.1.1:8888");
|
|
}
|
|
other => panic!("unexpected: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_proxy_scheme_parse_default_http_with_auth() {
|
|
// this should fail because `foo` is interpreted as the scheme and no host can be found
|
|
let ps = "foo:bar@localhost:1239".into_proxy_scheme().unwrap();
|
|
|
|
match ps {
|
|
ProxyScheme::Http { auth, host } => {
|
|
assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
|
|
assert_eq!(host, "localhost:1239");
|
|
}
|
|
other => panic!("unexpected: {:?}", other),
|
|
}
|
|
}
|
|
|
|
// Smallest possible content for a mutex
|
|
struct MutexInner;
|
|
|
|
static ENVLOCK: Lazy<Mutex<MutexInner>> = Lazy::new(|| Mutex::new(MutexInner));
|
|
|
|
#[test]
|
|
fn test_get_sys_proxies_parsing() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("HTTP_PROXY");
|
|
let _g2 = env_guard("http_proxy");
|
|
|
|
// Mock ENV, get the results, before doing assertions
|
|
// to avoid assert! -> panic! -> Mutex Poisoned.
|
|
let baseline_proxies = get_sys_proxies(None);
|
|
// the system proxy setting url is invalid.
|
|
env::set_var("http_proxy", "file://123465");
|
|
let invalid_proxies = get_sys_proxies(None);
|
|
// set valid proxy
|
|
env::set_var("http_proxy", "127.0.0.1/");
|
|
let valid_proxies = get_sys_proxies(None);
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
|
|
assert!(!baseline_proxies.contains_key("http"));
|
|
assert!(!invalid_proxies.contains_key("http"));
|
|
|
|
let p = &valid_proxies["http"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1");
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[test]
|
|
fn test_get_sys_proxies_registry_parsing() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("HTTP_PROXY");
|
|
let _g2 = env_guard("http_proxy");
|
|
|
|
// Mock ENV, get the results, before doing assertions
|
|
// to avoid assert! -> panic! -> Mutex Poisoned.
|
|
let baseline_proxies = get_sys_proxies(None);
|
|
// the system proxy in the registry has been disabled
|
|
let disabled_proxies = get_sys_proxies(Some((0, String::from("http://127.0.0.1/"))));
|
|
// set valid proxy
|
|
let valid_proxies = get_sys_proxies(Some((1, String::from("http://127.0.0.1/"))));
|
|
let valid_proxies_no_scheme = get_sys_proxies(Some((1, String::from("127.0.0.1"))));
|
|
let valid_proxies_explicit_https =
|
|
get_sys_proxies(Some((1, String::from("https://127.0.0.1/"))));
|
|
let multiple_proxies = get_sys_proxies(Some((
|
|
1,
|
|
String::from("http=127.0.0.1:8888;https=127.0.0.2:8888"),
|
|
)));
|
|
let multiple_proxies_explicit_scheme = get_sys_proxies(Some((
|
|
1,
|
|
String::from("http=http://127.0.0.1:8888;https=https://127.0.0.2:8888"),
|
|
)));
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
|
|
assert_eq!(baseline_proxies.contains_key("http"), false);
|
|
assert_eq!(disabled_proxies.contains_key("http"), false);
|
|
|
|
let p = &valid_proxies["http"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1");
|
|
|
|
let p = &valid_proxies_no_scheme["http"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1");
|
|
|
|
let p = &valid_proxies_no_scheme["https"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1");
|
|
|
|
let p = &valid_proxies_explicit_https["https"];
|
|
assert_eq!(p.scheme(), "https");
|
|
assert_eq!(p.host(), "127.0.0.1");
|
|
|
|
let p = &multiple_proxies["http"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1:8888");
|
|
|
|
let p = &multiple_proxies["https"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.2:8888");
|
|
|
|
let p = &multiple_proxies_explicit_scheme["http"];
|
|
assert_eq!(p.scheme(), "http");
|
|
assert_eq!(p.host(), "127.0.0.1:8888");
|
|
|
|
let p = &multiple_proxies_explicit_scheme["https"];
|
|
assert_eq!(p.scheme(), "https");
|
|
assert_eq!(p.host(), "127.0.0.2:8888");
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_sys_proxies_in_cgi() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("REQUEST_METHOD");
|
|
let _g2 = env_guard("HTTP_PROXY");
|
|
|
|
// Mock ENV, get the results, before doing assertions
|
|
// to avoid assert! -> panic! -> Mutex Poisoned.
|
|
env::set_var("HTTP_PROXY", "http://evil/");
|
|
|
|
let baseline_proxies = get_sys_proxies(None);
|
|
// set like we're in CGI
|
|
env::set_var("REQUEST_METHOD", "GET");
|
|
|
|
let cgi_proxies = get_sys_proxies(None);
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
|
|
// not in CGI yet
|
|
assert_eq!(baseline_proxies["http"].host(), "evil");
|
|
// In CGI
|
|
assert!(!cgi_proxies.contains_key("http"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_sys_no_proxy() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("HTTP_PROXY");
|
|
let _g2 = env_guard("NO_PROXY");
|
|
|
|
let target = "http://example.domain/";
|
|
env::set_var("HTTP_PROXY", target);
|
|
|
|
env::set_var(
|
|
"NO_PROXY",
|
|
".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17",
|
|
);
|
|
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
|
|
// random url, not in no_proxy
|
|
assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
|
|
// make sure that random non-subdomain string prefixes don't match
|
|
assert_eq!(intercepted_uri(&p, "http://notfoo.bar"), target);
|
|
// make sure that random non-subdomain string prefixes don't match
|
|
assert_eq!(intercepted_uri(&p, "http://notbar.baz"), target);
|
|
// ipv4 address out of range
|
|
assert_eq!(intercepted_uri(&p, "http://10.43.1.1"), target);
|
|
// ipv4 address out of range
|
|
assert_eq!(intercepted_uri(&p, "http://10.124.7.7"), target);
|
|
// ipv6 address out of range
|
|
assert_eq!(intercepted_uri(&p, "http://[ffff:db8:a0b:12f0::1]"), target);
|
|
// ipv6 address out of range
|
|
assert_eq!(intercepted_uri(&p, "http://[2005:db8:a0b:12f0::1]"), target);
|
|
|
|
// make sure subdomains (with leading .) match
|
|
assert!(p.intercept(&url("http://hello.foo.bar")).is_none());
|
|
// make sure exact matches (without leading .) match (also makes sure spaces between entries work)
|
|
assert!(p.intercept(&url("http://bar.baz")).is_none());
|
|
// check case sensitivity
|
|
assert!(p.intercept(&url("http://BAR.baz")).is_none());
|
|
// make sure subdomains (without leading . in no_proxy) match
|
|
assert!(p.intercept(&url("http://foo.bar.baz")).is_none());
|
|
// make sure subdomains (without leading . in no_proxy) match - this differs from cURL
|
|
assert!(p.intercept(&url("http://foo.bar")).is_none());
|
|
// ipv4 address match within range
|
|
assert!(p.intercept(&url("http://10.42.1.100")).is_none());
|
|
// ipv6 address exact match
|
|
assert!(p.intercept(&url("http://[::1]")).is_none());
|
|
// ipv6 address match within range
|
|
assert!(p.intercept(&url("http://[2001:db8:a0b:12f0::1]")).is_none());
|
|
// ipv4 address exact match
|
|
assert!(p.intercept(&url("http://10.124.7.8")).is_none());
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
}
|
|
|
|
#[test]
|
|
fn test_wildcard_sys_no_proxy() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("HTTP_PROXY");
|
|
let _g2 = env_guard("NO_PROXY");
|
|
|
|
let target = "http://example.domain/";
|
|
env::set_var("HTTP_PROXY", target);
|
|
|
|
env::set_var("NO_PROXY", "*");
|
|
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
|
|
assert!(p.intercept(&url("http://foo.bar")).is_none());
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_sys_no_proxy() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
// save system setting first.
|
|
let _g1 = env_guard("HTTP_PROXY");
|
|
let _g2 = env_guard("NO_PROXY");
|
|
|
|
let target = "http://example.domain/";
|
|
env::set_var("HTTP_PROXY", target);
|
|
|
|
env::set_var("NO_PROXY", ",");
|
|
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
|
|
// everything should go through proxy, "effectively" nothing is in no_proxy
|
|
assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_proxy_load() {
|
|
// Stop other threads from modifying process-global ENV while we are.
|
|
let _lock = ENVLOCK.lock();
|
|
|
|
let _g1 = env_guard("no_proxy");
|
|
let domain = "lower.case";
|
|
env::set_var("no_proxy", domain);
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
assert_eq!(
|
|
p.no_proxy.expect("should have a no proxy set").domains.0[0],
|
|
domain
|
|
);
|
|
|
|
env::remove_var("no_proxy");
|
|
let _g2 = env_guard("NO_PROXY");
|
|
let domain = "upper.case";
|
|
env::set_var("NO_PROXY", domain);
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
assert_eq!(
|
|
p.no_proxy.expect("should have a no proxy set").domains.0[0],
|
|
domain
|
|
);
|
|
|
|
let _g3 = env_guard("HTTP_PROXY");
|
|
env::remove_var("NO_PROXY");
|
|
env::remove_var("no_proxy");
|
|
let target = "http://example.domain/";
|
|
env::set_var("HTTP_PROXY", target);
|
|
|
|
// Manually construct this so we aren't use the cache
|
|
let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
|
|
p.no_proxy = NoProxy::new();
|
|
assert!(p.no_proxy.is_none(), "NoProxy shouldn't have been created");
|
|
|
|
assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
|
|
|
|
// reset user setting when guards drop
|
|
drop(_g1);
|
|
drop(_g2);
|
|
drop(_g3);
|
|
// Let other threads run now
|
|
drop(_lock);
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[test]
|
|
fn test_type_prefix_extraction() {
|
|
assert!(extract_type_prefix("test").is_none());
|
|
assert!(extract_type_prefix("://test").is_none());
|
|
assert!(extract_type_prefix("some:prefix://test").is_none());
|
|
assert!(extract_type_prefix("some/prefix://test").is_none());
|
|
|
|
assert_eq!(extract_type_prefix("http://test").unwrap(), "http");
|
|
assert_eq!(extract_type_prefix("a://test").unwrap(), "a");
|
|
}
|
|
|
|
/// Guard an environment variable, resetting it to the original value
|
|
/// when dropped.
|
|
fn env_guard(name: impl Into<String>) -> EnvGuard {
|
|
let name = name.into();
|
|
let orig_val = env::var(&name).ok();
|
|
env::remove_var(&name);
|
|
EnvGuard { name, orig_val }
|
|
}
|
|
|
|
struct EnvGuard {
|
|
name: String,
|
|
orig_val: Option<String>,
|
|
}
|
|
|
|
impl Drop for EnvGuard {
|
|
fn drop(&mut self) {
|
|
if let Some(val) = self.orig_val.take() {
|
|
env::set_var(&self.name, val);
|
|
} else {
|
|
env::remove_var(&self.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_has_http_auth() {
|
|
let http_proxy_with_auth = Proxy {
|
|
intercept: Intercept::Http(ProxyScheme::Http {
|
|
auth: Some(HeaderValue::from_static("auth1")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(http_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
Some(HeaderValue::from_static("auth1"))
|
|
);
|
|
|
|
let http_proxy_without_auth = Proxy {
|
|
intercept: Intercept::Http(ProxyScheme::Http {
|
|
auth: None,
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(!http_proxy_without_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
http_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
None
|
|
);
|
|
|
|
let https_proxy_with_auth = Proxy {
|
|
intercept: Intercept::Http(ProxyScheme::Https {
|
|
auth: Some(HeaderValue::from_static("auth2")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(https_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
Some(HeaderValue::from_static("auth2"))
|
|
);
|
|
|
|
let all_http_proxy_with_auth = Proxy {
|
|
intercept: Intercept::All(ProxyScheme::Http {
|
|
auth: Some(HeaderValue::from_static("auth3")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(all_http_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
all_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
Some(HeaderValue::from_static("auth3"))
|
|
);
|
|
|
|
let all_https_proxy_with_auth = Proxy {
|
|
intercept: Intercept::All(ProxyScheme::Https {
|
|
auth: Some(HeaderValue::from_static("auth4")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(all_https_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
all_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
Some(HeaderValue::from_static("auth4"))
|
|
);
|
|
|
|
let all_https_proxy_without_auth = Proxy {
|
|
intercept: Intercept::All(ProxyScheme::Https {
|
|
auth: None,
|
|
host: http::uri::Authority::from_static("authority"),
|
|
}),
|
|
no_proxy: None,
|
|
};
|
|
assert!(!all_https_proxy_without_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
all_https_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
None
|
|
);
|
|
|
|
let system_http_proxy_with_auth = Proxy {
|
|
intercept: Intercept::System(Arc::new({
|
|
let mut m = HashMap::new();
|
|
m.insert(
|
|
"http".into(),
|
|
ProxyScheme::Http {
|
|
auth: Some(HeaderValue::from_static("auth5")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
},
|
|
);
|
|
m
|
|
})),
|
|
no_proxy: None,
|
|
};
|
|
assert!(system_http_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
system_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
Some(HeaderValue::from_static("auth5"))
|
|
);
|
|
|
|
let system_https_proxy_with_auth = Proxy {
|
|
intercept: Intercept::System(Arc::new({
|
|
let mut m = HashMap::new();
|
|
m.insert(
|
|
"https".into(),
|
|
ProxyScheme::Https {
|
|
auth: Some(HeaderValue::from_static("auth6")),
|
|
host: http::uri::Authority::from_static("authority"),
|
|
},
|
|
);
|
|
m
|
|
})),
|
|
no_proxy: None,
|
|
};
|
|
assert!(!system_https_proxy_with_auth.maybe_has_http_auth());
|
|
assert_eq!(
|
|
system_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
|
|
None
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
mod into_proxy_scheme {
|
|
use crate::Proxy;
|
|
use std::error::Error;
|
|
use std::mem::discriminant;
|
|
|
|
fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool {
|
|
let mut source = haystack.source();
|
|
while let Some(error) = source {
|
|
if let Some(parse_error) = error.downcast_ref::<url::ParseError>() {
|
|
if discriminant(parse_error) == discriminant(&needle) {
|
|
return true;
|
|
}
|
|
}
|
|
source = error.source();
|
|
}
|
|
false
|
|
}
|
|
|
|
fn check_parse_error(url: &str, needle: url::ParseError) {
|
|
let error = Proxy::http(url).unwrap_err();
|
|
if !includes(&error, needle) {
|
|
panic!("{:?} expected; {:?}, {} found", needle, error, error);
|
|
}
|
|
}
|
|
|
|
mod when_scheme_missing {
|
|
mod and_url_is_valid {
|
|
use crate::Proxy;
|
|
|
|
#[test]
|
|
fn lookback_works() {
|
|
let _ = Proxy::http("127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_port_works() {
|
|
let _ = Proxy::http("127.0.0.1:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_works() {
|
|
let _ = Proxy::http("username@127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_password_works() {
|
|
let _ = Proxy::http("username:password@127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_password_port_works() {
|
|
let _ = Proxy::http("ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_works() {
|
|
let _ = Proxy::http("proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_port_works() {
|
|
let _ = Proxy::http("proxy.example.com:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_works() {
|
|
let _ = Proxy::http("username@proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_password_works() {
|
|
let _ = Proxy::http("username:password@proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_password_port_works() {
|
|
let _ =
|
|
Proxy::http("ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080").unwrap();
|
|
}
|
|
}
|
|
mod and_url_has_bad {
|
|
use super::super::check_parse_error;
|
|
|
|
#[test]
|
|
fn host() {
|
|
check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase);
|
|
}
|
|
|
|
#[test]
|
|
fn idna_encoding() {
|
|
check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase);
|
|
}
|
|
|
|
#[test]
|
|
fn port() {
|
|
check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase);
|
|
}
|
|
|
|
#[test]
|
|
fn ip_v4_address() {
|
|
check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase);
|
|
}
|
|
|
|
#[test]
|
|
fn ip_v6_address() {
|
|
check_parse_error(
|
|
"[56FE::2159:5BBC::6594]",
|
|
url::ParseError::RelativeUrlWithoutBase,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_domain_character() {
|
|
check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase);
|
|
}
|
|
}
|
|
}
|
|
|
|
mod when_scheme_present {
|
|
mod and_url_is_valid {
|
|
use crate::Proxy;
|
|
|
|
#[test]
|
|
fn loopback_works() {
|
|
let _ = Proxy::http("http://127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_port_works() {
|
|
let _ = Proxy::http("https://127.0.0.1:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_works() {
|
|
let _ = Proxy::http("http://username@127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_password_works() {
|
|
let _ = Proxy::http("https://username:password@127.0.0.1").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loopback_username_password_port_works() {
|
|
let _ =
|
|
Proxy::http("http://ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_works() {
|
|
let _ = Proxy::http("https://proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_port_works() {
|
|
let _ = Proxy::http("http://proxy.example.com:8080").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_works() {
|
|
let _ = Proxy::http("https://username@proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_password_works() {
|
|
let _ = Proxy::http("http://username:password@proxy.example.com").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn domain_username_password_port_works() {
|
|
let _ =
|
|
Proxy::http("https://ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080")
|
|
.unwrap();
|
|
}
|
|
}
|
|
mod and_url_has_bad {
|
|
use super::super::check_parse_error;
|
|
|
|
#[test]
|
|
fn host() {
|
|
check_parse_error("http://username@", url::ParseError::EmptyHost);
|
|
}
|
|
|
|
#[test]
|
|
fn idna_encoding() {
|
|
check_parse_error("http://xn---", url::ParseError::IdnaError);
|
|
}
|
|
|
|
#[test]
|
|
fn port() {
|
|
check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort);
|
|
}
|
|
|
|
#[test]
|
|
fn ip_v4_address() {
|
|
check_parse_error(
|
|
"http://421.627.718.469",
|
|
url::ParseError::InvalidIpv4Address,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ip_v6_address() {
|
|
check_parse_error(
|
|
"http://[56FE::2159:5BBC::6594]",
|
|
url::ParseError::InvalidIpv6Address,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_domain_character() {
|
|
check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|