diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec64ad95..3722158b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,9 @@ jobs: - ubuntu-latest - macos-latest - windows-latest + features: + - "http2,text-decoding,cookies,psl,unstable-interceptors,native-tls,static-curl" + - "http2,text-decoding,cookies,psl,unstable-interceptors,rustls-tls-native-certs" runs-on: ${{ matrix.os }} timeout-minutes: 20 env: @@ -35,7 +38,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: "1.46.0" + toolchain: "1.56" default: true - name: Update Cargo.lock @@ -55,7 +58,7 @@ jobs: cargo-${{ runner.os }}- - name: Run tests - run: cargo test --features ${{ env.FEATURES }},spnego,unstable-interceptors + run: cargo test --no-default-features --features ${{ matrix.features }} - name: Run example program run: cargo run --release --example simple diff --git a/Cargo.toml b/Cargo.toml index cd9f4eb3..722404fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,25 +19,25 @@ features = ["cookies", "json", "nightly"] status = "actively-developed" [features] -default = ["http2", "static-curl", "text-decoding"] +default = ["http2", "native-tls", "static-curl", "text-decoding"] cookies = ["httpdate"] http2 = ["curl/http2"] json = ["serde", "serde_json"] +native-tls = ["curl/ssl", "curl-sys/ssl"] nightly = [] psl = ["httpdate", "parking_lot", "publicsuffix"] +rustls-tls = ["rustls-ffi", "curl/rustls", "curl/static-curl"] +rustls-tls-native-certs = ["rustls-tls", "data-encoding", "rustls-native-certs"] spnego = ["curl-sys/spnego"] static-curl = ["curl/static-curl"] static-ssl = ["curl/static-ssl"] text-decoding = ["encoding_rs", "mime"] -unstable-rustls-tls = ["curl/rustls"] unstable-interceptors = [] [dependencies] async-channel = "1.4.2" castaway = "0.1.1" crossbeam-utils = ">=0.7.0, <0.9.0" -curl = "0.4.42" -curl-sys = "0.4.52" event-listener = "2.3.3" futures-lite = "1.10.1" http = "0.2.1" @@ -49,6 +49,18 @@ sluice = "0.5.4" url = "2.1" waker-fn = "1" +[dependencies.curl] +version = "0.4.43" +default-features = false + +[dependencies.curl-sys] +version = "0.4.53" +default-features = false + +[dependencies.data-encoding] +version = "2" +optional = true + [dependencies.encoding_rs] version = "0.8" optional = true @@ -70,6 +82,14 @@ version = "2.0.6" features = ["std"] optional = true +[dependencies.rustls-ffi] +version = "0.8" +optional = true + +[dependencies.rustls-native-certs] +version = "0.6" +optional = true + [dependencies.serde] version = "1.0" optional = true diff --git a/src/config/mod.rs b/src/config/mod.rs index 1f8dc0db..0d639434 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -27,12 +27,12 @@ pub(crate) mod dns; pub(crate) mod proxy; pub(crate) mod redirect; pub(crate) mod request; -pub(crate) mod ssl; +pub(crate) mod tls; pub use dial::{Dialer, DialerParseError}; pub use dns::{DnsCache, ResolveMap}; pub use redirect::RedirectPolicy; -pub use ssl::{CaCertificate, ClientCertificate, PrivateKey, SslOption}; +pub use tls::{CaCertificate, ClientCertificate, PrivateKey, SslOption}; /// Provides additional methods when building a request for configuring various /// execution-related options on how the request should be sent. diff --git a/src/config/request.rs b/src/config/request.rs index 6e6551f1..1cc2e3bd 100644 --- a/src/config/request.rs +++ b/src/config/request.rs @@ -32,19 +32,6 @@ macro_rules! define_request_config { } impl RequestConfig { - pub(crate) fn client_defaults() -> Self { - Self { - // Always start out with latest compatible HTTP version. - version_negotiation: Some(VersionNegotiation::default()), - // Enable automatic decompression by default for convenience - // (and maintain backwards compatibility). - automatic_decompression: Some(true), - // Erase curl's default auth method of Basic. - authentication: Some(Authentication::default()), - ..Default::default() - } - } - /// Merge another request configuration into this one. Unspecified /// values in this config are replaced with values in the given /// config. @@ -84,7 +71,7 @@ define_request_config! { max_download_speed: Option, ssl_client_certificate: Option, ssl_ca_certificate: Option, - ssl_ciphers: Option, + ssl_ciphers: Option, ssl_options: Option, enable_metrics: Option, @@ -94,6 +81,23 @@ define_request_config! { title_case_headers: Option, } +impl RequestConfig { + pub(crate) fn client_defaults() -> Self { + Self { + // Always start out with latest compatible HTTP version. + version_negotiation: Some(VersionNegotiation::default()), + // Enable automatic decompression by default for convenience + // (and maintain backwards compatibility). + automatic_decompression: Some(true), + // Erase curl's default auth method of Basic. + authentication: Some(Authentication::default()), + // Set native root certificates if available. + ssl_ca_certificate: CaCertificate::native(), + ..Default::default() + } + } +} + impl SetOpt for RequestConfig { fn set_opt(&self, easy: &mut Easy2) -> Result<(), curl::Error> { if let Some(timeout) = self.timeout { diff --git a/src/config/ssl.rs b/src/config/tls.rs similarity index 82% rename from src/config/ssl.rs rename to src/config/tls.rs index bbcadfa8..aa292429 100644 --- a/src/config/ssl.rs +++ b/src/config/tls.rs @@ -2,6 +2,7 @@ use super::SetOpt; use curl::easy::{Easy2, SslOpt}; +use once_cell::sync::Lazy; use std::{ iter::FromIterator, ops::{BitOr, BitOrAssign}, @@ -284,12 +285,58 @@ impl SetOpt for PrivateKey { /// A public CA certificate bundle file. #[derive(Clone, Debug)] pub struct CaCertificate { - /// Path to the certificate bundle file. Currently only file paths are - /// supported. - path: PathBuf, + /// The certificate data, either a path or a blob. + data: PathOrBlob, } impl CaCertificate { + /// Use one or more PEM-encoded certificates in the given byte buffer. + /// + /// The certificate object takes ownership of the byte buffer. If a borrowed + /// type is supplied, such as `&[u8]`, then the bytes will be copied. + /// + /// The certificates are not parsed or validated here. If a certificate is + /// malformed or the format is not supported by the underlying SSL/TLS + /// engine, an error will be returned when attempting to send a request + /// using the offending certificate. + pub fn pem(bytes: B) -> Self + where + B: Into>, + { + Self { + data: PathOrBlob::Blob(bytes.into()), + } + } + + /// Use one or more DER-encoded certificates stored in memory. + /// + /// The certificates are not parsed or validated here. If a certificate is + /// malformed or the format is not supported by the underlying SSL/TLS + /// engine, an error will be returned when attempting to send a request + /// using the offending certificate. + #[cfg(feature = "data-encoding")] + #[allow(dead_code)] + pub(crate) fn der_multiple(certificates: I) -> Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + let mut base64_spec = data_encoding::BASE64.specification(); + base64_spec.wrap.width = 64; + base64_spec.wrap.separator.push_str("\r\n"); + let base64 = base64_spec.encoding().unwrap(); + + let mut pem = String::new(); + + for certificate in certificates { + pem.push_str("-----BEGIN CERTIFICATE-----\r\n"); + base64.encode_append(certificate.as_ref(), &mut pem); + pem.push_str("-----END CERTIFICATE-----\r\n"); + } + + Self::pem(pem) + } + /// Get a CA certificate from a path to a certificate bundle file. /// /// The certificate file is not loaded or validated here. If the file does @@ -298,14 +345,43 @@ impl CaCertificate { /// using the offending certificate. pub fn file(ca_bundle_path: impl Into) -> Self { Self { - path: ca_bundle_path.into(), + data: PathOrBlob::Path(ca_bundle_path.into()), } } + + /// Get native root certificates trusted by the operating system, if any. + #[allow(unreachable_code)] + pub(crate) fn native() -> Option { + static NATIVE: Lazy> = Lazy::new(|| { + #[cfg(feature = "rustls-native-certs")] + { + match rustls_native_certs::load_native_certs() { + Ok(certs) => { + if !certs.is_empty() { + return Some(CaCertificate::der_multiple( + certs.into_iter().map(|cert| cert.0), + )); + } + } + Err(e) => { + log::warn!("failed to load native certificate chain: {}", e); + } + } + } + + None + }); + + NATIVE.clone() + } } impl SetOpt for CaCertificate { fn set_opt(&self, easy: &mut Easy2) -> Result<(), curl::Error> { - easy.cainfo(&self.path) + match &self.data { + PathOrBlob::Path(path) => easy.cainfo(path.as_path()), + PathOrBlob::Blob(bytes) => easy.ssl_cainfo_blob(bytes.as_slice()), + } } }