feat(client): proper proxy and tunneling in Client

Closes #774
This commit is contained in:
Sean McArthur
2016-04-27 11:20:07 -07:00
parent 3a3e08687b
commit f36c6b255f
10 changed files with 406 additions and 64 deletions

View File

@@ -71,10 +71,12 @@ use method::Method;
use net::{NetworkConnector, NetworkStream};
use Error;
use self::proxy::tunnel;
pub use self::pool::Pool;
pub use self::request::Request;
pub use self::response::Response;
mod proxy;
pub mod pool;
pub mod request;
pub mod response;
@@ -90,7 +92,7 @@ pub struct Client {
redirect_policy: RedirectPolicy,
read_timeout: Option<Duration>,
write_timeout: Option<Duration>,
proxy: Option<(Cow<'static, str>, Cow<'static, str>, u16)>
proxy: Option<(Cow<'static, str>, u16)>
}
impl fmt::Debug for Client {
@@ -116,6 +118,15 @@ impl Client {
Client::with_connector(Pool::new(config))
}
pub fn with_http_proxy<H>(host: H, port: u16) -> Client
where H: Into<Cow<'static, str>> {
let host = host.into();
let proxy = tunnel((host.clone(), port));
let mut client = Client::with_connector(Pool::with_connector(Default::default(), proxy));
client.proxy = Some((host, port));
client
}
/// Create a new client with a specific connector.
pub fn with_connector<C, S>(connector: C) -> Client
where C: NetworkConnector<Stream=S> + Send + Sync + 'static, S: NetworkStream + Send {
@@ -148,12 +159,6 @@ impl Client {
self.write_timeout = dur;
}
/// Set a proxy for requests of this Client.
pub fn set_proxy<S, H>(&mut self, scheme: S, host: H, port: u16)
where S: Into<Cow<'static, str>>, H: Into<Cow<'static, str>> {
self.proxy = Some((scheme.into(), host.into(), port));
}
/// Build a Get request.
pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::Get, url)
@@ -271,13 +276,12 @@ impl<'a> RequestBuilder<'a> {
loop {
let mut req = {
let (scheme, host, port) = match client.proxy {
Some(ref proxy) => (proxy.0.as_ref(), proxy.1.as_ref(), proxy.2),
None => {
let hp = try!(get_host_and_port(&url));
(url.scheme(), hp.0, hp.1)
}
};
let (host, port) = try!(get_host_and_port(&url));
let mut message = try!(client.protocol.new_message(&host, port, url.scheme()));
if url.scheme() == "http" && client.proxy.is_some() {
message.set_proxied(true);
}
let mut headers = match headers {
Some(ref headers) => headers.clone(),
None => Headers::new(),
@@ -286,7 +290,6 @@ impl<'a> RequestBuilder<'a> {
hostname: host.to_owned(),
port: Some(port),
});
let message = try!(client.protocol.new_message(&host, port, scheme));
Request::with_headers_and_message(method.clone(), url.clone(), headers, message)
};
@@ -460,6 +463,7 @@ impl Default for RedirectPolicy {
}
}
fn get_host_and_port(url: &Url) -> ::Result<(&str, u16)> {
let host = match url.host_str() {
Some(host) => host,
@@ -479,8 +483,9 @@ mod tests {
use std::io::Read;
use header::Server;
use http::h1::Http11Message;
use mock::{MockStream};
use mock::{MockStream, MockSsl};
use super::{Client, RedirectPolicy};
use super::proxy::Proxy;
use super::pool::Pool;
use url::Url;
@@ -505,24 +510,61 @@ mod tests {
#[test]
fn test_proxy() {
use super::pool::PooledStream;
type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
mock_connector!(ProxyConnector {
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
});
let mut client = Client::with_connector(Pool::with_connector(Default::default(), ProxyConnector));
client.set_proxy("http", "example.proxy", 8008);
let tunnel = Proxy {
connector: ProxyConnector,
proxy: ("example.proxy".into(), 8008),
ssl: MockSsl,
};
let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
client.proxy = Some(("example.proxy".into(), 8008));
let mut dump = vec![];
client.get("http://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
{
let box_message = client.protocol.new_message("example.proxy", 8008, "http").unwrap();
let message = box_message.downcast::<Http11Message>().unwrap();
let stream = message.into_inner().downcast::<PooledStream<MockStream>>().unwrap().into_inner();
let s = ::std::str::from_utf8(&stream.write).unwrap();
let request_line = "GET http://127.0.0.1/foo/bar HTTP/1.1\r\n";
assert_eq!(&s[..request_line.len()], request_line);
assert!(s.contains("Host: example.proxy:8008\r\n"));
}
let box_message = client.protocol.new_message("127.0.0.1", 80, "http").unwrap();
let message = box_message.downcast::<Http11Message>().unwrap();
let stream = message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_normal().unwrap();;
let s = ::std::str::from_utf8(&stream.write).unwrap();
let request_line = "GET http://127.0.0.1/foo/bar HTTP/1.1\r\n";
assert!(s.starts_with(request_line), "{:?} doesn't start with {:?}", s, request_line);
assert!(s.contains("Host: 127.0.0.1\r\n"));
}
#[test]
fn test_proxy_tunnel() {
use super::pool::PooledStream;
type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
mock_connector!(ProxyConnector {
b"HTTP/1.1 200 OK\r\n\r\n",
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
});
let tunnel = Proxy {
connector: ProxyConnector,
proxy: ("example.proxy".into(), 8008),
ssl: MockSsl,
};
let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
client.proxy = Some(("example.proxy".into(), 8008));
let mut dump = vec![];
client.get("https://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
let box_message = client.protocol.new_message("127.0.0.1", 443, "https").unwrap();
let message = box_message.downcast::<Http11Message>().unwrap();
let stream = message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_tunneled().unwrap();
let s = ::std::str::from_utf8(&stream.write).unwrap();
let connect_line = "CONNECT 127.0.0.1:443 HTTP/1.1\r\nHost: 127.0.0.1:443\r\n\r\n";
assert_eq!(&s[..connect_line.len()], connect_line);
let s = &s[connect_line.len()..];
let request_line = "GET /foo/bar HTTP/1.1\r\n";
assert_eq!(&s[..request_line.len()], request_line);
assert!(s.contains("Host: 127.0.0.1\r\n"));
}
#[test]

View File

@@ -127,6 +127,7 @@ impl<C: NetworkConnector<Stream=S>, S: NetworkStream + Send> NetworkConnector fo
}
/// A Stream that will try to be returned to the Pool when dropped.
#[derive(Debug)]
pub struct PooledStream<S> {
inner: Option<PooledStreamInner<S>>,
is_closed: bool,

240
src/client/proxy.rs Normal file
View File

@@ -0,0 +1,240 @@
use std::borrow::Cow;
use std::io;
use std::net::{SocketAddr, Shutdown};
use std::time::Duration;
use method::Method;
use net::{NetworkConnector, HttpConnector, NetworkStream, SslClient};
#[cfg(all(feature = "openssl", not(feature = "security-framework")))]
pub fn tunnel(proxy: (Cow<'static, str>, u16)) -> Proxy<HttpConnector, ::net::Openssl> {
Proxy {
connector: HttpConnector,
proxy: proxy,
ssl: Default::default()
}
}
#[cfg(feature = "security-framework")]
pub fn tunnel(proxy: (Cow<'static, str>, u16)) -> Proxy<HttpConnector, ::net::Openssl> {
Proxy {
connector: HttpConnector,
proxy: proxy,
ssl: Default::default()
}
}
#[cfg(not(any(feature = "openssl", feature = "security-framework")))]
pub fn tunnel(proxy: (Cow<'static, str>, u16)) -> Proxy<HttpConnector, self::no_ssl::Plaintext> {
Proxy {
connector: HttpConnector,
proxy: proxy,
ssl: self::no_ssl::Plaintext,
}
}
pub struct Proxy<C, S>
where C: NetworkConnector + Send + Sync + 'static,
C::Stream: NetworkStream + Send + Clone,
S: SslClient<C::Stream> {
pub connector: C,
pub proxy: (Cow<'static, str>, u16),
pub ssl: S,
}
impl<C, S> NetworkConnector for Proxy<C, S>
where C: NetworkConnector + Send + Sync + 'static,
C::Stream: NetworkStream + Send + Clone,
S: SslClient<C::Stream> {
type Stream = Proxied<C::Stream, S::Stream>;
fn connect(&self, host: &str, port: u16, scheme: &str) -> ::Result<Self::Stream> {
use httparse;
use std::io::{Read, Write};
use ::version::HttpVersion::Http11;
trace!("{:?} proxy for '{}://{}:{}'", self.proxy, scheme, host, port);
match scheme {
"http" => {
self.connector.connect(self.proxy.0.as_ref(), self.proxy.1, "http")
.map(Proxied::Normal)
},
"https" => {
let mut stream = try!(self.connector.connect(self.proxy.0.as_ref(), self.proxy.1, "http"));
trace!("{:?} CONNECT {}:{}", self.proxy, host, port);
try!(write!(&mut stream, "{method} {host}:{port} {version}\r\nHost: {host}:{port}\r\n\r\n",
method=Method::Connect, host=host, port=port, version=Http11));
try!(stream.flush());
let mut buf = [0; 1024];
let mut n = 0;
while n < buf.len() {
n += try!(stream.read(&mut buf[n..]));
let mut headers = [httparse::EMPTY_HEADER; 10];
let mut res = httparse::Response::new(&mut headers);
if try!(res.parse(&buf[..n])).is_complete() {
let code = res.code.expect("complete parsing lost code");
if code >= 200 && code < 300 {
trace!("CONNECT success = {:?}", code);
return self.ssl.wrap_client(stream, host)
.map(Proxied::Tunneled)
} else {
trace!("CONNECT response = {:?}", code);
return Err(::Error::Status);
}
}
}
Err(::Error::TooLarge)
},
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid scheme").into())
}
}
}
#[derive(Debug)]
pub enum Proxied<T1, T2> {
Normal(T1),
Tunneled(T2)
}
#[cfg(test)]
impl<T1, T2> Proxied<T1, T2> {
pub fn into_normal(self) -> Result<T1, Self> {
match self {
Proxied::Normal(t1) => Ok(t1),
_ => Err(self)
}
}
pub fn into_tunneled(self) -> Result<T2, Self> {
match self {
Proxied::Tunneled(t2) => Ok(t2),
_ => Err(self)
}
}
}
impl<T1: NetworkStream, T2: NetworkStream> io::Read for Proxied<T1, T2> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Proxied::Normal(ref mut t) => io::Read::read(t, buf),
Proxied::Tunneled(ref mut t) => io::Read::read(t, buf),
}
}
}
impl<T1: NetworkStream, T2: NetworkStream> io::Write for Proxied<T1, T2> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
Proxied::Normal(ref mut t) => io::Write::write(t, buf),
Proxied::Tunneled(ref mut t) => io::Write::write(t, buf),
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
match *self {
Proxied::Normal(ref mut t) => io::Write::flush(t),
Proxied::Tunneled(ref mut t) => io::Write::flush(t),
}
}
}
impl<T1: NetworkStream, T2: NetworkStream> NetworkStream for Proxied<T1, T2> {
#[inline]
fn peer_addr(&mut self) -> io::Result<SocketAddr> {
match *self {
Proxied::Normal(ref mut s) => s.peer_addr(),
Proxied::Tunneled(ref mut s) => s.peer_addr()
}
}
#[inline]
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
match *self {
Proxied::Normal(ref inner) => inner.set_read_timeout(dur),
Proxied::Tunneled(ref inner) => inner.set_read_timeout(dur)
}
}
#[inline]
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
match *self {
Proxied::Normal(ref inner) => inner.set_write_timeout(dur),
Proxied::Tunneled(ref inner) => inner.set_write_timeout(dur)
}
}
#[inline]
fn close(&mut self, how: Shutdown) -> io::Result<()> {
match *self {
Proxied::Normal(ref mut s) => s.close(how),
Proxied::Tunneled(ref mut s) => s.close(how)
}
}
}
#[cfg(not(any(feature = "openssl", feature = "security-framework")))]
mod no_ssl {
use std::io;
use std::net::{Shutdown, SocketAddr};
use std::time::Duration;
use net::{SslClient, NetworkStream};
pub struct Plaintext;
#[derive(Clone)]
pub enum Void {}
impl io::Read for Void {
#[inline]
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
match *self {}
}
}
impl io::Write for Void {
#[inline]
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
match *self {}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
match *self {}
}
}
impl NetworkStream for Void {
#[inline]
fn peer_addr(&mut self) -> io::Result<SocketAddr> {
match *self {}
}
#[inline]
fn set_read_timeout(&self, _dur: Option<Duration>) -> io::Result<()> {
match *self {}
}
#[inline]
fn set_write_timeout(&self, _dur: Option<Duration>) -> io::Result<()> {
match *self {}
}
#[inline]
fn close(&mut self, _how: Shutdown) -> io::Result<()> {
match *self {}
}
}
impl<T: NetworkStream + Send + Clone> SslClient<T> for Plaintext {
type Stream = Void;
fn wrap_client(&self, _stream: T, _host: &str) -> ::Result<Self::Stream> {
Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid scheme").into())
}
}
}

View File

@@ -268,16 +268,15 @@ mod tests {
#[test]
fn test_proxy() {
let url = Url::parse("http://example.dom").unwrap();
let proxy_url = Url::parse("http://pro.xy").unwrap();
let mut req = Request::with_connector(
Get, proxy_url, &mut MockConnector
Get, url, &mut MockConnector
).unwrap();
req.url = url;
req.message.set_proxied(true);
let bytes = run_request(req);
let s = from_utf8(&bytes[..]).unwrap();
let request_line = "GET http://example.dom/ HTTP/1.1";
assert_eq!(&s[..request_line.len()], request_line);
assert!(s.contains("Host: pro.xy"));
assert!(s.contains("Host: example.dom"));
}
#[test]