Skip to content

Commit 2f54bba

Browse files
Simon Bernier St-Pierresbstp
authored andcommitted
Support ALL_PROXY & mimic curl's behavior for NO_PROXY
1 parent f0d7219 commit 2f54bba

2 files changed

Lines changed: 123 additions & 18 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ serde_urlencoded = {version = "0.7", optional = true}
3030
url = "2"
3131
webpki = {version = "0.22", optional = true}
3232
webpki-roots = {version = "0.22", optional = true}
33-
wildmatch = "2"
3433

3534
[dev-dependencies]
3635
anyhow = "1"
3736
env_logger = "0.9"
3837
futures = "0.3"
3938
futures-util = "0.3"
4039
hyper = "0.14"
40+
lazy_static = "1"
4141
tokio = {version = "1", features = ["full"]}
4242
tokio-rustls = "0.22"
4343
tokio-stream = {version = "0.1", features = ["net"]}

src/request/proxy.rs

Lines changed: 122 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use std::env;
1+
use std::{env, vec};
22

33
use url::Url;
4-
use wildmatch::WildMatch;
54

65
fn get_env(name: &str) -> Option<String> {
76
match env::var(name.to_ascii_lowercase()).or_else(|_| env::var(name.to_ascii_uppercase())) {
@@ -50,7 +49,8 @@ fn get_env_url(name: &str) -> Option<Url> {
5049
pub struct ProxySettings {
5150
http_proxy: Option<Url>,
5251
https_proxy: Option<Url>,
53-
no_proxy_patterns: Vec<WildMatch>,
52+
disable_proxies: bool,
53+
no_proxy_hosts: Vec<String>,
5454
}
5555

5656
impl ProxySettings {
@@ -61,21 +61,32 @@ impl ProxySettings {
6161

6262
/// Get the proxy configuration from the environment using the `curl`/Unix proxy conventions.
6363
///
64-
/// Only `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` are supported.
65-
/// `NO_PROXY` supports wildcard patterns.
64+
/// Only `ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` are supported.
65+
/// Proxies can be disabled on all requests by setting `NO_PROXY` to `*`, similar to `curl`.
66+
/// `HTTP_PROXY` or `HTTPS_PROXY` take precedence over values set by `ALL_PROXY` for their
67+
/// respective schemes.
68+
///
69+
/// See <https://curl.se/docs/manpage.html#--noproxy>
6670
pub fn from_env() -> ProxySettings {
71+
let all_proxy = get_env_url("all_proxy");
6772
let http_proxy = get_env_url("http_proxy");
6873
let https_proxy = get_env_url("https_proxy");
6974
let no_proxy = get_env("no_proxy");
7075

71-
let no_proxy_patterns = no_proxy
72-
.map(|x| x.split(',').map(|pat| WildMatch::new(pat.trim())).collect::<Vec<_>>())
73-
.unwrap_or_default();
76+
let disable_proxies = no_proxy.as_deref().unwrap_or("") == "*";
77+
let mut no_proxy_hosts = vec![];
78+
79+
if !disable_proxies {
80+
if let Some(no_proxy) = no_proxy {
81+
no_proxy_hosts.extend(no_proxy.split(',').map(|s| s.trim().to_lowercase()));
82+
}
83+
}
7484

7585
ProxySettings {
76-
http_proxy,
77-
https_proxy,
78-
no_proxy_patterns,
86+
http_proxy: http_proxy.or_else(|| all_proxy.clone()),
87+
https_proxy: https_proxy.or(all_proxy),
88+
disable_proxies,
89+
no_proxy_hosts,
7990
}
8091
}
8192

@@ -84,8 +95,12 @@ impl ProxySettings {
8495
/// None is returned if there is no proxy configured for the scheme or if the hostname
8596
/// matches a pattern in the no proxy list.
8697
pub fn for_url(&self, url: &Url) -> Option<&Url> {
98+
if self.disable_proxies {
99+
return None;
100+
}
101+
87102
if let Some(host) = url.host_str() {
88-
if !self.no_proxy_patterns.iter().any(|x| x.matches(host)) {
103+
if !self.no_proxy_hosts.iter().any(|x| x.to_lowercase() == host) {
89104
return match url.scheme() {
90105
"http" => self.http_proxy.as_ref(),
91106
"https" => self.https_proxy.as_ref(),
@@ -110,7 +125,8 @@ impl ProxySettingsBuilder {
110125
inner: ProxySettings {
111126
http_proxy: None,
112127
https_proxy: None,
113-
no_proxy_patterns: vec![],
128+
disable_proxies: false,
129+
no_proxy_hosts: vec![],
114130
},
115131
}
116132
}
@@ -135,10 +151,10 @@ impl ProxySettingsBuilder {
135151

136152
/// Add a hostname pattern to ignore when finding the proxy to use for a URL.
137153
///
138-
/// For instance `*.mycompany.local` will make every hostname which ends with `.mycompany.local`
154+
/// For instance `mycompany.local` will make requests with the hostname `mycompany.local`
139155
/// not go trough the proxy.
140-
pub fn add_no_proxy_pattern(mut self, pattern: impl AsRef<str>) -> Self {
141-
self.inner.no_proxy_patterns.push(WildMatch::new(pattern.as_ref()));
156+
pub fn add_no_proxy_host(mut self, pattern: impl AsRef<str>) -> Self {
157+
self.inner.no_proxy_hosts.push(pattern.as_ref().to_lowercase());
142158
self
143159
}
144160

@@ -159,7 +175,8 @@ fn test_proxy_for_url() {
159175
let s = ProxySettings {
160176
http_proxy: Some("http://proxy1:3128".parse().unwrap()),
161177
https_proxy: Some("http://proxy2:3128".parse().unwrap()),
162-
no_proxy_patterns: vec![WildMatch::new("*.com")],
178+
disable_proxies: false,
179+
no_proxy_hosts: vec!["reddit.com".into()],
163180
};
164181

165182
assert_eq!(
@@ -174,3 +191,91 @@ fn test_proxy_for_url() {
174191

175192
assert_eq!(s.for_url(&Url::parse("https://reddit.com").unwrap()), None);
176193
}
194+
195+
#[test]
196+
fn test_proxy_for_url_disabled() {
197+
let s = ProxySettings {
198+
http_proxy: Some("http://proxy1:3128".parse().unwrap()),
199+
https_proxy: Some("http://proxy2:3128".parse().unwrap()),
200+
disable_proxies: true,
201+
no_proxy_hosts: vec![],
202+
};
203+
204+
assert_eq!(s.for_url(&Url::parse("https://reddit.com").unwrap()), None);
205+
assert_eq!(s.for_url(&Url::parse("https://www.google.ca").unwrap()), None);
206+
}
207+
208+
#[cfg(test)]
209+
fn with_reset_proxy_vars<T>(test: T)
210+
where
211+
T: FnOnce() + std::panic::UnwindSafe,
212+
{
213+
use std::sync::Mutex;
214+
215+
lazy_static::lazy_static! {
216+
static ref LOCK: Mutex<()> = Mutex::new(());
217+
};
218+
219+
let _guard = LOCK.lock().unwrap();
220+
221+
env::remove_var("ALL_PROXY");
222+
env::remove_var("HTTP_PROXY");
223+
env::remove_var("HTTPS_PROXY");
224+
env::remove_var("NO_PROXY");
225+
226+
let result = std::panic::catch_unwind(test);
227+
228+
// teardown if ever needed
229+
230+
if let Err(ctx) = result {
231+
std::panic::resume_unwind(ctx);
232+
}
233+
}
234+
235+
#[test]
236+
fn test_proxy_from_env_all_proxy() {
237+
with_reset_proxy_vars(|| {
238+
env::set_var("ALL_PROXY", "http://proxy:3128");
239+
240+
let s = ProxySettings::from_env();
241+
242+
assert_eq!(s.http_proxy.unwrap().as_str(), "http://proxy:3128/");
243+
assert_eq!(s.https_proxy.unwrap().as_str(), "http://proxy:3128/");
244+
});
245+
}
246+
247+
#[test]
248+
fn test_proxy_from_env_override() {
249+
with_reset_proxy_vars(|| {
250+
env::set_var("ALL_PROXY", "http://proxy:3128");
251+
env::set_var("HTTP_PROXY", "http://proxy:3129");
252+
env::set_var("HTTPS_PROXY", "http://proxy:3130");
253+
254+
let s = ProxySettings::from_env();
255+
256+
assert_eq!(s.http_proxy.unwrap().as_str(), "http://proxy:3129/");
257+
assert_eq!(s.https_proxy.unwrap().as_str(), "http://proxy:3130/");
258+
});
259+
}
260+
261+
#[test]
262+
fn test_proxy_from_env_no_proxy_wildcard() {
263+
with_reset_proxy_vars(|| {
264+
env::set_var("NO_PROXY", "*");
265+
266+
let s = ProxySettings::from_env();
267+
268+
assert!(s.disable_proxies);
269+
});
270+
}
271+
272+
#[test]
273+
fn test_proxy_from_env_no_proxy() {
274+
with_reset_proxy_vars(|| {
275+
env::set_var("NO_PROXY", "example.com, www.reddit.com, google.ca ");
276+
277+
let s = ProxySettings::from_env();
278+
279+
assert_eq!(s.no_proxy_hosts, vec!["example.com", "www.reddit.com", "google.ca"]);
280+
});
281+
}

0 commit comments

Comments
 (0)