Skip to content

Commit f36c6b2

Browse files
committed
feat(client): proper proxy and tunneling in Client
Closes #774
1 parent 3a3e086 commit f36c6b2

File tree

10 files changed

+406
-64
lines changed

10 files changed

+406
-64
lines changed

examples/client.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,20 @@ fn main() {
2020
}
2121
};
2222

23-
let client = Client::new();
23+
let client = match env::var("HTTP_PROXY") {
24+
Ok(mut proxy) => {
25+
// parse the proxy, message if it doesn't make sense
26+
let mut port = 80;
27+
if let Some(colon) = proxy.rfind(':') {
28+
port = proxy[colon + 1..].parse().unwrap_or_else(|e| {
29+
panic!("HTTP_PROXY is malformed: {:?}, port parse error: {}", proxy, e);
30+
});
31+
proxy.truncate(colon);
32+
}
33+
Client::with_http_proxy(proxy, port)
34+
},
35+
_ => Client::new()
36+
};
2437

2538
let mut res = client.get(&*url)
2639
.header(Connection::close())

src/client/mod.rs

+69-27
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,12 @@ use method::Method;
7171
use net::{NetworkConnector, NetworkStream};
7272
use Error;
7373

74+
use self::proxy::tunnel;
7475
pub use self::pool::Pool;
7576
pub use self::request::Request;
7677
pub use self::response::Response;
7778

79+
mod proxy;
7880
pub mod pool;
7981
pub mod request;
8082
pub mod response;
@@ -90,7 +92,7 @@ pub struct Client {
9092
redirect_policy: RedirectPolicy,
9193
read_timeout: Option<Duration>,
9294
write_timeout: Option<Duration>,
93-
proxy: Option<(Cow<'static, str>, Cow<'static, str>, u16)>
95+
proxy: Option<(Cow<'static, str>, u16)>
9496
}
9597

9698
impl fmt::Debug for Client {
@@ -116,6 +118,15 @@ impl Client {
116118
Client::with_connector(Pool::new(config))
117119
}
118120

121+
pub fn with_http_proxy<H>(host: H, port: u16) -> Client
122+
where H: Into<Cow<'static, str>> {
123+
let host = host.into();
124+
let proxy = tunnel((host.clone(), port));
125+
let mut client = Client::with_connector(Pool::with_connector(Default::default(), proxy));
126+
client.proxy = Some((host, port));
127+
client
128+
}
129+
119130
/// Create a new client with a specific connector.
120131
pub fn with_connector<C, S>(connector: C) -> Client
121132
where C: NetworkConnector<Stream=S> + Send + Sync + 'static, S: NetworkStream + Send {
@@ -148,12 +159,6 @@ impl Client {
148159
self.write_timeout = dur;
149160
}
150161

151-
/// Set a proxy for requests of this Client.
152-
pub fn set_proxy<S, H>(&mut self, scheme: S, host: H, port: u16)
153-
where S: Into<Cow<'static, str>>, H: Into<Cow<'static, str>> {
154-
self.proxy = Some((scheme.into(), host.into(), port));
155-
}
156-
157162
/// Build a Get request.
158163
pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
159164
self.request(Method::Get, url)
@@ -271,13 +276,12 @@ impl<'a> RequestBuilder<'a> {
271276

272277
loop {
273278
let mut req = {
274-
let (scheme, host, port) = match client.proxy {
275-
Some(ref proxy) => (proxy.0.as_ref(), proxy.1.as_ref(), proxy.2),
276-
None => {
277-
let hp = try!(get_host_and_port(&url));
278-
(url.scheme(), hp.0, hp.1)
279-
}
280-
};
279+
let (host, port) = try!(get_host_and_port(&url));
280+
let mut message = try!(client.protocol.new_message(&host, port, url.scheme()));
281+
if url.scheme() == "http" && client.proxy.is_some() {
282+
message.set_proxied(true);
283+
}
284+
281285
let mut headers = match headers {
282286
Some(ref headers) => headers.clone(),
283287
None => Headers::new(),
@@ -286,7 +290,6 @@ impl<'a> RequestBuilder<'a> {
286290
hostname: host.to_owned(),
287291
port: Some(port),
288292
});
289-
let message = try!(client.protocol.new_message(&host, port, scheme));
290293
Request::with_headers_and_message(method.clone(), url.clone(), headers, message)
291294
};
292295

@@ -460,6 +463,7 @@ impl Default for RedirectPolicy {
460463
}
461464
}
462465

466+
463467
fn get_host_and_port(url: &Url) -> ::Result<(&str, u16)> {
464468
let host = match url.host_str() {
465469
Some(host) => host,
@@ -479,8 +483,9 @@ mod tests {
479483
use std::io::Read;
480484
use header::Server;
481485
use http::h1::Http11Message;
482-
use mock::{MockStream};
486+
use mock::{MockStream, MockSsl};
483487
use super::{Client, RedirectPolicy};
488+
use super::proxy::Proxy;
484489
use super::pool::Pool;
485490
use url::Url;
486491

@@ -505,24 +510,61 @@ mod tests {
505510
#[test]
506511
fn test_proxy() {
507512
use super::pool::PooledStream;
513+
type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
508514
mock_connector!(ProxyConnector {
509515
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
510516
});
511-
let mut client = Client::with_connector(Pool::with_connector(Default::default(), ProxyConnector));
512-
client.set_proxy("http", "example.proxy", 8008);
517+
let tunnel = Proxy {
518+
connector: ProxyConnector,
519+
proxy: ("example.proxy".into(), 8008),
520+
ssl: MockSsl,
521+
};
522+
let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
523+
client.proxy = Some(("example.proxy".into(), 8008));
513524
let mut dump = vec![];
514525
client.get("http://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
515526

516-
{
517-
let box_message = client.protocol.new_message("example.proxy", 8008, "http").unwrap();
518-
let message = box_message.downcast::<Http11Message>().unwrap();
519-
let stream = message.into_inner().downcast::<PooledStream<MockStream>>().unwrap().into_inner();
520-
let s = ::std::str::from_utf8(&stream.write).unwrap();
521-
let request_line = "GET http://127.0.0.1/foo/bar HTTP/1.1\r\n";
522-
assert_eq!(&s[..request_line.len()], request_line);
523-
assert!(s.contains("Host: example.proxy:8008\r\n"));
524-
}
527+
let box_message = client.protocol.new_message("127.0.0.1", 80, "http").unwrap();
528+
let message = box_message.downcast::<Http11Message>().unwrap();
529+
let stream = message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_normal().unwrap();;
530+
531+
let s = ::std::str::from_utf8(&stream.write).unwrap();
532+
let request_line = "GET http://127.0.0.1/foo/bar HTTP/1.1\r\n";
533+
assert!(s.starts_with(request_line), "{:?} doesn't start with {:?}", s, request_line);
534+
assert!(s.contains("Host: 127.0.0.1\r\n"));
535+
}
536+
537+
#[test]
538+
fn test_proxy_tunnel() {
539+
use super::pool::PooledStream;
540+
type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
541+
542+
mock_connector!(ProxyConnector {
543+
b"HTTP/1.1 200 OK\r\n\r\n",
544+
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
545+
});
546+
let tunnel = Proxy {
547+
connector: ProxyConnector,
548+
proxy: ("example.proxy".into(), 8008),
549+
ssl: MockSsl,
550+
};
551+
let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
552+
client.proxy = Some(("example.proxy".into(), 8008));
553+
let mut dump = vec![];
554+
client.get("https://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
555+
556+
let box_message = client.protocol.new_message("127.0.0.1", 443, "https").unwrap();
557+
let message = box_message.downcast::<Http11Message>().unwrap();
558+
let stream = message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_tunneled().unwrap();
559+
560+
let s = ::std::str::from_utf8(&stream.write).unwrap();
561+
let connect_line = "CONNECT 127.0.0.1:443 HTTP/1.1\r\nHost: 127.0.0.1:443\r\n\r\n";
562+
assert_eq!(&s[..connect_line.len()], connect_line);
525563

564+
let s = &s[connect_line.len()..];
565+
let request_line = "GET /foo/bar HTTP/1.1\r\n";
566+
assert_eq!(&s[..request_line.len()], request_line);
567+
assert!(s.contains("Host: 127.0.0.1\r\n"));
526568
}
527569

528570
#[test]

src/client/pool.rs

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ impl<C: NetworkConnector<Stream=S>, S: NetworkStream + Send> NetworkConnector fo
127127
}
128128

129129
/// A Stream that will try to be returned to the Pool when dropped.
130+
#[derive(Debug)]
130131
pub struct PooledStream<S> {
131132
inner: Option<PooledStreamInner<S>>,
132133
is_closed: bool,

0 commit comments

Comments
 (0)