use rama_core::error::BoxErrorExt as _;
use super::InnerHttpProxyConnector;
use pin_project_lite::pin_project;
use rama_core::{
Service,
error::{BoxError, ErrorContext as _},
extensions::{Extension, Extensions, ExtensionsRef},
io::Io,
telemetry::tracing,
};
use rama_http::{
HeaderMap, HeaderValue,
header::{HOST, IntoHeaderName, PROXY_AUTHORIZATION},
io::upgrade,
};
use rama_http_headers::ProxyAuthorization;
use rama_http_types::Version;
use rama_net::{
AuthorityInputExt, Protocol, ProtocolInputExt,
address::ProxyAddress,
client::{ConnectorService, ConnectorTarget, EstablishedClientConnection},
user::ProxyCredential,
};
use rama_utils::macros::define_inner_service_accessors;
use rama_utils::macros::generate_set_and_with;
use std::fmt::Debug;
use std::pin::Pin;
use std::task::{self, Poll};
use std::{ops, sync::Arc};
use tokio::io::{AsyncRead, AsyncWrite};
#[cfg(feature = "tls")]
use rama_tls::TlsTunnel;
#[derive(Debug, Clone)]
pub struct HttpProxyConnector<S> {
pub(super) inner: S,
pub(super) required: bool,
pub(super) version: Option<Version>,
pub(super) headers: Option<HeaderMap>,
}
impl<S> HttpProxyConnector<S> {
pub(super) fn new(inner: S, required: bool) -> Self {
Self {
inner,
required,
version: Some(Version::HTTP_11),
headers: None,
}
}
generate_set_and_with! {
pub fn version(mut self, version: Version) -> Self {
self.version = Some(version);
self
}
}
generate_set_and_with! {
pub fn custom_header(
mut self,
name: impl IntoHeaderName,
value: HeaderValue,
) -> Self {
self.headers.get_or_insert_default().append(name, value);
self
}
}
#[must_use]
pub fn optional(inner: S) -> Self {
Self::new(inner, false)
}
#[must_use]
pub fn required(inner: S) -> Self {
Self::new(inner, true)
}
define_inner_service_accessors!();
}
impl<S, Input> Service<Input> for HttpProxyConnector<S>
where
S: ConnectorService<Input, Connection: Io + Unpin>,
Input: AuthorityInputExt + ProtocolInputExt + Send + ExtensionsRef + 'static,
{
type Output = EstablishedClientConnection<MaybeHttpProxiedConnection<S::Connection>, Input>;
type Error = BoxError;
async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
let maybe_proxy_info = input.extensions().get_ref::<ProxyAddress>().cloned();
let Some(proxy_info) = maybe_proxy_info else {
return if self.required {
Err("http proxy required but none is defined".into())
} else {
tracing::trace!(
"http proxy connector: no proxy required or set: proceed with direct connection"
);
let EstablishedClientConnection { input, conn } =
self.inner
.connect(input)
.await
.context("establish direct connection (no http proxy given or required)")?;
return Ok(EstablishedClientConnection {
input,
conn: MaybeHttpProxiedConnection::direct(conn),
});
};
};
if !proxy_info
.protocol
.as_ref()
.map(|p| p.is_http())
.unwrap_or(true)
{
return Err(BoxError::from_static_str(
"http proxy connector can only serve http protocol",
));
}
let authority = input
.authority()
.context("http proxy connector: resolve authority")?;
let app_protocol = input.protocol().cloned();
input
.extensions()
.insert(ConnectorTarget(proxy_info.address.clone()));
#[cfg(feature = "tls")]
if proxy_info
.protocol
.as_ref()
.map(|p| p.is_secure())
.unwrap_or_default()
{
tracing::trace!(
server.address = %proxy_info.address.host,
server.port = proxy_info.address.port,
"http proxy connector: preparing proxy connection for tls tunnel",
);
input.extensions().insert(TlsTunnel {
sni: Some(proxy_info.address.host.clone()),
});
}
let EstablishedClientConnection { input, conn } = self
.inner
.connect(input)
.await
.context("establish connection to proxy")
.context_field("address", proxy_info.address.clone())
.context_debug_field("protocol", proxy_info.protocol.clone())?;
tracing::trace!(
server.address = %authority.host,
server.port = authority.port_u16(),
"http proxy connector: connected to proxy",
);
if !app_protocol
.as_ref()
.map(|p| p.is_secure())
.unwrap_or_else(|| authority.port.as_u16() == Some(Protocol::HTTPS_DEFAULT_PORT))
{
return Ok(EstablishedClientConnection {
input,
conn: MaybeHttpProxiedConnection::proxied(conn),
});
}
let mut connector =
InnerHttpProxyConnector::new(authority.clone(), input.extensions().clone())?;
if let Some(version) = self.version {
connector.set_version(version);
}
if let Some(credential) = proxy_info.credential.clone() {
match credential {
ProxyCredential::Basic(basic) => {
connector.set_typed_header(ProxyAuthorization(basic));
}
ProxyCredential::Bearer(bearer) => {
connector.set_typed_header(ProxyAuthorization(bearer));
}
}
}
if let Some(headers) = self.headers.clone() {
for (name, value) in headers.into_ordered_iter() {
if name != PROXY_AUTHORIZATION && name != HOST {
connector.set_header(name, value);
}
}
}
let (headers, conn) = connector
.handshake(conn)
.await
.context("http proxy handshake")?;
let conn = MaybeHttpProxiedConnection::upgraded_proxy(conn);
tracing::trace!("inserting HttpProxyHeaders in context");
conn.extensions()
.insert(HttpProxyConnectResponseHeaders::new(headers));
tracing::trace!(
server.address = %authority.host,
server.port = authority.port_u16(),
"http proxy connector: connected to proxy: ready secure request",
);
Ok(EstablishedClientConnection { input, conn })
}
}
#[derive(Clone, Debug, Extension)]
#[extension(tags(http, proxy))]
pub struct HttpProxyConnectResponseHeaders(Arc<HeaderMap>);
impl HttpProxyConnectResponseHeaders {
fn new(headers: HeaderMap) -> Self {
Self(Arc::new(headers))
}
}
impl AsRef<HeaderMap> for HttpProxyConnectResponseHeaders {
fn as_ref(&self) -> &HeaderMap {
&self.0
}
}
impl ops::Deref for HttpProxyConnectResponseHeaders {
type Target = HeaderMap;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pin_project! {
pub struct MaybeHttpProxiedConnection<S> {
#[pin]
inner: Connection<S>,
}
}
impl<S: ExtensionsRef + Unpin + Io> MaybeHttpProxiedConnection<S> {
fn direct(conn: S) -> Self {
Self {
inner: Connection::Direct { conn },
}
}
fn proxied(conn: S) -> Self {
Self {
inner: Connection::Proxied { conn },
}
}
fn upgraded_proxy(conn: upgrade::Upgraded) -> Self {
Self {
inner: Connection::UpgradedProxy { conn },
}
}
}
impl<S: Debug> Debug for MaybeHttpProxiedConnection<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MaybeHttpProxiedConnection")
.field("inner", &self.inner)
.finish()
}
}
pin_project! {
#[project = ConnectionProj]
enum Connection<S> {
Direct{ #[pin] conn: S },
Proxied{ #[pin] conn: S },
UpgradedProxy{ #[pin] conn: upgrade::Upgraded },
}
}
impl<S: Debug> Debug for Connection<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Direct { conn } => f.debug_struct("Direct").field("conn", conn).finish(),
Self::Proxied { conn } => f.debug_struct("Proxied").field("conn", conn).finish(),
Self::UpgradedProxy { conn } => {
f.debug_struct("UpgradedProxy").field("conn", conn).finish()
}
}
}
}
impl<S: ExtensionsRef> ExtensionsRef for MaybeHttpProxiedConnection<S> {
fn extensions(&self) -> &Extensions {
match &self.inner {
Connection::Direct { conn } | Connection::Proxied { conn } => conn.extensions(),
Connection::UpgradedProxy { conn } => conn.extensions(),
}
}
}
#[warn(clippy::missing_trait_methods)]
impl<Conn: AsyncWrite> AsyncWrite for MaybeHttpProxiedConnection<Conn> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
match self.project().inner.project() {
ConnectionProj::Direct { conn } | ConnectionProj::Proxied { conn } => {
conn.poll_write(cx, buf)
}
ConnectionProj::UpgradedProxy { conn } => conn.poll_write(cx, buf),
}
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
match self.project().inner.project() {
ConnectionProj::Direct { conn } | ConnectionProj::Proxied { conn } => {
conn.poll_flush(cx)
}
ConnectionProj::UpgradedProxy { conn } => conn.poll_flush(cx),
}
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
match self.project().inner.project() {
ConnectionProj::Direct { conn } | ConnectionProj::Proxied { conn } => {
conn.poll_shutdown(cx)
}
ConnectionProj::UpgradedProxy { conn } => conn.poll_shutdown(cx),
}
}
fn is_write_vectored(&self) -> bool {
match &self.inner {
Connection::Direct { conn } | Connection::Proxied { conn } => conn.is_write_vectored(),
Connection::UpgradedProxy { conn } => conn.is_write_vectored(),
}
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>> {
match self.project().inner.project() {
ConnectionProj::Direct { conn } | ConnectionProj::Proxied { conn } => {
conn.poll_write_vectored(cx, bufs)
}
ConnectionProj::UpgradedProxy { conn } => conn.poll_write_vectored(cx, bufs),
}
}
}
#[warn(clippy::missing_trait_methods)]
impl<Conn: AsyncRead> AsyncRead for MaybeHttpProxiedConnection<Conn> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
match self.project().inner.project() {
ConnectionProj::Direct { conn } | ConnectionProj::Proxied { conn } => {
conn.poll_read(cx, buf)
}
ConnectionProj::UpgradedProxy { conn } => conn.poll_read(cx, buf),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{client::proxy::layer::HttpProxyConnectorLayer, server::HttpServer};
use rama_core::{Layer, layer::MapOutputLayer, rt::Executor, service::service_fn};
use rama_http_types::{Body, Request, Response};
use rama_net::{
Protocol,
address::{HostWithPort, ProxyAddress},
test_utils::client::{MockConnectorService, MockSocket},
};
use std::convert::Infallible;
#[derive(Debug, Clone, Extension)]
#[extension(tags(http))]
struct ConnMarker(u32);
#[tokio::test]
async fn connection_extensions_preserved_across_proxy_connect_upgrade() {
let http_server =
HttpServer::auto(Executor::default()).service(service_fn(async |_req: Request| {
Ok::<_, Infallible>(Response::new(Body::empty()))
}));
let proxy_connector = (
HttpProxyConnectorLayer::required(),
MapOutputLayer::new(|out: EstablishedClientConnection<MockSocket, Request>| {
out.conn.extensions().insert(ConnMarker(42));
out
}),
)
.into_layer(MockConnectorService::new(move || http_server.clone()));
let req = Request::builder()
.uri("https://example.com")
.body(Body::empty())
.unwrap();
req.extensions().insert(ProxyAddress {
address: HostWithPort::example_domain_http(),
credential: None,
protocol: Some(Protocol::HTTP),
});
let EstablishedClientConnection { conn, .. } = proxy_connector
.serve(req)
.await
.expect("proxy CONNECT handshake succeeds");
let marker = conn
.extensions()
.get_ref::<ConnMarker>()
.expect("ConnMarker set on the pre-CONNECT connection must survive the upgrade");
assert_eq!(marker.0, 42);
}
}