Skip to content

Commit 7237179

Browse files
committed
feat(client): add proxy::Tunnel legacy util
1 parent 7afb1ed commit 7237179

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

src/client/legacy/connect/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ pub mod dns;
7474
#[cfg(feature = "tokio")]
7575
mod http;
7676

77+
pub mod proxy;
78+
7779
pub(crate) mod capture;
7880
pub use capture::{capture_connection, CaptureConnection};
7981

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//! Proxy helpers
2+
3+
mod tunnel;
4+
5+
pub use self::tunnel::Tunnel;
+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use std::future::Future;
2+
use std::marker::{PhantomData, Unpin};
3+
use std::pin::Pin;
4+
use std::task::{self, Poll};
5+
6+
use http::{HeaderValue, Uri};
7+
use hyper::rt::{Read, Write};
8+
use pin_project_lite::pin_project;
9+
use tower_service::Service;
10+
11+
/// Tunnel Proxy via HTTP CONNECT
12+
#[derive(Debug)]
13+
pub struct Tunnel<C> {
14+
auth: Option<HeaderValue>,
15+
inner: C,
16+
proxy_dst: Uri,
17+
}
18+
19+
#[derive(Debug)]
20+
pub enum TunnelError<C> {
21+
Inner(C),
22+
Io(std::io::Error),
23+
MissingHost,
24+
ProxyAuthRequired,
25+
ProxyHeadersTooLong,
26+
TunnelUnexpectedEof,
27+
TunnelUnsuccessful,
28+
}
29+
30+
pin_project! {
31+
// Not publicly exported (so missing_docs doesn't trigger).
32+
//
33+
// We return this `Future` instead of the `Pin<Box<dyn Future>>` directly
34+
// so that users don't rely on it fitting in a `Pin<Box<dyn Future>>` slot
35+
// (and thus we can change the type in the future).
36+
#[must_use = "futures do nothing unless polled"]
37+
#[allow(missing_debug_implementations)]
38+
pub struct Tunneling<F, T, E> {
39+
#[pin]
40+
fut: BoxTunneling<T, E>,
41+
_marker: PhantomData<F>,
42+
}
43+
}
44+
45+
type BoxTunneling<T, E> = Pin<Box<dyn Future<Output = Result<T, TunnelError<E>>> + Send>>;
46+
47+
impl<C> Tunnel<C> {
48+
/// Create a new Tunnel service.
49+
pub fn new(proxy_dst: Uri, connector: C) -> Self {
50+
Self {
51+
auth: None,
52+
inner: connector,
53+
proxy_dst,
54+
}
55+
}
56+
57+
/// Add `proxy-authorization` header value to the CONNECT request.
58+
pub fn with_auth(mut self, mut auth: HeaderValue) -> Self {
59+
// just in case the user forgot
60+
auth.set_sensitive(true);
61+
self.auth = Some(auth);
62+
self
63+
}
64+
}
65+
66+
impl<C> Service<Uri> for Tunnel<C>
67+
where
68+
C: Service<Uri>,
69+
C::Future: Send + 'static,
70+
C::Response: Read + Write + Unpin + Send + 'static,
71+
C::Error: Send + 'static,
72+
{
73+
type Response = C::Response;
74+
type Error = TunnelError<C::Error>;
75+
type Future = Tunneling<C::Future, C::Response, C::Error>;
76+
77+
fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
78+
futures_util::ready!(self.inner.poll_ready(cx)).map_err(TunnelError::Inner)?;
79+
Poll::Ready(Ok(()))
80+
}
81+
82+
fn call(&mut self, dst: Uri) -> Self::Future {
83+
let connecting = self.inner.call(self.proxy_dst.clone());
84+
85+
Tunneling {
86+
fut: Box::pin(async move {
87+
let conn = connecting.await.map_err(TunnelError::Inner)?;
88+
tunnel(
89+
conn,
90+
dst.host().ok_or(TunnelError::MissingHost)?,
91+
dst.port().map(|p| p.as_u16()).unwrap_or(443),
92+
None,
93+
None,
94+
)
95+
.await
96+
}),
97+
_marker: PhantomData,
98+
}
99+
}
100+
}
101+
102+
impl<F, T, E> Future for Tunneling<F, T, E>
103+
where
104+
F: Future<Output = Result<T, E>>,
105+
{
106+
type Output = Result<T, TunnelError<E>>;
107+
108+
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
109+
self.project().fut.poll(cx)
110+
}
111+
}
112+
113+
async fn tunnel<T, E>(
114+
mut conn: T,
115+
host: &str,
116+
port: u16,
117+
user_agent: Option<HeaderValue>,
118+
auth: Option<HeaderValue>,
119+
) -> Result<T, TunnelError<E>>
120+
where
121+
T: Read + Write + Unpin,
122+
{
123+
let mut buf = format!(
124+
"\
125+
CONNECT {host}:{port} HTTP/1.1\r\n\
126+
Host: {host}:{port}\r\n\
127+
"
128+
)
129+
.into_bytes();
130+
131+
// user-agent
132+
if let Some(user_agent) = user_agent {
133+
buf.extend_from_slice(b"User-Agent: ");
134+
buf.extend_from_slice(user_agent.as_bytes());
135+
buf.extend_from_slice(b"\r\n");
136+
}
137+
138+
// proxy-authorization
139+
if let Some(value) = auth {
140+
//log::debug!("tunnel to {host}:{port} using basic auth");
141+
buf.extend_from_slice(b"Proxy-Authorization: ");
142+
buf.extend_from_slice(value.as_bytes());
143+
buf.extend_from_slice(b"\r\n");
144+
}
145+
146+
// headers end
147+
buf.extend_from_slice(b"\r\n");
148+
149+
crate::rt::write_all(&mut conn, &buf)
150+
.await
151+
.map_err(TunnelError::Io)?;
152+
153+
let mut buf = [0; 8192];
154+
let mut pos = 0;
155+
156+
loop {
157+
let n = crate::rt::read(&mut conn, &mut buf[pos..])
158+
.await
159+
.map_err(TunnelError::Io)?;
160+
161+
if n == 0 {
162+
return Err(TunnelError::TunnelUnexpectedEof);
163+
}
164+
pos += n;
165+
166+
let recvd = &buf[..pos];
167+
if recvd.starts_with(b"HTTP/1.1 200") || recvd.starts_with(b"HTTP/1.0 200") {
168+
if recvd.ends_with(b"\r\n\r\n") {
169+
return Ok(conn);
170+
}
171+
if pos == buf.len() {
172+
return Err(TunnelError::ProxyHeadersTooLong);
173+
}
174+
// else read more
175+
} else if recvd.starts_with(b"HTTP/1.1 407") {
176+
return Err(TunnelError::ProxyAuthRequired);
177+
} else {
178+
return Err(TunnelError::TunnelUnsuccessful);
179+
}
180+
}
181+
}

src/rt/io.rs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use std::marker::Unpin;
2+
use std::pin::Pin;
3+
use std::task::Poll;
4+
5+
use futures_util::future;
6+
use futures_util::ready;
7+
use hyper::rt::{Read, ReadBuf, Write};
8+
9+
pub(crate) async fn read<T>(io: &mut T, buf: &mut [u8]) -> Result<usize, std::io::Error>
10+
where
11+
T: Read + Unpin,
12+
{
13+
future::poll_fn(move |cx| {
14+
let mut buf = ReadBuf::new(buf);
15+
ready!(Pin::new(&mut *io).poll_read(cx, buf.unfilled()))?;
16+
Poll::Ready(Ok(buf.filled().len()))
17+
})
18+
.await
19+
}
20+
21+
pub(crate) async fn write_all<T>(io: &mut T, buf: &[u8]) -> Result<(), std::io::Error>
22+
where
23+
T: Write + Unpin,
24+
{
25+
let mut n = 0;
26+
future::poll_fn(move |cx| {
27+
while n < buf.len() {
28+
n += ready!(Pin::new(&mut *io).poll_write(cx, &buf[n..])?);
29+
}
30+
Poll::Ready(Ok(()))
31+
})
32+
.await
33+
}

src/rt/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
//! Runtime utilities
22
3+
#[cfg(feature = "client-legacy")]
4+
mod io;
5+
#[cfg(feature = "client-legacy")]
6+
pub(crate) use self::io::{read, write_all};
7+
38
#[cfg(feature = "tokio")]
49
pub mod tokio;
510

tests/proxy.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
2+
use tokio::net::TcpListener;
3+
use tower_service::Service;
4+
5+
use hyper_util::client::legacy::connect::{proxy::Tunnel, HttpConnector};
6+
7+
#[tokio::test]
8+
async fn test_tunnel_works() {
9+
let tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind");
10+
let addr = tcp.local_addr().expect("local_addr");
11+
12+
let proxy_dst = format!("http://{}", addr).parse().expect("uri");
13+
let mut connector = Tunnel::new(proxy_dst, HttpConnector::new());
14+
let t1 = tokio::spawn(async move {
15+
let _conn = connector
16+
.call("https://hyper.rs".parse().unwrap())
17+
.await
18+
.expect("tunnel");
19+
});
20+
21+
let t2 = tokio::spawn(async move {
22+
let (mut io, _) = tcp.accept().await.expect("accept");
23+
let mut buf = [0u8; 64];
24+
let n = io.read(&mut buf).await.expect("read 1");
25+
assert_eq!(
26+
&buf[..n],
27+
b"CONNECT hyper.rs:443 HTTP/1.1\r\nHost: hyper.rs:443\r\n\r\n"
28+
);
29+
io.write_all(b"HTTP/1.1 200 OK\r\n\r\n")
30+
.await
31+
.expect("write 1");
32+
});
33+
34+
t1.await.expect("task 1");
35+
t2.await.expect("task 2");
36+
}

0 commit comments

Comments
 (0)