1use base64::prelude::BASE64_STANDARD;
2use base64::Engine;
3use std::convert::{TryFrom, TryInto};
4use std::fmt;
5use std::io::Write;
6use std::sync::Arc;
7use ureq_proto::http::uri::{PathAndQuery, Scheme};
8use ureq_proto::parser::try_parse_response;
9
10use http::{StatusCode, Uri};
11
12use crate::config::DEFAULT_USER_AGENT;
13use crate::http;
14use crate::transport::{ConnectionDetails, Connector, Either, Transport, TransportAdapter};
15use crate::util::{AuthorityExt, DebugUri, SchemeExt, UriExt};
16use crate::Error;
17
18#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
20#[non_exhaustive]
21pub(crate) enum Proto {
22 Http,
23 Https,
24 Socks4,
25 Socks4A,
26 Socks5,
27}
28
29impl Proto {
30 pub fn default_port(&self) -> u16 {
31 match self {
32 Proto::Http => 80,
33 Proto::Https => 443,
34 Proto::Socks4 | Proto::Socks4A | Proto::Socks5 => 1080,
35 }
36 }
37
38 pub fn is_socks(&self) -> bool {
39 matches!(self, Self::Socks4 | Self::Socks4A | Self::Socks5)
40 }
41
42 pub(crate) fn is_connect(&self) -> bool {
43 matches!(self, Self::Http | Self::Https)
44 }
45}
46
47#[derive(Clone, Eq, Hash, PartialEq)]
49pub struct Proxy {
50 inner: Arc<ProxyInner>,
51}
52
53#[derive(Eq, Hash, PartialEq)]
54struct ProxyInner {
55 proto: Proto,
56 uri: Uri,
57 from_env: bool,
58}
59
60impl Proxy {
61 pub fn new(proxy: &str) -> Result<Self, Error> {
83 Self::new_with_flag(proxy, false)
84 }
85
86 fn new_with_flag(proxy: &str, from_env: bool) -> Result<Self, Error> {
87 let mut uri = proxy.parse::<Uri>().or(Err(Error::InvalidProxyUrl))?;
88
89 let _ = uri.authority().ok_or(Error::InvalidProxyUrl)?;
92
93 let scheme = match uri.scheme_str() {
94 Some(v) => v,
95 None => {
96 uri = insert_default_scheme(uri);
99 "http"
100 }
101 };
102 let proto = scheme.try_into()?;
103
104 let inner = ProxyInner {
105 proto,
106 uri,
107 from_env,
108 };
109
110 Ok(Self {
111 inner: Arc::new(inner),
112 })
113 }
114
115 pub fn try_from_env() -> Option<Self> {
126 if let Ok(env) = std::env::var("ALL_PROXY") {
127 if let Ok(proxy) = Self::new_with_flag(&env, true) {
128 return Some(proxy);
129 }
130 }
131
132 if let Ok(env) = std::env::var("all_proxy") {
133 if let Ok(proxy) = Self::new_with_flag(&env, true) {
134 return Some(proxy);
135 }
136 }
137
138 if let Ok(env) = std::env::var("HTTPS_PROXY") {
139 if let Ok(proxy) = Self::new_with_flag(&env, true) {
140 return Some(proxy);
141 }
142 }
143
144 if let Ok(env) = std::env::var("https_proxy") {
145 if let Ok(proxy) = Self::new_with_flag(&env, true) {
146 return Some(proxy);
147 }
148 }
149
150 if let Ok(env) = std::env::var("HTTP_PROXY") {
151 if let Ok(proxy) = Self::new_with_flag(&env, true) {
152 return Some(proxy);
153 }
154 }
155
156 if let Ok(env) = std::env::var("http_proxy") {
157 if let Ok(proxy) = Self::new_with_flag(&env, true) {
158 return Some(proxy);
159 }
160 }
161 None
162 }
163
164 pub(crate) fn proto(&self) -> Proto {
165 self.inner.proto
166 }
167
168 pub fn uri(&self) -> &Uri {
170 &self.inner.uri
171 }
172
173 pub fn host(&self) -> &str {
175 self.inner
176 .uri
177 .authority()
178 .map(|a| a.host())
179 .expect("constructor to ensure there is an authority")
180 }
181
182 pub fn port(&self) -> u16 {
184 self.inner
185 .uri
186 .authority()
187 .and_then(|a| a.port_u16())
188 .unwrap_or_else(|| self.inner.proto.default_port())
189 }
190
191 pub fn username(&self) -> Option<&str> {
193 self.inner.uri.authority().and_then(|a| a.username())
194 }
195
196 pub fn password(&self) -> Option<&str> {
198 self.inner.uri.authority().and_then(|a| a.password())
199 }
200
201 pub fn is_from_env(&self) -> bool {
204 self.inner.from_env
205 }
206}
207
208fn insert_default_scheme(uri: Uri) -> Uri {
209 let mut parts = uri.into_parts();
210
211 parts.scheme = Some(Scheme::HTTP);
212
213 parts.path_and_query = parts
216 .path_and_query
217 .or_else(|| Some(PathAndQuery::from_static("/")));
218
219 Uri::from_parts(parts).unwrap()
220}
221
222#[derive(Default)]
227pub struct ConnectProxyConnector(());
228
229impl<In: Transport> Connector<In> for ConnectProxyConnector {
230 type Out = Either<In, Box<dyn Transport>>;
231
232 fn connect(
233 &self,
234 details: &ConnectionDetails,
235 chained: Option<In>,
236 ) -> Result<Option<Self::Out>, Error> {
237 if let Some(transport) = chained {
239 return Ok(Some(Either::A(transport)));
240 }
241
242 let maybe_connect_uri = details.config.connect_proxy_uri();
244
245 let Some(connect_uri) = maybe_connect_uri else {
246 return Ok(None);
248 };
249
250 let proxied = details.uri;
251
252 let addrs = details
256 .resolver
257 .resolve(connect_uri, details.config, details.timeout)?;
258
259 let proxy_config = details.config.clone_without_proxy();
260
261 let proxy_details = ConnectionDetails {
264 uri: connect_uri,
265 addrs,
266 config: &proxy_config,
267 request_level: details.request_level,
268 resolver: details.resolver,
269 now: details.now,
270 timeout: details.timeout,
271 run_connector: details.run_connector.clone(),
272 };
273
274 let transport = (details.run_connector)(&proxy_details)?;
275
276 let proxy = details.config.proxy().unwrap();
278
279 let mut w = TransportAdapter::new(transport);
280
281 proxied.ensure_valid_url()?;
282
283 let proxied_host = proxied.host().unwrap();
284 let proxied_port = proxied
285 .port_u16()
286 .unwrap_or(proxied.scheme().unwrap().default_port().unwrap());
287
288 write!(w, "CONNECT {}:{} HTTP/1.1\r\n", proxied_host, proxied_port)?;
289 write!(w, "Host: {}:{}\r\n", proxied_host, proxied_port)?;
290 if let Some(v) = details.config.user_agent().as_str(DEFAULT_USER_AGENT) {
291 write!(w, "User-Agent: {}\r\n", v)?;
292 }
293 write!(w, "Proxy-Connection: Keep-Alive\r\n")?;
294
295 let use_creds = proxy.username().is_some() || proxy.password().is_some();
296
297 if use_creds {
298 let user = proxy.username().unwrap_or_default();
299 let pass = proxy.password().unwrap_or_default();
300 let creds = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
301 write!(w, "Proxy-Authorization: basic {}\r\n", creds)?;
302 }
303
304 write!(w, "\r\n")?;
305 w.flush()?;
306
307 let mut transport = w.into_inner();
308
309 let response = loop {
310 let made_progress = transport.maybe_await_input(details.timeout)?;
311 let buffers = transport.buffers();
312 let input = buffers.input();
313 let Some((used_input, response)) = try_parse_response::<20>(input)? else {
314 if !made_progress {
315 let reason = "proxy server did not respond".to_string();
316 return Err(Error::ConnectProxyFailed(reason));
317 }
318 continue;
319 };
320 buffers.input_consume(used_input);
321 break response;
322 };
323
324 match response.status() {
325 StatusCode::OK => {
326 trace!("CONNECT proxy connected");
327 }
328 x => {
329 let reason = format!("proxy server responded {}/{}", x.as_u16(), x.as_str());
330 return Err(Error::ConnectProxyFailed(reason));
331 }
332 }
333
334 Ok(Some(Either::B(transport)))
335 }
336}
337
338impl TryFrom<&str> for Proto {
339 type Error = Error;
340
341 fn try_from(scheme: &str) -> Result<Self, Self::Error> {
342 match scheme.to_ascii_lowercase().as_str() {
343 "http" => Ok(Proto::Http),
344 "https" => Ok(Proto::Https),
345 "socks4" => Ok(Proto::Socks4),
346 "socks4a" => Ok(Proto::Socks4A),
347 "socks" => Ok(Proto::Socks5),
348 "socks5" => Ok(Proto::Socks5),
349 _ => Err(Error::InvalidProxyUrl),
350 }
351 }
352}
353
354impl fmt::Debug for Proxy {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 f.debug_struct("Proxy")
357 .field("proto", &self.inner.proto)
358 .field("uri", &DebugUri(&self.inner.uri))
359 .field("from_env", &self.inner.from_env)
360 .finish()
361 }
362}
363
364impl fmt::Display for Proto {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 match self {
367 Proto::Http => write!(f, "HTTP"),
368 Proto::Https => write!(f, "HTTPS"),
369 Proto::Socks4 => write!(f, "SOCKS4"),
370 Proto::Socks4A => write!(f, "SOCKS4a"),
371 Proto::Socks5 => write!(f, "SOCKS5"),
372 }
373 }
374}
375
376impl fmt::Debug for ConnectProxyConnector {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.debug_struct("ProxyConnector").finish()
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn parse_proxy_fakeproto() {
388 assert!(Proxy::new("fakeproto://localhost").is_err());
389 }
390
391 #[test]
392 fn parse_proxy_http_user_pass_server_port() {
393 let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999").unwrap();
394 assert_eq!(proxy.username(), Some("user"));
395 assert_eq!(proxy.password(), Some("p@ssw0rd"));
396 assert_eq!(proxy.host(), "localhost");
397 assert_eq!(proxy.port(), 9999);
398 assert_eq!(proxy.inner.proto, Proto::Http);
399 }
400
401 #[test]
402 fn parse_proxy_http_user_pass_server_port_trailing_slash() {
403 let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999/").unwrap();
404 assert_eq!(proxy.username(), Some("user"));
405 assert_eq!(proxy.password(), Some("p@ssw0rd"));
406 assert_eq!(proxy.host(), "localhost");
407 assert_eq!(proxy.port(), 9999);
408 assert_eq!(proxy.inner.proto, Proto::Http);
409 }
410
411 #[test]
412 fn parse_proxy_socks4_user_pass_server_port() {
413 let proxy = Proxy::new("socks4://user:p@ssw0rd@localhost:9999").unwrap();
414 assert_eq!(proxy.username(), Some("user"));
415 assert_eq!(proxy.password(), Some("p@ssw0rd"));
416 assert_eq!(proxy.host(), "localhost");
417 assert_eq!(proxy.port(), 9999);
418 assert_eq!(proxy.inner.proto, Proto::Socks4);
419 }
420
421 #[test]
422 fn parse_proxy_socks4a_user_pass_server_port() {
423 let proxy = Proxy::new("socks4a://user:p@ssw0rd@localhost:9999").unwrap();
424 assert_eq!(proxy.username(), Some("user"));
425 assert_eq!(proxy.password(), Some("p@ssw0rd"));
426 assert_eq!(proxy.host(), "localhost");
427 assert_eq!(proxy.port(), 9999);
428 assert_eq!(proxy.inner.proto, Proto::Socks4A);
429 }
430
431 #[test]
432 fn parse_proxy_socks_user_pass_server_port() {
433 let proxy = Proxy::new("socks://user:p@ssw0rd@localhost:9999").unwrap();
434 assert_eq!(proxy.username(), Some("user"));
435 assert_eq!(proxy.password(), Some("p@ssw0rd"));
436 assert_eq!(proxy.host(), "localhost");
437 assert_eq!(proxy.port(), 9999);
438 assert_eq!(proxy.inner.proto, Proto::Socks5);
439 }
440
441 #[test]
442 fn parse_proxy_socks5_user_pass_server_port() {
443 let proxy = Proxy::new("socks5://user:p@ssw0rd@localhost:9999").unwrap();
444 assert_eq!(proxy.username(), Some("user"));
445 assert_eq!(proxy.password(), Some("p@ssw0rd"));
446 assert_eq!(proxy.host(), "localhost");
447 assert_eq!(proxy.port(), 9999);
448 assert_eq!(proxy.inner.proto, Proto::Socks5);
449 }
450
451 #[test]
452 fn parse_proxy_user_pass_server_port() {
453 let proxy = Proxy::new("user:p@ssw0rd@localhost:9999").unwrap();
454 assert_eq!(proxy.username(), Some("user"));
455 assert_eq!(proxy.password(), Some("p@ssw0rd"));
456 assert_eq!(proxy.host(), "localhost");
457 assert_eq!(proxy.port(), 9999);
458 assert_eq!(proxy.inner.proto, Proto::Http);
459 }
460
461 #[test]
462 fn parse_proxy_server_port() {
463 let proxy = Proxy::new("localhost:9999").unwrap();
464 assert_eq!(proxy.username(), None);
465 assert_eq!(proxy.password(), None);
466 assert_eq!(proxy.host(), "localhost");
467 assert_eq!(proxy.port(), 9999);
468 assert_eq!(proxy.inner.proto, Proto::Http);
469 }
470
471 #[test]
472 fn parse_proxy_server() {
473 let proxy = Proxy::new("localhost").unwrap();
474 assert_eq!(proxy.username(), None);
475 assert_eq!(proxy.password(), None);
476 assert_eq!(proxy.host(), "localhost");
477 assert_eq!(proxy.port(), 80);
478 assert_eq!(proxy.inner.proto, Proto::Http);
479 }
480}
481
482#[cfg(test)]
483mod test {
484 use super::*;
485 use assert_no_alloc::*;
486
487 #[test]
488 fn proxy_clone_does_not_allocate() {
489 let c = Proxy::new("socks://1.2.3.4").unwrap();
490 assert_no_alloc(|| c.clone());
491 }
492
493 #[test]
494 fn proxy_new_default_scheme() {
495 let c = Proxy::new("localhost:1234").unwrap();
496 assert_eq!(c.proto(), Proto::Http);
497 assert_eq!(c.uri(), "http://localhost:1234");
498 }
499
500 #[test]
501 fn proxy_empty_env_url() {
502 let result = Proxy::new_with_flag("", false);
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn proxy_invalid_env_url() {
508 let result = Proxy::new_with_flag("r32/?//52:**", false);
509 assert!(result.is_err());
510 }
511}