attohttp/request/
proxy.rs

1use std::{env, vec};
2
3use url::Url;
4
5fn get_env(name: &str) -> Option<String> {
6    match env::var(name.to_ascii_lowercase()).or_else(|_| env::var(name.to_ascii_uppercase())) {
7        Ok(s) => Some(s),
8        Err(env::VarError::NotPresent) => None,
9        Err(env::VarError::NotUnicode(_)) => {
10            warn!(
11                "Environment variable {} contains non-unicode characters",
12                name.to_ascii_uppercase()
13            );
14            None
15        }
16    }
17}
18
19fn get_env_url(name: &str) -> Option<Url> {
20    match get_env(name) {
21        Some(val) if val.trim().is_empty() => None,
22        Some(val) => match Url::parse(&val) {
23            Ok(url) => match url.scheme() {
24                "http" | "https" => Some(url),
25                _ => {
26                    warn!(
27                        "Environment variable {} contains unsupported proxy scheme: {}",
28                        name.to_ascii_uppercase(),
29                        url.scheme()
30                    );
31                    None
32                }
33            },
34            Err(err) => {
35                warn!(
36                    "Environment variable {} contains invalid URL: {}",
37                    name.to_ascii_uppercase(),
38                    err
39                );
40                None
41            }
42        },
43        None => None,
44    }
45}
46
47/// Contains proxy settings and utilities to find which proxy to use for a given URL.
48#[derive(Clone, Debug)]
49pub struct ProxySettings {
50    http_proxy: Option<Url>,
51    https_proxy: Option<Url>,
52    disable_proxies: bool,
53    no_proxy_hosts: Vec<String>,
54}
55
56impl ProxySettings {
57    /// Get a new builder for ProxySettings.
58    pub fn builder() -> ProxySettingsBuilder {
59        ProxySettingsBuilder::new()
60    }
61
62    /// Get the proxy configuration from the environment using the `curl`/Unix proxy conventions.
63    ///
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>
70    pub fn from_env() -> ProxySettings {
71        let all_proxy = get_env_url("all_proxy");
72        let http_proxy = get_env_url("http_proxy");
73        let https_proxy = get_env_url("https_proxy");
74        let no_proxy = get_env("no_proxy");
75
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(
82                    no_proxy
83                        .split(',')
84                        .map(|s| s.trim().trim_start_matches('.').to_lowercase()),
85                );
86            }
87        }
88
89        ProxySettings {
90            http_proxy: http_proxy.or_else(|| all_proxy.clone()),
91            https_proxy: https_proxy.or(all_proxy),
92            disable_proxies,
93            no_proxy_hosts,
94        }
95    }
96
97    /// Get the proxy URL to use for the given URL.
98    ///
99    /// None is returned if there is no proxy configured for the scheme or if the hostname
100    /// matches a pattern in the no proxy list.
101    pub fn for_url(&self, url: &Url) -> Option<&Url> {
102        if self.disable_proxies {
103            return None;
104        }
105
106        if let Some(host) = url.host_str() {
107            if !self
108                .no_proxy_hosts
109                .iter()
110                .any(|x| host.ends_with(x.to_lowercase().as_str()))
111            {
112                return match url.scheme() {
113                    "http" => self.http_proxy.as_ref(),
114                    "https" => self.https_proxy.as_ref(),
115                    _ => None,
116                };
117            }
118        }
119        None
120    }
121}
122
123/// Utility to build ProxySettings easily.
124#[derive(Clone, Debug)]
125pub struct ProxySettingsBuilder {
126    inner: ProxySettings,
127}
128
129impl ProxySettingsBuilder {
130    /// Create a new ProxySetting builder with no initial configuration.
131    pub fn new() -> Self {
132        ProxySettingsBuilder {
133            inner: ProxySettings {
134                http_proxy: None,
135                https_proxy: None,
136                disable_proxies: false,
137                no_proxy_hosts: vec![],
138            },
139        }
140    }
141
142    /// Set the proxy for http requests.
143    pub fn http_proxy<V>(mut self, val: V) -> Self
144    where
145        V: Into<Option<Url>>,
146    {
147        self.inner.http_proxy = val.into();
148        self
149    }
150
151    /// Set the proxy for https requests.
152    pub fn https_proxy<V>(mut self, val: V) -> Self
153    where
154        V: Into<Option<Url>>,
155    {
156        self.inner.https_proxy = val.into();
157        self
158    }
159
160    /// Add a hostname pattern to ignore when finding the proxy to use for a URL.
161    ///
162    /// For instance `mycompany.local` will make requests with the hostname `mycompany.local`
163    /// not go trough the proxy.
164    pub fn add_no_proxy_host(mut self, pattern: impl AsRef<str>) -> Self {
165        self.inner.no_proxy_hosts.push(pattern.as_ref().to_lowercase());
166        self
167    }
168
169    /// Build the settings.
170    pub fn build(self) -> ProxySettings {
171        self.inner
172    }
173}
174
175impl Default for ProxySettingsBuilder {
176    fn default() -> Self {
177        ProxySettingsBuilder::new()
178    }
179}
180
181#[test]
182fn test_proxy_for_url() {
183    let s = ProxySettings {
184        http_proxy: Some("http://proxy1:3128".parse().unwrap()),
185        https_proxy: Some("http://proxy2:3128".parse().unwrap()),
186        disable_proxies: false,
187        no_proxy_hosts: vec!["reddit.com".into()],
188    };
189
190    assert_eq!(
191        s.for_url(&Url::parse("http://google.ca").unwrap()),
192        Some(&"http://proxy1:3128".parse().unwrap())
193    );
194
195    assert_eq!(
196        s.for_url(&Url::parse("https://google.ca").unwrap()),
197        Some(&"http://proxy2:3128".parse().unwrap())
198    );
199
200    assert_eq!(s.for_url(&Url::parse("https://reddit.com").unwrap()), None);
201}
202
203#[test]
204fn test_proxy_for_url_disabled() {
205    let s = ProxySettings {
206        http_proxy: Some("http://proxy1:3128".parse().unwrap()),
207        https_proxy: Some("http://proxy2:3128".parse().unwrap()),
208        disable_proxies: true,
209        no_proxy_hosts: vec![],
210    };
211
212    assert_eq!(s.for_url(&Url::parse("https://reddit.com").unwrap()), None);
213    assert_eq!(s.for_url(&Url::parse("https://www.google.ca").unwrap()), None);
214}
215
216#[cfg(test)]
217fn with_reset_proxy_vars<T>(test: T)
218where
219    T: FnOnce() + std::panic::UnwindSafe,
220{
221    use std::sync::Mutex;
222
223    lazy_static::lazy_static! {
224        static ref LOCK: Mutex<()> = Mutex::new(());
225    };
226
227    let _guard = LOCK.lock().unwrap();
228
229    env::remove_var("ALL_PROXY");
230    env::remove_var("HTTP_PROXY");
231    env::remove_var("HTTPS_PROXY");
232    env::remove_var("NO_PROXY");
233
234    let result = std::panic::catch_unwind(test);
235
236    // teardown if ever needed
237
238    if let Err(ctx) = result {
239        std::panic::resume_unwind(ctx);
240    }
241}
242
243#[test]
244fn test_proxy_from_env_all_proxy() {
245    with_reset_proxy_vars(|| {
246        env::set_var("ALL_PROXY", "http://proxy:3128");
247
248        let s = ProxySettings::from_env();
249
250        assert_eq!(s.http_proxy.unwrap().as_str(), "http://proxy:3128/");
251        assert_eq!(s.https_proxy.unwrap().as_str(), "http://proxy:3128/");
252    });
253}
254
255#[test]
256fn test_proxy_from_env_override() {
257    with_reset_proxy_vars(|| {
258        env::set_var("ALL_PROXY", "http://proxy:3128");
259        env::set_var("HTTP_PROXY", "http://proxy:3129");
260        env::set_var("HTTPS_PROXY", "http://proxy:3130");
261
262        let s = ProxySettings::from_env();
263
264        assert_eq!(s.http_proxy.unwrap().as_str(), "http://proxy:3129/");
265        assert_eq!(s.https_proxy.unwrap().as_str(), "http://proxy:3130/");
266    });
267}
268
269#[test]
270fn test_proxy_from_env_no_proxy_wildcard() {
271    with_reset_proxy_vars(|| {
272        env::set_var("NO_PROXY", "*");
273
274        let s = ProxySettings::from_env();
275
276        assert!(s.disable_proxies);
277    });
278}
279
280#[test]
281fn test_proxy_from_env_no_proxy_root_domain() {
282    with_reset_proxy_vars(|| {
283        env::set_var("NO_PROXY", ".myroot.com");
284
285        let s = ProxySettings::from_env();
286
287        let url = Url::parse("https://mysub.myroot.com").unwrap();
288        assert!(s.for_url(&url).is_none());
289        assert_eq!(s.no_proxy_hosts[0], "myroot.com");
290    });
291}
292
293#[test]
294fn test_proxy_from_env_no_proxy() {
295    with_reset_proxy_vars(|| {
296        env::set_var("NO_PROXY", "example.com, www.reddit.com, google.ca ");
297
298        let s = ProxySettings::from_env();
299
300        assert_eq!(s.no_proxy_hosts, vec!["example.com", "www.reddit.com", "google.ca"]);
301    });
302}