1#![allow(missing_docs)]
60
61mod rt;
62mod stream;
63mod tunnel;
64
65use std::{fmt, io, sync::Arc};
66use std::{
67 future::Future,
68 pin::Pin,
69 task::{Context, Poll},
70 time::Duration,
71};
72
73use futures_util::future::TryFutureExt;
74use headers::{authorization::Credentials, Authorization, HeaderMapExt, ProxyAuthorization};
75use http::header::{HeaderMap, HeaderName, HeaderValue};
76use hyper::rt::{Read, Write};
77use hyper::Uri;
78use tower_service::Service;
79
80pub use stream::ProxyStream;
81
82#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
83use native_tls::TlsConnector as NativeTlsConnector;
84
85#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
86use tokio_native_tls::TlsConnector;
87
88#[cfg(feature = "__rustls")]
89use hyper_rustls::ConfigBuilderExt;
90
91#[cfg(feature = "__rustls")]
92use tokio_rustls::TlsConnector;
93
94#[cfg(feature = "__rustls")]
95use tokio_rustls::rustls::pki_types::ServerName;
96
97type BoxError = Box<dyn std::error::Error + Send + Sync>;
98
99#[derive(Debug, Clone)]
101pub enum Intercept {
102 All,
104 Http,
106 Https,
108 None,
110 Custom(Custom),
112}
113
114pub trait Dst {
116 fn scheme(&self) -> Option<&str>;
118 fn host(&self) -> Option<&str>;
120 fn port(&self) -> Option<u16>;
122}
123
124impl Dst for Uri {
125 fn scheme(&self) -> Option<&str> {
126 self.scheme_str()
127 }
128
129 fn host(&self) -> Option<&str> {
130 self.host()
131 }
132
133 fn port(&self) -> Option<u16> {
134 self.port_u16()
135 }
136}
137
138#[inline]
139pub(crate) fn io_err<E: Into<Box<dyn std::error::Error + Send + Sync>>>(e: E) -> io::Error {
140 io::Error::new(io::ErrorKind::Other, e)
141}
142
143pub type CustomProxyCallback =
144 dyn Fn(Option<&str>, Option<&str>, Option<u16>) -> bool + Send + Sync;
145
146#[derive(Clone)]
148pub struct Custom(Arc<CustomProxyCallback>);
149
150impl fmt::Debug for Custom {
151 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
152 write!(f, "_")
153 }
154}
155
156impl<F: Fn(Option<&str>, Option<&str>, Option<u16>) -> bool + Send + Sync + 'static> From<F>
157 for Custom
158{
159 fn from(f: F) -> Custom {
160 Custom(Arc::new(f))
161 }
162}
163
164impl Intercept {
165 pub fn matches<D: Dst>(&self, uri: &D) -> bool {
167 match (self, uri.scheme()) {
168 (&Intercept::All, _)
169 | (&Intercept::Http, Some("http"))
170 | (&Intercept::Https, Some("https")) => true,
171 (&Intercept::Custom(Custom(ref f)), _) => f(uri.scheme(), uri.host(), uri.port()),
172 _ => false,
173 }
174 }
175}
176
177impl<F: Fn(Option<&str>, Option<&str>, Option<u16>) -> bool + Send + Sync + 'static> From<F>
178 for Intercept
179{
180 fn from(f: F) -> Intercept {
181 Intercept::Custom(f.into())
182 }
183}
184
185#[derive(Clone, Debug)]
187pub struct Proxy {
188 intercept: Intercept,
189 force_connect: bool,
190 headers: HeaderMap,
191 uri: Uri,
192}
193
194impl Proxy {
195 pub fn new<I: Into<Intercept>>(intercept: I, uri: Uri) -> Proxy {
197 let mut proxy = Proxy {
198 intercept: intercept.into(),
199 uri: uri.clone(),
200 headers: HeaderMap::new(),
201 force_connect: false,
202 };
203
204 if let Some((user, pass)) = extract_user_pass(&uri) {
205 proxy.set_authorization(Authorization::basic(user, pass));
206 }
207
208 proxy
209 }
210
211 pub fn set_authorization<C: Credentials + Clone>(&mut self, credentials: Authorization<C>) {
213 match self.intercept {
214 Intercept::Http => {
215 self.headers.typed_insert(Authorization(credentials.0));
216 }
217 Intercept::Https => {
218 self.headers.typed_insert(ProxyAuthorization(credentials.0));
219 }
220 _ => {
221 self.headers
222 .typed_insert(Authorization(credentials.0.clone()));
223 self.headers.typed_insert(ProxyAuthorization(credentials.0));
224 }
225 }
226 }
227
228 pub fn force_connect(&mut self) {
230 self.force_connect = true;
231 }
232
233 pub fn set_header(&mut self, name: HeaderName, value: HeaderValue) {
235 self.headers.insert(name, value);
236 }
237
238 pub fn intercept(&self) -> &Intercept {
240 &self.intercept
241 }
242
243 pub fn headers(&self) -> &HeaderMap {
245 &self.headers
246 }
247
248 pub fn uri(&self) -> &Uri {
250 &self.uri
251 }
252}
253
254#[derive(Clone)]
256pub struct ProxyConnector<C> {
257 proxies: Vec<Proxy>,
258 connector: C,
259 tls_handshake_timeout: Option<Duration>,
260
261 #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
262 tls: Option<NativeTlsConnector>,
263
264 #[cfg(feature = "__rustls")]
265 tls: Option<TlsConnector>,
266
267 #[cfg(not(feature = "__tls"))]
268 tls: Option<()>,
269}
270
271impl<C: fmt::Debug> fmt::Debug for ProxyConnector<C> {
272 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
273 write!(
274 f,
275 "ProxyConnector {}{{ proxies: {:?}, connector: {:?} }}",
276 if self.tls.is_some() {
277 ""
278 } else {
279 "(unsecured)"
280 },
281 self.proxies,
282 self.connector
283 )
284 }
285}
286
287impl<C> ProxyConnector<C> {
288 #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
290 pub fn new(connector: C) -> Result<Self, io::Error> {
291 let tls = NativeTlsConnector::builder()
292 .build()
293 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
294
295 Ok(ProxyConnector {
296 proxies: Vec::new(),
297 connector: connector,
298 tls_handshake_timeout: None,
299 tls: Some(tls),
300 })
301 }
302
303 #[cfg(feature = "__rustls")]
305 pub fn new(connector: C) -> Result<Self, io::Error> {
306 let config = tokio_rustls::rustls::ClientConfig::builder();
307
308 #[cfg(feature = "rustls-tls-native-roots")]
309 let config = config.with_native_roots()?;
310
311 #[cfg(feature = "rustls-tls-webpki-roots")]
312 let config = config.with_webpki_roots();
313
314 let cfg = Arc::new(config.with_no_client_auth());
315 let tls = TlsConnector::from(cfg);
316
317 Ok(ProxyConnector {
318 proxies: Vec::new(),
319 connector,
320 tls_handshake_timeout: None,
321 tls: Some(tls),
322 })
323 }
324
325 pub fn unsecured(connector: C) -> Self {
327 ProxyConnector {
328 proxies: Vec::new(),
329 connector,
330 tls_handshake_timeout: None,
331 tls: None,
332 }
333 }
334
335 #[cfg(feature = "__tls")]
337 pub fn from_proxy(connector: C, proxy: Proxy) -> Result<Self, io::Error> {
338 let mut c = ProxyConnector::new(connector)?;
339 c.proxies.push(proxy);
340 Ok(c)
341 }
342
343 pub fn from_proxy_unsecured(connector: C, proxy: Proxy) -> Self {
345 let mut c = ProxyConnector::unsecured(connector);
346 c.proxies.push(proxy);
347 c
348 }
349
350 pub fn with_connector<CC>(self, connector: CC) -> ProxyConnector<CC> {
352 ProxyConnector {
353 connector,
354 proxies: self.proxies,
355 tls_handshake_timeout: self.tls_handshake_timeout,
356 tls: self.tls,
357 }
358 }
359
360 #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
362 pub fn set_tls(&mut self, tls: Option<NativeTlsConnector>) {
363 self.tls = tls;
364 }
365
366 #[cfg(feature = "__rustls")]
368 pub fn set_tls(&mut self, tls: Option<TlsConnector>) {
369 self.tls = tls;
370 }
371
372 pub fn set_tls_handshake_timeout(&mut self, timeout: Option<Duration>) {
377 self.tls_handshake_timeout = timeout;
378 }
379
380 pub fn proxies(&self) -> &[Proxy] {
382 &self.proxies
383 }
384
385 pub fn add_proxy(&mut self, proxy: Proxy) {
387 self.proxies.push(proxy);
388 }
389
390 pub fn extend_proxies<I: IntoIterator<Item = Proxy>>(&mut self, proxies: I) {
392 self.proxies.extend(proxies)
393 }
394
395 pub fn http_headers(&self, uri: &Uri) -> Option<&HeaderMap> {
400 if uri.scheme_str() != Some("http") {
401 return None;
402 }
403
404 self.match_proxy(uri).map(|p| &p.headers)
405 }
406
407 fn match_proxy<D: Dst>(&self, uri: &D) -> Option<&Proxy> {
408 self.proxies.iter().find(|p| p.intercept.matches(uri))
409 }
410}
411
412macro_rules! mtry {
413 ($e:expr) => {
414 match $e {
415 Ok(v) => v,
416 Err(e) => break Err(e.into()),
417 }
418 };
419}
420
421impl<C> Service<Uri> for ProxyConnector<C>
422where
423 C: Service<Uri>,
424 C::Response: Read + Write + Send + Unpin + 'static,
425 C::Future: Send + 'static,
426 C::Error: Into<BoxError>,
427{
428 type Response = ProxyStream<C::Response>;
429 type Error = io::Error;
430 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
431
432 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
433 match self.connector.poll_ready(cx) {
434 Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
435 Poll::Ready(Err(e)) => Poll::Ready(Err(io_err(e.into()))),
436 Poll::Pending => Poll::Pending,
437 }
438 }
439
440 fn call(&mut self, uri: Uri) -> Self::Future {
441 if let (Some(p), Some(host)) = (self.match_proxy(&uri), uri.host()) {
442 if uri.scheme() == Some(&http::uri::Scheme::HTTPS) || p.force_connect {
443 let host = host.to_owned();
444 let port =
445 uri.port_u16()
446 .unwrap_or(if uri.scheme() == Some(&http::uri::Scheme::HTTP) {
447 80
448 } else {
449 443
450 });
451
452 let tunnel = tunnel::new(&host, port, &p.headers);
453 let connection =
454 proxy_dst(&uri, &p.uri).map(|proxy_url| self.connector.call(proxy_url));
455 let tls = if uri.scheme() == Some(&http::uri::Scheme::HTTPS) {
456 self.tls.clone()
457 } else {
458 None
459 };
460 #[cfg(feature = "__tls")]
461 let tls_handshake_timeout = self.tls_handshake_timeout;
462
463 Box::pin(async move {
464 #[allow(clippy::never_loop)]
466 loop {
467 let proxy_stream = mtry!(mtry!(connection).await.map_err(io_err));
468 let tunnel_stream = mtry!(tunnel.with_stream(proxy_stream).await);
469
470 break match tls {
471 #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))]
472 Some(tls) => {
473 use hyper_util::rt::TokioIo;
474 let tls = TlsConnector::from(tls);
475 let secure_stream = mtry!(mtry!(
476 with_optional_timeout(
477 tls_handshake_timeout,
478 tls.connect(&host, TokioIo::new(tunnel_stream))
479 )
480 .await
481 )
482 .map_err(io_err));
483
484 Ok(ProxyStream::Secured(Box::new(TokioIo::new(secure_stream))))
485 }
486
487 #[cfg(feature = "__rustls")]
488 Some(tls) => {
489 use hyper_util::rt::TokioIo;
490 let server_name =
491 mtry!(ServerName::try_from(host.to_string()).map_err(io_err));
492 let secure_stream = mtry!(mtry!(
493 with_optional_timeout(
494 tls_handshake_timeout,
495 tls.connect(server_name, TokioIo::new(tunnel_stream))
496 )
497 .await
498 )
499 .map_err(io_err));
500
501 Ok(ProxyStream::Secured(Box::new(TokioIo::new(secure_stream))))
502 }
503
504 #[cfg(not(feature = "__tls",))]
505 Some(_) => panic!("hyper-proxy was not built with TLS support"),
506
507 None => Ok(ProxyStream::Regular(tunnel_stream)),
508 };
509 }
510 })
511 } else {
512 match proxy_dst(&uri, &p.uri) {
513 Ok(proxy_uri) => Box::pin(
514 self.connector
515 .call(proxy_uri)
516 .map_ok(ProxyStream::Regular)
517 .map_err(|err| io_err(err.into())),
518 ),
519 Err(err) => Box::pin(futures_util::future::err(io_err(err))),
520 }
521 }
522 } else {
523 Box::pin(
524 self.connector
525 .call(uri)
526 .map_ok(ProxyStream::NoProxy)
527 .map_err(|err| io_err(err.into())),
528 )
529 }
530 }
531}
532
533#[cfg(feature = "__tls")]
537async fn with_optional_timeout<F: Future>(
538 timeout: Option<Duration>,
539 fut: F,
540) -> io::Result<F::Output> {
541 match timeout {
542 Some(timeout) => tokio::time::timeout(timeout, fut)
543 .await
544 .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "TLS handshake timed out")),
545 None => Ok(fut.await),
546 }
547}
548
549fn proxy_dst(dst: &Uri, proxy: &Uri) -> io::Result<Uri> {
550 Uri::builder()
551 .scheme(
552 proxy
553 .scheme_str()
554 .ok_or_else(|| io_err(format!("proxy uri missing scheme: {}", proxy)))?,
555 )
556 .authority(
557 proxy
558 .authority()
559 .ok_or_else(|| io_err(format!("proxy uri missing host: {}", proxy)))?
560 .clone(),
561 )
562 .path_and_query(dst.path_and_query().unwrap().clone())
563 .build()
564 .map_err(|err| io_err(format!("other error: {}", err)))
565}
566
567fn extract_user_pass(uri: &Uri) -> Option<(&str, &str)> {
569 let authority = uri.authority()?.as_str();
570 let (userinfo, _) = authority.rsplit_once('@')?;
571 let (username, password) = userinfo.split_once(':')?;
572 Some((username, password))
573}
574
575#[cfg(test)]
576mod tests {
577 use http::Uri;
578
579 use crate::{Intercept, Proxy};
580
581 #[test]
582 fn test_new_proxy_with_authorization() {
583 let proxy = Proxy::new(
584 Intercept::All,
585 Uri::from_static("https://bob:secret@my-proxy:8080"),
586 );
587
588 assert_eq!(
589 proxy
590 .headers()
591 .get("authorization")
592 .unwrap()
593 .to_str()
594 .unwrap(),
595 "Basic Ym9iOnNlY3JldA=="
596 );
597 }
598
599 #[test]
600 fn test_new_proxy_without_authorization() {
601 let proxy = Proxy::new(Intercept::All, Uri::from_static("https://my-proxy:8080"));
602
603 assert_eq!(proxy.headers().get("authorization"), None);
604 }
605
606 #[cfg(feature = "__tls")]
607 #[tokio::test]
608 async fn tls_handshake_timeout_fires_against_a_stalled_proxy_tunnel() {
609 use std::time::Duration;
610
611 use hyper_util::client::legacy::connect::HttpConnector;
612 use tokio::io::{AsyncReadExt, AsyncWriteExt};
613 use tokio::net::TcpListener;
614 use tokio::time::timeout;
615 use tower_service::Service as _;
616
617 use crate::{Proxy, ProxyConnector};
618
619 #[cfg(feature = "__rustls")]
620 let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
621
622 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
623 let addr = listener.local_addr().unwrap();
624
625 let proxy_task = tokio::spawn(async move {
628 let (mut stream, _) = listener.accept().await.unwrap();
629 let mut buf = [0u8; 4096];
630 let _ = stream.read(&mut buf).await.unwrap();
631 stream.write_all(b"HTTP/1.1 200 OK\r\n\r\n").await.unwrap();
632 tokio::time::sleep(Duration::from_secs(30)).await;
633 drop(stream);
634 });
635
636 let proxy_uri: Uri = format!("http://{addr}").parse().unwrap();
637 let proxy = Proxy::new(Intercept::All, proxy_uri);
638
639 let mut proxy_connector = ProxyConnector::new(HttpConnector::new()).unwrap();
640 proxy_connector.add_proxy(proxy);
641 proxy_connector.set_tls_handshake_timeout(Some(Duration::from_millis(200)));
642
643 let dst: Uri = "https://example.invalid/".parse().unwrap();
644 let result = timeout(Duration::from_secs(5), proxy_connector.call(dst))
645 .await
646 .expect("request should not hit the outer test timeout");
647 let error = match result {
648 Ok(_) => panic!("handshake should time out before completing"),
649 Err(error) => error,
650 };
651
652 assert!(
653 error.to_string().contains("TLS handshake timed out"),
654 "expected a TLS handshake timeout, got: {error}"
655 );
656
657 proxy_task.abort();
658 }
659}