From 989686286b927968a8f916adbdfc047f1db89ca6 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 28 Jul 2022 21:26:20 -0400 Subject: [PATCH 01/29] Add and hook up simplistic HTTP/3 Client Signed-off-by: Miguel Guarniz --- .gitignore | 1 + Cargo.toml | 5 ++ examples/h3_simple.rs | 49 ++++++++++++ src/async_impl/client.rs | 75 +++++++++++++------ src/async_impl/h3_client/mod.rs | 128 ++++++++++++++++++++++++++++++++ src/async_impl/mod.rs | 1 + src/connect.rs | 26 +++++++ 7 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 examples/h3_simple.rs create mode 100644 src/async_impl/h3_client/mod.rs diff --git a/.gitignore b/.gitignore index d4f917d3d..a57891807 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target Cargo.lock *.swp +.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a83ba602e..caffb52ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,11 @@ tokio-socks = { version = "0.5.1", optional = true } ## trust-dns trust-dns-resolver = { version = "0.21", optional = true } +# http3 experimental support +h3 = { git = "https://github.com/hyperium/h3" } +h3-quinn = { git = "https://github.com/hyperium/h3" } +quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"] } + [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] env_logger = "0.8" hyper = { version = "0.14", default-features = false, features = ["tcp", "stream", "http1", "http2", "client", "server", "runtime"] } diff --git a/examples/h3_simple.rs b/examples/h3_simple.rs new file mode 100644 index 000000000..72cffcc0b --- /dev/null +++ b/examples/h3_simple.rs @@ -0,0 +1,49 @@ +#![deny(warnings)] + +use http::Version; +use reqwest::{Client, IntoUrl, Response}; + +async fn get(url: T) -> reqwest::Result { + Client::builder().build()?.get(url).version(Version::HTTP_3).send().await +} + + +// This is using the `tokio` runtime. You'll need the following dependency: +// +// `tokio = { version = "1", features = ["full"] }` +#[cfg(not(target_arch = "wasm32"))] +#[tokio::main] +async fn main() -> Result<(), reqwest::Error> { + // Some simple CLI args requirements... + let url = match std::env::args().nth(1) { + Some(url) => url, + None => { + println!("No CLI URL provided, using default."); + "https://hyper.rs".into() + } + }; + + eprintln!("Fetching {:?}...", url); + + // reqwest::get() is a convenience function. + // + // In most cases, you should create/build a reqwest::Client and reuse + // it for all requests. + let res = get(url).await?; + + eprintln!("Response: {:?} {}", res.version(), res.status()); + eprintln!("Headers: {:#?}\n", res.headers()); + + let body = res.text().await?; + + println!("{}", body); + + Ok(()) +} + +// The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function +// for wasm32 target, because tokio isn't compatible with wasm32. +// If you aren't building for wasm32, you don't need that line. +// The two lines below avoid the "'main' function not found" error when building for wasm32 target. +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 4517328c9..192ff8c5b 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -13,7 +13,7 @@ use http::header::{ }; use http::uri::Scheme; use http::Uri; -use hyper::client::ResponseFuture; +use hyper::client::ResponseFuture as HyperResponseFuture; #[cfg(feature = "native-tls-crate")] use native_tls_crate::TlsConnector; use pin_project_lite::pin_project; @@ -23,7 +23,7 @@ use std::task::{Context, Poll}; use tokio::time::Sleep; use log::{debug, trace}; - +use super::h3_client; use super::decoder::Accepts; use super::request::{Request, RequestBuilder}; use super::response::Response; @@ -518,6 +518,7 @@ impl ClientBuilder { #[cfg(feature = "cookies")] cookie_store: config.cookie_store, hyper: hyper_client, + h3_client: h3_client::H3Client::new(), headers: config.headers, redirect_policy: config.redirect_policy, referer: config.referer, @@ -1492,22 +1493,34 @@ impl Client { self.proxy_auth(&uri, &mut headers); - let mut req = hyper::Request::builder() - .method(method.clone()) - .uri(uri) - .version(version) - .body(body.into_stream()) - .expect("valid request parts"); + let in_flight = match version { + http::Version::HTTP_3 => { + let req = hyper::Request::builder() + .method(method.clone()) + .uri(uri) + .version(version) + .body(()) + .expect("valid request parts"); + ResponseFuture::H3(self.inner.h3_client.request(req)) + } + _ => { + let mut req = hyper::Request::builder() + .method(method.clone()) + .uri(uri) + .version(version) + .body(body.into_stream()) + .expect("valid request parts"); + + *req.headers_mut() = headers.clone(); + ResponseFuture::Default(self.inner.hyper.request(req)) + } + }; let timeout = timeout .or(self.inner.request_timeout) .map(tokio::time::sleep) .map(Box::pin); - *req.headers_mut() = headers.clone(); - - let in_flight = self.inner.hyper.request(req); - Pending { inner: PendingInner::Request(PendingRequest { method, @@ -1698,6 +1711,7 @@ struct ClientRef { cookie_store: Option>, headers: HeaderMap, hyper: HyperClient, + h3_client: h3_client::H3Client, redirect_policy: redirect::Policy, referer: bool, request_timeout: Option, @@ -1772,6 +1786,11 @@ pin_project! { } } +enum ResponseFuture { + Default(HyperResponseFuture), + H3(h3_client::H3ResponseFuture), +} + impl PendingRequest { fn in_flight(self: Pin<&mut Self>) -> Pin<&mut ResponseFuture> { self.project().in_flight @@ -1820,7 +1839,7 @@ impl PendingRequest { *req.headers_mut() = self.headers.clone(); - *self.as_mut().in_flight().get_mut() = self.client.hyper.request(req); + *self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req)); true } @@ -1877,15 +1896,29 @@ impl Future for PendingRequest { } loop { - let res = match self.as_mut().in_flight().as_mut().poll(cx) { - Poll::Ready(Err(e)) => { - if self.as_mut().retry_error(&e) { - continue; + let res = match self.as_mut().in_flight().get_mut() { + ResponseFuture::Default(r) => { + match Pin::new(r).poll(cx) { + Poll::Ready(Err(e)) => { + if self.as_mut().retry_error(&e) { + continue; + } + return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone()))); + } + Poll::Ready(Ok(res)) => res, + Poll::Pending => return Poll::Pending, + } + } + ResponseFuture::H3(r) => match Pin::new(r).poll(cx) { + Poll::Ready(Err(e)) => { + if self.as_mut().retry_error(&e) { + continue; + } + return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone()))); } - return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone()))); + Poll::Ready(Ok(res)) => res, + Poll::Pending => return Poll::Pending, } - Poll::Ready(Ok(res)) => res, - Poll::Pending => return Poll::Pending, }; #[cfg(feature = "cookies")] @@ -2001,7 +2034,7 @@ impl Future for PendingRequest { *req.headers_mut() = headers.clone(); std::mem::swap(self.as_mut().headers(), &mut headers); - *self.as_mut().in_flight().get_mut() = self.client.hyper.request(req); + *self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req)); continue; } redirect::ActionKind::Stop => { diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs new file mode 100644 index 000000000..912d0039c --- /dev/null +++ b/src/async_impl/h3_client/mod.rs @@ -0,0 +1,128 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::SystemTime; +use rustls::client::ServerCertVerified; +use rustls::{Error, ServerName}; +use bytes::Bytes; +use h3::client::SendRequest; +use http::{Request, Response, Uri}; +use crate::error::BoxError; +use hyper::Body; +use bytes::Buf; +use futures_util::future; + +static ALPN: &[u8] = b"h3"; + +// hyper Client +#[derive(Clone)] +pub struct H3Client { + connector: H3Connector, +} + +impl H3Client { + #[cfg(feature = "__rustls")] + pub fn new() -> Self { + let tls_config_builder = rustls::ClientConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_protocol_versions(&[&rustls::version::TLS13]).unwrap(); + let mut tls_config = tls_config_builder + .with_custom_certificate_verifier(Arc::new(YesVerifier)) + .with_no_client_auth(); + + tls_config.enable_early_data = true; + tls_config.alpn_protocols = vec![ALPN.into()]; + + Self { + connector: H3Connector { + config: quinn::ClientConfig::new(Arc::new(tls_config)), + }, + } + } + + pub(super) fn request(&self, req: Request<()>) -> H3ResponseFuture { + // Connect via connector + //H3ResponseFuture{inner: Box::pin(self.clone().connect_request(req))} + let mut connector = self.connector.clone(); + H3ResponseFuture{inner: Box::pin(async move { + eprintln!("Trying http3 ..."); + let mut send_request = connector.connect_to(req.uri().clone()).await.unwrap(); + let mut stream = send_request.send_request(req).await.unwrap(); + stream.finish().await.unwrap(); + + eprintln!("Receiving response ..."); + let resp = stream.recv_response().await.unwrap(); + eprintln!("Response h3 {:?}", resp); + + while let Some(chunk) = stream.recv_data().await.unwrap() { + eprintln!("Chunk: {:?}", chunk.chunk()); + } + + Ok(resp.map(|_| { + Body::empty() + })) + })} + } +} + +struct YesVerifier; + +impl rustls::client::ServerCertVerifier for YesVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } +} + +// hyper HttpConnector +#[derive(Clone)] +pub struct H3Connector { + // TODO: is cloning this config expensive? + config: quinn::ClientConfig, +} + +impl H3Connector { + async fn connect_to(&mut self, dest: Uri) -> Result, BoxError> { + let auth = dest + .authority() + .ok_or("destination must have a host")? + .clone(); + let port = auth.port_u16().unwrap_or(443); + let addr = tokio::net::lookup_host((auth.host(), port)) + .await? + .next() + .ok_or("dns found no addresses")?; + eprintln!("URI {}", dest); + let mut client_endpoint = h3_quinn::quinn::Endpoint::client("[::]:0".parse().unwrap())?; + client_endpoint.set_default_client_config(self.config.clone()); + let quinn_conn = h3_quinn::Connection::new(client_endpoint.connect(addr, auth.host())?.await?); + let (mut driver, send_request) = h3::client::new(quinn_conn).await?; + tokio::spawn(async move { + future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); + }); + Ok(send_request) + } +} + + +pub struct H3ResponseFuture { + inner: Pin, crate::Error>> + Send>>, +} + + +impl Future for H3ResponseFuture { + type Output = Result, crate::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.inner.as_mut().poll(cx) + } +} diff --git a/src/async_impl/mod.rs b/src/async_impl/mod.rs index 6fc29fc29..6432c2aec 100644 --- a/src/async_impl/mod.rs +++ b/src/async_impl/mod.rs @@ -14,3 +14,4 @@ pub mod multipart; pub(crate) mod request; mod response; mod upgrade; +pub mod h3_client; diff --git a/src/connect.rs b/src/connect.rs index 4f2c3dba5..fc4b8e2f6 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -31,6 +31,8 @@ use crate::dns::TrustDnsResolver; use crate::error::BoxError; use crate::proxy::{Proxy, ProxyScheme}; + +// TODO: add http3 connector #[derive(Clone)] pub(crate) enum HttpConnector { Gai(hyper::client::HttpConnector), @@ -170,6 +172,7 @@ pub(crate) struct Connector { user_agent: Option, } +// TODO: add http3 connector #[derive(Clone)] enum Inner { #[cfg(not(feature = "__tls"))] @@ -286,6 +289,28 @@ impl Connector { } } + // TODO: add connector for http3 + // pub(crate) fn new_http3_connector( + // mut http: h3_client::Http3Connector, + // proxies: Arc>, + // user_agent: Option, + // nodelay: bool, + // ) -> Connector + // where + // T: Into>, + // { + // Connector { + // inner: Inner::TlsForH3 { + // http, + // }, + // proxies, + // verbose: verbose::OFF, + // timeout: None, + // nodelay, + // user_agent, + // } + // } + pub(crate) fn set_timeout(&mut self, timeout: Option) { self.timeout = timeout; } @@ -352,6 +377,7 @@ impl Connector { }) } + // TODO: add http3 logic async fn connect_with_maybe_proxy(self, dst: Uri, is_proxy: bool) -> Result { match self.inner { #[cfg(not(feature = "__tls"))] From bcf2ddd5ec914b8429eba183a58e2226ca83d3c1 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 30 Jul 2022 17:41:02 -0400 Subject: [PATCH 02/29] Pass Rustls config from Builder to H3 client constructor Signed-off-by: Miguel Guarniz --- examples/h3_simple.rs | 12 +++++++----- src/async_impl/client.rs | 28 ++++++++++++++++++++++------ src/async_impl/h3_client/mod.rs | 23 +++++++---------------- src/connect.rs | 32 +++++++++----------------------- 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/examples/h3_simple.rs b/examples/h3_simple.rs index 72cffcc0b..fbfb939e7 100644 --- a/examples/h3_simple.rs +++ b/examples/h3_simple.rs @@ -4,7 +4,13 @@ use http::Version; use reqwest::{Client, IntoUrl, Response}; async fn get(url: T) -> reqwest::Result { - Client::builder().build()?.get(url).version(Version::HTTP_3).send().await + Client::builder() + .http3_prior_knowledge() + .build()? + .get(url) + .version(Version::HTTP_3) + .send() + .await } @@ -25,10 +31,6 @@ async fn main() -> Result<(), reqwest::Error> { eprintln!("Fetching {:?}...", url); - // reqwest::get() is a convenience function. - // - // In most cases, you should create/build a reqwest::Client and reuse - // it for all requests. let res = get(url).await?; eprintln!("Response: {:?} {}", res.version(), res.status()); diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 192ff8c5b..eac28ed9c 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -69,6 +69,7 @@ pub struct ClientBuilder { enum HttpVersionPref { Http1, Http2, + Http3, All, } @@ -222,7 +223,7 @@ impl ClientBuilder { if config.dns_overrides.is_empty() { HttpConnector::new_gai() } else { - HttpConnector::new_gai_with_overrides(config.dns_overrides) + HttpConnector::new_gai_with_overrides(config.dns_overrides.clone()) } } #[cfg(feature = "trust-dns")] @@ -230,14 +231,14 @@ impl ClientBuilder { if config.dns_overrides.is_empty() { HttpConnector::new_trust_dns()? } else { - HttpConnector::new_trust_dns_with_overrides(config.dns_overrides)? + HttpConnector::new_trust_dns_with_overrides(config.dns_overrides.clone())? } } #[cfg(not(feature = "trust-dns"))] true => unreachable!("trust-dns shouldn't be enabled unless the feature is"), }; - #[cfg(feature = "__tls")] + #[cfg(any(feature = "__tls", feature = "http3"))] match config.tls { #[cfg(feature = "default-tls")] TlsBackend::Default => { @@ -252,6 +253,9 @@ impl ClientBuilder { HttpVersionPref::Http2 => { tls.request_alpns(&["h2"]); } + HttpVersionPref::Http3 => { + unreachable!("HTTP/3 shouldn't be enabled unless the feature is") + }, HttpVersionPref::All => { tls.request_alpns(&["h2", "http/1.1"]); } @@ -326,7 +330,7 @@ impl ClientBuilder { config.local_address, config.nodelay, ), - #[cfg(feature = "__rustls")] + #[cfg(any(feature = "__rustls", feature = "http3"))] TlsBackend::Rustls => { use crate::tls::NoVerifier; @@ -434,6 +438,9 @@ impl ClientBuilder { HttpVersionPref::Http2 => { tls.alpn_protocols = vec!["h2".into()]; } + HttpVersionPref::Http3 => { + tls.alpn_protocols = vec!["h3".into()]; + } HttpVersionPref::All => { tls.alpn_protocols = vec!["h2".into(), "http/1.1".into()]; } @@ -456,7 +463,7 @@ impl ClientBuilder { } } - #[cfg(not(feature = "__tls"))] + #[cfg(all(not(feature = "__tls"), not(feature = "http3")))] Connector::new(http, proxies.clone(), config.local_address, config.nodelay) }; @@ -508,6 +515,7 @@ impl ClientBuilder { builder.http1_allow_obsolete_multiline_headers_in_responses(true); } + let h3_client = h3_client::H3Client::new(connector.deep_clone_tls()); let hyper_client = builder.build(connector); let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); @@ -518,7 +526,8 @@ impl ClientBuilder { #[cfg(feature = "cookies")] cookie_store: config.cookie_store, hyper: hyper_client, - h3_client: h3_client::H3Client::new(), + #[cfg(feature = "http3")] + h3_client, headers: config.headers, redirect_policy: config.redirect_policy, referer: config.referer, @@ -915,6 +924,12 @@ impl ClientBuilder { self } + /// Only use HTTP/3. + pub fn http3_prior_knowledge(mut self) -> ClientBuilder { + self.config.http_version_pref = HttpVersionPref::Http3; + self + } + /// Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control. /// /// Default is currently 65,535 but may change internally to optimize for common uses. @@ -1711,6 +1726,7 @@ struct ClientRef { cookie_store: Option>, headers: HeaderMap, hyper: HyperClient, + #[cfg(feature = "http3")] h3_client: h3_client::H3Client, redirect_policy: redirect::Policy, referer: bool, diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 912d0039c..030590d4b 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -13,31 +13,22 @@ use hyper::Body; use bytes::Buf; use futures_util::future; -static ALPN: &[u8] = b"h3"; - // hyper Client #[derive(Clone)] pub struct H3Client { connector: H3Connector, + // TODO: Since resolution is perform internally in Hyper, + // we have no way of leveraging that functionality here. + // resolver: Box, } impl H3Client { - #[cfg(feature = "__rustls")] - pub fn new() -> Self { - let tls_config_builder = rustls::ClientConfig::builder() - .with_safe_default_cipher_suites() - .with_safe_default_kx_groups() - .with_protocol_versions(&[&rustls::version::TLS13]).unwrap(); - let mut tls_config = tls_config_builder - .with_custom_certificate_verifier(Arc::new(YesVerifier)) - .with_no_client_auth(); - - tls_config.enable_early_data = true; - tls_config.alpn_protocols = vec![ALPN.into()]; - + #[cfg(feature = "http3")] + pub fn new(mut tls: rustls::ClientConfig) -> Self { + tls.enable_early_data = true; Self { connector: H3Connector { - config: quinn::ClientConfig::new(Arc::new(tls_config)), + config: quinn::ClientConfig::new(Arc::new(tls)), }, } } diff --git a/src/connect.rs b/src/connect.rs index fc4b8e2f6..85a415367 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -32,7 +32,6 @@ use crate::error::BoxError; use crate::proxy::{Proxy, ProxyScheme}; -// TODO: add http3 connector #[derive(Clone)] pub(crate) enum HttpConnector { Gai(hyper::client::HttpConnector), @@ -289,28 +288,6 @@ impl Connector { } } - // TODO: add connector for http3 - // pub(crate) fn new_http3_connector( - // mut http: h3_client::Http3Connector, - // proxies: Arc>, - // user_agent: Option, - // nodelay: bool, - // ) -> Connector - // where - // T: Into>, - // { - // Connector { - // inner: Inner::TlsForH3 { - // http, - // }, - // proxies, - // verbose: verbose::OFF, - // timeout: None, - // nodelay, - // user_agent, - // } - // } - pub(crate) fn set_timeout(&mut self, timeout: Option) { self.timeout = timeout; } @@ -546,6 +523,15 @@ impl Connector { Inner::Http(http) => http.set_keepalive(dur), } } + + #[cfg(feature = "http3")] + pub fn deep_clone_tls(&self) -> rustls::ClientConfig { + match &self.inner { + Inner::RustlsTls { tls, .. } => { + (*(*tls)).clone() + } + } + } } fn into_uri(scheme: Scheme, host: Authority) -> Uri { From 797edbf094f567aef2b01466e29f5a4558cc86e6 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 4 Aug 2022 17:46:09 -0400 Subject: [PATCH 03/29] Add Pool code from Hyper Signed-off-by: Miguel Guarniz --- Cargo.toml | 7 +- src/async_impl/h3_client/mod.rs | 112 ++++++++++---------------- src/async_impl/h3_client/pool.rs | 132 +++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 72 deletions(-) create mode 100644 src/async_impl/h3_client/pool.rs diff --git a/Cargo.toml b/Cargo.toml index caffb52ba..383d45e65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ features = [ ] [features] -default = ["default-tls"] +default = [] # Note: this doesn't enable the 'native-tls' feature, which adds specific # functionality for it. @@ -62,6 +62,9 @@ stream = ["tokio/fs", "tokio-util"] socks = ["tokio-socks"] +# Experimental HTTP/3 client. +http3 = ["rustls-tls"] + # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at anytime. @@ -140,6 +143,8 @@ trust-dns-resolver = { version = "0.21", optional = true } h3 = { git = "https://github.com/hyperium/h3" } h3-quinn = { git = "https://github.com/hyperium/h3" } quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"] } +futures-channel = "0.3" + [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] env_logger = "0.8" diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 030590d4b..f90a1d98d 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -1,22 +1,20 @@ +mod pool; + use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use std::time::SystemTime; -use rustls::client::ServerCertVerified; -use rustls::{Error, ServerName}; -use bytes::Bytes; -use h3::client::SendRequest; -use http::{Request, Response, Uri}; -use crate::error::BoxError; +use http::{Request, Response}; +use crate::error::{BoxError, Error}; use hyper::Body; -use bytes::Buf; use futures_util::future; +use h3_quinn::Connection; +use crate::async_impl::h3_client::pool::{Key, PoolClient}; -// hyper Client #[derive(Clone)] pub struct H3Client { - connector: H3Connector, + endpoint: quinn::Endpoint, + // pool: Arc> // TODO: Since resolution is perform internally in Hyper, // we have no way of leveraging that functionality here. // resolver: Box, @@ -26,63 +24,16 @@ impl H3Client { #[cfg(feature = "http3")] pub fn new(mut tls: rustls::ClientConfig) -> Self { tls.enable_early_data = true; + let config = quinn::ClientConfig::new(Arc::new(tls)); + let mut endpoint = quinn::Endpoint::client("[::]:0".parse().unwrap()).unwrap(); + endpoint.set_default_client_config(config); Self { - connector: H3Connector { - config: quinn::ClientConfig::new(Arc::new(tls)), - }, + endpoint, } } - pub(super) fn request(&self, req: Request<()>) -> H3ResponseFuture { - // Connect via connector - //H3ResponseFuture{inner: Box::pin(self.clone().connect_request(req))} - let mut connector = self.connector.clone(); - H3ResponseFuture{inner: Box::pin(async move { - eprintln!("Trying http3 ..."); - let mut send_request = connector.connect_to(req.uri().clone()).await.unwrap(); - let mut stream = send_request.send_request(req).await.unwrap(); - stream.finish().await.unwrap(); - - eprintln!("Receiving response ..."); - let resp = stream.recv_response().await.unwrap(); - eprintln!("Response h3 {:?}", resp); - - while let Some(chunk) = stream.recv_data().await.unwrap() { - eprintln!("Chunk: {:?}", chunk.chunk()); - } - - Ok(resp.map(|_| { - Body::empty() - })) - })} - } -} - -struct YesVerifier; - -impl rustls::client::ServerCertVerifier for YesVerifier { - fn verify_server_cert( - &self, - _end_entity: &rustls::Certificate, - _intermediates: &[rustls::Certificate], - _server_name: &ServerName, - _scts: &mut dyn Iterator, - _ocsp_response: &[u8], - _now: SystemTime, - ) -> Result { - Ok(ServerCertVerified::assertion()) - } -} - -// hyper HttpConnector -#[derive(Clone)] -pub struct H3Connector { - // TODO: is cloning this config expensive? - config: quinn::ClientConfig, -} - -impl H3Connector { - async fn connect_to(&mut self, dest: Uri) -> Result, BoxError> { + async fn get_pooled_client(&self, key: Key) -> Result { + let dest = pool::domain_as_uri(key); let auth = dest .authority() .ok_or("destination must have a host")? @@ -92,24 +43,43 @@ impl H3Connector { .await? .next() .ok_or("dns found no addresses")?; - eprintln!("URI {}", dest); - let mut client_endpoint = h3_quinn::quinn::Endpoint::client("[::]:0".parse().unwrap())?; - client_endpoint.set_default_client_config(self.config.clone()); - let quinn_conn = h3_quinn::Connection::new(client_endpoint.connect(addr, auth.host())?.await?); - let (mut driver, send_request) = h3::client::new(quinn_conn).await?; + + let quinn_conn = Connection::new( + self.endpoint.connect(addr, auth.host())?.await? + ); + let (mut driver, tx) = h3::client::new(quinn_conn).await?; + + // TODO: What does poll_close() do? tokio::spawn(async move { future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); }); - Ok(send_request) + + Ok(PoolClient::new(tx)) } -} + async fn send_request(self, key: Key, req: Request<()>) -> Result, Error> { + eprintln!("Trying http3 ..."); + let mut pooled = match self.get_pooled_client(key).await { + Ok(client) => client, + Err(_) => panic!("failed to get pooled client") + }; + // TODO: how will requests pass + pooled.send_request(req).await + } + + pub(super) fn request(&self, mut req: Request<()>) -> H3ResponseFuture { + let pool_key = match pool::extract_domain(req.uri_mut(), false) { + Ok(s) => s, + Err(_) => panic!("invalid pool key") + }; + H3ResponseFuture{inner: Box::pin(self.clone().send_request(pool_key, req))} + } +} pub struct H3ResponseFuture { inner: Pin, crate::Error>> + Send>>, } - impl Future for H3ResponseFuture { type Output = Result, crate::Error>; diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs new file mode 100644 index 000000000..bbf78645b --- /dev/null +++ b/src/async_impl/h3_client/pool.rs @@ -0,0 +1,132 @@ +use std::collections::{HashMap, HashSet}; +use std::mem; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use bytes::Bytes; +use tokio::time::Instant; + +use h3::client::SendRequest; +use http::{Request, Response, Uri}; +use http::uri::{Authority, Scheme}; +use hyper::Body; +use crate::error::{Kind, Error}; +use bytes::Buf; + +pub(super) type Key = (Scheme, Authority); //Arc; + +struct Pool { + inner: Arc> +} + +impl Pool { + fn connecting(&self, key: Key) -> bool { + let mut inner = self.inner.lock().unwrap(); + inner.connecting.insert(key) + } +} + +struct PoolInner { + // A flag that a connection is being established, and the connection + // should be shared. This prevents making multiple HTTP/2 connections + // to the same host. + connecting: HashSet, + // These are internal Conns sitting in the event loop in the KeepAlive + // state, waiting to receive a new Request to send on the socket. + idle: HashMap>, + max_idle_per_host: usize, + timeout: Option, +} + +impl PoolInner { + fn put(&mut self, key: Key, client: PoolClient) { + if self.idle.contains_key(&key) { + eprintln!("connection alread exists for key {:?}", key); + return; + } + + let idle_list = self.idle.entry(key).or_default(); + idle_list.push(Idle { + idle_at: Instant::now(), + value: client + }); + } +} + +pub(crate) struct PoolClient { + tx: SendRequest +} + +impl PoolClient { + pub fn new(tx: SendRequest) -> Self { + Self { + tx + } + } + + pub async fn send_request(&mut self, req: Request<()>) -> Result, Error> { + let mut stream = self.tx.send_request(req).await.unwrap(); + stream.finish().await.unwrap(); + + eprintln!("Receiving response ..."); + let resp = stream.recv_response().await.unwrap(); + eprintln!("Response h3 {:?}", resp); + + while let Some(chunk) = stream.recv_data().await.unwrap() { + eprintln!("Chunk: {:?}", chunk.chunk()); + } + + Ok(resp.map(|_| { + Body::empty() + })) + } +} + +struct Idle { + idle_at: Instant, + value: PoolClient, +} + + +fn set_scheme(uri: &mut Uri, scheme: Scheme) { + debug_assert!( + uri.scheme().is_none(), + "set_scheme expects no existing scheme" + ); + let old = mem::replace(uri, Uri::default()); + let mut parts: ::http::uri::Parts = old.into(); + parts.scheme = Some(scheme); + parts.path_and_query = Some("/".parse().expect("slash is a valid path")); + *uri = Uri::from_parts(parts).expect("scheme is valid"); +} + +pub(crate) fn extract_domain(uri: &mut Uri, is_http_connect: bool) -> Result { + let uri_clone = uri.clone(); + match (uri_clone.scheme(), uri_clone.authority()) { + (Some(scheme), Some(auth)) => Ok((scheme.clone(), auth.clone())), + (None, Some(auth)) if is_http_connect => { + let scheme = match auth.port_u16() { + Some(443) => { + set_scheme(uri, Scheme::HTTPS); + Scheme::HTTPS + } + _ => { + set_scheme(uri, Scheme::HTTP); + Scheme::HTTP + } + }; + Ok((scheme, auth.clone())) + } + _ => { + Err(crate::Error::new(Kind::Request, None::)) + } + } +} + +pub(crate) fn domain_as_uri((scheme, auth): Key) -> Uri { + http::uri::Builder::new() + .scheme(scheme) + .authority(auth) + .path_and_query("/") + .build() + .expect("domain is valid Uri") +} \ No newline at end of file From 5dd0e87e3711d00d9df06f3d67e1719e028a7785 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 4 Aug 2022 19:20:02 -0400 Subject: [PATCH 04/29] Add connection pool to HTTP/3 Client Signed-off-by: Miguel Guarniz --- examples/h3_simple.rs | 13 +++++-- src/async_impl/h3_client/mod.rs | 24 +++++++----- src/async_impl/h3_client/pool.rs | 64 ++++++++++++++++++++++++-------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/examples/h3_simple.rs b/examples/h3_simple.rs index fbfb939e7..568c35d88 100644 --- a/examples/h3_simple.rs +++ b/examples/h3_simple.rs @@ -3,11 +3,16 @@ use http::Version; use reqwest::{Client, IntoUrl, Response}; -async fn get(url: T) -> reqwest::Result { - Client::builder() +async fn get(url: T) -> reqwest::Result { + let client = Client::builder() .http3_prior_knowledge() - .build()? - .get(url) + .build()?; + + client.get(url.clone()) + .version(Version::HTTP_3) + .send() + .await.unwrap(); + client.get(url) .version(Version::HTTP_3) .send() .await diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index f90a1d98d..d9c2aa0d7 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -9,19 +9,18 @@ use crate::error::{BoxError, Error}; use hyper::Body; use futures_util::future; use h3_quinn::Connection; -use crate::async_impl::h3_client::pool::{Key, PoolClient}; +use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; #[derive(Clone)] pub struct H3Client { endpoint: quinn::Endpoint, - // pool: Arc> + pool: Pool, // TODO: Since resolution is perform internally in Hyper, // we have no way of leveraging that functionality here. // resolver: Box, } impl H3Client { - #[cfg(feature = "http3")] pub fn new(mut tls: rustls::ClientConfig) -> Self { tls.enable_early_data = true; let config = quinn::ClientConfig::new(Arc::new(tls)); @@ -29,11 +28,17 @@ impl H3Client { endpoint.set_default_client_config(config); Self { endpoint, + pool: Pool::new() } } async fn get_pooled_client(&self, key: Key) -> Result { - let dest = pool::domain_as_uri(key); + if let Some(client) = self.pool.try_pool(&key) { + eprintln!("found a client for {:?} in the pool", key); + return Ok(client); + } + + let dest = pool::domain_as_uri(key.clone()); let auth = dest .authority() .ok_or("destination must have a host")? @@ -54,7 +59,9 @@ impl H3Client { future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); }); - Ok(PoolClient::new(tx)) + let client = PoolClient::new(tx); + self.pool.put(key, client.clone()); + Ok(client) } async fn send_request(self, key: Key, req: Request<()>) -> Result, Error> { @@ -63,11 +70,10 @@ impl H3Client { Ok(client) => client, Err(_) => panic!("failed to get pooled client") }; - // TODO: how will requests pass pooled.send_request(req).await } - pub(super) fn request(&self, mut req: Request<()>) -> H3ResponseFuture { + pub fn request(&self, mut req: Request<()>) -> H3ResponseFuture { let pool_key = match pool::extract_domain(req.uri_mut(), false) { Ok(s) => s, Err(_) => panic!("invalid pool key") @@ -77,11 +83,11 @@ impl H3Client { } pub struct H3ResponseFuture { - inner: Pin, crate::Error>> + Send>>, + inner: Pin, Error>> + Send>>, } impl Future for H3ResponseFuture { - type Output = Result, crate::Error>; + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.inner.as_mut().poll(cx) diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index bbf78645b..af32fd33d 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::mem; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -10,26 +10,52 @@ use http::{Request, Response, Uri}; use http::uri::{Authority, Scheme}; use hyper::Body; use crate::error::{Kind, Error}; -use bytes::Buf; -pub(super) type Key = (Scheme, Authority); //Arc; +pub(super) type Key = (Scheme, Authority); -struct Pool { +#[derive(Clone)] +pub struct Pool { inner: Arc> } impl Pool { - fn connecting(&self, key: Key) -> bool { + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(PoolInner { + idle: HashMap::new(), + // TODO: we should get this from some config. + max_idle_per_host: std::usize::MAX, + timeout: None, + })) + } + } + + pub fn put(&self, key: Key, client: PoolClient) { let mut inner = self.inner.lock().unwrap(); - inner.connecting.insert(key) + inner.put(key, client) + } + + pub fn try_pool(&self, key: &Key) -> Option { + let mut inner = self.inner.lock().unwrap(); + let timeout = inner.timeout; + inner.idle.get_mut(&key).and_then(|list| { + match list.pop() { + Some(idle) => { + if let Some(duration) = timeout { + if Instant::now().saturating_duration_since(idle.idle_at) > duration { + eprintln!("pooled client expired"); + return None; + } + } + Some(idle.value) + }, + None => None, + } + }) } } struct PoolInner { - // A flag that a connection is being established, and the connection - // should be shared. This prevents making multiple HTTP/2 connections - // to the same host. - connecting: HashSet, // These are internal Conns sitting in the event loop in the KeepAlive // state, waiting to receive a new Request to send on the socket. idle: HashMap>, @@ -44,7 +70,13 @@ impl PoolInner { return; } - let idle_list = self.idle.entry(key).or_default(); + let idle_list = self.idle.entry(key.clone()).or_default(); + + if idle_list.len() >= self.max_idle_per_host { + eprintln!("max idle per host for {:?}, dropping connection", key); + return; + } + idle_list.push(Idle { idle_at: Instant::now(), value: client @@ -52,7 +84,8 @@ impl PoolInner { } } -pub(crate) struct PoolClient { +#[derive(Clone)] +pub struct PoolClient { tx: SendRequest } @@ -71,8 +104,9 @@ impl PoolClient { let resp = stream.recv_response().await.unwrap(); eprintln!("Response h3 {:?}", resp); - while let Some(chunk) = stream.recv_data().await.unwrap() { - eprintln!("Chunk: {:?}", chunk.chunk()); + while let Some(_chunk) = stream.recv_data().await.unwrap() { + // eprintln!("Chunk: {:?}", chunk.chunk()); + //eprintln!("A chunk"); } Ok(resp.map(|_| { @@ -117,7 +151,7 @@ pub(crate) fn extract_domain(uri: &mut Uri, is_http_connect: bool) -> Result { - Err(crate::Error::new(Kind::Request, None::)) + Err(Error::new(Kind::Request, None::)) } } } From 107372c88bd81b2f65691f482e754d8e496f5e64 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 4 Aug 2022 21:40:44 -0400 Subject: [PATCH 05/29] Construct http::Body from chunks when creating response Signed-off-by: Miguel Guarniz --- src/async_impl/h3_client/mod.rs | 4 ++-- src/async_impl/h3_client/pool.rs | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index d9c2aa0d7..4c002cc2f 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -24,6 +24,7 @@ impl H3Client { pub fn new(mut tls: rustls::ClientConfig) -> Self { tls.enable_early_data = true; let config = quinn::ClientConfig::new(Arc::new(tls)); + // TODO: local address should be configurable. let mut endpoint = quinn::Endpoint::client("[::]:0".parse().unwrap()).unwrap(); endpoint.set_default_client_config(config); Self { @@ -34,7 +35,7 @@ impl H3Client { async fn get_pooled_client(&self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { - eprintln!("found a client for {:?} in the pool", key); + log::debug!("getting client from pool with key {:?}", key); return Ok(client); } @@ -65,7 +66,6 @@ impl H3Client { } async fn send_request(self, key: Key, req: Request<()>) -> Result, Error> { - eprintln!("Trying http3 ..."); let mut pooled = match self.get_pooled_client(key).await { Ok(client) => client, Err(_) => panic!("failed to get pooled client") diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index af32fd33d..84e869e16 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -10,6 +10,7 @@ use http::{Request, Response, Uri}; use http::uri::{Authority, Scheme}; use hyper::Body; use crate::error::{Kind, Error}; +use bytes::Buf; pub(super) type Key = (Scheme, Authority); @@ -100,17 +101,15 @@ impl PoolClient { let mut stream = self.tx.send_request(req).await.unwrap(); stream.finish().await.unwrap(); - eprintln!("Receiving response ..."); let resp = stream.recv_response().await.unwrap(); - eprintln!("Response h3 {:?}", resp); - while let Some(_chunk) = stream.recv_data().await.unwrap() { - // eprintln!("Chunk: {:?}", chunk.chunk()); - //eprintln!("A chunk"); + let mut data = Vec::new(); + while let Some(chunk) = stream.recv_data().await.unwrap() { + data.extend(chunk.chunk()) } Ok(resp.map(|_| { - Body::empty() + Body::from(data) })) } } From 808c0cc126dce4ae79088dc97a8294de754e4bbf Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 4 Aug 2022 22:21:08 -0400 Subject: [PATCH 06/29] Add feature flags to example Signed-off-by: Miguel Guarniz --- Cargo.toml | 2 +- examples/h3_simple.rs | 40 ++++++++++++++++---------------- src/async_impl/client.rs | 24 ++++++++++--------- src/async_impl/h3_client/mod.rs | 13 +++++++---- src/async_impl/h3_client/pool.rs | 2 ++ src/connect.rs | 2 ++ src/tls.rs | 4 ++-- 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 383d45e65..8868f0195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ features = [ ] [features] -default = [] +default = ["default-tls"] # Note: this doesn't enable the 'native-tls' feature, which adds specific # functionality for it. diff --git a/examples/h3_simple.rs b/examples/h3_simple.rs index 568c35d88..29ce8d35d 100644 --- a/examples/h3_simple.rs +++ b/examples/h3_simple.rs @@ -1,30 +1,30 @@ #![deny(warnings)] -use http::Version; -use reqwest::{Client, IntoUrl, Response}; - -async fn get(url: T) -> reqwest::Result { - let client = Client::builder() - .http3_prior_knowledge() - .build()?; - - client.get(url.clone()) - .version(Version::HTTP_3) - .send() - .await.unwrap(); - client.get(url) - .version(Version::HTTP_3) - .send() - .await -} - - // This is using the `tokio` runtime. You'll need the following dependency: // // `tokio = { version = "1", features = ["full"] }` +#[cfg(feature = "http3")] #[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() -> Result<(), reqwest::Error> { + use http::Version; + use reqwest::{Client, IntoUrl, Response}; + + async fn get(url: T) -> reqwest::Result { + let client = Client::builder() + .http3_prior_knowledge() + .build()?; + + client.get(url.clone()) + .version(Version::HTTP_3) + .send() + .await.unwrap(); + client.get(url) + .version(Version::HTTP_3) + .send() + .await + } + // Some simple CLI args requirements... let url = match std::env::args().nth(1) { Some(url) => url, @@ -52,5 +52,5 @@ async fn main() -> Result<(), reqwest::Error> { // for wasm32 target, because tokio isn't compatible with wasm32. // If you aren't building for wasm32, you don't need that line. // The two lines below avoid the "'main' function not found" error when building for wasm32 target. -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", not(feature = "http3")))] fn main() {} diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index eac28ed9c..ec7d70571 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -23,7 +23,6 @@ use std::task::{Context, Poll}; use tokio::time::Sleep; use log::{debug, trace}; -use super::h3_client; use super::decoder::Accepts; use super::request::{Request, RequestBuilder}; use super::response::Response; @@ -41,6 +40,8 @@ use crate::Certificate; #[cfg(any(feature = "native-tls", feature = "__rustls"))] use crate::Identity; use crate::{IntoUrl, Method, Proxy, StatusCode, Url}; +#[cfg(feature = "http3")] +use crate::async_impl::h3_client::{H3Client, H3ResponseFuture}; /// An asynchronous `Client` to make Requests with. /// @@ -238,7 +239,7 @@ impl ClientBuilder { true => unreachable!("trust-dns shouldn't be enabled unless the feature is"), }; - #[cfg(any(feature = "__tls", feature = "http3"))] + #[cfg(feature = "__tls")] match config.tls { #[cfg(feature = "default-tls")] TlsBackend::Default => { @@ -463,7 +464,7 @@ impl ClientBuilder { } } - #[cfg(all(not(feature = "__tls"), not(feature = "http3")))] + #[cfg(not(feature = "__tls"))] Connector::new(http, proxies.clone(), config.local_address, config.nodelay) }; @@ -515,9 +516,6 @@ impl ClientBuilder { builder.http1_allow_obsolete_multiline_headers_in_responses(true); } - let h3_client = h3_client::H3Client::new(connector.deep_clone_tls()); - let hyper_client = builder.build(connector); - let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); Ok(Client { @@ -525,9 +523,9 @@ impl ClientBuilder { accepts: config.accepts, #[cfg(feature = "cookies")] cookie_store: config.cookie_store, - hyper: hyper_client, #[cfg(feature = "http3")] - h3_client, + h3_client: H3Client::new(connector.deep_clone_tls(), config.local_address.into()), + hyper: builder.build(connector), headers: config.headers, redirect_policy: config.redirect_policy, referer: config.referer, @@ -1509,13 +1507,15 @@ impl Client { self.proxy_auth(&uri, &mut headers); let in_flight = match version { + #[cfg(feature = "http3")] http::Version::HTTP_3 => { - let req = hyper::Request::builder() + let mut req = hyper::Request::builder() .method(method.clone()) .uri(uri) .version(version) .body(()) .expect("valid request parts"); + *req.headers_mut() = headers.clone(); ResponseFuture::H3(self.inner.h3_client.request(req)) } _ => { @@ -1727,7 +1727,7 @@ struct ClientRef { headers: HeaderMap, hyper: HyperClient, #[cfg(feature = "http3")] - h3_client: h3_client::H3Client, + h3_client: H3Client, redirect_policy: redirect::Policy, referer: bool, request_timeout: Option, @@ -1804,7 +1804,8 @@ pin_project! { enum ResponseFuture { Default(HyperResponseFuture), - H3(h3_client::H3ResponseFuture), + #[cfg(feature = "http3")] + H3(H3ResponseFuture), } impl PendingRequest { @@ -1925,6 +1926,7 @@ impl Future for PendingRequest { Poll::Pending => return Poll::Pending, } } + #[cfg(feature = "http3")] ResponseFuture::H3(r) => match Pin::new(r).poll(cx) { Poll::Ready(Err(e)) => { if self.as_mut().retry_error(&e) { diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 4c002cc2f..38ebcb333 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -1,6 +1,9 @@ +#![cfg(feature = "http3")] + mod pool; use std::future::Future; +use std::net::{IpAddr, SocketAddr}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; @@ -21,11 +24,13 @@ pub struct H3Client { } impl H3Client { - pub fn new(mut tls: rustls::ClientConfig) -> Self { - tls.enable_early_data = true; + pub fn new(tls: rustls::ClientConfig, local_addr: Option) -> Self { let config = quinn::ClientConfig::new(Arc::new(tls)); - // TODO: local address should be configurable. - let mut endpoint = quinn::Endpoint::client("[::]:0".parse().unwrap()).unwrap(); + let socket_addr = match local_addr { + Some(ip) => SocketAddr::new(ip, 0), + None => "[::]:0".parse::().unwrap(), + }; + let mut endpoint = quinn::Endpoint::client(socket_addr).unwrap(); endpoint.set_default_client_config(config); Self { endpoint, diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 84e869e16..9835c0d38 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "http3")] + use std::collections::HashMap; use std::mem; use std::sync::{Arc, Mutex}; diff --git a/src/connect.rs b/src/connect.rs index 85a415367..257c42adb 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -530,6 +530,8 @@ impl Connector { Inner::RustlsTls { tls, .. } => { (*(*tls)).clone() } + #[cfg(feature = "default-tls")] + _ => unreachable!("HTTP/3 should only be enabled with Rustls") } } } diff --git a/src/tls.rs b/src/tls.rs index 052fb76b4..b1419a965 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -373,12 +373,12 @@ impl fmt::Debug for TlsBackend { impl Default for TlsBackend { fn default() -> TlsBackend { - #[cfg(feature = "default-tls")] + #[cfg(all(feature = "default-tls", not(feature = "http3")))] { TlsBackend::Default } - #[cfg(all(feature = "__rustls", not(feature = "default-tls")))] + #[cfg(any(all(feature = "__rustls", not(feature = "default-tls")), feature = "http3"))] { TlsBackend::Rustls } From d62d538bda75a719a59f6f4e1fc8a62f52b85829 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Fri, 5 Aug 2022 14:24:26 -0400 Subject: [PATCH 07/29] Clean up code Signed-off-by: Miguel Guarniz --- src/async_impl/h3_client/mod.rs | 20 +++++++---- src/async_impl/h3_client/pool.rs | 58 +++++++++----------------------- src/connect.rs | 2 -- 3 files changed, 28 insertions(+), 52 deletions(-) diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 38ebcb333..b6ada34aa 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -8,11 +8,13 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use http::{Request, Response}; -use crate::error::{BoxError, Error}; +use crate::error::{BoxError, Error, Kind}; use hyper::Body; use futures_util::future; use h3_quinn::Connection; +use log::debug; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; +use crate::error; #[derive(Clone)] pub struct H3Client { @@ -30,7 +32,8 @@ impl H3Client { Some(ip) => SocketAddr::new(ip, 0), None => "[::]:0".parse::().unwrap(), }; - let mut endpoint = quinn::Endpoint::client(socket_addr).unwrap(); + let mut endpoint = quinn::Endpoint::client(socket_addr) + .expect("unable to create QUIC endpoint"); endpoint.set_default_client_config(config); Self { endpoint, @@ -40,7 +43,7 @@ impl H3Client { async fn get_pooled_client(&self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { - log::debug!("getting client from pool with key {:?}", key); + debug!("getting client from pool with key {:?}", key); return Ok(client); } @@ -73,15 +76,18 @@ impl H3Client { async fn send_request(self, key: Key, req: Request<()>) -> Result, Error> { let mut pooled = match self.get_pooled_client(key).await { Ok(client) => client, - Err(_) => panic!("failed to get pooled client") + Err(e) => return Err(error::request(e)), }; - pooled.send_request(req).await + pooled + .send_request(req) + .await + .map_err(|e| Error::new(Kind::Request, Some(e))) } pub fn request(&self, mut req: Request<()>) -> H3ResponseFuture { - let pool_key = match pool::extract_domain(req.uri_mut(), false) { + let pool_key = match pool::extract_domain(req.uri_mut()) { Ok(s) => s, - Err(_) => panic!("invalid pool key") + Err(e) => return H3ResponseFuture{inner: Box::pin(future::err(e))}, }; H3ResponseFuture{inner: Box::pin(self.clone().send_request(pool_key, req))} } diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 9835c0d38..31ed1dff5 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,7 +1,6 @@ #![cfg(feature = "http3")] use std::collections::HashMap; -use std::mem; use std::sync::{Arc, Mutex}; use std::time::Duration; use bytes::Bytes; @@ -11,8 +10,9 @@ use h3::client::SendRequest; use http::{Request, Response, Uri}; use http::uri::{Authority, Scheme}; use hyper::Body; -use crate::error::{Kind, Error}; +use crate::error::{BoxError, Error, Kind}; use bytes::Buf; +use log::debug; pub(super) type Key = (Scheme, Authority); @@ -46,7 +46,7 @@ impl Pool { Some(idle) => { if let Some(duration) = timeout { if Instant::now().saturating_duration_since(idle.idle_at) > duration { - eprintln!("pooled client expired"); + debug!("pooled client expired"); return None; } } @@ -69,14 +69,14 @@ struct PoolInner { impl PoolInner { fn put(&mut self, key: Key, client: PoolClient) { if self.idle.contains_key(&key) { - eprintln!("connection alread exists for key {:?}", key); + debug!("connection already exists for key {:?}", key); return; } let idle_list = self.idle.entry(key.clone()).or_default(); if idle_list.len() >= self.max_idle_per_host { - eprintln!("max idle per host for {:?}, dropping connection", key); + debug!("max idle per host for {:?}, dropping connection", key); return; } @@ -99,19 +99,19 @@ impl PoolClient { } } - pub async fn send_request(&mut self, req: Request<()>) -> Result, Error> { - let mut stream = self.tx.send_request(req).await.unwrap(); - stream.finish().await.unwrap(); + pub async fn send_request(&mut self, req: Request<()>) -> Result, BoxError> { + let mut stream = self.tx.send_request(req).await?; + stream.finish().await?; - let resp = stream.recv_response().await.unwrap(); + let resp = stream.recv_response().await?; - let mut data = Vec::new(); - while let Some(chunk) = stream.recv_data().await.unwrap() { - data.extend(chunk.chunk()) + let mut body = Vec::new(); + while let Some(chunk) = stream.recv_data().await? { + body.extend(chunk.chunk()) } Ok(resp.map(|_| { - Body::from(data) + Body::from(body) })) } } @@ -121,39 +121,11 @@ struct Idle { value: PoolClient, } - -fn set_scheme(uri: &mut Uri, scheme: Scheme) { - debug_assert!( - uri.scheme().is_none(), - "set_scheme expects no existing scheme" - ); - let old = mem::replace(uri, Uri::default()); - let mut parts: ::http::uri::Parts = old.into(); - parts.scheme = Some(scheme); - parts.path_and_query = Some("/".parse().expect("slash is a valid path")); - *uri = Uri::from_parts(parts).expect("scheme is valid"); -} - -pub(crate) fn extract_domain(uri: &mut Uri, is_http_connect: bool) -> Result { +pub(crate) fn extract_domain(uri: &mut Uri) -> Result { let uri_clone = uri.clone(); match (uri_clone.scheme(), uri_clone.authority()) { (Some(scheme), Some(auth)) => Ok((scheme.clone(), auth.clone())), - (None, Some(auth)) if is_http_connect => { - let scheme = match auth.port_u16() { - Some(443) => { - set_scheme(uri, Scheme::HTTPS); - Scheme::HTTPS - } - _ => { - set_scheme(uri, Scheme::HTTP); - Scheme::HTTP - } - }; - Ok((scheme, auth.clone())) - } - _ => { - Err(Error::new(Kind::Request, None::)) - } + _ => Err(Error::new(Kind::Request, None::)), } } diff --git a/src/connect.rs b/src/connect.rs index 257c42adb..39314edc6 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -171,7 +171,6 @@ pub(crate) struct Connector { user_agent: Option, } -// TODO: add http3 connector #[derive(Clone)] enum Inner { #[cfg(not(feature = "__tls"))] @@ -354,7 +353,6 @@ impl Connector { }) } - // TODO: add http3 logic async fn connect_with_maybe_proxy(self, dst: Uri, is_proxy: bool) -> Result { match self.inner { #[cfg(not(feature = "__tls"))] From 57c6ab82768257c6485b833186462e15b3b4cb98 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Fri, 5 Aug 2022 18:34:01 -0400 Subject: [PATCH 08/29] Add HTTP/3-client builder Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 31 +++++++++++++++-- src/async_impl/h3_client/mod.rs | 57 +++++++++++++++++++++++++------- src/async_impl/h3_client/pool.rs | 9 ++--- src/tls.rs | 2 ++ 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index ec7d70571..c9ba48595 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -41,7 +41,7 @@ use crate::Certificate; use crate::Identity; use crate::{IntoUrl, Method, Proxy, StatusCode, Url}; #[cfg(feature = "http3")] -use crate::async_impl::h3_client::{H3Client, H3ResponseFuture}; +use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; /// An asynchronous `Client` to make Requests with. /// @@ -123,6 +123,8 @@ struct Config { error: Option, https_only: bool, dns_overrides: HashMap, + #[cfg(feature = "http3")] + tls_enable_early_data: bool } impl Default for ClientBuilder { @@ -190,6 +192,8 @@ impl ClientBuilder { cookie_store: None, https_only: false, dns_overrides: HashMap::new(), + #[cfg(feature = "http3")] + tls_enable_early_data: false }, } } @@ -331,7 +335,7 @@ impl ClientBuilder { config.local_address, config.nodelay, ), - #[cfg(any(feature = "__rustls", feature = "http3"))] + #[cfg(feature = "__rustls")] TlsBackend::Rustls => { use crate::tls::NoVerifier; @@ -447,6 +451,11 @@ impl ClientBuilder { } } + #[cfg(feature = "http3")] + { + tls.enable_early_data = config.tls_enable_early_data; + } + Connector::new_rustls_tls( http, tls, @@ -518,13 +527,22 @@ impl ClientBuilder { let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); + #[cfg(feature = "http3")] + let h3_builder = { + let mut h3_builder = H3Builder::default(); + h3_builder.set_local_addr(config.local_address); + h3_builder.set_pool_idle_timeout(config.pool_idle_timeout); + h3_builder.set_pool_max_idle_per_host(config.pool_max_idle_per_host); + h3_builder + }; + Ok(Client { inner: Arc::new(ClientRef { accepts: config.accepts, #[cfg(feature = "cookies")] cookie_store: config.cookie_store, #[cfg(feature = "http3")] - h3_client: H3Client::new(connector.deep_clone_tls(), config.local_address.into()), + h3_client: h3_builder.build(connector.deep_clone_tls()), hyper: builder.build(connector), headers: config.headers, redirect_policy: config.redirect_policy, @@ -1717,6 +1735,13 @@ impl Config { if !self.dns_overrides.is_empty() { f.field("dns_overrides", &self.dns_overrides); } + + #[cfg(feature = "http3")] + { + if self.tls_enable_early_data { + f.field("tls_enable_early_data", &true); + } + } } } diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index b6ada34aa..a504c58a5 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -7,6 +7,7 @@ use std::net::{IpAddr, SocketAddr}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use std::time::Duration; use http::{Request, Response}; use crate::error::{BoxError, Error, Kind}; use hyper::Body; @@ -16,31 +17,63 @@ use log::debug; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; use crate::error; -#[derive(Clone)] -pub struct H3Client { - endpoint: quinn::Endpoint, - pool: Pool, - // TODO: Since resolution is perform internally in Hyper, - // we have no way of leveraging that functionality here. - // resolver: Box, +pub struct H3Builder { + pool_idle_timeout: Option, + pool_max_idle_per_host: usize, + local_addr: Option, } -impl H3Client { - pub fn new(tls: rustls::ClientConfig, local_addr: Option) -> Self { +impl Default for H3Builder { + fn default() -> Self { + Self { + pool_idle_timeout: Some(Duration::from_secs(90)), + pool_max_idle_per_host: usize::MAX, + local_addr: None, + } + } +} + +impl H3Builder { + pub fn build(self, tls: rustls::ClientConfig) -> H3Client { let config = quinn::ClientConfig::new(Arc::new(tls)); - let socket_addr = match local_addr { + let socket_addr = match self.local_addr { Some(ip) => SocketAddr::new(ip, 0), None => "[::]:0".parse::().unwrap(), }; + let mut endpoint = quinn::Endpoint::client(socket_addr) .expect("unable to create QUIC endpoint"); endpoint.set_default_client_config(config); - Self { + + H3Client { endpoint, - pool: Pool::new() + pool: Pool::new(self.pool_max_idle_per_host, self.pool_idle_timeout), } } + pub fn set_pool_idle_timeout(&mut self, timeout: Option) { + self.pool_idle_timeout = timeout; + } + + pub fn set_pool_max_idle_per_host(&mut self, max: usize) { + self.pool_max_idle_per_host = max; + } + + pub fn set_local_addr(&mut self, addr: Option) { + self.local_addr = addr; + } +} + +#[derive(Clone)] +pub struct H3Client { + endpoint: quinn::Endpoint, + pool: Pool, + // TODO: Since resolution is perform internally in Hyper, + // we have no way of leveraging that functionality here. + // resolver: Box, +} + +impl H3Client { async fn get_pooled_client(&self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { debug!("getting client from pool with key {:?}", key); diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 31ed1dff5..3cf635834 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "http3")] - use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -22,13 +20,12 @@ pub struct Pool { } impl Pool { - pub fn new() -> Self { + pub fn new(max_idle_per_host: usize, timeout: Option) -> Self { Self { inner: Arc::new(Mutex::new(PoolInner { idle: HashMap::new(), - // TODO: we should get this from some config. - max_idle_per_host: std::usize::MAX, - timeout: None, + max_idle_per_host, + timeout, })) } } diff --git a/src/tls.rs b/src/tls.rs index b1419a965..4530dc44d 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -342,6 +342,8 @@ impl Version { } pub(crate) enum TlsBackend { + // This is the default and HTTP/3 feature does not use it so suppress it. + #[allow(dead_code)] #[cfg(feature = "default-tls")] Default, #[cfg(feature = "native-tls")] From 4a5f526217c13247119329b8228e09352d4ad568 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Fri, 5 Aug 2022 20:01:43 -0400 Subject: [PATCH 09/29] Use feature flag when creating Request for hyper and h3 clients Signed-off-by: Miguel Guarniz --- Cargo.toml | 2 +- examples/h3_simple.rs | 11 +++-------- src/async_impl/client.rs | 24 +++++++++--------------- src/async_impl/h3_client/pool.rs | 1 + 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8868f0195..555500fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,7 +139,7 @@ tokio-socks = { version = "0.5.1", optional = true } ## trust-dns trust-dns-resolver = { version = "0.21", optional = true } -# http3 experimental support +# HTTP/3 experimental support h3 = { git = "https://github.com/hyperium/h3" } h3-quinn = { git = "https://github.com/hyperium/h3" } quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"] } diff --git a/examples/h3_simple.rs b/examples/h3_simple.rs index 29ce8d35d..ba9667f32 100644 --- a/examples/h3_simple.rs +++ b/examples/h3_simple.rs @@ -11,15 +11,10 @@ async fn main() -> Result<(), reqwest::Error> { use reqwest::{Client, IntoUrl, Response}; async fn get(url: T) -> reqwest::Result { - let client = Client::builder() + Client::builder() .http3_prior_knowledge() - .build()?; - - client.get(url.clone()) - .version(Version::HTTP_3) - .send() - .await.unwrap(); - client.get(url) + .build()? + .get(url) .version(Version::HTTP_3) .send() .await diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index c9ba48595..ada883639 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1524,29 +1524,23 @@ impl Client { self.proxy_auth(&uri, &mut headers); + let builder = hyper::Request::builder() + .method(method.clone()) + .uri(uri) + .version(version); + let in_flight = match version { #[cfg(feature = "http3")] http::Version::HTTP_3 => { - let mut req = hyper::Request::builder() - .method(method.clone()) - .uri(uri) - .version(version) - .body(()) - .expect("valid request parts"); + let mut req = builder.body(()).expect("valid request parts"); *req.headers_mut() = headers.clone(); ResponseFuture::H3(self.inner.h3_client.request(req)) - } + }, _ => { - let mut req = hyper::Request::builder() - .method(method.clone()) - .uri(uri) - .version(version) - .body(body.into_stream()) - .expect("valid request parts"); - + let mut req = builder.body(body.into_stream()).expect("valid request parts"); *req.headers_mut() = headers.clone(); ResponseFuture::Default(self.inner.hyper.request(req)) - } + }, }; let timeout = timeout diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 3cf635834..090ba41c5 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -96,6 +96,7 @@ impl PoolClient { } } + // TODO: add support for sending data. pub async fn send_request(&mut self, req: Request<()>) -> Result, BoxError> { let mut stream = self.tx.send_request(req).await?; stream.finish().await?; From eb66d19a937c279fc26ec77235f5cc6b96e04d05 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 6 Aug 2022 11:39:02 -0400 Subject: [PATCH 10/29] Make HTTP/3 dependencies optional Signed-off-by: Miguel Guarniz --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 555500fab..7e9391dfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ stream = ["tokio/fs", "tokio-util"] socks = ["tokio-socks"] # Experimental HTTP/3 client. -http3 = ["rustls-tls"] +http3 = ["rustls-tls", "h3", "h3-quinn", "quinn", "futures-channel"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at anytime. @@ -140,10 +140,10 @@ tokio-socks = { version = "0.5.1", optional = true } trust-dns-resolver = { version = "0.21", optional = true } # HTTP/3 experimental support -h3 = { git = "https://github.com/hyperium/h3" } -h3-quinn = { git = "https://github.com/hyperium/h3" } -quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"] } -futures-channel = "0.3" +h3 = { git = "https://github.com/hyperium/h3", optional = true } +h3-quinn = { git = "https://github.com/hyperium/h3", optional = true } +quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"], optional = true } +futures-channel = { version="0.3", optional = true} [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] From 1b68cfc19a4afdaec1131dd398f17360991c4355 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 6 Aug 2022 11:53:24 -0400 Subject: [PATCH 11/29] Add cfg to HTTP version enum Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 58 ++++++++++++++++++-------------- src/async_impl/h3_client/mod.rs | 34 ++++++++++--------- src/async_impl/h3_client/pool.rs | 46 +++++++++++-------------- src/async_impl/mod.rs | 2 +- src/connect.rs | 7 ++-- src/tls.rs | 5 ++- 6 files changed, 78 insertions(+), 74 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index ada883639..398584e16 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -22,11 +22,12 @@ use std::pin::Pin; use std::task::{Context, Poll}; use tokio::time::Sleep; -use log::{debug, trace}; use super::decoder::Accepts; use super::request::{Request, RequestBuilder}; use super::response::Response; use super::Body; +#[cfg(feature = "http3")] +use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; use crate::connect::{Connector, HttpConnector}; #[cfg(feature = "cookies")] use crate::cookie; @@ -40,8 +41,7 @@ use crate::Certificate; #[cfg(any(feature = "native-tls", feature = "__rustls"))] use crate::Identity; use crate::{IntoUrl, Method, Proxy, StatusCode, Url}; -#[cfg(feature = "http3")] -use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; +use log::{debug, trace}; /// An asynchronous `Client` to make Requests with. /// @@ -70,6 +70,7 @@ pub struct ClientBuilder { enum HttpVersionPref { Http1, Http2, + #[cfg(feature = "http3")] Http3, All, } @@ -124,7 +125,7 @@ struct Config { https_only: bool, dns_overrides: HashMap, #[cfg(feature = "http3")] - tls_enable_early_data: bool + tls_enable_early_data: bool, } impl Default for ClientBuilder { @@ -193,7 +194,7 @@ impl ClientBuilder { https_only: false, dns_overrides: HashMap::new(), #[cfg(feature = "http3")] - tls_enable_early_data: false + tls_enable_early_data: false, }, } } @@ -249,7 +250,7 @@ impl ClientBuilder { TlsBackend::Default => { let mut tls = TlsConnector::builder(); - #[cfg(feature = "native-tls-alpn")] + #[cfg(all(feature = "native-tls-alpn", not(feature = "http3")))] { match config.http_version_pref { HttpVersionPref::Http1 => { @@ -258,9 +259,6 @@ impl ClientBuilder { HttpVersionPref::Http2 => { tls.request_alpns(&["h2"]); } - HttpVersionPref::Http3 => { - unreachable!("HTTP/3 shouldn't be enabled unless the feature is") - }, HttpVersionPref::All => { tls.request_alpns(&["h2", "http/1.1"]); } @@ -443,6 +441,7 @@ impl ClientBuilder { HttpVersionPref::Http2 => { tls.alpn_protocols = vec!["h2".into()]; } + #[cfg(feature = "http3")] HttpVersionPref::Http3 => { tls.alpn_protocols = vec!["h3".into()]; } @@ -941,6 +940,7 @@ impl ClientBuilder { } /// Only use HTTP/3. + #[cfg(feature = "http3")] pub fn http3_prior_knowledge(mut self) -> ClientBuilder { self.config.http_version_pref = HttpVersionPref::Http3; self @@ -1535,12 +1535,14 @@ impl Client { let mut req = builder.body(()).expect("valid request parts"); *req.headers_mut() = headers.clone(); ResponseFuture::H3(self.inner.h3_client.request(req)) - }, + } _ => { - let mut req = builder.body(body.into_stream()).expect("valid request parts"); + let mut req = builder + .body(body.into_stream()) + .expect("valid request parts"); *req.headers_mut() = headers.clone(); ResponseFuture::Default(self.inner.hyper.request(req)) - }, + } }; let timeout = timeout @@ -1875,7 +1877,8 @@ impl PendingRequest { *req.headers_mut() = self.headers.clone(); - *self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req)); + *self.as_mut().in_flight().get_mut() = + ResponseFuture::Default(self.client.hyper.request(req)); true } @@ -1933,29 +1936,31 @@ impl Future for PendingRequest { loop { let res = match self.as_mut().in_flight().get_mut() { - ResponseFuture::Default(r) => { - match Pin::new(r).poll(cx) { - Poll::Ready(Err(e)) => { - if self.as_mut().retry_error(&e) { - continue; - } - return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone()))); + ResponseFuture::Default(r) => match Pin::new(r).poll(cx) { + Poll::Ready(Err(e)) => { + if self.as_mut().retry_error(&e) { + continue; } - Poll::Ready(Ok(res)) => res, - Poll::Pending => return Poll::Pending, + return Poll::Ready(Err( + crate::error::request(e).with_url(self.url.clone()) + )); } - } + Poll::Ready(Ok(res)) => res, + Poll::Pending => return Poll::Pending, + }, #[cfg(feature = "http3")] ResponseFuture::H3(r) => match Pin::new(r).poll(cx) { Poll::Ready(Err(e)) => { if self.as_mut().retry_error(&e) { continue; } - return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone()))); + return Poll::Ready(Err( + crate::error::request(e).with_url(self.url.clone()) + )); } Poll::Ready(Ok(res)) => res, Poll::Pending => return Poll::Pending, - } + }, }; #[cfg(feature = "cookies")] @@ -2071,7 +2076,8 @@ impl Future for PendingRequest { *req.headers_mut() = headers.clone(); std::mem::swap(self.as_mut().headers(), &mut headers); - *self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req)); + *self.as_mut().in_flight().get_mut() = + ResponseFuture::Default(self.client.hyper.request(req)); continue; } redirect::ActionKind::Stop => { diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index a504c58a5..5bbd29da3 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -2,20 +2,20 @@ mod pool; +use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; +use crate::error; +use crate::error::{BoxError, Error, Kind}; +use futures_util::future; +use h3_quinn::Connection; +use http::{Request, Response}; +use hyper::Body; +use log::debug; use std::future::Future; use std::net::{IpAddr, SocketAddr}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; -use http::{Request, Response}; -use crate::error::{BoxError, Error, Kind}; -use hyper::Body; -use futures_util::future; -use h3_quinn::Connection; -use log::debug; -use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; -use crate::error; pub struct H3Builder { pool_idle_timeout: Option, @@ -41,8 +41,8 @@ impl H3Builder { None => "[::]:0".parse::().unwrap(), }; - let mut endpoint = quinn::Endpoint::client(socket_addr) - .expect("unable to create QUIC endpoint"); + let mut endpoint = + quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); endpoint.set_default_client_config(config); H3Client { @@ -91,9 +91,7 @@ impl H3Client { .next() .ok_or("dns found no addresses")?; - let quinn_conn = Connection::new( - self.endpoint.connect(addr, auth.host())?.await? - ); + let quinn_conn = Connection::new(self.endpoint.connect(addr, auth.host())?.await?); let (mut driver, tx) = h3::client::new(quinn_conn).await?; // TODO: What does poll_close() do? @@ -120,9 +118,15 @@ impl H3Client { pub fn request(&self, mut req: Request<()>) -> H3ResponseFuture { let pool_key = match pool::extract_domain(req.uri_mut()) { Ok(s) => s, - Err(e) => return H3ResponseFuture{inner: Box::pin(future::err(e))}, + Err(e) => { + return H3ResponseFuture { + inner: Box::pin(future::err(e)), + } + } }; - H3ResponseFuture{inner: Box::pin(self.clone().send_request(pool_key, req))} + H3ResponseFuture { + inner: Box::pin(self.clone().send_request(pool_key, req)), + } } } diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 090ba41c5..7f30262ac 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,22 +1,22 @@ +use bytes::Bytes; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; -use bytes::Bytes; use tokio::time::Instant; +use crate::error::{BoxError, Error, Kind}; +use bytes::Buf; use h3::client::SendRequest; -use http::{Request, Response, Uri}; use http::uri::{Authority, Scheme}; +use http::{Request, Response, Uri}; use hyper::Body; -use crate::error::{BoxError, Error, Kind}; -use bytes::Buf; use log::debug; pub(super) type Key = (Scheme, Authority); #[derive(Clone)] pub struct Pool { - inner: Arc> + inner: Arc>, } impl Pool { @@ -26,7 +26,7 @@ impl Pool { idle: HashMap::new(), max_idle_per_host, timeout, - })) + })), } } @@ -38,19 +38,17 @@ impl Pool { pub fn try_pool(&self, key: &Key) -> Option { let mut inner = self.inner.lock().unwrap(); let timeout = inner.timeout; - inner.idle.get_mut(&key).and_then(|list| { - match list.pop() { - Some(idle) => { - if let Some(duration) = timeout { - if Instant::now().saturating_duration_since(idle.idle_at) > duration { - debug!("pooled client expired"); - return None; - } + inner.idle.get_mut(&key).and_then(|list| match list.pop() { + Some(idle) => { + if let Some(duration) = timeout { + if Instant::now().saturating_duration_since(idle.idle_at) > duration { + debug!("pooled client expired"); + return None; } - Some(idle.value) - }, - None => None, + } + Some(idle.value) } + None => None, }) } } @@ -79,21 +77,19 @@ impl PoolInner { idle_list.push(Idle { idle_at: Instant::now(), - value: client + value: client, }); } } #[derive(Clone)] pub struct PoolClient { - tx: SendRequest + tx: SendRequest, } impl PoolClient { pub fn new(tx: SendRequest) -> Self { - Self { - tx - } + Self { tx } } // TODO: add support for sending data. @@ -108,9 +104,7 @@ impl PoolClient { body.extend(chunk.chunk()) } - Ok(resp.map(|_| { - Body::from(body) - })) + Ok(resp.map(|_| Body::from(body))) } } @@ -134,4 +128,4 @@ pub(crate) fn domain_as_uri((scheme, auth): Key) -> Uri { .path_and_query("/") .build() .expect("domain is valid Uri") -} \ No newline at end of file +} diff --git a/src/async_impl/mod.rs b/src/async_impl/mod.rs index 6432c2aec..5ec393ab9 100644 --- a/src/async_impl/mod.rs +++ b/src/async_impl/mod.rs @@ -9,9 +9,9 @@ pub(crate) use self::decoder::Decoder; pub mod body; pub mod client; pub mod decoder; +pub mod h3_client; #[cfg(feature = "multipart")] pub mod multipart; pub(crate) mod request; mod response; mod upgrade; -pub mod h3_client; diff --git a/src/connect.rs b/src/connect.rs index 39314edc6..dda5079f4 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -31,7 +31,6 @@ use crate::dns::TrustDnsResolver; use crate::error::BoxError; use crate::proxy::{Proxy, ProxyScheme}; - #[derive(Clone)] pub(crate) enum HttpConnector { Gai(hyper::client::HttpConnector), @@ -525,11 +524,9 @@ impl Connector { #[cfg(feature = "http3")] pub fn deep_clone_tls(&self) -> rustls::ClientConfig { match &self.inner { - Inner::RustlsTls { tls, .. } => { - (*(*tls)).clone() - } + Inner::RustlsTls { tls, .. } => (*(*tls)).clone(), #[cfg(feature = "default-tls")] - _ => unreachable!("HTTP/3 should only be enabled with Rustls") + _ => unreachable!("HTTP/3 should only be enabled with Rustls"), } } } diff --git a/src/tls.rs b/src/tls.rs index 4530dc44d..78215dbb9 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -380,7 +380,10 @@ impl Default for TlsBackend { TlsBackend::Default } - #[cfg(any(all(feature = "__rustls", not(feature = "default-tls")), feature = "http3"))] + #[cfg(any( + all(feature = "__rustls", not(feature = "default-tls")), + feature = "http3" + ))] { TlsBackend::Rustls } From 8e3819e30ba90e919bb91e11103badbde2116e5d Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 6 Aug 2022 13:51:04 -0400 Subject: [PATCH 12/29] Send payload data in requests Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 2 +- src/async_impl/h3_client/mod.rs | 16 ++++++++++------ src/async_impl/h3_client/pool.rs | 25 +++++++++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 398584e16..e39bce903 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1532,7 +1532,7 @@ impl Client { let in_flight = match version { #[cfg(feature = "http3")] http::Version::HTTP_3 => { - let mut req = builder.body(()).expect("valid request parts"); + let mut req = builder.body(body).expect("valid request parts"); *req.headers_mut() = headers.clone(); ResponseFuture::H3(self.inner.h3_client.request(req)) } diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 5bbd29da3..6f582611f 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -3,12 +3,12 @@ mod pool; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; -use crate::error; use crate::error::{BoxError, Error, Kind}; +use crate::{error, Body}; use futures_util::future; use h3_quinn::Connection; use http::{Request, Response}; -use hyper::Body; +use hyper::Body as HyperBody; use log::debug; use std::future::Future; use std::net::{IpAddr, SocketAddr}; @@ -104,7 +104,11 @@ impl H3Client { Ok(client) } - async fn send_request(self, key: Key, req: Request<()>) -> Result, Error> { + async fn send_request( + self, + key: Key, + req: Request, + ) -> Result, Error> { let mut pooled = match self.get_pooled_client(key).await { Ok(client) => client, Err(e) => return Err(error::request(e)), @@ -115,7 +119,7 @@ impl H3Client { .map_err(|e| Error::new(Kind::Request, Some(e))) } - pub fn request(&self, mut req: Request<()>) -> H3ResponseFuture { + pub fn request(&self, mut req: Request) -> H3ResponseFuture { let pool_key = match pool::extract_domain(req.uri_mut()) { Ok(s) => s, Err(e) => { @@ -131,11 +135,11 @@ impl H3Client { } pub struct H3ResponseFuture { - inner: Pin, Error>> + Send>>, + inner: Pin, Error>> + Send>>, } impl Future for H3ResponseFuture { - type Output = Result, Error>; + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.inner.as_mut().poll(cx) diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 7f30262ac..3a622a97f 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -5,11 +5,12 @@ use std::time::Duration; use tokio::time::Instant; use crate::error::{BoxError, Error, Kind}; +use crate::Body; use bytes::Buf; use h3::client::SendRequest; use http::uri::{Authority, Scheme}; use http::{Request, Response, Uri}; -use hyper::Body; +use hyper::Body as HyperBody; use log::debug; pub(super) type Key = (Scheme, Authority); @@ -92,19 +93,31 @@ impl PoolClient { Self { tx } } - // TODO: add support for sending data. - pub async fn send_request(&mut self, req: Request<()>) -> Result, BoxError> { + pub async fn send_request( + &mut self, + req: Request, + ) -> Result, BoxError> { + let (head, req_body) = req.into_parts(); + let req = Request::from_parts(head, ()); let mut stream = self.tx.send_request(req).await?; + + match req_body.as_bytes() { + Some(b) if !b.is_empty() => { + stream.send_data(Bytes::copy_from_slice(b)).await?; + } + _ => {} + } + stream.finish().await?; let resp = stream.recv_response().await?; - let mut body = Vec::new(); + let mut resp_body = Vec::new(); while let Some(chunk) = stream.recv_data().await? { - body.extend(chunk.chunk()) + resp_body.extend(chunk.chunk()) } - Ok(resp.map(|_| Body::from(body))) + Ok(resp.map(|_| HyperBody::from(resp_body))) } } From 4c040363ed33b14966a819ce9a169aad0d115e7b Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Tue, 9 Aug 2022 11:17:22 -0400 Subject: [PATCH 13/29] Change visivility of HTTP/3-client builder Signed-off-by: Miguel Guarniz --- src/async_impl/h3_client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 6f582611f..897928751 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; -pub struct H3Builder { +pub(crate) struct H3Builder { pool_idle_timeout: Option, pool_max_idle_per_host: usize, local_addr: Option, From d79fac4ab373afc743b162b4b95ff064c9f9b7f6 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Tue, 9 Aug 2022 12:07:42 -0400 Subject: [PATCH 14/29] Use the same type of request when resending it Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 62 +++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index e39bce903..ffcbe85b6 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1869,16 +1869,28 @@ impl PendingRequest { self.retry_count += 1; let uri = expect_uri(&self.url); - let mut req = hyper::Request::builder() - .method(self.method.clone()) - .uri(uri) - .body(body.into_stream()) - .expect("valid request parts"); - - *req.headers_mut() = self.headers.clone(); - *self.as_mut().in_flight().get_mut() = - ResponseFuture::Default(self.client.hyper.request(req)); + *self.as_mut().in_flight().get_mut() = match *self.as_mut().in_flight().as_ref() { + #[cfg(feature = "http3")] + ResponseFuture::H3(_) => { + let mut req = hyper::Request::builder() + .method(self.method.clone()) + .uri(uri) + .body(body) + .expect("valid request parts"); + *req.headers_mut() = self.headers.clone(); + ResponseFuture::H3(self.client.h3_client.request(req)) + } + _ => { + let mut req = hyper::Request::builder() + .method(self.method.clone()) + .uri(uri) + .body(body.into_stream()) + .expect("valid request parts"); + *req.headers_mut() = self.headers.clone(); + ResponseFuture::Default(self.client.hyper.request(req)) + } + }; true } @@ -2060,11 +2072,6 @@ impl Future for PendingRequest { Some(Some(ref body)) => Body::reusable(body.clone()), _ => Body::empty(), }; - let mut req = hyper::Request::builder() - .method(self.method.clone()) - .uri(uri.clone()) - .body(body.into_stream()) - .expect("valid request parts"); // Add cookies from the cookie store. #[cfg(feature = "cookies")] @@ -2074,10 +2081,31 @@ impl Future for PendingRequest { } } - *req.headers_mut() = headers.clone(); - std::mem::swap(self.as_mut().headers(), &mut headers); *self.as_mut().in_flight().get_mut() = - ResponseFuture::Default(self.client.hyper.request(req)); + match *self.as_mut().in_flight().as_ref() { + #[cfg(feature = "http3")] + ResponseFuture::H3(_) => { + let mut req = hyper::Request::builder() + .method(self.method.clone()) + .uri(uri.clone()) + .body(body) + .expect("valid request parts"); + *req.headers_mut() = headers.clone(); + std::mem::swap(self.as_mut().headers(), &mut headers); + ResponseFuture::H3(self.client.h3_client.request(req)) + } + _ => { + let mut req = hyper::Request::builder() + .method(self.method.clone()) + .uri(uri.clone()) + .body(body.into_stream()) + .expect("valid request parts"); + *req.headers_mut() = headers.clone(); + std::mem::swap(self.as_mut().headers(), &mut headers); + ResponseFuture::Default(self.client.hyper.request(req)) + } + }; + continue; } redirect::ActionKind::Stop => { From 040384de93c35f18440c7568d3bf29107aad602c Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Wed, 10 Aug 2022 17:37:32 -0400 Subject: [PATCH 15/29] Add HTTP/3 connector that reuses DNS resolvers Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 1 + src/async_impl/h3_client/dns.rs | 43 ++++++++++++++ src/async_impl/h3_client/mod.rs | 101 +++++++++++++++++++++++--------- 3 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 src/async_impl/h3_client/dns.rs diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index ffcbe85b6..7b9ab1755 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1897,6 +1897,7 @@ impl PendingRequest { } fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { + // TODO: Does the h3 API provide a way to determine this same type of case? if let Some(cause) = err.source() { if let Some(err) = cause.downcast_ref::() { // They sent us a graceful shutdown, try with a new connection! diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs new file mode 100644 index 000000000..9cb50d1e3 --- /dev/null +++ b/src/async_impl/h3_client/dns.rs @@ -0,0 +1,43 @@ +use core::task; +use hyper::client::connect::dns::Name; +use std::future::Future; +use std::net::SocketAddr; +use std::task::Poll; +use tower_service::Service; + +// Trait from hyper to implement DNS resolution for HTTP/3 client. +pub trait Resolve { + type Addrs: Iterator; + type Error: Into>; + type Future: Future>; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll>; + fn resolve(&mut self, name: Name) -> Self::Future; +} + +impl Resolve for S +where + S: Service, + S::Response: Iterator, + S::Error: Into>, +{ + type Addrs = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { + Service::poll_ready(self, cx) + } + + fn resolve(&mut self, name: Name) -> Self::Future { + Service::call(self, name) + } +} + +pub(super) async fn resolve(resolver: &mut R, name: Name) -> Result +where + R: Resolve, +{ + futures_util::future::poll_fn(|cx| resolver.poll_ready(cx)).await?; + resolver.resolve(name).await +} diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 897928751..befbc5e11 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -1,18 +1,24 @@ #![cfg(feature = "http3")] +mod dns; mod pool; +use crate::async_impl::h3_client::dns::Resolve; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; use crate::error::{BoxError, Error, Kind}; use crate::{error, Body}; +use bytes::Bytes; use futures_util::future; -use h3_quinn::Connection; -use http::{Request, Response}; +use h3::client::SendRequest; +use h3_quinn::{Connection, OpenStreams}; +use http::{Request, Response, Uri}; +use hyper::client::connect::dns::{GaiResolver, Name}; use hyper::Body as HyperBody; use log::debug; use std::future::Future; use std::net::{IpAddr, SocketAddr}; use std::pin::Pin; +use std::str::FromStr; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; @@ -46,8 +52,11 @@ impl H3Builder { endpoint.set_default_client_config(config); H3Client { - endpoint, pool: Pool::new(self.pool_max_idle_per_host, self.pool_idle_timeout), + connector: H3Connector { + resolver: GaiResolver::new(), + endpoint, + }, } } @@ -66,46 +75,26 @@ impl H3Builder { #[derive(Clone)] pub struct H3Client { - endpoint: quinn::Endpoint, pool: Pool, - // TODO: Since resolution is perform internally in Hyper, - // we have no way of leveraging that functionality here. - // resolver: Box, + connector: H3Connector, } impl H3Client { - async fn get_pooled_client(&self, key: Key) -> Result { + async fn get_pooled_client(&mut self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { debug!("getting client from pool with key {:?}", key); return Ok(client); } let dest = pool::domain_as_uri(key.clone()); - let auth = dest - .authority() - .ok_or("destination must have a host")? - .clone(); - let port = auth.port_u16().unwrap_or(443); - let addr = tokio::net::lookup_host((auth.host(), port)) - .await? - .next() - .ok_or("dns found no addresses")?; - - let quinn_conn = Connection::new(self.endpoint.connect(addr, auth.host())?.await?); - let (mut driver, tx) = h3::client::new(quinn_conn).await?; - - // TODO: What does poll_close() do? - tokio::spawn(async move { - future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); - }); - + let tx = self.connector.connect(dest).await?; let client = PoolClient::new(tx); self.pool.put(key, client.clone()); Ok(client) } async fn send_request( - self, + mut self, key: Key, req: Request, ) -> Result, Error> { @@ -145,3 +134,61 @@ impl Future for H3ResponseFuture { self.inner.as_mut().poll(cx) } } + +#[derive(Clone)] +pub(crate) struct H3Connector { + resolver: R, + endpoint: quinn::Endpoint, +} + +impl H3Connector +where + R: Resolve + Clone + Send + Sync + 'static, +{ + pub async fn connect( + &mut self, + dest: Uri, + ) -> Result, BoxError> { + let host = dest.host().ok_or("destination must have a host")?; + let port = dest.port_u16().unwrap_or(443); + let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { + vec![SocketAddr::new(addr, port)] + } else { + let addrs = dns::resolve(&mut self.resolver, Name::from_str(host)?) + .await + .map_err(|e| e.into())?; + let addrs = addrs.map(|mut addr| { + addr.set_port(port); + addr + }); + addrs.collect() + }; + self.remote_connect(addrs, host).await + } + + async fn remote_connect( + &mut self, + addrs: Vec, + server_name: &str, + ) -> Result, BoxError> { + let mut err = None; + for addr in addrs { + match self.endpoint.connect(addr, server_name)?.await { + Ok(new_conn) => { + let quinn_conn = Connection::new(new_conn); + let (mut driver, tx) = h3::client::new(quinn_conn).await?; + tokio::spawn(async move { + future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); + }); + return Ok(tx); + } + Err(e) => err = Some(e), + } + } + + match err { + Some(e) => Err(Box::new(e) as BoxError), + None => Err("failed to establish connection for HTTP/3 request".into()), + } + } +} From 9286bbb8d9da1ad200550d06dac14f353a32c8c5 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 11 Aug 2022 12:07:47 -0400 Subject: [PATCH 16/29] Add Resolver enum to hold different kinds of resolvers Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 27 +++++++++++++-- src/async_impl/h3_client/dns.rs | 37 ++++++++++++++++++++- src/async_impl/h3_client/mod.rs | 59 +++++++++++++++------------------ src/connect.rs | 11 +----- 4 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 7b9ab1755..7a3da19c4 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -27,6 +27,10 @@ use super::request::{Request, RequestBuilder}; use super::response::Response; use super::Body; #[cfg(feature = "http3")] +use crate::async_impl::h3_client::dns::Resolver; +#[cfg(feature = "http3")] +use crate::async_impl::h3_client::H3Connector; +#[cfg(feature = "http3")] use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; use crate::connect::{Connector, HttpConnector}; #[cfg(feature = "cookies")] @@ -218,14 +222,28 @@ impl ClientBuilder { } let proxies = Arc::new(proxies); + #[allow(unused)] + #[cfg(feature = "http3")] + let mut h3_connector = None; + let mut connector = { #[cfg(feature = "__tls")] fn user_agent(headers: &HeaderMap) -> Option { headers.get(USER_AGENT).cloned() } + #[cfg(feature = "http3")] + let resolver; + let http = match config.trust_dns { false => { + #[cfg(feature = "http3")] + if config.dns_overrides.is_empty() { + resolver = Resolver::new_gai(); + } else { + resolver = Resolver::new_gai_with_overrides(config.dns_overrides.clone()) + } + if config.dns_overrides.is_empty() { HttpConnector::new_gai() } else { @@ -453,6 +471,12 @@ impl ClientBuilder { #[cfg(feature = "http3")] { tls.enable_early_data = config.tls_enable_early_data; + + h3_connector = Some(H3Connector::new( + resolver, + tls.clone(), + config.local_address, + )); } Connector::new_rustls_tls( @@ -529,7 +553,6 @@ impl ClientBuilder { #[cfg(feature = "http3")] let h3_builder = { let mut h3_builder = H3Builder::default(); - h3_builder.set_local_addr(config.local_address); h3_builder.set_pool_idle_timeout(config.pool_idle_timeout); h3_builder.set_pool_max_idle_per_host(config.pool_max_idle_per_host); h3_builder @@ -541,7 +564,7 @@ impl ClientBuilder { #[cfg(feature = "cookies")] cookie_store: config.cookie_store, #[cfg(feature = "http3")] - h3_client: h3_builder.build(connector.deep_clone_tls()), + h3_client: h3_builder.build(h3_connector.expect("missing HTTP/3 connector")), hyper: builder.build(connector), headers: config.headers, redirect_policy: config.redirect_policy, diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index 9cb50d1e3..76598bdc4 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -1,10 +1,45 @@ +use crate::connect::DnsResolverWithOverrides; use core::task; -use hyper::client::connect::dns::Name; +use hyper::client::connect::dns::{GaiResolver, Name}; +use std::collections::HashMap; use std::future::Future; use std::net::SocketAddr; +use std::str::FromStr; use std::task::Poll; use tower_service::Service; +#[derive(Clone)] +pub(crate) enum Resolver { + Gai(GaiResolver), + GaiWithDnsOverrides(DnsResolverWithOverrides), +} + +impl Resolver { + pub fn new_gai() -> Self { + Resolver::Gai(GaiResolver::new()) + } + + pub fn new_gai_with_overrides(overrides: HashMap) -> Self { + Resolver::GaiWithDnsOverrides(DnsResolverWithOverrides::new(GaiResolver::new(), overrides)) + } + + pub async fn resolve(&mut self, server_name: &str) -> Vec { + let res: Vec = match self { + Resolver::Gai(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) + .await + .unwrap() + .collect(), + Resolver::GaiWithDnsOverrides(resolver) => { + resolve(resolver, Name::from_str(server_name).unwrap()) + .await + .unwrap() + .collect() + } + }; + res + } +} + // Trait from hyper to implement DNS resolution for HTTP/3 client. pub trait Resolve { type Addrs: Iterator; diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index befbc5e11..57e91d78a 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -1,9 +1,9 @@ #![cfg(feature = "http3")] -mod dns; +pub(crate) mod dns; mod pool; -use crate::async_impl::h3_client::dns::Resolve; +use crate::async_impl::h3_client::dns::Resolver; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; use crate::error::{BoxError, Error, Kind}; use crate::{error, Body}; @@ -12,7 +12,6 @@ use futures_util::future; use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::{Request, Response, Uri}; -use hyper::client::connect::dns::{GaiResolver, Name}; use hyper::Body as HyperBody; use log::debug; use std::future::Future; @@ -26,7 +25,6 @@ use std::time::Duration; pub(crate) struct H3Builder { pool_idle_timeout: Option, pool_max_idle_per_host: usize, - local_addr: Option, } impl Default for H3Builder { @@ -34,29 +32,15 @@ impl Default for H3Builder { Self { pool_idle_timeout: Some(Duration::from_secs(90)), pool_max_idle_per_host: usize::MAX, - local_addr: None, } } } impl H3Builder { - pub fn build(self, tls: rustls::ClientConfig) -> H3Client { - let config = quinn::ClientConfig::new(Arc::new(tls)); - let socket_addr = match self.local_addr { - Some(ip) => SocketAddr::new(ip, 0), - None => "[::]:0".parse::().unwrap(), - }; - - let mut endpoint = - quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); - endpoint.set_default_client_config(config); - + pub fn build(self, connector: H3Connector) -> H3Client { H3Client { pool: Pool::new(self.pool_max_idle_per_host, self.pool_idle_timeout), - connector: H3Connector { - resolver: GaiResolver::new(), - endpoint, - }, + connector, } } @@ -67,10 +51,6 @@ impl H3Builder { pub fn set_pool_max_idle_per_host(&mut self, max: usize) { self.pool_max_idle_per_host = max; } - - pub fn set_local_addr(&mut self, addr: Option) { - self.local_addr = addr; - } } #[derive(Clone)] @@ -136,15 +116,30 @@ impl Future for H3ResponseFuture { } #[derive(Clone)] -pub(crate) struct H3Connector { - resolver: R, +pub(crate) struct H3Connector { + resolver: Resolver, endpoint: quinn::Endpoint, } -impl H3Connector -where - R: Resolve + Clone + Send + Sync + 'static, -{ +impl H3Connector { + pub fn new( + resolver: Resolver, + tls: rustls::ClientConfig, + local_addr: Option, + ) -> H3Connector { + let config = quinn::ClientConfig::new(Arc::new(tls)); + let socket_addr = match local_addr { + Some(ip) => SocketAddr::new(ip, 0), + None => "[::]:0".parse::().unwrap(), + }; + + let mut endpoint = + quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); + endpoint.set_default_client_config(config); + + Self { resolver, endpoint } + } + pub async fn connect( &mut self, dest: Uri, @@ -154,9 +149,7 @@ where let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { vec![SocketAddr::new(addr, port)] } else { - let addrs = dns::resolve(&mut self.resolver, Name::from_str(host)?) - .await - .map_err(|e| e.into())?; + let addrs = self.resolver.resolve(host).await.into_iter(); let addrs = addrs.map(|mut addr| { addr.set_port(port); addr diff --git a/src/connect.rs b/src/connect.rs index dda5079f4..fae3f8b2e 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -520,15 +520,6 @@ impl Connector { Inner::Http(http) => http.set_keepalive(dur), } } - - #[cfg(feature = "http3")] - pub fn deep_clone_tls(&self) -> rustls::ClientConfig { - match &self.inner { - Inner::RustlsTls { tls, .. } => (*(*tls)).clone(), - #[cfg(feature = "default-tls")] - _ => unreachable!("HTTP/3 should only be enabled with Rustls"), - } - } } fn into_uri(scheme: Scheme, host: Authority) -> Uri { @@ -1023,7 +1014,7 @@ where } impl DnsResolverWithOverrides { - fn new(dns_resolver: Resolver, overrides: HashMap) -> Self { + pub(crate) fn new(dns_resolver: Resolver, overrides: HashMap) -> Self { DnsResolverWithOverrides { dns_resolver, overrides: Arc::new(overrides), From 1fc2fb1324cb2a430c70279a6445ef5ef5158415 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 11 Aug 2022 12:28:06 -0400 Subject: [PATCH 17/29] Add trust-dns resolvers for HTTP/3 Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 8 ++++++ src/async_impl/h3_client/dns.rs | 43 ++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 7a3da19c4..3677f1751 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -252,6 +252,14 @@ impl ClientBuilder { } #[cfg(feature = "trust-dns")] true => { + #[cfg(feature = "http3")] + if config.dns_overrides.is_empty() { + resolver = Resolver::new_trust_dns()?; + } else { + resolver = + Resolver::new_trust_dns_with_overrides(config.dns_overrides.clone())?; + } + if config.dns_overrides.is_empty() { HttpConnector::new_trust_dns()? } else { diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index 76598bdc4..dfdff7502 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -1,4 +1,6 @@ use crate::connect::DnsResolverWithOverrides; +#[cfg(feature = "trust-dns")] +use crate::dns::TrustDnsResolver; use core::task; use hyper::client::connect::dns::{GaiResolver, Name}; use std::collections::HashMap; @@ -12,24 +14,57 @@ use tower_service::Service; pub(crate) enum Resolver { Gai(GaiResolver), GaiWithDnsOverrides(DnsResolverWithOverrides), + #[cfg(feature = "trust-dns")] + TrustDns(TrustDnsResolver), + #[cfg(feature = "trust-dns")] + TrustDnsWithOverrides(DnsResolverWithOverrides), } impl Resolver { pub fn new_gai() -> Self { - Resolver::Gai(GaiResolver::new()) + Self::Gai(GaiResolver::new()) } pub fn new_gai_with_overrides(overrides: HashMap) -> Self { - Resolver::GaiWithDnsOverrides(DnsResolverWithOverrides::new(GaiResolver::new(), overrides)) + Self::GaiWithDnsOverrides(DnsResolverWithOverrides::new(GaiResolver::new(), overrides)) + } + + #[cfg(feature = "trust-dns")] + pub fn new_trust_dns() -> crate::Result { + TrustDnsResolver::new() + .map(Self::TrustDns) + .map_err(crate::error::builder) + } + + #[cfg(feature = "trust-dns")] + pub fn new_trust_dns_with_overrides( + overrides: HashMap, + ) -> crate::Result { + TrustDnsResolver::new() + .map(|trust_resolver| DnsResolverWithOverrides::new(trust_resolver, overrides)) + .map(Self::TrustDnsWithOverrides) + .map_err(crate::error::builder) } pub async fn resolve(&mut self, server_name: &str) -> Vec { let res: Vec = match self { - Resolver::Gai(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) + Self::Gai(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) + .await + .unwrap() + .collect(), + Self::GaiWithDnsOverrides(resolver) => { + resolve(resolver, Name::from_str(server_name).unwrap()) + .await + .unwrap() + .collect() + } + #[cfg(feature = "trust-dns")] + Self::TrustDns(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) .await .unwrap() .collect(), - Resolver::GaiWithDnsOverrides(resolver) => { + #[cfg(feature = "trust-dns")] + Self::TrustDnsWithOverrides(resolver) => { resolve(resolver, Name::from_str(server_name).unwrap()) .await .unwrap() From 08e46afbfe31b881271c4bee728f45e034e028b1 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 11 Aug 2022 16:51:08 -0400 Subject: [PATCH 18/29] Move connector to its own module Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 4 +- src/async_impl/h3_client/connect.rs | 85 +++++++++++++++++++++++++++++ src/async_impl/h3_client/mod.rs | 82 +--------------------------- 3 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 src/async_impl/h3_client/connect.rs diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 3677f1751..6a1b450f0 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -27,9 +27,9 @@ use super::request::{Request, RequestBuilder}; use super::response::Response; use super::Body; #[cfg(feature = "http3")] -use crate::async_impl::h3_client::dns::Resolver; +use crate::async_impl::h3_client::connect::H3Connector; #[cfg(feature = "http3")] -use crate::async_impl::h3_client::H3Connector; +use crate::async_impl::h3_client::dns::Resolver; #[cfg(feature = "http3")] use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; use crate::connect::{Connector, HttpConnector}; diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs new file mode 100644 index 000000000..642f91667 --- /dev/null +++ b/src/async_impl/h3_client/connect.rs @@ -0,0 +1,85 @@ +use crate::async_impl::h3_client::dns::Resolver; +use crate::error::BoxError; +use bytes::Bytes; +use futures_util::future; +use h3::client::SendRequest; +use h3_quinn::{Connection, OpenStreams}; +use http::Uri; +use std::net::{IpAddr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; + +#[derive(Clone)] +pub(crate) struct H3Connector { + resolver: Resolver, + endpoint: quinn::Endpoint, +} + +impl H3Connector { + pub fn new( + resolver: Resolver, + tls: rustls::ClientConfig, + local_addr: Option, + ) -> H3Connector { + let config = quinn::ClientConfig::new(Arc::new(tls)); + + let socket_addr = match local_addr { + Some(ip) => SocketAddr::new(ip, 0), + None => "[::]:0".parse::().unwrap(), + }; + + let mut endpoint = + quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); + endpoint.set_default_client_config(config); + + Self { resolver, endpoint } + } + + pub async fn connect( + &mut self, + dest: Uri, + ) -> Result, BoxError> { + let host = dest.host().ok_or("destination must have a host")?; + let port = dest.port_u16().unwrap_or(443); + + let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { + // If the host is already an IP address, skip resolving. + vec![SocketAddr::new(addr, port)] + } else { + let addrs = self.resolver.resolve(host).await.into_iter(); + let addrs = addrs.map(|mut addr| { + addr.set_port(port); + addr + }); + addrs.collect() + }; + + self.remote_connect(addrs, host).await + } + + async fn remote_connect( + &mut self, + addrs: Vec, + server_name: &str, + ) -> Result, BoxError> { + let mut err = None; + for addr in addrs { + match self.endpoint.connect(addr, server_name)?.await { + Ok(new_conn) => { + let quinn_conn = Connection::new(new_conn); + let (mut driver, tx) = h3::client::new(quinn_conn).await?; + tokio::spawn(async move { + future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); + }); + return Ok(tx); + } + Err(e) => err = Some(e), + } + } + + match err { + Some(e) => Err(Box::new(e) as BoxError), + None => Err("failed to establish connection for HTTP/3 request".into()), + } + } +} diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 57e91d78a..5f0cfb720 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -1,24 +1,19 @@ #![cfg(feature = "http3")] +pub(crate) mod connect; pub(crate) mod dns; mod pool; -use crate::async_impl::h3_client::dns::Resolver; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; use crate::error::{BoxError, Error, Kind}; use crate::{error, Body}; -use bytes::Bytes; +use connect::H3Connector; use futures_util::future; -use h3::client::SendRequest; -use h3_quinn::{Connection, OpenStreams}; -use http::{Request, Response, Uri}; +use http::{Request, Response}; use hyper::Body as HyperBody; use log::debug; use std::future::Future; -use std::net::{IpAddr, SocketAddr}; use std::pin::Pin; -use std::str::FromStr; -use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; @@ -114,74 +109,3 @@ impl Future for H3ResponseFuture { self.inner.as_mut().poll(cx) } } - -#[derive(Clone)] -pub(crate) struct H3Connector { - resolver: Resolver, - endpoint: quinn::Endpoint, -} - -impl H3Connector { - pub fn new( - resolver: Resolver, - tls: rustls::ClientConfig, - local_addr: Option, - ) -> H3Connector { - let config = quinn::ClientConfig::new(Arc::new(tls)); - let socket_addr = match local_addr { - Some(ip) => SocketAddr::new(ip, 0), - None => "[::]:0".parse::().unwrap(), - }; - - let mut endpoint = - quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); - endpoint.set_default_client_config(config); - - Self { resolver, endpoint } - } - - pub async fn connect( - &mut self, - dest: Uri, - ) -> Result, BoxError> { - let host = dest.host().ok_or("destination must have a host")?; - let port = dest.port_u16().unwrap_or(443); - let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { - vec![SocketAddr::new(addr, port)] - } else { - let addrs = self.resolver.resolve(host).await.into_iter(); - let addrs = addrs.map(|mut addr| { - addr.set_port(port); - addr - }); - addrs.collect() - }; - self.remote_connect(addrs, host).await - } - - async fn remote_connect( - &mut self, - addrs: Vec, - server_name: &str, - ) -> Result, BoxError> { - let mut err = None; - for addr in addrs { - match self.endpoint.connect(addr, server_name)?.await { - Ok(new_conn) => { - let quinn_conn = Connection::new(new_conn); - let (mut driver, tx) = h3::client::new(quinn_conn).await?; - tokio::spawn(async move { - future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); - }); - return Ok(tx); - } - Err(e) => err = Some(e), - } - } - - match err { - Some(e) => Err(Box::new(e) as BoxError), - None => Err("failed to establish connection for HTTP/3 request".into()), - } - } -} From 7931f0c531ab83bf4a2ebc69bf9c8da26849cd0f Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Thu, 11 Aug 2022 17:33:36 -0400 Subject: [PATCH 19/29] Add setter for enabling early data in TLS 1.3 Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 6a1b450f0..1f5f011f1 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1389,6 +1389,16 @@ impl ClientBuilder { self.config.dns_overrides.insert(domain.to_string(), addr); self } + + /// Whether to send data on the first flight ("early data") in TLS 1.3 handshakes + /// for HTTP/3 connections. + /// + /// The default is false. + #[cfg(feature = "http3")] + pub fn set_tls_enable_early_data(mut self, enabled: bool) -> ClientBuilder { + self.config.tls_enable_early_data = enabled; + self + } } type HyperClient = hyper::Client; From b10295070e47f945724fee70a3841f1ba2eace2d Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Fri, 12 Aug 2022 17:38:17 -0400 Subject: [PATCH 20/29] Make pooling more robust Add PoolConnection to listen for errors and hold a list of PoolClients that can be reused. Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 11 ++- src/async_impl/h3_client/connect.rs | 19 ++-- src/async_impl/h3_client/mod.rs | 24 ++--- src/async_impl/h3_client/pool.rs | 130 ++++++++++++++++++---------- 4 files changed, 111 insertions(+), 73 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 1f5f011f1..ed992a522 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -562,7 +562,6 @@ impl ClientBuilder { let h3_builder = { let mut h3_builder = H3Builder::default(); h3_builder.set_pool_idle_timeout(config.pool_idle_timeout); - h3_builder.set_pool_max_idle_per_host(config.pool_max_idle_per_host); h3_builder }; @@ -1938,7 +1937,15 @@ impl PendingRequest { } fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { - // TODO: Does the h3 API provide a way to determine this same type of case? + #[cfg(feature = "http3")] + if let Some(cause) = err.source() { + if let Some(err) = cause.downcast_ref::() { + debug!("determining if HTTP/3 error {} can be retried", err); + // TODO: Does h3 provide an API for checking the error? + return err.to_string().as_str() == "timeout"; + } + } + if let Some(cause) = err.source() { if let Some(err) = cause.downcast_ref::() { // They sent us a graceful shutdown, try with a new connection! diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index 642f91667..9611655a5 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -1,7 +1,6 @@ use crate::async_impl::h3_client::dns::Resolver; use crate::error::BoxError; use bytes::Bytes; -use futures_util::future; use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::Uri; @@ -9,6 +8,11 @@ use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; +type H3Connection = ( + h3::client::Connection, + SendRequest, +); + #[derive(Clone)] pub(crate) struct H3Connector { resolver: Resolver, @@ -35,10 +39,7 @@ impl H3Connector { Self { resolver, endpoint } } - pub async fn connect( - &mut self, - dest: Uri, - ) -> Result, BoxError> { + pub async fn connect(&mut self, dest: Uri) -> Result { let host = dest.host().ok_or("destination must have a host")?; let port = dest.port_u16().unwrap_or(443); @@ -61,17 +62,13 @@ impl H3Connector { &mut self, addrs: Vec, server_name: &str, - ) -> Result, BoxError> { + ) -> Result { let mut err = None; for addr in addrs { match self.endpoint.connect(addr, server_name)?.await { Ok(new_conn) => { let quinn_conn = Connection::new(new_conn); - let (mut driver, tx) = h3::client::new(quinn_conn).await?; - tokio::spawn(async move { - future::poll_fn(|cx| driver.poll_close(cx)).await.unwrap(); - }); - return Ok(tx); + return Ok(h3::client::new(quinn_conn).await?); } Err(e) => err = Some(e), } diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 5f0cfb720..ec85ba6e4 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -11,7 +11,7 @@ use connect::H3Connector; use futures_util::future; use http::{Request, Response}; use hyper::Body as HyperBody; -use log::debug; +use log::trace; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; @@ -19,14 +19,12 @@ use std::time::Duration; pub(crate) struct H3Builder { pool_idle_timeout: Option, - pool_max_idle_per_host: usize, } impl Default for H3Builder { fn default() -> Self { Self { pool_idle_timeout: Some(Duration::from_secs(90)), - pool_max_idle_per_host: usize::MAX, } } } @@ -34,7 +32,7 @@ impl Default for H3Builder { impl H3Builder { pub fn build(self, connector: H3Connector) -> H3Client { H3Client { - pool: Pool::new(self.pool_max_idle_per_host, self.pool_idle_timeout), + pool: Pool::new(self.pool_idle_timeout), connector, } } @@ -42,14 +40,10 @@ impl H3Builder { pub fn set_pool_idle_timeout(&mut self, timeout: Option) { self.pool_idle_timeout = timeout; } - - pub fn set_pool_max_idle_per_host(&mut self, max: usize) { - self.pool_max_idle_per_host = max; - } } #[derive(Clone)] -pub struct H3Client { +pub(crate) struct H3Client { pool: Pool, connector: H3Connector, } @@ -57,15 +51,15 @@ pub struct H3Client { impl H3Client { async fn get_pooled_client(&mut self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { - debug!("getting client from pool with key {:?}", key); + trace!("getting client from pool with key {:?}", key); return Ok(client); } + trace!("did not find connection {:?} in pool so connecting...", key); + let dest = pool::domain_as_uri(key.clone()); - let tx = self.connector.connect(dest).await?; - let client = PoolClient::new(tx); - self.pool.put(key, client.clone()); - Ok(client) + let (driver, tx) = self.connector.connect(dest).await?; + Ok(self.pool.new_connection(key, driver, tx)) } async fn send_request( @@ -98,7 +92,7 @@ impl H3Client { } } -pub struct H3ResponseFuture { +pub(crate) struct H3ResponseFuture { inner: Pin, Error>> + Send>>, } diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 3a622a97f..c31102aab 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,5 +1,6 @@ use bytes::Bytes; use std::collections::HashMap; +use std::sync::mpsc::{Receiver, TryRecvError}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time::Instant; @@ -7,11 +8,13 @@ use tokio::time::Instant; use crate::error::{BoxError, Error, Kind}; use crate::Body; use bytes::Buf; +use futures_util::future; use h3::client::SendRequest; +use h3_quinn::{Connection, OpenStreams}; use http::uri::{Authority, Scheme}; use http::{Request, Response, Uri}; use hyper::Body as HyperBody; -use log::debug; +use log::trace; pub(super) type Key = (Scheme, Authority); @@ -21,76 +24,88 @@ pub struct Pool { } impl Pool { - pub fn new(max_idle_per_host: usize, timeout: Option) -> Self { + pub fn new(timeout: Option) -> Self { Self { inner: Arc::new(Mutex::new(PoolInner { - idle: HashMap::new(), - max_idle_per_host, + idle_conns: HashMap::new(), timeout, })), } } - pub fn put(&self, key: Key, client: PoolClient) { - let mut inner = self.inner.lock().unwrap(); - inner.put(key, client) - } - pub fn try_pool(&self, key: &Key) -> Option { let mut inner = self.inner.lock().unwrap(); let timeout = inner.timeout; - inner.idle.get_mut(&key).and_then(|list| match list.pop() { - Some(idle) => { - if let Some(duration) = timeout { - if Instant::now().saturating_duration_since(idle.idle_at) > duration { - debug!("pooled client expired"); - return None; - } + if let Some(conn) = inner.idle_conns.get(&key) { + // We check first if the connection still valid + // and if not, we remove it from the pool. + if conn.is_invalid() { + trace!("pooled HTTP/3 connection is invalid so removing it..."); + inner.idle_conns.remove(&key); + return None; + } + + if let Some(duration) = timeout { + if Instant::now().saturating_duration_since(conn.idle_timeout) > duration { + trace!("pooled connection expired"); + return None; } - Some(idle.value) } - None => None, - }) + } + + inner + .idle_conns + .get_mut(&key) + .and_then(|conn| Some(conn.pool())) + } + + pub fn new_connection( + &mut self, + key: Key, + mut driver: h3::client::Connection, + tx: SendRequest, + ) -> PoolClient { + let (close_tx, close_rx) = std::sync::mpsc::channel(); + tokio::spawn(async move { + if let Err(e) = future::poll_fn(|cx| driver.poll_close(cx)).await { + trace!("poll_close returned error {:?}", e); + close_tx.send(e).ok(); + } + }); + + let mut inner = self.inner.lock().unwrap(); + + let client = PoolClient::new(tx); + let conn = PoolConnection::new(client.clone(), close_rx); + inner.insert(key, conn); + + client } } struct PoolInner { - // These are internal Conns sitting in the event loop in the KeepAlive - // state, waiting to receive a new Request to send on the socket. - idle: HashMap>, - max_idle_per_host: usize, + idle_conns: HashMap, timeout: Option, } impl PoolInner { - fn put(&mut self, key: Key, client: PoolClient) { - if self.idle.contains_key(&key) { - debug!("connection already exists for key {:?}", key); - return; - } - - let idle_list = self.idle.entry(key.clone()).or_default(); - - if idle_list.len() >= self.max_idle_per_host { - debug!("max idle per host for {:?}, dropping connection", key); - return; + fn insert(&mut self, key: Key, conn: PoolConnection) { + if self.idle_conns.contains_key(&key) { + trace!("connection already exists for key {:?}", key); } - idle_list.push(Idle { - idle_at: Instant::now(), - value: client, - }); + self.idle_conns.insert(key, conn); } } #[derive(Clone)] pub struct PoolClient { - tx: SendRequest, + inner: SendRequest, } impl PoolClient { - pub fn new(tx: SendRequest) -> Self { - Self { tx } + pub fn new(tx: SendRequest) -> Self { + Self { inner: tx } } pub async fn send_request( @@ -99,7 +114,7 @@ impl PoolClient { ) -> Result, BoxError> { let (head, req_body) = req.into_parts(); let req = Request::from_parts(head, ()); - let mut stream = self.tx.send_request(req).await?; + let mut stream = self.inner.send_request(req).await?; match req_body.as_bytes() { Some(b) if !b.is_empty() => { @@ -121,9 +136,34 @@ impl PoolClient { } } -struct Idle { - idle_at: Instant, - value: PoolClient, +pub struct PoolConnection { + // This receives errors from polling h3 driver. + close_rx: Receiver, + client: PoolClient, + idle_timeout: Instant, +} + +impl PoolConnection { + pub fn new(client: PoolClient, close_rx: Receiver) -> Self { + Self { + close_rx, + client, + idle_timeout: Instant::now(), + } + } + + pub fn pool(&mut self) -> PoolClient { + self.idle_timeout = Instant::now(); + self.client.clone() + } + + pub fn is_invalid(&self) -> bool { + match self.close_rx.try_recv() { + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => true, + Ok(_) => true, + } + } } pub(crate) fn extract_domain(uri: &mut Uri) -> Result { From 26a14b84df74863ba812260abde9c075c4d82e74 Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 13 Aug 2022 19:03:47 -0400 Subject: [PATCH 21/29] Add setters to configure some QUIC parameters Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 87 +++++++++++++++++++++++++++++ src/async_impl/h3_client/connect.rs | 11 ++-- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index ed992a522..1b0d3adcd 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -46,6 +46,10 @@ use crate::Certificate; use crate::Identity; use crate::{IntoUrl, Method, Proxy, StatusCode, Url}; use log::{debug, trace}; +#[cfg(feature = "http3")] +use quinn::TransportConfig; +#[cfg(feature = "http3")] +use quinn::VarInt; /// An asynchronous `Client` to make Requests with. /// @@ -130,6 +134,14 @@ struct Config { dns_overrides: HashMap, #[cfg(feature = "http3")] tls_enable_early_data: bool, + #[cfg(feature = "http3")] + quic_max_idle_timeout: Option, + #[cfg(feature = "http3")] + quic_stream_receive_window: Option, + #[cfg(feature = "http3")] + quic_receive_window: Option, + #[cfg(feature = "http3")] + quic_send_window: Option, } impl Default for ClientBuilder { @@ -199,6 +211,14 @@ impl ClientBuilder { dns_overrides: HashMap::new(), #[cfg(feature = "http3")] tls_enable_early_data: false, + #[cfg(feature = "http3")] + quic_max_idle_timeout: None, + #[cfg(feature = "http3")] + quic_stream_receive_window: None, + #[cfg(feature = "http3")] + quic_receive_window: None, + #[cfg(feature = "http3")] + quic_send_window: None, }, } } @@ -480,10 +500,31 @@ impl ClientBuilder { { tls.enable_early_data = config.tls_enable_early_data; + let mut transport_config = TransportConfig::default(); + + if let Some(max_idle_timeout) = config.quic_max_idle_timeout { + transport_config.max_idle_timeout(Some( + max_idle_timeout.try_into().map_err(error::builder)?, + )); + } + + if let Some(stream_receive_window) = config.quic_stream_receive_window { + transport_config.stream_receive_window(stream_receive_window); + } + + if let Some(receive_window) = config.quic_receive_window { + transport_config.receive_window(receive_window); + } + + if let Some(send_window) = config.quic_send_window { + transport_config.send_window(send_window); + } + h3_connector = Some(H3Connector::new( resolver, tls.clone(), config.local_address, + transport_config, )); } @@ -1398,6 +1439,52 @@ impl ClientBuilder { self.config.tls_enable_early_data = enabled; self } + + /// Maximum duration of inactivity to accept before timing out the QUIC connection. + /// + /// Please see docs in [`TransportConfig`] in [`quinn`]. + /// + /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + #[cfg(feature = "http3")] + pub fn set_quic_max_idle_timeout(mut self, value: Duration) -> ClientBuilder { + self.config.quic_max_idle_timeout = Some(value); + self + } + + /// Maximum number of bytes the peer may transmit without acknowledgement on any one stream + /// before becoming blocked. + /// + /// Please see docs in [`TransportConfig`] in [`quinn`]. + /// + /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + #[cfg(feature = "http3")] + pub fn set_quic_stream_receive_window(mut self, value: VarInt) -> ClientBuilder { + self.config.quic_stream_receive_window = Some(value); + self + } + + /// Maximum number of bytes the peer may transmit across all streams of a connection before + /// becoming blocked. + /// + /// Please see docs in [`TransportConfig`] in [`quinn`]. + /// + /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + #[cfg(feature = "http3")] + pub fn set_quic_receive_window(mut self, value: VarInt) -> ClientBuilder { + self.config.quic_receive_window = Some(value); + self + } + + /// Maximum number of bytes to transmit to a peer without acknowledgment + /// + /// Please see docs in [`TransportConfig`] in [`quinn`]. + /// + /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + #[cfg(feature = "http3")] + pub fn set_quic_send_window(mut self, value: u64) -> ClientBuilder { + self.config.quic_send_window = Some(value); + self + } } type HyperClient = hyper::Client; diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index 9611655a5..79ae8ce14 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -4,6 +4,7 @@ use bytes::Bytes; use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::Uri; +use quinn::{ClientConfig, Endpoint, TransportConfig}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; @@ -16,7 +17,7 @@ type H3Connection = ( #[derive(Clone)] pub(crate) struct H3Connector { resolver: Resolver, - endpoint: quinn::Endpoint, + endpoint: Endpoint, } impl H3Connector { @@ -24,16 +25,18 @@ impl H3Connector { resolver: Resolver, tls: rustls::ClientConfig, local_addr: Option, + transport_config: TransportConfig, ) -> H3Connector { - let config = quinn::ClientConfig::new(Arc::new(tls)); + let mut config = ClientConfig::new(Arc::new(tls)); + // FIXME: Replace this when there is a setter. + config.transport = Arc::new(transport_config); let socket_addr = match local_addr { Some(ip) => SocketAddr::new(ip, 0), None => "[::]:0".parse::().unwrap(), }; - let mut endpoint = - quinn::Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); + let mut endpoint = Endpoint::client(socket_addr).expect("unable to create QUIC endpoint"); endpoint.set_default_client_config(config); Self { resolver, endpoint } From 35ed469d72fc5ca755142df9eb85f271edb2379c Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Sat, 13 Aug 2022 19:11:55 -0400 Subject: [PATCH 22/29] Remove client builder Signed-off-by: Miguel Guarniz --- src/async_impl/client.rs | 14 +++++--------- src/async_impl/h3_client/mod.rs | 32 +++++++------------------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 1b0d3adcd..504c1b256 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -31,7 +31,7 @@ use crate::async_impl::h3_client::connect::H3Connector; #[cfg(feature = "http3")] use crate::async_impl::h3_client::dns::Resolver; #[cfg(feature = "http3")] -use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture}; +use crate::async_impl::h3_client::{H3Client, H3ResponseFuture}; use crate::connect::{Connector, HttpConnector}; #[cfg(feature = "cookies")] use crate::cookie; @@ -599,20 +599,16 @@ impl ClientBuilder { let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); - #[cfg(feature = "http3")] - let h3_builder = { - let mut h3_builder = H3Builder::default(); - h3_builder.set_pool_idle_timeout(config.pool_idle_timeout); - h3_builder - }; - Ok(Client { inner: Arc::new(ClientRef { accepts: config.accepts, #[cfg(feature = "cookies")] cookie_store: config.cookie_store, #[cfg(feature = "http3")] - h3_client: h3_builder.build(h3_connector.expect("missing HTTP/3 connector")), + h3_client: H3Client::new( + h3_connector.expect("missing HTTP/3 connector"), + config.pool_idle_timeout, + ), hyper: builder.build(connector), headers: config.headers, redirect_policy: config.redirect_policy, diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index ec85ba6e4..59e3a7667 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -17,31 +17,6 @@ use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Duration; -pub(crate) struct H3Builder { - pool_idle_timeout: Option, -} - -impl Default for H3Builder { - fn default() -> Self { - Self { - pool_idle_timeout: Some(Duration::from_secs(90)), - } - } -} - -impl H3Builder { - pub fn build(self, connector: H3Connector) -> H3Client { - H3Client { - pool: Pool::new(self.pool_idle_timeout), - connector, - } - } - - pub fn set_pool_idle_timeout(&mut self, timeout: Option) { - self.pool_idle_timeout = timeout; - } -} - #[derive(Clone)] pub(crate) struct H3Client { pool: Pool, @@ -49,6 +24,13 @@ pub(crate) struct H3Client { } impl H3Client { + pub fn new(connector: H3Connector, pool_timeout: Option) -> Self { + H3Client { + pool: Pool::new(pool_timeout), + connector, + } + } + async fn get_pooled_client(&mut self, key: Key) -> Result { if let Some(client) = self.pool.try_pool(&key) { trace!("getting client from pool with key {:?}", key); From fe362576bceacd27e992b0b318d24977fae7869d Mon Sep 17 00:00:00 2001 From: Miguel Guarniz Date: Mon, 15 Aug 2022 12:58:41 -0400 Subject: [PATCH 23/29] Prevent multiple QUIC connections to the same host Signed-off-by: Miguel Guarniz --- src/async_impl/h3_client/mod.rs | 1 + src/async_impl/h3_client/pool.rs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index 59e3a7667..919e13c0a 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -40,6 +40,7 @@ impl H3Client { trace!("did not find connection {:?} in pool so connecting...", key); let dest = pool::domain_as_uri(key.clone()); + self.pool.connecting(key.clone())?; let (driver, tx) = self.connector.connect(dest).await?; Ok(self.pool.new_connection(key, driver, tx)) } diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index c31102aab..6fcb8e719 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -1,5 +1,5 @@ use bytes::Bytes; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::mpsc::{Receiver, TryRecvError}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -27,12 +27,21 @@ impl Pool { pub fn new(timeout: Option) -> Self { Self { inner: Arc::new(Mutex::new(PoolInner { + connecting: HashSet::new(), idle_conns: HashMap::new(), timeout, })), } } + pub fn connecting(&self, key: Key) -> Result<(), BoxError> { + let mut inner = self.inner.lock().unwrap(); + if !inner.connecting.insert(key.clone()) { + return Err(format!("HTTP/3 connecting already in progress for {:?}", key).into()); + } + return Ok(()); + } + pub fn try_pool(&self, key: &Key) -> Option { let mut inner = self.inner.lock().unwrap(); let timeout = inner.timeout; @@ -77,13 +86,18 @@ impl Pool { let client = PoolClient::new(tx); let conn = PoolConnection::new(client.clone(), close_rx); - inner.insert(key, conn); + inner.insert(key.clone(), conn); + + // We clean up "connecting" here so we don't have to acquire the lock again. + let existed = inner.connecting.remove(&key); + debug_assert!(existed, "key not in connecting set"); client } } struct PoolInner { + connecting: HashSet, idle_conns: HashMap, timeout: Option, } From f6ea3b83e42d0277d43a51e40bde38d10a828425 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 18:26:36 -0400 Subject: [PATCH 24/29] Update dns resolver --- src/async_impl/h3_client/dns.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index dfdff7502..426c72e72 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -1,23 +1,24 @@ -use crate::connect::DnsResolverWithOverrides; +use crate::dns::DnsResolverWithOverrides; #[cfg(feature = "trust-dns")] -use crate::dns::TrustDnsResolver; +use crate::dns::trust_dns::TrustDnsResolver; use core::task; use hyper::client::connect::dns::{GaiResolver, Name}; use std::collections::HashMap; use std::future::Future; use std::net::SocketAddr; use std::str::FromStr; +use std::sync::Arc; use std::task::Poll; use tower_service::Service; #[derive(Clone)] pub(crate) enum Resolver { Gai(GaiResolver), - GaiWithDnsOverrides(DnsResolverWithOverrides), + GaiWithDnsOverrides(DnsResolverWithOverrides), #[cfg(feature = "trust-dns")] TrustDns(TrustDnsResolver), #[cfg(feature = "trust-dns")] - TrustDnsWithOverrides(DnsResolverWithOverrides), + TrustDnsWithOverrides(DnsResolverWithOverrides), } impl Resolver { @@ -25,8 +26,8 @@ impl Resolver { Self::Gai(GaiResolver::new()) } - pub fn new_gai_with_overrides(overrides: HashMap) -> Self { - Self::GaiWithDnsOverrides(DnsResolverWithOverrides::new(GaiResolver::new(), overrides)) + pub fn new_gai_with_overrides(overrides: HashMap>) -> Self { + Self::GaiWithDnsOverrides(DnsResolverWithOverrides::new(Arc::new(GaiResolver::new()), overrides)) } #[cfg(feature = "trust-dns")] @@ -38,10 +39,10 @@ impl Resolver { #[cfg(feature = "trust-dns")] pub fn new_trust_dns_with_overrides( - overrides: HashMap, + overrides: HashMap>, ) -> crate::Result { TrustDnsResolver::new() - .map(|trust_resolver| DnsResolverWithOverrides::new(trust_resolver, overrides)) + .map(|trust_resolver| DnsResolverWithOverrides::new(Arc::new(trust_resolver), overrides)) .map(Self::TrustDnsWithOverrides) .map_err(crate::error::builder) } From 90fa6e18405cc07b85c356640fecb84dc275778e Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 19:15:17 -0400 Subject: [PATCH 25/29] Use new DynResolver --- src/async_impl/client.rs | 24 +++------- src/async_impl/h3_client/connect.rs | 10 +++-- src/async_impl/h3_client/dns.rs | 70 +---------------------------- 3 files changed, 12 insertions(+), 92 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 568989db6..b52fd25e0 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -29,8 +29,6 @@ use super::Body; #[cfg(feature = "http3")] use crate::async_impl::h3_client::connect::H3Connector; #[cfg(feature = "http3")] -use crate::async_impl::h3_client::dns::Resolver; -#[cfg(feature = "http3")] use crate::async_impl::h3_client::{H3Client, H3ResponseFuture}; use crate::connect::Connector; #[cfg(feature = "cookies")] @@ -262,22 +260,10 @@ impl ClientBuilder { } #[cfg(feature = "http3")] - let h3_resolver = match config.trust_dns { - false => { - if config.dns_overrides.is_empty() { - Resolver::new_gai() - } else { - Resolver::new_gai_with_overrides(config.dns_overrides.clone()) - } - } + let h3_resolver: Arc = match config.trust_dns { + false => Arc::new(GaiResolver::new()), #[cfg(feature = "trust-dns")] - true => { - if config.dns_overrides.is_empty() { - Resolver::new_trust_dns()? - } else { - Resolver::new_trust_dns_with_overrides(config.dns_overrides.clone())? - } - } + true => Arc::new(TrustDnsResolver::new().map_err(crate::error::builder)?), #[cfg(not(feature = "trust-dns"))] true => unreachable!("trust-dns shouldn't be enabled unless the feature is"), }; @@ -298,7 +284,7 @@ impl ClientBuilder { config.dns_overrides, )); } - let http = HttpConnector::new_with_resolver(DynResolver::new(resolver)); + let http = HttpConnector::new_with_resolver(DynResolver::new(resolver.clone())); #[cfg(feature = "__tls")] match config.tls { @@ -535,7 +521,7 @@ impl ClientBuilder { } h3_connector = Some(H3Connector::new( - h3_resolver, + DynResolver::new(h3_resolver), tls.clone(), config.local_address, transport_config, diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index 79ae8ce14..a8f1e1706 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -1,4 +1,4 @@ -use crate::async_impl::h3_client::dns::Resolver; +use crate::async_impl::h3_client::dns::resolve; use crate::error::BoxError; use bytes::Bytes; use h3::client::SendRequest; @@ -8,6 +8,8 @@ use quinn::{ClientConfig, Endpoint, TransportConfig}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; +use hyper::client::connect::dns::Name; +use crate::dns::DynResolver; type H3Connection = ( h3::client::Connection, @@ -16,13 +18,13 @@ type H3Connection = ( #[derive(Clone)] pub(crate) struct H3Connector { - resolver: Resolver, + resolver: DynResolver, endpoint: Endpoint, } impl H3Connector { pub fn new( - resolver: Resolver, + resolver: DynResolver, tls: rustls::ClientConfig, local_addr: Option, transport_config: TransportConfig, @@ -50,7 +52,7 @@ impl H3Connector { // If the host is already an IP address, skip resolving. vec![SocketAddr::new(addr, port)] } else { - let addrs = self.resolver.resolve(host).await.into_iter(); + let addrs = resolve(&mut self.resolver, Name::from_str(host)?).await?; let addrs = addrs.map(|mut addr| { addr.set_port(port); addr diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index 426c72e72..8ffee5289 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -1,80 +1,12 @@ -use crate::dns::DnsResolverWithOverrides; #[cfg(feature = "trust-dns")] use crate::dns::trust_dns::TrustDnsResolver; use core::task; -use hyper::client::connect::dns::{GaiResolver, Name}; -use std::collections::HashMap; +use hyper::client::connect::dns::Name; use std::future::Future; use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; use std::task::Poll; use tower_service::Service; -#[derive(Clone)] -pub(crate) enum Resolver { - Gai(GaiResolver), - GaiWithDnsOverrides(DnsResolverWithOverrides), - #[cfg(feature = "trust-dns")] - TrustDns(TrustDnsResolver), - #[cfg(feature = "trust-dns")] - TrustDnsWithOverrides(DnsResolverWithOverrides), -} - -impl Resolver { - pub fn new_gai() -> Self { - Self::Gai(GaiResolver::new()) - } - - pub fn new_gai_with_overrides(overrides: HashMap>) -> Self { - Self::GaiWithDnsOverrides(DnsResolverWithOverrides::new(Arc::new(GaiResolver::new()), overrides)) - } - - #[cfg(feature = "trust-dns")] - pub fn new_trust_dns() -> crate::Result { - TrustDnsResolver::new() - .map(Self::TrustDns) - .map_err(crate::error::builder) - } - - #[cfg(feature = "trust-dns")] - pub fn new_trust_dns_with_overrides( - overrides: HashMap>, - ) -> crate::Result { - TrustDnsResolver::new() - .map(|trust_resolver| DnsResolverWithOverrides::new(Arc::new(trust_resolver), overrides)) - .map(Self::TrustDnsWithOverrides) - .map_err(crate::error::builder) - } - - pub async fn resolve(&mut self, server_name: &str) -> Vec { - let res: Vec = match self { - Self::Gai(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) - .await - .unwrap() - .collect(), - Self::GaiWithDnsOverrides(resolver) => { - resolve(resolver, Name::from_str(server_name).unwrap()) - .await - .unwrap() - .collect() - } - #[cfg(feature = "trust-dns")] - Self::TrustDns(resolver) => resolve(resolver, Name::from_str(server_name).unwrap()) - .await - .unwrap() - .collect(), - #[cfg(feature = "trust-dns")] - Self::TrustDnsWithOverrides(resolver) => { - resolve(resolver, Name::from_str(server_name).unwrap()) - .await - .unwrap() - .collect() - } - }; - res - } -} // Trait from hyper to implement DNS resolution for HTTP/3 client. pub trait Resolve { From 8efb857e4e8e7de32ba1e2d2e14b5523f5adf49c Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 19:17:54 -0400 Subject: [PATCH 26/29] Reuse dyn Resolver in client --- src/async_impl/client.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index b52fd25e0..559e5f365 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -259,15 +259,6 @@ impl ClientBuilder { headers.get(USER_AGENT).cloned() } - #[cfg(feature = "http3")] - let h3_resolver: Arc = match config.trust_dns { - false => Arc::new(GaiResolver::new()), - #[cfg(feature = "trust-dns")] - true => Arc::new(TrustDnsResolver::new().map_err(crate::error::builder)?), - #[cfg(not(feature = "trust-dns"))] - true => unreachable!("trust-dns shouldn't be enabled unless the feature is"), - }; - let mut resolver: Arc = match config.trust_dns { false => Arc::new(GaiResolver::new()), #[cfg(feature = "trust-dns")] @@ -521,7 +512,7 @@ impl ClientBuilder { } h3_connector = Some(H3Connector::new( - DynResolver::new(h3_resolver), + DynResolver::new(resolver), tls.clone(), config.local_address, transport_config, From b9e6ea0e6ef593a70c4fb2340d4de4576dfbb444 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 19:21:17 -0400 Subject: [PATCH 27/29] Remove unused import --- src/async_impl/h3_client/dns.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index 8ffee5289..33be489f8 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "trust-dns")] -use crate::dns::trust_dns::TrustDnsResolver; use core::task; use hyper::client::connect::dns::Name; use std::future::Future; From 6f5af0ed9fbf65d4c18c679ab1fdd93e6dc5e330 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 19:22:56 -0400 Subject: [PATCH 28/29] Run fmt --- src/async_impl/h3_client/connect.rs | 4 ++-- src/async_impl/h3_client/dns.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index a8f1e1706..755864590 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -1,15 +1,15 @@ use crate::async_impl::h3_client::dns::resolve; +use crate::dns::DynResolver; use crate::error::BoxError; use bytes::Bytes; use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::Uri; +use hyper::client::connect::dns::Name; use quinn::{ClientConfig, Endpoint, TransportConfig}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; -use hyper::client::connect::dns::Name; -use crate::dns::DynResolver; type H3Connection = ( h3::client::Connection, diff --git a/src/async_impl/h3_client/dns.rs b/src/async_impl/h3_client/dns.rs index 33be489f8..9cb50d1e3 100644 --- a/src/async_impl/h3_client/dns.rs +++ b/src/async_impl/h3_client/dns.rs @@ -5,7 +5,6 @@ use std::net::SocketAddr; use std::task::Poll; use tower_service::Service; - // Trait from hyper to implement DNS resolution for HTTP/3 client. pub trait Resolve { type Addrs: Iterator; From e2338415e0f8feb9dba4814e1909a8c76380e5fc Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 15 Mar 2023 19:38:06 -0400 Subject: [PATCH 29/29] Update deps --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5fe4d246..a82c7c403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,8 +139,8 @@ tokio-socks = { version = "0.5.1", optional = true } trust-dns-resolver = { version = "0.22", optional = true } # HTTP/3 experimental support -h3 = { git = "https://github.com/hyperium/h3", optional = true } -h3-quinn = { git = "https://github.com/hyperium/h3", optional = true } +h3 = { version="0.0.1", optional = true } +h3-quinn = { version="0.0.1", optional = true } quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"], optional = true } futures-channel = { version="0.3", optional = true}