Skip to content

Commit 558a07e

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 f0ffcc6 commit 558a07e

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