| @@ -4,10 +4,24 @@ use std::time::Duration; | ||||
|  | ||||
| use bytes::Bytes; | ||||
| use futures::{Async, Future, Poll}; | ||||
| use header::{ | ||||
|     HeaderMap, | ||||
|     HeaderValue, | ||||
|     ACCEPT, | ||||
|     ACCEPT_ENCODING, | ||||
|     CONTENT_LENGTH, | ||||
|     CONTENT_ENCODING, | ||||
|     CONTENT_TYPE, | ||||
|     LOCATION, | ||||
|     PROXY_AUTHORIZATION, | ||||
|     RANGE, | ||||
|     REFERER, | ||||
|     TRANSFER_ENCODING, | ||||
|     USER_AGENT, | ||||
| }; | ||||
| use http::Uri; | ||||
| use hyper::client::ResponseFuture; | ||||
| use header::{HeaderMap, HeaderValue, LOCATION, USER_AGENT, REFERER, ACCEPT, | ||||
|              ACCEPT_ENCODING, RANGE, TRANSFER_ENCODING, CONTENT_TYPE, CONTENT_LENGTH, CONTENT_ENCODING}; | ||||
| use mime::{self}; | ||||
| use mime; | ||||
| #[cfg(feature = "default-tls")] | ||||
| use native_tls::TlsConnector; | ||||
|  | ||||
| @@ -197,6 +211,10 @@ impl ClientBuilder { | ||||
|         let hyper_client = ::hyper::Client::builder() | ||||
|             .build(connector); | ||||
|  | ||||
|         let proxies_maybe_http_auth = proxies | ||||
|             .iter() | ||||
|             .any(|p| p.maybe_has_http_auth()); | ||||
|  | ||||
|         Ok(Client { | ||||
|             inner: Arc::new(ClientRef { | ||||
|                 gzip: config.gzip, | ||||
| @@ -204,6 +222,8 @@ impl ClientBuilder { | ||||
|                 headers: config.headers, | ||||
|                 redirect_policy: config.redirect_policy, | ||||
|                 referer: config.referer, | ||||
|                 proxies, | ||||
|                 proxies_maybe_http_auth, | ||||
|             }), | ||||
|         }) | ||||
|     } | ||||
| @@ -470,6 +490,8 @@ impl Client { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         self.proxy_auth(&uri, &mut headers); | ||||
|  | ||||
|         let mut req = ::hyper::Request::builder() | ||||
|             .method(method.clone()) | ||||
|             .uri(uri.clone()) | ||||
| @@ -495,6 +517,40 @@ impl Client { | ||||
|             }), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn proxy_auth(&self, dst: &Uri, headers: &mut HeaderMap) { | ||||
|         if !self.inner.proxies_maybe_http_auth { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Only set the header here if the destination scheme is 'http', | ||||
|         // since otherwise, the header will be included in the CONNECT tunnel | ||||
|         // request instead. | ||||
|         if dst.scheme_part() != Some(&::http::uri::Scheme::HTTP) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if headers.contains_key(PROXY_AUTHORIZATION) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         for proxy in self.inner.proxies.iter() { | ||||
|             if proxy.is_match(dst) { | ||||
|                 match proxy.auth() { | ||||
|                     Some(::proxy::Auth::Basic(ref header)) => { | ||||
|                         headers.insert( | ||||
|                             PROXY_AUTHORIZATION, | ||||
|                             header.clone() | ||||
|                         ); | ||||
|                     }, | ||||
|                     None => (), | ||||
|                 } | ||||
|  | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl fmt::Debug for Client { | ||||
| @@ -520,6 +576,8 @@ struct ClientRef { | ||||
|     hyper: HyperClient, | ||||
|     redirect_policy: RedirectPolicy, | ||||
|     referer: bool, | ||||
|     proxies: Arc<Vec<Proxy>>, | ||||
|     proxies_maybe_http_auth: bool, | ||||
| } | ||||
|  | ||||
| pub struct Pending { | ||||
|   | ||||
| @@ -114,6 +114,9 @@ impl Connect for Connector { | ||||
|  | ||||
|                 ndst.set_port(puri.port_part().map(|port| port.as_u16())); | ||||
|  | ||||
|                 #[cfg(feature = "tls")] | ||||
|                 let auth = prox.auth().cloned(); | ||||
|  | ||||
|                 match &self.inner { | ||||
|                     #[cfg(feature = "default-tls")] | ||||
|                     Inner::DefaultTls(http, tls) => if dst.scheme() == "https" { | ||||
| @@ -125,7 +128,7 @@ impl Connect for Connector { | ||||
|                         let tls = tls.clone(); | ||||
|                         return Box::new(http.connect(ndst).and_then(move |(conn, connected)| { | ||||
|                             trace!("tunneling HTTPS over proxy"); | ||||
|                             tunnel(conn, host.clone(), port) | ||||
|                             tunnel(conn, host.clone(), port, auth) | ||||
|                                 .and_then(move |tunneled| { | ||||
|                                     tls.connect_async(&host, tunneled) | ||||
|                                         .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) | ||||
| @@ -148,7 +151,7 @@ impl Connect for Connector { | ||||
|                             let maybe_dnsname = DNSNameRef::try_from_ascii_str(&host) | ||||
|                                 .map(|dnsname| dnsname.to_owned()) | ||||
|                                 .map_err(|_| io::Error::new(io::ErrorKind::Other, "Invalid DNS Name")); | ||||
|                             tunnel(conn, host, port) | ||||
|                             tunnel(conn, host, port, auth) | ||||
|                                 .and_then(move |tunneled| Ok((maybe_dnsname?, tunneled))) | ||||
|                                 .and_then(move |(dnsname, tunneled)| { | ||||
|                                     RustlsConnector::from(tls).connect(dnsname.as_ref(), tunneled) | ||||
| @@ -176,18 +179,30 @@ pub(crate) type Conn = Box<dyn AsyncConn + Send + Sync + 'static>; | ||||
| pub(crate) type Connecting = Box<Future<Item=(Conn, Connected), Error=io::Error> + Send>; | ||||
|  | ||||
| #[cfg(feature = "tls")] | ||||
| fn tunnel<T>(conn: T, host: String, port: u16) -> Tunnel<T> { | ||||
|      let buf = format!("\ | ||||
| fn tunnel<T>(conn: T, host: String, port: u16, auth: Option<::proxy::Auth>) -> Tunnel<T> { | ||||
|     let mut buf = format!("\ | ||||
|         CONNECT {0}:{1} HTTP/1.1\r\n\ | ||||
|         Host: {0}:{1}\r\n\ | ||||
|         \r\n\ | ||||
|     ", host, port).into_bytes(); | ||||
|  | ||||
|      Tunnel { | ||||
|     match auth { | ||||
|         Some(::proxy::Auth::Basic(value)) => { | ||||
|             debug!("tunnel to {}:{} using basic auth", host, port); | ||||
|             buf.extend_from_slice(b"Proxy-Authorization: "); | ||||
|             buf.extend_from_slice(value.as_bytes()); | ||||
|             buf.extend_from_slice(b"\r\n"); | ||||
|         }, | ||||
|         None => (), | ||||
|     } | ||||
|  | ||||
|     // headers end | ||||
|     buf.extend_from_slice(b"\r\n"); | ||||
|  | ||||
|     Tunnel { | ||||
|         buf: io::Cursor::new(buf), | ||||
|         conn: Some(conn), | ||||
|         state: TunnelState::Writing, | ||||
|      } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "tls")] | ||||
| @@ -230,6 +245,8 @@ where T: AsyncRead + AsyncWrite { | ||||
|                             return Ok(self.conn.take().unwrap().into()); | ||||
|                         } | ||||
|                         // else read more | ||||
|                     } else if read.starts_with(b"HTTP/1.1 407") { | ||||
|                         return Err(io::Error::new(io::ErrorKind::Other, "proxy authentication required")); | ||||
|                     } else { | ||||
|                         return Err(io::Error::new(io::ErrorKind::Other, "unsuccessful tunnel")); | ||||
|                     } | ||||
| @@ -258,23 +275,29 @@ mod tests { | ||||
|     use tokio::runtime::current_thread::Runtime; | ||||
|     use tokio::net::TcpStream; | ||||
|     use super::tunnel; | ||||
|     use proxy; | ||||
|  | ||||
|     static TUNNEL_OK: &'static [u8] = b"\ | ||||
|         HTTP/1.1 200 OK\r\n\ | ||||
|         \r\n\ | ||||
|     "; | ||||
|  | ||||
|     macro_rules! mock_tunnel { | ||||
|         () => ({ | ||||
|             mock_tunnel!(b"\ | ||||
|                 HTTP/1.1 200 OK\r\n\ | ||||
|                 \r\n\ | ||||
|             ") | ||||
|             mock_tunnel!(TUNNEL_OK) | ||||
|         }); | ||||
|         ($write:expr) => ({ | ||||
|             mock_tunnel!($write, "") | ||||
|         }); | ||||
|         ($write:expr, $auth:expr) => ({ | ||||
|             let listener = TcpListener::bind("127.0.0.1:0").unwrap(); | ||||
|             let addr = listener.local_addr().unwrap(); | ||||
|             let connect_expected = format!("\ | ||||
|                 CONNECT {0}:{1} HTTP/1.1\r\n\ | ||||
|                 Host: {0}:{1}\r\n\ | ||||
|                 {2}\ | ||||
|                 \r\n\ | ||||
|             ", addr.ip(), addr.port()).into_bytes(); | ||||
|             ", addr.ip(), addr.port(), $auth).into_bytes(); | ||||
|  | ||||
|             thread::spawn(move || { | ||||
|                 let (mut sock, _) = listener.accept().unwrap(); | ||||
| @@ -297,7 +320,7 @@ mod tests { | ||||
|         let host = addr.ip().to_string(); | ||||
|         let port = addr.port(); | ||||
|         let work = work.and_then(|tcp| { | ||||
|             tunnel(tcp, host, port) | ||||
|             tunnel(tcp, host, port, None) | ||||
|         }); | ||||
|  | ||||
|         rt.block_on(work).unwrap(); | ||||
| @@ -312,14 +335,14 @@ mod tests { | ||||
|         let host = addr.ip().to_string(); | ||||
|         let port = addr.port(); | ||||
|         let work = work.and_then(|tcp| { | ||||
|             tunnel(tcp, host, port) | ||||
|             tunnel(tcp, host, port, None) | ||||
|         }); | ||||
|  | ||||
|         rt.block_on(work).unwrap_err(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_tunnel_bad_response() { | ||||
|     fn test_tunnel_non_http_response() { | ||||
|         let addr = mock_tunnel!(b"foo bar baz hallo"); | ||||
|  | ||||
|         let mut rt = Runtime::new().unwrap(); | ||||
| @@ -327,9 +350,47 @@ mod tests { | ||||
|         let host = addr.ip().to_string(); | ||||
|         let port = addr.port(); | ||||
|         let work = work.and_then(|tcp| { | ||||
|             tunnel(tcp, host, port) | ||||
|             tunnel(tcp, host, port, None) | ||||
|         }); | ||||
|  | ||||
|         rt.block_on(work).unwrap_err(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_tunnel_proxy_unauthorized() { | ||||
|         let addr = mock_tunnel!(b"\ | ||||
|             HTTP/1.1 407 Proxy Authentication Required\r\n\ | ||||
|             Proxy-Authenticate: Basic realm=\"nope\"\r\n\ | ||||
|             \r\n\ | ||||
|         "); | ||||
|  | ||||
|         let mut rt = Runtime::new().unwrap(); | ||||
|         let work = TcpStream::connect(&addr); | ||||
|         let host = addr.ip().to_string(); | ||||
|         let port = addr.port(); | ||||
|         let work = work.and_then(|tcp| { | ||||
|             tunnel(tcp, host, port, None) | ||||
|         }); | ||||
|  | ||||
|         let error = rt.block_on(work).unwrap_err(); | ||||
|         assert_eq!(error.to_string(), "proxy authentication required"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_tunnel_basic_auth() { | ||||
|         let addr = mock_tunnel!( | ||||
|             TUNNEL_OK, | ||||
|             "Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\r\n" | ||||
|         ); | ||||
|  | ||||
|         let mut rt = Runtime::new().unwrap(); | ||||
|         let work = TcpStream::connect(&addr); | ||||
|         let host = addr.ip().to_string(); | ||||
|         let port = addr.port(); | ||||
|         let work = work.and_then(|tcp| { | ||||
|             tunnel(tcp, host, port, Some(proxy::Auth::basic("Aladdin", "open sesame"))) | ||||
|         }); | ||||
|  | ||||
|         rt.block_on(work).unwrap(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										99
									
								
								src/proxy.rs
									
									
									
									
									
								
							
							
						
						
									
										99
									
								
								src/proxy.rs
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| use std::fmt; | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use http::{header::HeaderValue, Uri}; | ||||
| use hyper::client::connect::Destination; | ||||
| use {into_url, IntoUrl, Url}; | ||||
|  | ||||
| @@ -30,9 +31,15 @@ use {into_url, IntoUrl, Url}; | ||||
| /// would prevent a `Proxy` later in the list from ever working, so take care. | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct Proxy { | ||||
|     auth: Option<Auth>, | ||||
|     intercept: Intercept, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug)] | ||||
| pub(crate) enum Auth { | ||||
|     Basic(HeaderValue), | ||||
| } | ||||
|  | ||||
| impl Proxy { | ||||
|     /// Proxy all HTTP traffic to the passed URL. | ||||
|     /// | ||||
| @@ -124,7 +131,43 @@ impl Proxy { | ||||
|  | ||||
|     fn new(intercept: Intercept) -> Proxy { | ||||
|         Proxy { | ||||
|             intercept: intercept, | ||||
|             auth: None, | ||||
|             intercept, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Set the `Proxy-Authorization` header using Basic auth. | ||||
|     /// | ||||
|     /// # Example | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # extern crate reqwest; | ||||
|     /// # fn run() -> Result<(), Box<::std::error::Error>> { | ||||
|     /// let proxy = reqwest::Proxy::https("http://localhost:1234")? | ||||
|     ///     .basic_auth("Aladdin", "open sesame"); | ||||
|     /// # Ok(()) | ||||
|     /// # } | ||||
|     /// # fn main() {} | ||||
|     /// ``` | ||||
|     pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy { | ||||
|         self.auth = Some(Auth::basic(username, password)); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn auth(&self) -> Option<&Auth> { | ||||
|         self.auth.as_ref() | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn maybe_has_http_auth(&self) -> bool { | ||||
|         match self.auth { | ||||
|             Some(Auth::Basic(_)) => match self.intercept { | ||||
|                 Intercept::All(_) | | ||||
|                 Intercept::Http(_) | | ||||
|                 // Custom *may* match 'http', so assume so. | ||||
|                 Intercept::Custom(_) => true, | ||||
|                 Intercept::Https(_) => false, | ||||
|             }, | ||||
|             None => false, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -161,6 +204,31 @@ impl Proxy { | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn is_match<D: Dst>(&self, uri: &D) -> bool { | ||||
|         match self.intercept { | ||||
|             Intercept::All(_) => true, | ||||
|             Intercept::Http(_) => { | ||||
|                 uri.scheme() == "http" | ||||
|             }, | ||||
|             Intercept::Https(_) => { | ||||
|                 uri.scheme() == "https" | ||||
|             }, | ||||
|             Intercept::Custom(ref fun) => { | ||||
|                 (fun.0)( | ||||
|                     &format!( | ||||
|                         "{}://{}{}{}", | ||||
|                         uri.scheme(), | ||||
|                         uri.host(), | ||||
|                         uri.port().map(|_| ":").unwrap_or(""), | ||||
|                         uri.port().map(|p| p.to_string()).unwrap_or(String::new()) | ||||
|                     ) | ||||
|                         .parse() | ||||
|                         .expect("should be valid Url") | ||||
|                 ).is_some() | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug)] | ||||
| @@ -203,6 +271,35 @@ impl Dst for Destination { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[doc(hidden)] | ||||
| impl Dst for Uri { | ||||
|     fn scheme(&self) -> &str { | ||||
|         self.scheme_part() | ||||
|             .expect("Uri should have a scheme") | ||||
|             .as_str() | ||||
|     } | ||||
|  | ||||
|     fn host(&self) -> &str { | ||||
|         Uri::host(self) | ||||
|             .expect("<Uri as Dst>::host should have a str") | ||||
|     } | ||||
|  | ||||
|     fn port(&self) -> Option<u16> { | ||||
|         self.port_part().map(|p| p.as_u16()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Auth { | ||||
|     pub(crate) fn basic(username: &str, password: &str) -> Auth { | ||||
|         let val = format!("{}:{}", username, password); | ||||
|         let mut header = format!("Basic {}", base64::encode(&val)) | ||||
|             .parse::<HeaderValue>() | ||||
|             .expect("base64 is always valid HeaderValue"); | ||||
|         header.set_sensitive(true); | ||||
|         Auth::Basic(header) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|   | ||||
| @@ -1,6 +1,13 @@ | ||||
| use std::fmt; | ||||
|  | ||||
| use header::HeaderMap; | ||||
| use header::{ | ||||
|     HeaderMap, | ||||
|     AUTHORIZATION, | ||||
|     COOKIE, | ||||
|     PROXY_AUTHORIZATION, | ||||
|     WWW_AUTHENTICATE, | ||||
|  | ||||
| }; | ||||
| use hyper::StatusCode; | ||||
|  | ||||
| use Url; | ||||
| @@ -233,10 +240,11 @@ pub(crate) fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Url, prev | ||||
|         let cross_host = next.host_str() != previous.host_str() || | ||||
|                          next.port_or_known_default() != previous.port_or_known_default(); | ||||
|         if cross_host { | ||||
|             headers.remove("authorization"); | ||||
|             headers.remove("cookie"); | ||||
|             headers.remove(AUTHORIZATION); | ||||
|             headers.remove(COOKIE); | ||||
|             headers.remove("cookie2"); | ||||
|             headers.remove("www-authenticate"); | ||||
|             headers.remove(PROXY_AUTHORIZATION); | ||||
|             headers.remove(WWW_AUTHENTICATE); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ extern crate reqwest; | ||||
| mod support; | ||||
|  | ||||
| #[test] | ||||
| fn test_http_proxy() { | ||||
| fn http_proxy() { | ||||
|     let server = server! { | ||||
|         request: b"\ | ||||
|             GET http://hyper.rs/prox HTTP/1.1\r\n\ | ||||
| @@ -37,3 +37,43 @@ fn test_http_proxy() { | ||||
|     assert_eq!(res.status(), reqwest::StatusCode::OK); | ||||
|     assert_eq!(res.headers().get(reqwest::header::SERVER).unwrap(), &"proxied"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn http_proxy_basic_auth() { | ||||
|     let server = server! { | ||||
|         request: b"\ | ||||
|             GET http://hyper.rs/prox HTTP/1.1\r\n\ | ||||
|             user-agent: $USERAGENT\r\n\ | ||||
|             accept: */*\r\n\ | ||||
|             accept-encoding: gzip\r\n\ | ||||
|             proxy-authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\r\n\ | ||||
|             host: hyper.rs\r\n\ | ||||
|             \r\n\ | ||||
|             ", | ||||
|         response: b"\ | ||||
|             HTTP/1.1 200 OK\r\n\ | ||||
|             Server: proxied\r\n\ | ||||
|             Content-Length: 0\r\n\ | ||||
|             \r\n\ | ||||
|             " | ||||
|     }; | ||||
|  | ||||
|     let proxy = format!("http://{}", server.addr()); | ||||
|  | ||||
|     let url = "http://hyper.rs/prox"; | ||||
|     let res = reqwest::Client::builder() | ||||
|         .proxy( | ||||
|             reqwest::Proxy::http(&proxy) | ||||
|             .unwrap() | ||||
|             .basic_auth("Aladdin", "open sesame") | ||||
|         ) | ||||
|         .build() | ||||
|         .unwrap() | ||||
|         .get(url) | ||||
|         .send() | ||||
|         .unwrap(); | ||||
|  | ||||
|     assert_eq!(res.url().as_str(), url); | ||||
|     assert_eq!(res.status(), reqwest::StatusCode::OK); | ||||
|     assert_eq!(res.headers().get(reqwest::header::SERVER).unwrap(), &"proxied"); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user