attohttp/request/
proxy.rs1use 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#[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 pub fn builder() -> ProxySettingsBuilder {
59 ProxySettingsBuilder::new()
60 }
61
62 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 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#[derive(Clone, Debug)]
125pub struct ProxySettingsBuilder {
126 inner: ProxySettings,
127}
128
129impl ProxySettingsBuilder {
130 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 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 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 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 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 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}