Skip to content

Commit 6bc647f

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 5d30d12 commit 6bc647f

File tree

6 files changed

+369
-132
lines changed

6 files changed

+369
-132
lines changed

Cargo.toml

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

1212
[dependencies]
1313
log = "0.4.4"
14-
ct-logs = { version = "^0.9", optional = true }
15-
hyper = { version = "0.14", default-features = false, features = ["client", "http1"] }
14+
http = "0.2"
15+
hyper = { version = "0.14", default-features = false, features = ["client"] }
1616
rustls = "0.20"
17-
rustls-native-certs = { git = "https://github.com/djc/rustls-native-certs", branch = "no-rustls", optional = true }
18-
rustls-pemfile = { version = "0.2.1" }
17+
rustls-native-certs = { git = "https://github.com/rustls/rustls-native-certs", default-features = false, optional = true }
1918
tokio = "1.0"
20-
tokio-rustls = { version = "0.23", git = "https://github.com/tokio-rs/tls" }
21-
webpki = "0.22.0"
19+
tokio-rustls = "0.23"
2220
webpki-roots = { version = "0.22", optional = true }
2321

2422
[dev-dependencies]
2523
async-stream = "0.3.0"
2624
tokio = { version = "1.0", features = ["io-std", "macros", "net", "rt-multi-thread"] }
2725
hyper = { version = "0.14", features = ["full"] }
2826
futures-util = { version = "0.3.1", default-features = false }
27+
rustls-pemfile = { version = "0.2.1" }
2928

3029
[features]
3130
default = ["native-tokio", "http1"]
3231
http1 = ["hyper/http1"]
3332
http2 = ["hyper/http2"]
3433
webpki-tokio = ["tokio-runtime", "webpki-roots"]
3534
native-tokio = ["tokio-runtime", "rustls-native-certs"]
36-
tokio-runtime = ["hyper/runtime", "ct-logs"]
35+
tokio-runtime = ["hyper/runtime"]
3736

3837
[[example]]
3938
name = "client"
4039
path = "examples/client.rs"
41-
required-features = ["native-tokio", "tokio-runtime"]
40+
required-features = ["native-tokio", "http1"]
4241

4342
[[example]]
4443
name = "server"
@@ -48,6 +47,3 @@ required-features = ["tokio-runtime"]
4847
[package.metadata.docs.rs]
4948
all-features = true
5049
rustdoc-args = ["--cfg", "docsrs"]
51-
52-
[patch."crates-io"]
53-
rustls = { git = "https://github.com/ctz/rustls" }

examples/client.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! First parameter is the mandatory URL to GET.
44
//! Second parameter is an optional path to CA store.
55
use hyper::{body::to_bytes, client, Body, Uri};
6+
use hyper_rustls::ConfigBuilderExt;
67
use rustls::RootCertStore;
78

89
use std::str::FromStr;
@@ -42,28 +43,30 @@ async fn run_client() -> io::Result<()> {
4243
None => None,
4344
};
4445

45-
// Prepare the HTTPS connector.
46-
let https = match ca {
46+
// Prepare the TLS client config
47+
let config = match ca {
4748
Some(ref mut rd) => {
48-
// Build an HTTP connector which supports HTTPS too.
49-
let mut http = client::HttpConnector::new();
50-
http.enforce_http(false);
51-
// Read trust roots
5249
let certs = rustls_pemfile::certs(rd)
5350
.map_err(|_| error("failed to load custom CA store".into()))?;
5451
let mut roots = RootCertStore::empty();
5552
roots.add_parsable_certificates(&certs);
56-
// Build a TLS client, using the custom CA store for lookups.
57-
let tls = rustls::ClientConfig::builder()
53+
// TLS client config using the custom CA store for lookups
54+
rustls::ClientConfig::builder()
5855
.with_safe_defaults()
5956
.with_root_certificates(roots)
60-
.with_no_client_auth();
61-
// Join the above part into an HTTPS connector.
62-
hyper_rustls::HttpsConnector::from((http, tls))
57+
.with_no_client_auth()
6358
}
64-
// Default HTTPS connector.
65-
None => hyper_rustls::HttpsConnector::with_native_roots(),
59+
// Default TLS client config with native roots
60+
None => rustls::ClientConfig::builder()
61+
.with_safe_defaults()
62+
.with_native_roots(),
6663
};
64+
// Prepare the HTTPS connector
65+
let https = hyper_rustls::HttpsConnectorBuilder::new()
66+
.with_tls_config(config)
67+
.https_or_http()
68+
.enable_http1()
69+
.build();
6770

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

src/config.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use rustls::{ClientConfig, ConfigBuilder, WantsVerifier};
2+
3+
/// Methods for configuring roots
4+
///
5+
/// This adds methods (gated by crate features) for easily configuring
6+
/// TLS server roots a rustls ClientConfig will trust.
7+
pub trait ConfigBuilderExt {
8+
/// This configures the platform's trusted certs, as implemented by
9+
/// rustls-native-certs
10+
#[cfg(feature = "rustls-native-certs")]
11+
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
12+
fn with_native_roots(self) -> ClientConfig;
13+
14+
/// This configures the webpki roots, which are Mozilla's set of
15+
/// trusted roots as packaged by webpki-roots.
16+
#[cfg(feature = "webpki-roots")]
17+
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
18+
fn with_webpki_roots(self) -> ClientConfig;
19+
}
20+
21+
impl ConfigBuilderExt for ConfigBuilder<ClientConfig, WantsVerifier> {
22+
#[cfg(feature = "rustls-native-certs")]
23+
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
24+
fn with_native_roots(self) -> ClientConfig {
25+
let mut roots = rustls::RootCertStore::empty();
26+
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs")
27+
{
28+
roots.add(&rustls::Certificate(cert.0)).unwrap();
29+
}
30+
31+
assert!(!roots.is_empty(), "no CA certificates found");
32+
33+
self.with_root_certificates(roots).with_no_client_auth()
34+
}
35+
36+
#[cfg(feature = "webpki-roots")]
37+
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
38+
fn with_webpki_roots(self) -> ClientConfig {
39+
let mut roots = rustls::RootCertStore::empty();
40+
roots.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
41+
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
42+
ta.subject,
43+
ta.spki,
44+
ta.name_constraints,
45+
)
46+
}));
47+
self.with_root_certificates(roots).with_no_client_auth()
48+
}
49+
}

src/connector.rs

Lines changed: 41 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,26 @@
1+
use std::convert::TryFrom;
12
use std::future::Future;
23
use std::pin::Pin;
34
use std::sync::Arc;
45
use std::task::{Context, Poll};
56
use std::{fmt, io};
6-
use std::convert::TryFrom;
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, RootCertStore};
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-
/// Construct a new `HttpsConnector` using the OS root store
33-
#[cfg(feature = "rustls-native-certs")]
34-
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
35-
pub fn with_native_roots() -> Self {
36-
let certs = match rustls_native_certs::load_native_certs() {
37-
Ok(certs) => certs,
38-
Err(err) => Err(err).expect("cannot access native cert store"),
39-
};
40-
41-
if certs.is_empty() {
42-
panic!("no CA certificates found");
43-
}
44-
45-
let mut roots = RootCertStore::empty();
46-
for cert in certs {
47-
roots.add_parsable_certificates(&[cert.0]);
48-
}
49-
50-
Self::build(roots)
51-
}
52-
53-
/// Construct a new `HttpsConnector` using the `webpki_roots`
54-
#[cfg(feature = "webpki-roots")]
55-
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
56-
pub fn with_webpki_roots() -> Self {
57-
let mut roots = rustls::RootCertStore::empty();
58-
roots.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0);
59-
Self::build(roots)
60-
}
61-
62-
/// Force the use of HTTPS when connecting.
63-
///
64-
/// If a URL is not `https` when connecting, an error is returned. Disabled by default.
65-
pub fn https_only(&mut self, enable: bool) {
66-
self.force_https = enable;
67-
}
68-
69-
fn build(mut config: ClientConfig) -> Self {
70-
let mut http = HttpConnector::new();
71-
http.enforce_http(false);
72-
73-
config.alpn_protocols.clear();
74-
#[cfg(feature = "http2")]
75-
{
76-
config.alpn_protocols.push(b"h2".to_vec());
77-
}
78-
79-
#[cfg(feature = "http1")]
80-
{
81-
config.alpn_protocols.push(b"http/1.1".to_vec());
82-
}
83-
84-
//let mut config = ClientConfig::builder()
85-
// .with_safe_defaults()
86-
// .with_root_certificates(roots)
87-
// //.with_certificate_transparency_logs(ct_logs::LOGS, XXX)
88-
// .with_no_client_auth();
89-
90-
(http, config).into()
91-
}
23+
tls_config: Arc<rustls::ClientConfig>,
9224
}
9325

9426
impl<T> fmt::Debug for HttpsConnector<T> {
@@ -101,7 +33,7 @@ impl<T> fmt::Debug for HttpsConnector<T> {
10133

10234
impl<H, C> From<(H, C)> for HttpsConnector<H>
10335
where
104-
C: Into<Arc<ClientConfig>>,
36+
C: Into<Arc<rustls::ClientConfig>>,
10537
{
10638
fn from((http, cfg): (H, C)) -> Self {
10739
HttpsConnector {
@@ -135,38 +67,43 @@ where
13567
}
13668

13769
fn call(&mut self, dst: Uri) -> Self::Future {
138-
let is_https = dst.scheme_str() == Some("https");
139-
140-
if !is_https && self.force_https {
141-
// Early abort if HTTPS is forced but can't be used
142-
let err = io::Error::new(io::ErrorKind::Other, "https required but URI was not https");
143-
Box::pin(async move { Err(err.into()) })
144-
} else if !is_https {
145-
let connecting_future = self.http.call(dst);
146-
147-
let f = async move {
148-
let tcp = connecting_future.await.map_err(Into::into)?;
149-
150-
Ok(MaybeHttpsStream::Http(tcp))
151-
};
152-
Box::pin(f)
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);
75+
76+
let f = async move {
77+
let tcp = connecting_future.await.map_err(Into::into)?;
78+
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);
86+
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+
}
153104
} else {
154-
let cfg = self.tls_config.clone();
155-
let hostname = dst.host().unwrap_or_default().to_string();
156-
let connecting_future = self.http.call(dst);
157-
158-
let f = async move {
159-
let tcp = connecting_future.await.map_err(Into::into)?;
160-
let connector = TlsConnector::from(cfg);
161-
let dnsname = rustls::ServerName::try_from(hostname.as_str())
162-
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid dnsname"))?;
163-
let tls = connector
164-
.connect(dnsname, tcp)
165-
.await
166-
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
167-
Ok(MaybeHttpsStream::Https(tls))
168-
};
169-
Box::pin(f)
105+
let err = io::Error::new(io::ErrorKind::Other, "Missing scheme");
106+
Box::pin(async move { Err(err.into()) })
170107
}
171108
}
172109
}

0 commit comments

Comments
 (0)