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
111enum ProxyStream {
112 Tcp(TcpStream),
113 #[cfg(unix)]
114 Unix(UnixStream),
115}
116
117impl ProxyStream {
118 async fn connect(target_addr: &str) -> anyhow::Result<Self> {
119 if target_addr.starts_with("unix:/") {
120 #[cfg(unix)]
121 {
122 Ok(ProxyStream::Unix(
123 UnixStream::connect(&target_addr[5..]).await?,
124 ))
125 }
126 #[cfg(not(unix))]
127 {
128 Err(anyhow::anyhow!(
129 "Unix sockets are not supported on this platform"
130 ))
131 }
132 } else {
133 Ok(ProxyStream::Tcp(TcpStream::connect(target_addr).await?))
134 }
135 }
136}
137
138impl AsyncRead for ProxyStream {
139 fn poll_read(
140 self: Pin<&mut Self>,
141 cx: &mut Context<'_>,
142 buf: &mut ReadBuf<'_>,
143 ) -> Poll<io::Result<()>> {
144 match self.get_mut() {
145 ProxyStream::Tcp(s) => Pin::new(s).poll_read(cx, buf),
146 #[cfg(unix)]
147 ProxyStream::Unix(s) => Pin::new(s).poll_read(cx, buf),
148 }
149 }
150}
151
152impl AsyncWrite for ProxyStream {
153 fn poll_write(
154 self: Pin<&mut Self>,
155 cx: &mut Context<'_>,
156 buf: &[u8],
157 ) -> Poll<io::Result<usize>> {
158 match self.get_mut() {
159 ProxyStream::Tcp(s) => Pin::new(s).poll_write(cx, buf),
160 #[cfg(unix)]
161 ProxyStream::Unix(s) => Pin::new(s).poll_write(cx, buf),
162 }
163 }
164
165 fn poll_write_vectored(
166 self: Pin<&mut Self>,
167 cx: &mut Context<'_>,
168 bufs: &[io::IoSlice<'_>],
169 ) -> Poll<io::Result<usize>> {
170 match self.get_mut() {
171 ProxyStream::Tcp(s) => Pin::new(s).poll_write_vectored(cx, bufs),
172 #[cfg(unix)]
173 ProxyStream::Unix(s) => Pin::new(s).poll_write_vectored(cx, bufs),
174 }
175 }
176
177 fn is_write_vectored(&self) -> bool {
178 match self {
179 ProxyStream::Tcp(s) => s.is_write_vectored(),
180 #[cfg(unix)]
181 ProxyStream::Unix(s) => s.is_write_vectored(),
182 }
183 }
184
185 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
186 match self.get_mut() {
187 ProxyStream::Tcp(s) => Pin::new(s).poll_flush(cx),
188 #[cfg(unix)]
189 ProxyStream::Unix(s) => Pin::new(s).poll_flush(cx),
190 }
191 }
192
193 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
194 match self.get_mut() {
195 ProxyStream::Tcp(s) => Pin::new(s).poll_shutdown(cx),
196 #[cfg(unix)]
197 ProxyStream::Unix(s) => Pin::new(s).poll_shutdown(cx),
198 }
199 }
200}
201
202impl Connection for ProxyStream {
203 fn connected(&self) -> Connected {
204 match self {
205 ProxyStream::Tcp(s) => s.connected(),
206 #[cfg(unix)]
208 ProxyStream::Unix(_) => Connected::new(),
209 }
210 }
211}
212
213fn ensure_connect_authority_port(uri: tonic::transport::Uri) -> tonic::transport::Uri {
216 if uri.port().is_some() {
217 return uri;
218 }
219 let port = match uri.scheme_str() {
220 Some("https") => 443,
221 Some("http") => 80,
222 _ => return uri,
223 };
224 let mut parts = uri.into_parts();
225 if let Some(ref authority) = parts.authority
226 && let Ok(new_auth) = format!("{}:{}", authority.host(), port).parse()
227 {
228 parts.authority = Some(new_auth);
229 }
230 tonic::transport::Uri::from_parts(parts).expect("adding port to valid URI should not fail")
231}
232
233#[cfg(test)]
234mod tests {
235 use super::{HttpConnectProxyOptions, ProxyStream};
236 use crate::{
237 Client, ClientOptions, Connection as TemporalConnection, ConnectionOptions, RetryOptions,
238 grpc::WorkflowService,
239 };
240 use base64::prelude::*;
241 use futures_util::{FutureExt, future::BoxFuture};
242 use http::{Request, Response};
243 use http_body_util::Empty;
244 use hyper::{
245 body::{Bytes, Incoming},
246 server::conn::http1,
247 service::service_fn,
248 };
249 use hyper_util::rt::TokioIo;
250 use std::{
251 convert::Infallible,
252 io,
253 sync::{
254 Arc,
255 atomic::{AtomicUsize, Ordering},
256 },
257 task::{Context, Poll},
258 };
259 use temporalio_common::protos::temporal::api::workflowservice::v1::ListNamespacesRequest;
260 #[cfg(unix)]
261 use tokio::net::UnixListener;
262 use tokio::{
263 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
264 net::{TcpListener, TcpStream},
265 sync::oneshot,
266 };
267 use tokio_stream::wrappers::TcpListenerStream;
268 use tonic::{IntoRequest, body::Body, server::NamedService, transport::Server};
269 use tower::Service;
270 use tracing::warn;
271 use url::Url;
272
273 #[derive(Clone)]
274 struct FakeWorkflowService<F>(F);
275
276 impl<F> Service<Request<Body>> for FakeWorkflowService<F>
277 where
278 F: FnMut(Request<Body>) -> BoxFuture<'static, Response<Body>>,
279 {
280 type Response = Response<Body>;
281 type Error = Infallible;
282 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
283
284 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
285 Poll::Ready(Ok(()))
286 }
287
288 fn call(&mut self, request: Request<Body>) -> Self::Future {
289 let response = (self.0)(request);
290 async move { Ok(response.await) }.boxed()
291 }
292 }
293
294 impl<F> NamedService for FakeWorkflowService<F> {
295 const NAME: &'static str = "temporal.api.workflowservice.v1.WorkflowService";
296 }
297
298 struct FakeServer {
299 addr: std::net::SocketAddr,
300 shutdown_tx: oneshot::Sender<()>,
301 }
302
303 async fn fake_server<F>(response_maker: F) -> FakeServer
304 where
305 F: FnMut(Request<Body>) -> BoxFuture<'static, Response<Body>>
306 + Clone
307 + Send
308 + Sync
309 + 'static,
310 {
311 let (shutdown_tx, shutdown_rx) = oneshot::channel();
312 let listener = TcpListener::bind("[::]:0").await.unwrap();
313 let addr = listener.local_addr().unwrap();
314 tokio::spawn(async move {
315 Server::builder()
316 .add_service(FakeWorkflowService(response_maker))
317 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async move {
318 let _ = shutdown_rx.await;
319 })
320 .await
321 .unwrap();
322 });
323 FakeServer { addr, shutdown_tx }
324 }
325
326 struct HttpProxy {
327 proxy_hits: Arc<AtomicUsize>,
328 shutdown_tx: oneshot::Sender<()>,
329 }
330
331 impl HttpProxy {
332 fn spawn_tcp(listener: TcpListener) -> Self {
333 Self::spawn(ProxyListener::Tcp(listener))
334 }
335
336 #[cfg(unix)]
337 fn spawn_unix(listener: UnixListener) -> Self {
338 Self::spawn(ProxyListener::Unix(listener))
339 }
340
341 fn spawn(listener: ProxyListener) -> Self {
342 let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
343 let proxy_hits = Arc::new(AtomicUsize::new(0));
344 let proxy_hits_for_task = proxy_hits.clone();
345 tokio::spawn(async move {
346 loop {
347 let proxy_hits = proxy_hits_for_task.clone();
348 tokio::select! {
349 _ = &mut shutdown_rx => break,
350 stream = listener.accept() => {
351 let stream = match stream {
352 Ok(stream) => stream,
353 Err(error) => {
354 warn!(%error, "Proxy accept failed");
355 continue;
356 }
357 };
358 tokio::spawn(async move {
359 if let Err(error) = http1::Builder::new()
360 .serve_connection(
361 TokioIo::new(stream),
362 service_fn(move |request| {
363 handle_connect(request, proxy_hits.clone())
364 }),
365 )
366 .with_upgrades()
367 .await
368 {
369 warn!(%error, "Proxy connection failed");
370 }
371 });
372 }
373 }
374 }
375 });
376 Self {
377 proxy_hits,
378 shutdown_tx,
379 }
380 }
381
382 fn hit_count(&self) -> usize {
383 self.proxy_hits.load(Ordering::SeqCst)
384 }
385
386 fn shutdown(self) {
387 let _ = self.shutdown_tx.send(());
388 }
389 }
390
391 async fn handle_connect(
392 request: Request<Incoming>,
393 counter: Arc<AtomicUsize>,
394 ) -> Result<Response<Empty<Bytes>>, hyper::Error> {
395 if request.method() != hyper::Method::CONNECT {
396 return Ok(Response::builder()
397 .status(hyper::StatusCode::METHOD_NOT_ALLOWED)
398 .body(Empty::new())
399 .unwrap());
400 }
401
402 counter.fetch_add(1, Ordering::SeqCst);
403 tokio::spawn(async move {
404 if let Some(addr) = request
405 .uri()
406 .authority()
407 .map(|authority| authority.as_str())
408 && let Ok(mut server_stream) = TcpStream::connect(addr).await
409 && let Ok(upgraded) = hyper::upgrade::on(request).await
410 {
411 let mut upgraded = TokioIo::new(upgraded);
412 let _ = tokio::io::copy_bidirectional(&mut upgraded, &mut server_stream).await;
413 }
414 });
415
416 Ok(Response::builder()
417 .status(hyper::StatusCode::OK)
418 .body(Empty::new())
419 .unwrap())
420 }
421
422 enum ProxyListener {
423 Tcp(TcpListener),
424 #[cfg(unix)]
425 Unix(UnixListener),
426 }
427
428 impl ProxyListener {
429 async fn accept(&self) -> io::Result<ProxyStream> {
430 match self {
431 ProxyListener::Tcp(listener) => listener
432 .accept()
433 .await
434 .map(|(stream, _)| ProxyStream::Tcp(stream)),
435 #[cfg(unix)]
436 ProxyListener::Unix(listener) => listener
437 .accept()
438 .await
439 .map(|(stream, _)| ProxyStream::Unix(stream)),
440 }
441 }
442 }
443
444 struct CapturedConnect {
445 request_line: String,
446 headers: Vec<String>,
447 }
448
449 async fn mock_proxy() -> (String, tokio::task::JoinHandle<CapturedConnect>) {
450 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
451 let addr = listener.local_addr().unwrap().to_string();
452 let handle = tokio::spawn(async move {
453 let (stream, _) = listener.accept().await.unwrap();
454 let mut reader = BufReader::new(stream);
455 let mut request_line = String::new();
456 reader.read_line(&mut request_line).await.unwrap();
457 let mut headers = Vec::new();
458 loop {
459 let mut line = String::new();
460 reader.read_line(&mut line).await.unwrap();
461 if line == "\r\n" {
462 break;
463 }
464 headers.push(line.trim_end().to_string());
465 }
466 reader
467 .into_inner()
468 .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
469 .await
470 .unwrap();
471 CapturedConnect {
472 request_line,
473 headers,
474 }
475 });
476 (addr, handle)
477 }
478
479 #[rstest::rstest]
480 #[case("https://example.com/some/path", "CONNECT example.com:443 HTTP/1.1")]
481 #[case("http://example.com", "CONNECT example.com:80 HTTP/1.1")]
482 #[case("https://example.com:7233", "CONNECT example.com:7233 HTTP/1.1")]
483 #[tokio::test]
484 async fn connect_request_line(#[case] uri: &str, #[case] expected: &str) {
485 let (proxy_addr, handle) = mock_proxy().await;
486 let opts = HttpConnectProxyOptions::new(proxy_addr).build();
487 let uri: tonic::transport::Uri = uri.parse().unwrap();
488 let _ = opts.connect(uri).await;
489
490 let captured = handle.await.unwrap();
491 assert_eq!(captured.request_line.trim(), expected);
492 }
493
494 #[tokio::test]
495 async fn connect_includes_basic_auth() {
496 let (proxy_addr, handle) = mock_proxy().await;
497 let opts = HttpConnectProxyOptions::new(proxy_addr)
498 .basic_auth(("user".to_string(), "pass".to_string()))
499 .build();
500 let uri: tonic::transport::Uri = "https://example.com:7233".parse().unwrap();
501 let _ = opts.connect(uri).await;
502
503 let captured = handle.await.unwrap();
504 let creds = BASE64_STANDARD.encode("user:pass");
505 let auth_header = captured
506 .headers
507 .iter()
508 .find(|h| h.to_lowercase().starts_with("proxy-authorization:"))
509 .expect("missing proxy-authorization header");
510 assert_eq!(
511 auth_header.trim(),
512 format!("proxy-authorization: Basic {creds}")
513 );
514 }
515
516 #[tokio::test]
517 async fn connection_uses_http_connect_proxy() {
518 let call_count = Arc::new(AtomicUsize::new(0));
519 let call_count_for_server = call_count.clone();
520 let server = fake_server(move |_| {
521 call_count_for_server.fetch_add(1, Ordering::SeqCst);
522 async { Response::new(Body::empty()) }.boxed()
523 })
524 .await;
525
526 let tcp_proxy_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
527 let tcp_proxy_addr = tcp_proxy_listener.local_addr().unwrap();
528 let tcp_proxy = HttpProxy::spawn_tcp(tcp_proxy_listener);
529
530 let mut options = ConnectionOptions::new(
531 Url::parse(&format!("http://[::1]:{}", server.addr.port())).unwrap(),
532 )
533 .retry_options(RetryOptions::no_retries())
534 .skip_get_system_info(true)
535 .build();
536
537 let connection = TemporalConnection::connect(options.clone()).await.unwrap();
538 let client_options = ClientOptions::new("my-namespace").build();
539 let client = Client::new(connection, client_options).unwrap();
540 let _ = WorkflowService::list_namespaces(
541 &mut client.clone(),
542 ListNamespacesRequest::default().into_request(),
543 )
544 .await;
545 assert_eq!(call_count.load(Ordering::SeqCst), 1);
546 assert_eq!(tcp_proxy.hit_count(), 0);
547
548 options.http_connect_proxy =
549 Some(HttpConnectProxyOptions::new(tcp_proxy_addr.to_string()).build());
550 options.dns_load_balancing = None;
551 let connection = TemporalConnection::connect(options.clone()).await.unwrap();
552 let client_options = ClientOptions::new("my-namespace").build();
553 let proxied_client = Client::new(connection, client_options).unwrap();
554 let _ = WorkflowService::list_namespaces(
555 &mut proxied_client.clone(),
556 ListNamespacesRequest::default().into_request(),
557 )
558 .await;
559 assert_eq!(call_count.load(Ordering::SeqCst), 2);
560 assert_eq!(tcp_proxy.hit_count(), 1);
561
562 #[cfg(unix)]
563 {
564 let socket_dir = tempfile::tempdir().unwrap();
565 let socket_path = socket_dir.path().join("http-proxy.sock");
566 let unix_proxy = HttpProxy::spawn_unix(UnixListener::bind(&socket_path).unwrap());
567
568 options.http_connect_proxy = Some(
569 HttpConnectProxyOptions::new(format!("unix:{}", socket_path.display())).build(),
570 );
571 let connection = TemporalConnection::connect(options).await.unwrap();
572 let client_options = ClientOptions::new("my-namespace").build();
573 let proxied_client = Client::new(connection, client_options).unwrap();
574 let _ = WorkflowService::list_namespaces(
575 &mut proxied_client.clone(),
576 ListNamespacesRequest::default().into_request(),
577 )
578 .await;
579 assert_eq!(call_count.load(Ordering::SeqCst), 3);
580 assert_eq!(unix_proxy.hit_count(), 1);
581
582 unix_proxy.shutdown();
583 }
584
585 let _ = server.shutdown_tx.send(());
586 tcp_proxy.shutdown();
587 }
588}