Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ext/fetch): use correct ALPN to proxies #24696

Merged
merged 2 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ext/fetch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ hyper.workspace = true
hyper-rustls.workspace = true
hyper-util.workspace = true
ipnet.workspace = true
rustls-webpki.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
Expand Down
20 changes: 14 additions & 6 deletions ext/fetch/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod fs_fetch_handler;
mod proxy;
#[cfg(test)]
mod tests;

use std::borrow::Cow;
use std::cell::RefCell;
Expand Down Expand Up @@ -62,7 +64,6 @@ use http::Method;
use http::Uri;
use http_body_util::BodyExt;
use hyper::body::Frame;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::rt::TokioExecutor;
use hyper_util::rt::TokioIo;
Expand Down Expand Up @@ -975,6 +976,10 @@ pub fn create_http_client(
deno_tls::SocketUse::Http,
)?;

// Proxy TLS should not send ALPN
tls_config.alpn_protocols.clear();
let proxy_tls_config = Arc::from(tls_config.clone());

let mut alpn_protocols = vec![];
if options.http2 {
alpn_protocols.push("h2".into());
Expand All @@ -987,7 +992,6 @@ pub fn create_http_client(

let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
let connector = HttpsConnector::from((http_connector, tls_config.clone()));

let user_agent = user_agent
.parse::<HeaderValue>()
Expand All @@ -1008,9 +1012,13 @@ pub fn create_http_client(
proxies.prepend(intercept);
}
let proxies = Arc::new(proxies);
let mut connector =
proxy::ProxyConnector::new(proxies.clone(), connector, tls_config);
connector.user_agent(user_agent.clone());
let connector = proxy::ProxyConnector {
http: http_connector,
proxies: proxies.clone(),
tls: tls_config,
tls_proxy: proxy_tls_config,
user_agent: Some(user_agent.clone()),
};

if let Some(pool_max_idle_per_host) = options.pool_max_idle_per_host {
builder.pool_max_idle_per_host(pool_max_idle_per_host);
Expand Down Expand Up @@ -1059,7 +1067,7 @@ pub struct Client {
user_agent: HeaderValue,
}

type Connector = proxy::ProxyConnector<HttpsConnector<HttpConnector>>;
type Connector = proxy::ProxyConnector<HttpConnector>;

// clippy is wrong here
#[allow(clippy::declare_interior_mutable_const)]
Expand Down
61 changes: 31 additions & 30 deletions ext/fetch/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use deno_tls::rustls::ClientConfig as TlsConfig;
use http::header::HeaderValue;
use http::uri::Scheme;
use http::Uri;
use hyper_rustls::HttpsConnector;
use hyper_rustls::MaybeHttpsStream;
use hyper_util::client::legacy::connect::Connected;
use hyper_util::client::legacy::connect::Connection;
use hyper_util::rt::TokioIo;
Expand All @@ -29,10 +31,14 @@ use tower_service::Service;

#[derive(Debug, Clone)]
pub(crate) struct ProxyConnector<C> {
connector: C,
proxies: Arc<Proxies>,
tls: Arc<TlsConfig>,
user_agent: Option<HeaderValue>,
pub(crate) http: C,
pub(crate) proxies: Arc<Proxies>,
/// TLS config when destination is not a proxy
pub(crate) tls: Arc<TlsConfig>,
/// TLS config when destination is a proxy
/// Notably, does not include ALPN
pub(crate) tls_proxy: Arc<TlsConfig>,
pub(crate) user_agent: Option<HeaderValue>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -361,23 +367,6 @@ impl DomainMatcher {
}

impl<C> ProxyConnector<C> {
pub(crate) fn new(
proxies: Arc<Proxies>,
connector: C,
tls: Arc<TlsConfig>,
) -> Self {
ProxyConnector {
connector,
proxies,
tls,
user_agent: None,
}
}

pub(crate) fn user_agent(&mut self, val: HeaderValue) {
self.user_agent = Some(val);
}

fn intercept(&self, dst: &Uri) -> Option<&Intercept> {
self.proxies.intercept(dst)
}
Expand Down Expand Up @@ -438,20 +427,21 @@ pub enum Proxied<T> {

impl<C> Service<Uri> for ProxyConnector<C>
where
C: Service<Uri>,
C::Response: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
C: Service<Uri> + Clone,
C::Response:
hyper::rt::Read + hyper::rt::Write + Connection + Unpin + Send + 'static,
C::Future: Send + 'static,
C::Error: Into<BoxError> + 'static,
{
type Response = Proxied<C::Response>;
type Response = Proxied<MaybeHttpsStream<C::Response>>;
type Error = BoxError;
type Future = BoxFuture<Result<Self::Response, Self::Error>>;

fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.connector.poll_ready(cx).map_err(Into::into)
self.http.poll_ready(cx).map_err(Into::into)
}

fn call(&mut self, orig_dst: Uri) -> Self::Future {
Expand All @@ -467,10 +457,12 @@ where
dst: proxy_dst,
auth,
} => {
let connecting = self.connector.call(proxy_dst);
let mut connector =
HttpsConnector::from((self.http.clone(), self.tls_proxy.clone()));
let connecting = connector.call(proxy_dst);
let tls = TlsConnector::from(self.tls.clone());
Box::pin(async move {
let mut io = connecting.await.map_err(Into::into)?;
let mut io = connecting.await.map_err(Into::<BoxError>::into)?;

if is_https {
tunnel(&mut io, &orig_dst, user_agent, auth).await?;
Expand Down Expand Up @@ -529,9 +521,11 @@ where
}
};
}

let mut connector =
HttpsConnector::from((self.http.clone(), self.tls.clone()));
Box::pin(
self
.connector
connector
.call(orig_dst)
.map_ok(Proxied::PassThrough)
.map_err(Into::into),
Expand Down Expand Up @@ -721,7 +715,14 @@ where
match self {
Proxied::PassThrough(ref p) => p.connected(),
Proxied::HttpForward(ref p) => p.connected().proxy(true),
Proxied::HttpTunneled(ref p) => p.inner().get_ref().0.connected(),
Proxied::HttpTunneled(ref p) => {
let tunneled_tls = p.inner().get_ref();
if tunneled_tls.1.alpn_protocol() == Some(b"h2") {
tunneled_tls.0.connected().negotiated_h2()
} else {
tunneled_tls.0.connected()
}
}
Proxied::Socks(ref p) => p.connected(),
Proxied::SocksTls(ref p) => p.inner().get_ref().0.connected(),
}
Expand Down
188 changes: 188 additions & 0 deletions ext/fetch/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use std::net::SocketAddr;
use std::sync::Arc;

use bytes::Bytes;
use http_body_util::BodyExt;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;

use super::create_http_client;
use super::CreateHttpClientOptions;

static EXAMPLE_CRT: &[u8] = include_bytes!("../tls/testdata/example1_cert.der");
static EXAMPLE_KEY: &[u8] =
include_bytes!("../tls/testdata/example1_prikey.der");

#[tokio::test]
async fn test_https_proxy_http11() {
let src_addr = create_https_server(false).await;
let prx_addr = create_http_proxy(src_addr).await;
run_test_client(prx_addr, src_addr, false, http::Version::HTTP_11).await;
}

#[tokio::test]
async fn test_https_proxy_h2() {
let src_addr = create_https_server(true).await;
let prx_addr = create_http_proxy(src_addr).await;
run_test_client(prx_addr, src_addr, false, http::Version::HTTP_2).await;
}

#[tokio::test]
async fn test_https_proxy_https_h2() {
let src_addr = create_https_server(true).await;
let prx_addr = create_https_proxy(src_addr).await;
run_test_client(prx_addr, src_addr, true, http::Version::HTTP_2).await;
}

async fn run_test_client(
prx_addr: SocketAddr,
src_addr: SocketAddr,
https: bool,
ver: http::Version,
) {
let client = create_http_client(
"fetch/test",
CreateHttpClientOptions {
root_cert_store: None,
ca_certs: vec![],
proxy: Some(deno_tls::Proxy {
url: format!("http{}://{}", if https { "s" } else { "" }, prx_addr),
basic_auth: None,
}),
unsafely_ignore_certificate_errors: Some(vec![]),
client_cert_chain_and_key: None,
pool_max_idle_per_host: None,
pool_idle_timeout: None,
http1: true,
http2: true,
},
)
.unwrap();

let req = http::Request::builder()
.uri(format!("https://{}/foo", src_addr))
.body(
http_body_util::Empty::new()
.map_err(|err| match err {})
.boxed(),
)
.unwrap();
let resp = client.send(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
assert_eq!(resp.version(), ver);
let hello = resp.collect().await.unwrap().to_bytes();
assert_eq!(hello, "hello from server");
}

async fn create_https_server(allow_h2: bool) -> SocketAddr {
let mut tls_config = deno_tls::rustls::server::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![EXAMPLE_CRT.into()],
webpki::types::PrivateKeyDer::try_from(EXAMPLE_KEY).unwrap(),
)
.unwrap();
if allow_h2 {
tls_config.alpn_protocols.push("h2".into());
}
tls_config.alpn_protocols.push("http/1.1".into());
let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::from(tls_config));
let src_tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let src_addr = src_tcp.local_addr().unwrap();

tokio::spawn(async move {
while let Ok((sock, _)) = src_tcp.accept().await {
let conn = tls_acceptor.accept(sock).await.unwrap();
if conn.get_ref().1.alpn_protocol() == Some(b"h2") {
let fut = hyper::server::conn::http2::Builder::new(
hyper_util::rt::TokioExecutor::new(),
)
.serve_connection(
hyper_util::rt::TokioIo::new(conn),
hyper::service::service_fn(|_req| async {
Ok::<_, std::convert::Infallible>(http::Response::new(
http_body_util::Full::<Bytes>::new("hello from server".into()),
))
}),
);
tokio::spawn(fut);
} else {
let fut = hyper::server::conn::http1::Builder::new().serve_connection(
hyper_util::rt::TokioIo::new(conn),
hyper::service::service_fn(|_req| async {
Ok::<_, std::convert::Infallible>(http::Response::new(
http_body_util::Full::<Bytes>::new("hello from server".into()),
))
}),
);
tokio::spawn(fut);
}
}
});

src_addr
}

async fn create_http_proxy(src_addr: SocketAddr) -> SocketAddr {
let prx_tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let prx_addr = prx_tcp.local_addr().unwrap();

tokio::spawn(async move {
while let Ok((mut sock, _)) = prx_tcp.accept().await {
let fut = async move {
let mut buf = [0u8; 4096];
let _n = sock.read(&mut buf).await.unwrap();
assert_eq!(&buf[..7], b"CONNECT");
let mut dst_tcp =
tokio::net::TcpStream::connect(src_addr).await.unwrap();
sock.write_all(b"HTTP/1.1 200 OK\r\n\r\n").await.unwrap();
tokio::io::copy_bidirectional(&mut sock, &mut dst_tcp)
.await
.unwrap();
};
tokio::spawn(fut);
}
});

prx_addr
}

async fn create_https_proxy(src_addr: SocketAddr) -> SocketAddr {
let mut tls_config = deno_tls::rustls::server::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![EXAMPLE_CRT.into()],
webpki::types::PrivateKeyDer::try_from(EXAMPLE_KEY).unwrap(),
)
.unwrap();
// Set ALPN, to check our proxy connector. But we shouldn't receive anything.
tls_config.alpn_protocols.push("h2".into());
tls_config.alpn_protocols.push("http/1.1".into());
let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::from(tls_config));
let prx_tcp = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let prx_addr = prx_tcp.local_addr().unwrap();

tokio::spawn(async move {
while let Ok((sock, _)) = prx_tcp.accept().await {
let mut sock = tls_acceptor.accept(sock).await.unwrap();
assert_eq!(sock.get_ref().1.alpn_protocol(), None);

let fut = async move {
let mut buf = [0u8; 4096];
let _n = sock.read(&mut buf).await.unwrap();
assert_eq!(&buf[..7], b"CONNECT");
let mut dst_tcp =
tokio::net::TcpStream::connect(src_addr).await.unwrap();
sock.write_all(b"HTTP/1.1 200 OK\r\n\r\n").await.unwrap();
tokio::io::copy_bidirectional(&mut sock, &mut dst_tcp)
.await
.unwrap();
};
tokio::spawn(fut);
}
});

prx_addr
}