Skip to content

Commit 5ff82f0

Browse files
committed
Add HttpsConnectorBuilder
This gives more control over various rustls features, as well as ensures that enabling connector features like http1/http2 can only be done when the appropriate crate features are enabled.
1 parent 29493f0 commit 5ff82f0

File tree

5 files changed

+301
-59
lines changed

5 files changed

+301
-59
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ repository = "https://github.com/ctz/hyper-rustls"
1111

1212
[dependencies]
1313
log = "0.4.4"
14-
hyper = { version = "0.14", default-features = false, features = ["client", "http1"] }
14+
http = "0.2"
15+
hyper = { version = "0.14", default-features = false, features = ["client"] }
1516
rustls = "0.20"
1617
rustls-native-certs = { version = "0.6", optional = true }
1718
tokio = "1.0"

examples/client.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,23 @@ async fn run_client() -> io::Result<()> {
5151
.map_err(|_| error("failed to load custom CA store".into()))?;
5252
let mut roots = RootCertStore::empty();
5353
roots.add_parsable_certificates(&certs);
54-
// Build a TLS client, using the custom CA store for lookups.
54+
// TLS client config using the custom CA store for lookups
5555
rustls::ClientConfig::builder()
5656
.with_safe_defaults()
5757
.with_root_certificates(roots)
5858
.with_no_client_auth()
5959
}
60+
// Default TLS client config with native roots
6061
None => rustls::ClientConfig::builder()
6162
.with_safe_defaults()
6263
.with_native_roots(),
6364
};
64-
65-
// Build an HTTP connector which supports HTTPS too.
66-
let mut http = client::HttpConnector::new();
67-
http.enforce_http(false);
68-
69-
// Join the above parts into an HTTPS connector.
70-
let https = hyper_rustls::HttpsConnector::from((http, tls));
65+
// Prepare the HTTPS connector
66+
let https = hyper_rustls::HttpsConnectorBuilder::new()
67+
.with_tls_config(tls)
68+
.https_or_http()
69+
.enable_http1()
70+
.build();
7171

7272
// Build the hyper client from the HTTPS connector.
7373
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);

src/connector.rs

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,22 @@ use std::sync::Arc;
55
use std::task::{Context, Poll};
66
use std::{fmt, io};
77

8-
#[cfg(feature = "tokio-runtime")]
9-
use hyper::client::connect::HttpConnector;
108
use hyper::{client::connect::Connection, service::Service, Uri};
11-
use rustls::ClientConfig;
129
use tokio::io::{AsyncRead, AsyncWrite};
1310
use tokio_rustls::TlsConnector;
1411

1512
use crate::stream::MaybeHttpsStream;
1613

14+
pub mod builder;
15+
1716
type BoxError = Box<dyn std::error::Error + Send + Sync>;
1817

1918
/// A Connector for the `https` scheme.
2019
#[derive(Clone)]
2120
pub struct HttpsConnector<T> {
2221
force_https: bool,
2322
http: T,
24-
tls_config: Arc<ClientConfig>,
25-
}
26-
27-
#[cfg(all(
28-
any(feature = "rustls-native-certs", feature = "webpki-roots"),
29-
feature = "tokio-runtime"
30-
))]
31-
impl HttpsConnector<HttpConnector> {
32-
/// Force the use of HTTPS when connecting.
33-
///
34-
/// If a URL is not `https` when connecting, an error is returned. Disabled by default.
35-
pub fn https_only(&mut self, enable: bool) {
36-
self.force_https = enable;
37-
}
23+
tls_config: Arc<rustls::ClientConfig>,
3824
}
3925

4026
impl<T> fmt::Debug for HttpsConnector<T> {
@@ -47,7 +33,7 @@ impl<T> fmt::Debug for HttpsConnector<T> {
4733

4834
impl<H, C> From<(H, C)> for HttpsConnector<H>
4935
where
50-
C: Into<Arc<ClientConfig>>,
36+
C: Into<Arc<rustls::ClientConfig>>,
5137
{
5238
fn from((http, cfg): (H, C)) -> Self {
5339
HttpsConnector {
@@ -81,38 +67,43 @@ where
8167
}
8268

8369
fn call(&mut self, dst: Uri) -> Self::Future {
84-
let is_https = dst.scheme_str() == Some("https");
70+
// dst.scheme() would need to derive Eq to be matchable;
71+
// use an if cascade instead
72+
if let Some(sch) = dst.scheme() {
73+
if sch == &http::uri::Scheme::HTTP && !self.force_https {
74+
let connecting_future = self.http.call(dst);
8575

86-
if !is_https && self.force_https {
87-
// Early abort if HTTPS is forced but can't be used
88-
let err = io::Error::new(io::ErrorKind::Other, "https required but URI was not https");
89-
Box::pin(async move { Err(err.into()) })
90-
} else if !is_https {
91-
let connecting_future = self.http.call(dst);
76+
let f = async move {
77+
let tcp = connecting_future.await.map_err(Into::into)?;
9278

93-
let f = async move {
94-
let tcp = connecting_future.await.map_err(Into::into)?;
79+
Ok(MaybeHttpsStream::Http(tcp))
80+
};
81+
Box::pin(f)
82+
} else if sch == &http::uri::Scheme::HTTPS {
83+
let cfg = self.tls_config.clone();
84+
let hostname = dst.host().unwrap_or_default().to_string();
85+
let connecting_future = self.http.call(dst);
9586

96-
Ok(MaybeHttpsStream::Http(tcp))
97-
};
98-
Box::pin(f)
87+
let f = async move {
88+
let tcp = connecting_future.await.map_err(Into::into)?;
89+
let connector = TlsConnector::from(cfg);
90+
let dnsname = rustls::ServerName::try_from(hostname.as_str())
91+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid dnsname"))?;
92+
let tls = connector
93+
.connect(dnsname, tcp)
94+
.await
95+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
96+
Ok(MaybeHttpsStream::Https(tls))
97+
};
98+
Box::pin(f)
99+
} else {
100+
let err =
101+
io::Error::new(io::ErrorKind::Other, format!("Unsupported scheme {}", sch));
102+
Box::pin(async move { Err(err.into()) })
103+
}
99104
} else {
100-
let cfg = self.tls_config.clone();
101-
let hostname = dst.host().unwrap_or_default().to_string();
102-
let connecting_future = self.http.call(dst);
103-
104-
let f = async move {
105-
let tcp = connecting_future.await.map_err(Into::into)?;
106-
let connector = TlsConnector::from(cfg);
107-
let dnsname = rustls::ServerName::try_from(hostname.as_str())
108-
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid dnsname"))?;
109-
let tls = connector
110-
.connect(dnsname, tcp)
111-
.await
112-
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
113-
Ok(MaybeHttpsStream::Https(tls))
114-
};
115-
Box::pin(f)
105+
let err = io::Error::new(io::ErrorKind::Other, "Missing scheme");
106+
Box::pin(async move { Err(err.into()) })
116107
}
117108
}
118109
}

src/connector/builder.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use rustls::ClientConfig;
2+
3+
use super::HttpsConnector;
4+
#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
5+
use crate::config::ConfigBuilderExt;
6+
7+
#[cfg(feature = "tokio-runtime")]
8+
use hyper::client::HttpConnector;
9+
10+
/// A builder for an [`HttpsConnector`]
11+
///
12+
/// This makes configuration flexible and explicit and ensures connector
13+
/// features match crate features
14+
///
15+
/// # Examples
16+
///
17+
/// ```
18+
/// use hyper_rustls::HttpsConnectorBuilder;
19+
///
20+
/// # #[cfg(all(feature = "webpki-roots", feature = "tokio-runtime", feature = "http1"))]
21+
/// let https = HttpsConnectorBuilder::new()
22+
/// .with_webpki_roots()
23+
/// .https_only()
24+
/// .enable_http1()
25+
/// .build();
26+
/// ```
27+
pub struct ConnectorBuilder<State>(State);
28+
29+
/// State of a builder that needs a TLS client config next
30+
pub struct WantsTlsConfig(());
31+
32+
impl ConnectorBuilder<WantsTlsConfig> {
33+
/// Creates a new [`ConnectorBuilder`]
34+
pub fn new() -> Self {
35+
Self(WantsTlsConfig(()))
36+
}
37+
38+
/// Passes a rustls [`ClientConfig`] to configure the TLS connection
39+
///
40+
/// The [`alpn_protocols`](ClientConfig::alpn_protocols) field will be rewritten to
41+
/// match the enabled schemes (see
42+
/// [`enable_http1`](ConnectorBuilder::enable_http1),
43+
/// [`enable_http2`](ConnectorBuilder::enable_http2)) before the
44+
/// connector is built.
45+
pub fn with_tls_config(self, config: ClientConfig) -> ConnectorBuilder<WantsSchemes> {
46+
ConnectorBuilder(WantsSchemes { tls_config: config })
47+
}
48+
49+
/// Shorthand for using rustls' [safe defaults][with_safe_defaults]
50+
/// and native roots
51+
///
52+
/// See [`ConfigBuilderExt::with_native_roots`]
53+
///
54+
/// [with_safe_defaults]: rustls::ConfigBuilder::with_safe_defaults
55+
#[cfg(feature = "rustls-native-certs")]
56+
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
57+
pub fn with_native_roots(self) -> ConnectorBuilder<WantsSchemes> {
58+
self.with_tls_config(
59+
ClientConfig::builder()
60+
.with_safe_defaults()
61+
.with_native_roots(),
62+
)
63+
}
64+
65+
/// Shorthand for using rustls' [safe defaults][with_safe_defaults]
66+
/// and Mozilla roots
67+
///
68+
/// See [`ConfigBuilderExt::with_webpki_roots`]
69+
///
70+
/// [with_safe_defaults]: rustls::ConfigBuilder::with_safe_defaults
71+
#[cfg(feature = "webpki-roots")]
72+
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
73+
pub fn with_webpki_roots(self) -> ConnectorBuilder<WantsSchemes> {
74+
self.with_tls_config(
75+
ClientConfig::builder()
76+
.with_safe_defaults()
77+
.with_webpki_roots(),
78+
)
79+
}
80+
}
81+
82+
impl Default for ConnectorBuilder<WantsTlsConfig> {
83+
fn default() -> Self {
84+
Self::new()
85+
}
86+
}
87+
88+
/// State of a builder that needs schemes (https:// and http://) to be
89+
/// configured next
90+
pub struct WantsSchemes {
91+
tls_config: ClientConfig,
92+
}
93+
94+
impl ConnectorBuilder<WantsSchemes> {
95+
/// Enforce the use of HTTPS when connecting
96+
///
97+
/// Only URLs using the HTTPS scheme will be connectable.
98+
pub fn https_only(self) -> ConnectorBuilder<WantsProtocols1> {
99+
ConnectorBuilder(WantsProtocols1 {
100+
tls_config: self.0.tls_config,
101+
https_only: true,
102+
})
103+
}
104+
105+
/// Allow both HTTPS and HTTP when connecting
106+
///
107+
/// HTTPS URLs will be handled through rustls,
108+
/// HTTP URLs will be handled by the lower-level connector.
109+
pub fn https_or_http(self) -> ConnectorBuilder<WantsProtocols1> {
110+
ConnectorBuilder(WantsProtocols1 {
111+
tls_config: self.0.tls_config,
112+
https_only: false,
113+
})
114+
}
115+
}
116+
117+
/// State of a builder that needs to have some protocols (HTTP1 or later)
118+
/// enabled next
119+
///
120+
/// No protocol has been enabled at this point.
121+
pub struct WantsProtocols1 {
122+
tls_config: ClientConfig,
123+
https_only: bool,
124+
}
125+
126+
impl WantsProtocols1 {
127+
fn wrap_connector<H>(mut self, conn: H) -> HttpsConnector<H> {
128+
self.tls_config.alpn_protocols.clear();
129+
HttpsConnector {
130+
force_https: self.https_only,
131+
http: conn,
132+
tls_config: std::sync::Arc::new(self.tls_config),
133+
}
134+
}
135+
136+
#[cfg(feature = "tokio-runtime")]
137+
fn build(self) -> HttpsConnector<HttpConnector> {
138+
let mut http = HttpConnector::new();
139+
// HttpConnector won't enforce scheme, but HttpsConnector will
140+
http.enforce_http(false);
141+
self.wrap_connector(http)
142+
}
143+
}
144+
145+
impl ConnectorBuilder<WantsProtocols1> {
146+
/// Enable HTTP1
147+
///
148+
/// This needs to be called explicitly, no protocol is enabled by default
149+
#[cfg(feature = "http1")]
150+
pub fn enable_http1(self) -> ConnectorBuilder<WantsProtocols2> {
151+
ConnectorBuilder(WantsProtocols2 { inner: self.0 })
152+
}
153+
154+
/// Enable HTTP2
155+
///
156+
/// This needs to be called explicitly, no protocol is enabled by default
157+
#[cfg(feature = "http2")]
158+
pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
159+
self.0.tls_config.alpn_protocols = vec![b"h2".to_vec()];
160+
ConnectorBuilder(WantsProtocols3 {
161+
inner: self.0,
162+
enable_http1: false,
163+
})
164+
}
165+
}
166+
167+
/// State of a builder with HTTP1 enabled, that may have some other
168+
/// protocols (HTTP2 or later) enabled next
169+
///
170+
/// At this point a connector can be built, see
171+
/// [`build`](ConnectorBuilder<WantsProtocols2>::build) and
172+
/// [`wrap_connector`](ConnectorBuilder<WantsProtocols2>::wrap_connector).
173+
pub struct WantsProtocols2 {
174+
inner: WantsProtocols1,
175+
}
176+
177+
impl ConnectorBuilder<WantsProtocols2> {
178+
/// Enable HTTP2
179+
///
180+
/// This needs to be called explicitly, no protocol is enabled by default
181+
#[cfg(feature = "http2")]
182+
pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
183+
self.0.inner.tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
184+
ConnectorBuilder(WantsProtocols3 {
185+
inner: self.0.inner,
186+
enable_http1: true,
187+
})
188+
}
189+
190+
/// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
191+
#[cfg(feature = "tokio-runtime")]
192+
pub fn build(self) -> HttpsConnector<HttpConnector> {
193+
self.0.inner.build()
194+
}
195+
196+
/// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
197+
pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
198+
// HTTP1-only, alpn_protocols stays empty
199+
// HttpConnector doesn't have a way to say http1-only;
200+
// its connection pool may still support HTTP2
201+
// though it won't be used
202+
self.0.inner.wrap_connector(conn)
203+
}
204+
}
205+
206+
/// State of a builder with HTTP2 (and possibly HTTP1) enabled
207+
///
208+
/// At this point a connector can be built, see
209+
/// [`build`](ConnectorBuilder<WantsProtocols3>::build) and
210+
/// [`wrap_connector`](ConnectorBuilder<WantsProtocols3>::wrap_connector).
211+
#[cfg(feature = "http2")]
212+
pub struct WantsProtocols3 {
213+
inner: WantsProtocols1,
214+
// ALPN is built piecemeal without the need to read back this field
215+
#[allow(dead_code)]
216+
enable_http1: bool,
217+
}
218+
219+
#[cfg(feature = "http2")]
220+
impl ConnectorBuilder<WantsProtocols3> {
221+
/// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
222+
#[cfg(feature = "tokio-runtime")]
223+
pub fn build(self) -> HttpsConnector<HttpConnector> {
224+
self.0.inner.build()
225+
}
226+
227+
/// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
228+
pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
229+
// If HTTP1 is disabled, we can set http2_only
230+
// on the Client (a higher-level object that uses the connector)
231+
// client.http2_only(!self.0.enable_http1);
232+
self.0.inner.wrap_connector(conn)
233+
}
234+
}

0 commit comments

Comments
 (0)