//! Proxy matchers //! //! This module contains different matchers to configure rules for when a proxy //! should be used, and if so, with what arguments. //! //! A [`Matcher`] can be constructed either using environment variables, or //! a [`Matcher::builder()`]. //! //! Once constructed, the `Matcher` can be asked if it intercepts a `Uri` by //! calling [`Matcher::intercept()`]. //! //! An [`Intercept`] includes the destination for the proxy, and any parsed //! authentication to be used.
use std::fmt; use std::net::IpAddr;
use http::header::HeaderValue; use ipnet::IpNet; use percent_encoding::percent_decode_str;
/// A proxy matcher, usually built from environment variables. pubstruct Matcher {
http: Option<Intercept>,
https: Option<Intercept>,
no: NoProxy,
}
/// A matched proxy, /// /// This is returned by a matcher if a proxy should be used. #[derive(Clone)] pubstruct Intercept {
uri: http::Uri,
auth: Auth,
}
/// A builder to create a [`Matcher`]. /// /// Construct with [`Matcher::builder()`]. #[derive(Default)] pubstruct Builder {
is_cgi: bool,
all: String,
http: String,
https: String,
no: String,
}
/// A filter for proxy matchers. /// /// This type is based off the `NO_PROXY` rules used by curl. #[derive(Clone, Debug, Default)] struct NoProxy {
ips: IpMatcher,
domains: DomainMatcher,
}
#[derive(Clone, Debug)] enum Ip {
Address(IpAddr),
Network(IpNet),
}
// ===== impl Matcher =====
impl Matcher { /// Create a matcher reading the current environment variables. /// /// This checks for values in the following variables, treating them the /// same as curl does: /// /// - `ALL_PROXY`/`all_proxy` /// - `HTTPS_PROXY`/`https_proxy` /// - `HTTP_PROXY`/`http_proxy` /// - `NO_PROXY`/`no_proxy` pubfn from_env() -> Self {
Builder::from_env().build()
}
/// Create a matcher from the environment or system. /// /// This checks the same environment variables as `from_env()`, and if not /// set, checks the system configuration for values for the OS. /// /// This constructor is always available, but if the `client-proxy-system` /// feature is enabled, it will check more configuration. Use this /// constructor if you want to allow users to optionally enable more, or /// use `from_env` if you do not want the values to change based on an /// enabled feature. pubfn from_system() -> Self {
Builder::from_system().build()
}
/// Start a builder to configure a matcher. pubfn builder() -> Builder {
Builder::default()
}
/// Check if the destination should be intercepted by a proxy. /// /// If the proxy rules match the destination, a new `Uri` will be returned /// to connect to. pubfn intercept(&self, dst: &http::Uri) -> Option<Intercept> { // TODO(perf): don't need to check `no` if below doesn't match... ifself.no.contains(dst.host()?) { return None;
}
if !self.no.is_empty() {
b.field("no", &self.no);
}
b.finish()
}
}
// ===== impl Intercept =====
impl Intercept { /// Get the `http::Uri` for the target proxy. pubfn uri(&self) -> &http::Uri {
&self.uri
}
/// Get any configured basic authorization. /// /// This should usually be used with a `Proxy-Authorization` header, to /// send in Basic format. /// /// # Example /// /// ```rust /// # use hyper_util::client::proxy::matcher::Matcher; /// # let uri = http::Uri::from_static("https://hyper.rs"); /// let m = Matcher::builder() /// .all("https://Aladdin:opensesame@localhost:8887") /// .build(); /// /// let proxy = m.intercept(&uri).expect("example"); /// let auth = proxy.basic_auth().expect("example"); /// assert_eq!(auth, "Basic QWxhZGRpbjpvcGVuc2VzYW1l"); /// ``` pubfn basic_auth(&self) -> Option<&HeaderValue> { iflet Auth::Basic(ref val) = self.auth {
Some(val)
} else {
None
}
}
/// Get any configured raw authorization. /// /// If not detected as another scheme, this is the username and password /// that should be sent with whatever protocol the proxy handshake uses. /// /// # Example /// /// ```rust /// # use hyper_util::client::proxy::matcher::Matcher; /// # let uri = http::Uri::from_static("https://hyper.rs"); /// let m = Matcher::builder() /// .all("socks5h://Aladdin:opensesame@localhost:8887") /// .build(); /// /// let proxy = m.intercept(&uri).expect("example"); /// let auth = proxy.raw_auth().expect("example"); /// assert_eq!(auth, ("Aladdin", "opensesame")); /// ``` pubfn raw_auth(&self) -> Option<(&str, &str)> { iflet Auth::Raw(ref u, ref p) = self.auth {
Some((u.as_str(), p.as_str()))
} else {
None
}
}
}
impl fmt::Debug for Intercept { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Intercept")
.field("uri", &self.uri) // dont output auth, its sensitive
.finish()
}
}
/// Set the target proxy for all destinations. pubfn all<S>(mutself, val: S) -> Self where
S: IntoValue,
{ self.all = val.into_value(); self
}
/// Set the target proxy for HTTP destinations. pubfn http<S>(mutself, val: S) -> Self where
S: IntoValue,
{ self.http = val.into_value(); self
}
/// Set the target proxy for HTTPS destinations. pubfn https<S>(mutself, val: S) -> Self where
S: IntoValue,
{ self.https = val.into_value(); self
}
/// Set the "no" proxy filter. /// /// The rules are as follows: /// * 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. pubfn no<S>(mutself, val: S) -> Self where
S: IntoValue,
{ self.no = val.into_value(); self
}
/// Construct a [`Matcher`] using the configured values. pubfn build(self) -> Matcher { ifself.is_cgi { return Matcher {
http: None,
https: None,
no: NoProxy::empty(),
};
}
fn get_first_env(names: &[&str]) -> String { for name in names { iflet Ok(val) = std::env::var(name) { return val;
}
}
String::new()
}
fn parse_env_uri(val: &str) -> Option<Intercept> { use std::borrow::Cow;
let uri = val.parse::<http::Uri>().ok()?; letmut builder = http::Uri::builder(); letmut is_httpish = false; letmut auth = Auth::Empty;
builder = builder.scheme(match uri.scheme() {
Some(s) => { if s == &http::uri::Scheme::HTTP || s == &http::uri::Scheme::HTTPS {
is_httpish = true;
s.clone()
} elseif matches!(s.as_str(), "socks4" | "socks4a" | "socks5" | "socks5h") {
s.clone()
} else { // can't use this proxy scheme return None;
}
} // if no scheme provided, assume they meant 'http'
None => {
is_httpish = true;
http::uri::Scheme::HTTP
}
});
let authority = uri.authority()?;
iflet Some((userinfo, host_port)) = authority.as_str().split_once('@') { let (user, pass) = match userinfo.split_once(':') {
Some((user, pass)) => (user, Some(pass)),
None => (userinfo, None),
}; let user = percent_decode_str(user).decode_utf8_lossy(); let pass = pass.map(|pass| percent_decode_str(pass).decode_utf8_lossy()); if is_httpish {
auth = Auth::Basic(encode_basic_auth(&user, pass.as_deref()));
} else {
auth = Auth::Raw(
user.into_owned(),
pass.map_or_else(String::new, Cow::into_owned),
);
}
builder = builder.authority(host_port);
} else {
builder = builder.authority(authority.clone());
}
// removing any path, but we MUST specify one or the builder errors
builder = builder.path_and_query("/");
let dst = builder.build().ok()?;
Some(Intercept { uri: dst, auth })
}
fn encode_basic_auth(user: &str, pass: Option<&str>) -> HeaderValue { use base64::prelude::BASE64_STANDARD; use base64::write::EncoderWriter; use std::io::Write;
/// Returns a new no-proxy configuration based on a `no_proxy` string (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. pubfn from_string(no_proxy_list: &str) -> Self { letmut ips = Vec::new(); letmut domains = Vec::new(); let parts = no_proxy_list.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(_) => { if !part.trim().is_empty() {
domains.push(part.to_owned())
}
}
},
}
}
NoProxy {
ips: IpMatcher(ips),
domains: DomainMatcher(domains),
}
}
/// Return true if this matches the host (domain or IP). pubfn 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 { match ip {
Ip::Address(address) => { if &addr == address { returntrue;
}
}
Ip::Network(net) => { if net.contains(&addr) { returntrue;
}
}
}
} 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 { if d.eq_ignore_ascii_case(domain)
|| d.strip_prefix('.')
.map_or(false, |s| s.eq_ignore_ascii_case(domain))
{ returntrue;
} elseif domain
.get(domain_len.saturating_sub(d.len())..)
.map_or(false, |s| s.eq_ignore_ascii_case(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 returntrue;
} elseif domain.as_bytes().get(domain_len - d.len() - 1) == Some(&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 returntrue;
}
} elseif d == "*" { returntrue;
}
} false
}
}
mod builder { /// A type that can used as a `Builder` value. /// /// Private and sealed, only visible in docs. pubtrait IntoValue { #[doc(hidden)] fn into_value(self) -> String;
}
#[cfg(feature = "client-proxy-system")] #[cfg(target_os = "macos")] mod mac { use system_configuration::core_foundation::base::CFType; use system_configuration::core_foundation::dictionary::CFDictionary; use system_configuration::core_foundation::number::CFNumber; use system_configuration::core_foundation::string::{CFString, CFStringRef}; use system_configuration::dynamic_store::SCDynamicStoreBuilder; use system_configuration::sys::schema_definitions::{
kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy,
kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy,
};
pub(super) fn with_system(builder: &mutsuper::Builder) { let store = iflet Some(store) = SCDynamicStoreBuilder::new("hyper-util").build() {
store
} else { return;
};
#[test] fn test_domain_matcher() { let domains = vec![".foo.bar".into(), "bar.foo".into()]; let matcher = DomainMatcher(domains);
// domains match with leading `.`
assert!(matcher.contains("foo.bar"));
assert!(matcher.contains("FOO.BAR"));
// subdomains match with leading `.`
assert!(matcher.contains("www.foo.bar"));
assert!(matcher.contains("WWW.FOO.BAR"));
// domains match with no leading `.`
assert!(matcher.contains("bar.foo"));
assert!(matcher.contains("Bar.foo"));
// subdomains match with no leading `.`
assert!(matcher.contains("www.bar.foo"));
assert!(matcher.contains("WWW.BAR.FOO"));
// non-subdomain string prefixes don't match
assert!(!matcher.contains("notfoo.bar"));
assert!(!matcher.contains("notbar.foo"));
}
#[test] fn test_no_proxy_wildcard() { let no_proxy = NoProxy::from_string("*");
assert!(no_proxy.contains("any.where"));
}
#[test] fn test_no_proxy_ip_ranges() { let no_proxy =
NoProxy::from_string(".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17");
let should_not_match = [ // random url, not in no_proxy "hyper.rs", // make sure that random non-subdomain string prefixes don't match "notfoo.bar", // make sure that random non-subdomain string prefixes don't match "notbar.baz", // ipv4 address out of range "10.43.1.1", // ipv4 address out of range "10.124.7.7", // ipv6 address out of range "[ffff:db8:a0b:12f0::1]", // ipv6 address out of range "[2005:db8:a0b:12f0::1]",
];
for host in &should_not_match {
assert!(!no_proxy.contains(host), "should not contain {host:?}");
}
let should_match = [ // make sure subdomains (with leading .) match "hello.foo.bar", // make sure exact matches (without leading .) match (also makes sure spaces between entries work) "bar.baz", // make sure subdomains (without leading . in no_proxy) match "foo.bar.baz", // make sure subdomains (without leading . in no_proxy) match - this differs from cURL "foo.bar", // ipv4 address match within range "10.42.1.100", // ipv6 address exact match "[::1]", // ipv6 address match within range "[2001:db8:a0b:12f0::1]", // ipv4 address exact match "10.124.7.8",
];
for host in &should_match {
assert!(no_proxy.contains(host), "should contain {host:?}");
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.