1use base64::prelude::*;
2use http_body_util::Empty;
3use hyper::{body::Bytes, header};
4use hyper_util::{
5 client::legacy::{
6 Client,
7 connect::{Connected, Connection},
8 },
9 rt::{TokioExecutor, TokioIo},
10};
11use std::{
12 future::Future,
13 io,
14 pin::Pin,
15 task::{Context, Poll},
16};
17use tokio::{
18 io::{AsyncRead, AsyncWrite, ReadBuf},
19 net::TcpStream,
20};
21use tonic::transport::{Channel, Endpoint};
22use tower::{Service, service_fn};
23
24#[cfg(unix)]
25use tokio::net::UnixStream;
26
27#[derive(Clone, Debug, bon::Builder)]
29#[builder(start_fn = new, on(String, into))]
30#[non_exhaustive]
31pub struct HttpConnectProxyOptions {
32 #[builder(start_fn)]
35 pub target_addr: String,
36 pub basic_auth: Option<(String, String)>,
38}
39
40impl HttpConnectProxyOptions {
41 pub async fn connect_endpoint(
43 &self,
44 endpoint: &Endpoint,
45 ) -> Result<Channel, tonic::transport::Error> {
46 let proxy_options = self.clone();
47 let svc_fn = service_fn(move |uri: tonic::transport::Uri| {
48 let proxy_options = proxy_options.clone();
49 async move { proxy_options.connect(uri).await }
50 });
51 endpoint.connect_with_connector(svc_fn).await
52 }
53
54 async fn connect(
55 &self,
56 uri: tonic::transport::Uri,
57 ) -> anyhow::Result<hyper::upgrade::Upgraded> {
58 let uri = ensure_connect_authority_port(uri);
59 debug!("Connecting to {} via proxy at {}", uri, self.target_addr);
60 let mut req_build = hyper::Request::builder().method("CONNECT").uri(uri);
62 if let Some((user, pass)) = &self.basic_auth {
63 let creds = BASE64_STANDARD.encode(format!("{user}:{pass}"));
64 req_build = req_build.header(header::PROXY_AUTHORIZATION, format!("Basic {creds}"));
65 }
66 let req = req_build.body(Empty::<Bytes>::new())?;
67
68 let client = Client::builder(TokioExecutor::new())
71 .build(OverrideAddrConnector(self.target_addr.clone()));
72
73 let res = client.request(req).await?;
75 if res.status().is_success() {
76 Ok(hyper::upgrade::on(res).await?)
77 } else {
78 Err(anyhow::anyhow!(
79 "CONNECT call failed with status: {}",
80 res.status()
81 ))
82 }
83 }
84}
85
86#[derive(Clone)]
87struct OverrideAddrConnector(String);
88
89impl Service<hyper::Uri> for OverrideAddrConnector {
90 type Response = TokioIo<ProxyStream>;
91
92 type Error = anyhow::Error;
93
94 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
95
96 fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll<anyhow::Result<()>> {
97 Poll::Ready(Ok(()))
98 }
99
100 fn call(&mut self, _uri: hyper::Uri) -> Self::Future {
101 let target_addr = self.0.clone();
102 let fut = async move {
103 Ok(TokioIo::new(
104 ProxyStream::connect(target_addr.as_str()).await?,
105 ))
106 };
107 Box::pin(fut)
108 }
109}
110
111#[doc(hidden)]
113pub enum ProxyStream {
114 Tcp(TcpStream),
115 #[cfg(unix)]
116 Unix(UnixStream),
117}
118
119impl ProxyStream {
120 async fn connect(target_addr: &str) -> anyhow::Result<Self> {
121 if target_addr.starts_with("unix:/") {
122 #[cfg(unix)]
123 {
124 Ok(ProxyStream::Unix(
125 UnixStream::connect(&target_addr[5..]).await?,
126 ))
127 }
128 #[cfg(not(unix))]
129 {
130 Err(anyhow::anyhow!(
131 "Unix sockets are not supported on this platform"
132 ))
133 }
134 } else {
135 Ok(ProxyStream::Tcp(TcpStream::connect(target_addr).await?))
136 }
137 }
138}
139
140impl AsyncRead for ProxyStream {
141 fn poll_read(
142 self: Pin<&mut Self>,
143 cx: &mut Context<'_>,
144 buf: &mut ReadBuf<'_>,
145 ) -> Poll<io::Result<()>> {
146 match self.get_mut() {
147 ProxyStream::Tcp(s) => Pin::new(s).poll_read(cx, buf),
148 #[cfg(unix)]
149 ProxyStream::Unix(s) => Pin::new(s).poll_read(cx, buf),
150 }
151 }
152}
153
154impl AsyncWrite for ProxyStream {
155 fn poll_write(
156 self: Pin<&mut Self>,
157 cx: &mut Context<'_>,
158 buf: &[u8],
159 ) -> Poll<io::Result<usize>> {
160 match self.get_mut() {
161 ProxyStream::Tcp(s) => Pin::new(s).poll_write(cx, buf),
162 #[cfg(unix)]
163 ProxyStream::Unix(s) => Pin::new(s).poll_write(cx, buf),
164 }
165 }
166
167 fn poll_write_vectored(
168 self: Pin<&mut Self>,
169 cx: &mut Context<'_>,
170 bufs: &[io::IoSlice<'_>],
171 ) -> Poll<io::Result<usize>> {
172 match self.get_mut() {
173 ProxyStream::Tcp(s) => Pin::new(s).poll_write_vectored(cx, bufs),
174 #[cfg(unix)]
175 ProxyStream::Unix(s) => Pin::new(s).poll_write_vectored(cx, bufs),
176 }
177 }
178
179 fn is_write_vectored(&self) -> bool {
180 match self {
181 ProxyStream::Tcp(s) => s.is_write_vectored(),
182 #[cfg(unix)]
183 ProxyStream::Unix(s) => s.is_write_vectored(),
184 }
185 }
186
187 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
188 match self.get_mut() {
189 ProxyStream::Tcp(s) => Pin::new(s).poll_flush(cx),
190 #[cfg(unix)]
191 ProxyStream::Unix(s) => Pin::new(s).poll_flush(cx),
192 }
193 }
194
195 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
196 match self.get_mut() {
197 ProxyStream::Tcp(s) => Pin::new(s).poll_shutdown(cx),
198 #[cfg(unix)]
199 ProxyStream::Unix(s) => Pin::new(s).poll_shutdown(cx),
200 }
201 }
202}
203
204impl Connection for ProxyStream {
205 fn connected(&self) -> Connected {
206 match self {
207 ProxyStream::Tcp(s) => s.connected(),
208 #[cfg(unix)]
210 ProxyStream::Unix(_) => Connected::new(),
211 }
212 }
213}
214
215fn ensure_connect_authority_port(uri: tonic::transport::Uri) -> tonic::transport::Uri {
218 if uri.port().is_some() {
219 return uri;
220 }
221 let port = match uri.scheme_str() {
222 Some("https") => 443,
223 Some("http") => 80,
224 _ => return uri,
225 };
226 let mut parts = uri.into_parts();
227 if let Some(ref authority) = parts.authority
228 && let Ok(new_auth) = format!("{}:{}", authority.host(), port).parse()
229 {
230 parts.authority = Some(new_auth);
231 }
232 tonic::transport::Uri::from_parts(parts).expect("adding port to valid URI should not fail")
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use tokio::{
239 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
240 net::TcpListener,
241 };
242
243 struct CapturedConnect {
244 request_line: String,
245 headers: Vec<String>,
246 }
247
248 async fn mock_proxy() -> (String, tokio::task::JoinHandle<CapturedConnect>) {
249 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
250 let addr = listener.local_addr().unwrap().to_string();
251 let handle = tokio::spawn(async move {
252 let (stream, _) = listener.accept().await.unwrap();
253 let mut reader = BufReader::new(stream);
254 let mut request_line = String::new();
255 reader.read_line(&mut request_line).await.unwrap();
256 let mut headers = Vec::new();
257 loop {
258 let mut line = String::new();
259 reader.read_line(&mut line).await.unwrap();
260 if line == "\r\n" {
261 break;
262 }
263 headers.push(line.trim_end().to_string());
264 }
265 reader
266 .into_inner()
267 .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
268 .await
269 .unwrap();
270 CapturedConnect {
271 request_line,
272 headers,
273 }
274 });
275 (addr, handle)
276 }
277
278 #[rstest::rstest]
279 #[case("https://example.com/some/path", "CONNECT example.com:443 HTTP/1.1")]
280 #[case("http://example.com", "CONNECT example.com:80 HTTP/1.1")]
281 #[case("https://example.com:7233", "CONNECT example.com:7233 HTTP/1.1")]
282 #[tokio::test]
283 async fn connect_request_line(#[case] uri: &str, #[case] expected: &str) {
284 let (proxy_addr, handle) = mock_proxy().await;
285 let opts = HttpConnectProxyOptions::new(proxy_addr).build();
286 let uri: tonic::transport::Uri = uri.parse().unwrap();
287 let _ = opts.connect(uri).await;
288
289 let captured = handle.await.unwrap();
290 assert_eq!(captured.request_line.trim(), expected);
291 }
292
293 #[tokio::test]
294 async fn connect_includes_basic_auth() {
295 let (proxy_addr, handle) = mock_proxy().await;
296 let opts = HttpConnectProxyOptions::new(proxy_addr)
297 .basic_auth(("user".to_string(), "pass".to_string()))
298 .build();
299 let uri: tonic::transport::Uri = "https://example.com:7233".parse().unwrap();
300 let _ = opts.connect(uri).await;
301
302 let captured = handle.await.unwrap();
303 let creds = BASE64_STANDARD.encode("user:pass");
304 let auth_header = captured
305 .headers
306 .iter()
307 .find(|h| h.to_lowercase().starts_with("proxy-authorization:"))
308 .expect("missing proxy-authorization header");
309 assert_eq!(
310 auth_header.trim(),
311 format!("proxy-authorization: Basic {creds}")
312 );
313 }
314}