use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use apollo_opentelemetry::metrics::UpDownCounterExt as _;
use base64::Engine as _;
use bytes::Bytes;
use http::header::HeaderValue;
use http::uri::{Authority, Scheme};
use http_body_util::Empty;
use hyper::client::conn::http1;
use hyper_util::rt::TokioIo;
use opentelemetry::KeyValue;
use opentelemetry_semantic_conventions::attribute as semconv;
use rustls::ClientConfig;
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use tower::BoxError;
use tower::Service;
use crate::config::Protocol;
use crate::error::HttpClientError;
use crate::metrics::{ConnectionMetrics, STATE_CUSTOM_CONNECTING};
use crate::protocol::handshake::{FixedOrigin, Http1Handshake, Http2Handshake};
use crate::protocol::{HostSender, Origin};
pub(crate) struct ProxySenderConfig {
pub(crate) proxy_host: String,
pub(crate) proxy_port: u16,
pub(crate) proxy_auth: Option<HeaderValue>,
pub(crate) tls_config: Arc<ClientConfig>,
pub(crate) protocol: Protocol,
pub(crate) connect_timeout: Duration,
pub(crate) h1_max_idle: usize,
pub(crate) h1_idle_timeout: Duration,
pub(crate) h2_idle_timeout: Duration,
pub(crate) keep_alive_interval: Duration,
pub(crate) keep_alive_timeout: Duration,
pub(crate) keep_alive_while_idle: bool,
pub(crate) conn_metrics: ConnectionMetrics,
}
enum ProxyStream {
Plain(TokioIo<TcpStream>),
Tunnel(Box<TokioIo<tokio_rustls::client::TlsStream<TokioIo<hyper::upgrade::Upgraded>>>>),
}
impl ProxyStream {
fn alpn_protocol(&self) -> Option<&[u8]> {
match self {
ProxyStream::Plain(_) => None,
ProxyStream::Tunnel(tls_io) => {
let (_, conn) = tls_io.inner().get_ref();
conn.alpn_protocol()
}
}
}
}
impl hyper::rt::Read for ProxyStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll<io::Result<()>> {
match &mut *self {
ProxyStream::Plain(s) => Pin::new(s).poll_read(cx, buf),
ProxyStream::Tunnel(s) => Pin::new(&mut **s).poll_read(cx, buf),
}
}
}
impl hyper::rt::Write for ProxyStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
match &mut *self {
ProxyStream::Plain(s) => Pin::new(s).poll_write(cx, buf),
ProxyStream::Tunnel(s) => Pin::new(&mut **s).poll_write(cx, buf),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
ProxyStream::Plain(s) => Pin::new(s).poll_flush(cx),
ProxyStream::Tunnel(s) => Pin::new(&mut **s).poll_flush(cx),
}
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match &mut *self {
ProxyStream::Plain(s) => Pin::new(s).poll_shutdown(cx),
ProxyStream::Tunnel(s) => Pin::new(&mut **s).poll_shutdown(cx),
}
}
}
#[derive(Clone)]
struct ProxyConnector {
proxy_host: String,
proxy_port: u16,
proxy_auth: Option<HeaderValue>,
tls_config: Arc<ClientConfig>,
connect_timeout: Duration,
metrics: ConnectionMetrics,
}
impl ProxyConnector {
fn from_config(config: &ProxySenderConfig) -> Self {
Self {
proxy_host: config.proxy_host.clone(),
proxy_port: config.proxy_port,
proxy_auth: config.proxy_auth.clone(),
tls_config: config.tls_config.clone(),
connect_timeout: config.connect_timeout,
metrics: config.conn_metrics.clone(),
}
}
}
impl Service<Origin> for ProxyConnector {
type Response = ProxyStream;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<ProxyStream, BoxError>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, origin: Origin) -> Self::Future {
let proxy_host = self.proxy_host.clone();
let proxy_port = self.proxy_port;
let proxy_auth = self.proxy_auth.clone();
let tls_config = self.tls_config.clone();
let connect_timeout = self.connect_timeout;
let metrics = self.metrics.clone();
Box::pin(async move {
let addr = format!("{proxy_host}:{proxy_port}");
let mut attrs = metrics.connection_attrs(None, &origin);
attrs.push(KeyValue::new(
semconv::HTTP_CONNECTION_STATE,
STATE_CUSTOM_CONNECTING,
));
let _connecting = metrics.counter().track(attrs);
let setup = async {
let tcp = TcpStream::connect(&addr).await.map_err(|source| {
HttpClientError::Transport {
source: source.into(),
}
})?;
if origin.scheme == Scheme::HTTPS {
let tls =
connect_tunnel(tcp, &origin.authority, proxy_auth.as_ref(), tls_config)
.await?;
Ok::<_, BoxError>(ProxyStream::Tunnel(Box::new(TokioIo::new(tls))))
} else {
Ok(ProxyStream::Plain(TokioIo::new(tcp)))
}
};
match tokio::time::timeout(connect_timeout, setup).await {
Ok(result) => result,
Err(_) => Err(HttpClientError::ConnectionTimeout.into()),
}
})
}
}
async fn connect_tunnel(
tcp: TcpStream,
authority: &Authority,
proxy_auth: Option<&HeaderValue>,
tls_config: Arc<ClientConfig>,
) -> Result<tokio_rustls::client::TlsStream<TokioIo<hyper::upgrade::Upgraded>>, BoxError> {
let io = TokioIo::new(tcp);
let (mut sender, conn): (http1::SendRequest<Empty<Bytes>>, _) = http1::handshake(io).await?;
let driver = tokio::spawn(conn.with_upgrades());
let connect_target = format!(
"{}:{}",
authority.host(),
authority.port_u16().unwrap_or(443)
);
let mut req = http::Request::builder()
.method(http::Method::CONNECT)
.uri(
http::Uri::builder()
.authority(connect_target.as_str())
.build()?,
)
.version(http::Version::HTTP_11)
.header(http::header::HOST, authority.as_str())
.body(Empty::<Bytes>::new())?;
if let Some(auth) = proxy_auth {
req.headers_mut()
.insert(http::header::PROXY_AUTHORIZATION, auth.clone());
}
let resp = sender.send_request(req).await?;
let status = resp.status();
if status != http::StatusCode::OK {
return Err(HttpClientError::ProxyTunnel { status }.into());
}
let driver_outcome = driver_failure(driver);
tokio::pin!(driver_outcome);
let upgraded = tokio::select! {
upgraded = hyper::upgrade::on(resp) => upgraded?,
err = &mut driver_outcome => return Err(err),
};
let server_name = rustls::pki_types::ServerName::try_from(authority.host().to_owned())
.map_err(|e| format!("invalid proxy target server name: {e}"))?;
let tls = TlsConnector::from(tls_config)
.connect(server_name, TokioIo::new(upgraded))
.await?;
Ok(tls)
}
async fn driver_failure<E>(driver: tokio::task::JoinHandle<Result<(), E>>) -> BoxError
where
E: Into<BoxError>,
{
match driver.await {
Ok(Ok(())) => std::future::pending::<BoxError>().await,
Ok(Err(err)) => err.into(),
Err(join_err) => join_err.into(),
}
}
pub(crate) fn proxy_authorization(url: &url::Url) -> Option<HeaderValue> {
let username = url.username();
if username.is_empty() {
return None;
}
let username = percent_encoding::percent_decode_str(username).decode_utf8_lossy();
let password =
percent_encoding::percent_decode_str(url.password().unwrap_or("")).decode_utf8_lossy();
let encoded =
base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
Some(
HeaderValue::from_str(&format!("Basic {encoded}"))
.expect("base64 output is always a valid HeaderValue"),
)
}
pub(crate) fn new_sender(config: &Arc<ProxySenderConfig>, origin: Origin) -> HostSender {
match (config.protocol, origin.scheme.as_str()) {
(Protocol::Alpn, "https") => new_alpn_https_sender(config, origin),
(Protocol::Http2, "https") => new_h2_sender(config, origin),
_ => new_h1_sender(config, origin),
}
}
fn new_h1_sender(config: &Arc<ProxySenderConfig>, origin: Origin) -> HostSender {
let attrs = config
.conn_metrics
.connection_attrs(Some(http::Version::HTTP_11), &origin);
let connector = Http1Handshake::new(
ProxyConnector::from_config(config),
config.conn_metrics.clone(),
attrs,
);
let proxy_auth = if origin.scheme == Scheme::HTTPS {
None
} else {
config.proxy_auth.clone()
};
crate::protocol::h1::new_sender(
connector,
config.h1_max_idle,
config.h1_idle_timeout,
origin,
move |req| {
if let Some(auth) = &proxy_auth {
req.headers_mut()
.entry(http::header::PROXY_AUTHORIZATION)
.or_insert_with(|| auth.clone());
}
},
)
}
fn new_h2_sender(config: &Arc<ProxySenderConfig>, origin: Origin) -> HostSender {
let attrs = config
.conn_metrics
.connection_attrs(Some(http::Version::HTTP_2), &origin);
let handshake = Http2Handshake {
inner: FixedOrigin {
inner: ProxyConnector::from_config(config),
origin,
},
metrics: config.conn_metrics.clone(),
attrs,
keep_alive_interval: config.keep_alive_interval,
keep_alive_timeout: config.keep_alive_timeout,
keep_alive_while_idle: config.keep_alive_while_idle,
};
crate::protocol::h2::new_sender(handshake, config.h2_idle_timeout, (), |_| {})
}
fn new_alpn_https_sender(config: &Arc<ProxySenderConfig>, origin: Origin) -> HostSender {
crate::protocol::alpn::new_sender_with_connector(
ProxyConnector::from_config(config),
|io: &ProxyStream| io.alpn_protocol() == Some(b"h2"),
config.conn_metrics.clone(),
crate::protocol::alpn::AlpnPoolConfig {
h1_max_idle: config.h1_max_idle,
h1_idle_timeout: config.h1_idle_timeout,
h2_keep_alive_interval: config.keep_alive_interval,
h2_keep_alive_timeout: config.keep_alive_timeout,
h2_keep_alive_while_idle: config.keep_alive_while_idle,
h2_idle_timeout: config.h2_idle_timeout,
},
origin,
)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tower::BoxError;
use super::{driver_failure, proxy_authorization};
#[test]
fn proxy_authorization_returns_none_when_no_credentials() {
let url: url::Url = "http://proxy.example.com:3128".parse().unwrap();
assert!(proxy_authorization(&url).is_none());
}
#[test]
fn proxy_authorization_encodes_credentials_as_basic_auth() {
let url: url::Url = "http://user:pass@proxy.example.com:3128".parse().unwrap();
let header = proxy_authorization(&url).expect("credentials present");
assert_eq!(header.to_str().unwrap(), "Basic dXNlcjpwYXNz");
}
#[test]
fn proxy_authorization_handles_empty_password() {
let url: url::Url = "http://user:@proxy.example.com:3128".parse().unwrap();
let header = proxy_authorization(&url).expect("credentials present");
assert_eq!(header.to_str().unwrap(), "Basic dXNlcjo=");
}
#[test]
fn proxy_authorization_percent_decodes_credentials() {
let url: url::Url = "http://user:pass%3Aword@proxy.example.com:3128"
.parse()
.unwrap();
let header = proxy_authorization(&url).expect("credentials present");
assert_eq!(header.to_str().unwrap(), "Basic dXNlcjpwYXNzOndvcmQ=");
}
#[tokio::test]
async fn driver_failure_stays_pending_when_driver_exits_ok() {
let driver: tokio::task::JoinHandle<Result<(), BoxError>> = tokio::spawn(async { Ok(()) });
tokio::task::yield_now().await;
let timed_out = tokio::time::timeout(Duration::from_millis(50), driver_failure(driver))
.await
.is_err();
assert!(
timed_out,
"driver_failure should remain pending when driver exits Ok"
);
}
#[tokio::test]
async fn driver_failure_propagates_driver_error() {
let driver: tokio::task::JoinHandle<Result<(), BoxError>> =
tokio::spawn(async { Err("simulated driver IO error".into()) });
let err = driver_failure(driver).await;
assert_eq!(err.to_string(), "simulated driver IO error");
}
#[tokio::test]
async fn driver_failure_propagates_join_error_on_cancel() {
let driver: tokio::task::JoinHandle<Result<(), BoxError>> =
tokio::spawn(async { std::future::pending::<Result<(), BoxError>>().await });
driver.abort();
let err = driver_failure(driver).await;
assert!(
!err.to_string().is_empty(),
"JoinError should produce a non-empty BoxError"
);
}
}