diff --git a/.github/actions/cache/action.yaml b/.github/actions/cache/action.yaml index b5ae639..625d672 100644 --- a/.github/actions/cache/action.yaml +++ b/.github/actions/cache/action.yaml @@ -1,5 +1,5 @@ -name: '[rust] Checkout and cache' -description: '[rust] Checkout cache' +name: '[rust] Cache' +description: '[rust] Cache' runs: using: composite diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4dda76..f58698a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,11 @@ env: jobs: test: - name: cargo test --workspace - runs-on: ubuntu-latest + name: "cargo test --workspace #${{ matrix.platform }}" + runs-on: ${{ matrix.platform }} + strategy: + matrix: + platform: [windows-latest, ubuntu-latest, macos-latest] steps: - name: Checkout sources uses: actions/checkout@v2 diff --git a/Cargo.lock b/Cargo.lock index f2d3b44..decd95e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,8 +74,9 @@ dependencies = [ [[package]] name = "common-multipart-rfc7578" -version = "0.4.2" -source = "git+https://github.com/paullegranddc/rust-multipart-rfc7578.git?rev=8dcedc266e50876c04c91d24390fe9ac44f10b96#8dcedc266e50876c04c91d24390fe9ac44f10b96" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22328b3864f1d8dbe7036f3f2fdfdcb1f367af43dca418943d396fbf8c4b8021" dependencies = [ "bytes", "futures-core", @@ -115,6 +116,7 @@ dependencies = [ name = "ddprof-exporter" version = "0.5.0-rc.1" dependencies = [ + "anyhow", "bytes", "chrono", "futures", @@ -131,9 +133,12 @@ dependencies = [ "maplit", "mime_guess", "percent-encoding", - "pin-project-lite", + "pin-project", "regex", + "rustls", + "rustls-native-certs", "tokio", + "tokio-rustls", ] [[package]] @@ -365,8 +370,9 @@ dependencies = [ [[package]] name = "hyper-multipart-rfc7578" -version = "0.6.2" -source = "git+https://github.com/paullegranddc/rust-multipart-rfc7578.git?rev=8dcedc266e50876c04c91d24390fe9ac44f10b96#8dcedc266e50876c04c91d24390fe9ac44f10b96" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ca8108ac0ae98d310d41cddb11c6b822e8aca865dbe421366934e6f7f72e10" dependencies = [ "bytes", "common-multipart-rfc7578", @@ -566,6 +572,26 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.8" @@ -739,6 +765,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" dependencies = [ + "log", "ring", "sct", "webpki", @@ -895,9 +922,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "winapi", ] +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.23.2" diff --git a/ddprof-exporter/Cargo.toml b/ddprof-exporter/Cargo.toml index d5a45e4..1f50d24 100644 --- a/ddprof-exporter/Cargo.toml +++ b/ddprof-exporter/Cargo.toml @@ -11,6 +11,7 @@ license = "Apache-2.0" crate-type = ["cdylib", "lib"] [dependencies] +anyhow = "1.0" bytes = "1.0" chrono = "0.4" futures = "0.3" @@ -19,16 +20,19 @@ lazy_static = "1.4" libc = "0.2" regex = "1.5" hyper = { version = "0.14", features = ["http1", "client", "tcp", "stream"], default-features = false } -tokio = { version = "1.8", features = ["rt"]} +tokio = { version = "1.8", features = ["rt", "macros"]} +tokio-rustls = { version = "0.23" } percent-encoding = "2.1" futures-core = { version = "0.3.0", default-features = false } futures-util = { version = "0.3.0", default-features = false } mime_guess = { version = "2.0", default-features = false } http-body = "0.4" -pin-project-lite = "0.2.0" +pin-project = "1" +rustls = { version = "0.20.4", default-features = false } +rustls-native-certs = { version = "0.6" } hyper-rustls = { version = "0.23", default-features = false, features = ["native-tokio", "http1", "tls12"] } hex = "0.4" -hyper-multipart-rfc7578 = { git = "https://github.com/paullegranddc/rust-multipart-rfc7578.git", rev = "8dcedc266e50876c04c91d24390fe9ac44f10b96" } +hyper-multipart-rfc7578 = "0.7.0" [dev-dependencies] maplit = "1.0" diff --git a/ddprof-exporter/src/connector.rs b/ddprof-exporter/src/connector.rs deleted file mode 100644 index 23c091b..0000000 --- a/ddprof-exporter/src/connector.rs +++ /dev/null @@ -1,211 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc. - -use std::error::Error; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; - -// Tokio doesn't handle unix sockets on windows -#[cfg(unix)] -pub(crate) mod uds { - use pin_project_lite::pin_project; - use std::error::Error; - use std::ffi::OsString; - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - use std::path::{Path, PathBuf}; - - /// Creates a new Uri, with the `unix` scheme, and the path to the socket - /// encoded as a hex string, to prevent special characters in the url authority - pub fn socket_path_to_uri(path: &Path) -> Result> { - let path = hex::encode(path.as_os_str().as_bytes()); - Ok(hyper::Uri::builder() - .scheme("unix") - .authority(path) - .path_and_query("") - .build()?) - } - - pub fn socket_path_from_uri( - uri: &hyper::Uri, - ) -> Result> { - if uri.scheme_str() != Some("unix") { - return Err(crate::errors::Error::InvalidUrl.into()); - } - let path = hex::decode( - uri.authority() - .ok_or(crate::errors::Error::InvalidUrl)? - .as_str(), - ) - .map_err(|_| crate::errors::Error::InvalidUrl)?; - Ok(PathBuf::from(OsString::from_vec(path))) - } - - #[test] - fn test_encode_unix_socket_path_absolute() { - let expected_path = "/path/to/a/socket.sock".as_ref(); - let uri = socket_path_to_uri(expected_path).unwrap(); - assert_eq!(uri.scheme_str(), Some("unix")); - - let actual_path = socket_path_from_uri(&uri).unwrap(); - assert_eq!(actual_path.as_path(), Path::new(expected_path)) - } - - #[test] - fn test_encode_unix_socket_relative_path() { - let expected_path = "relative/path/to/a/socket.sock".as_ref(); - let uri = socket_path_to_uri(expected_path).unwrap(); - let actual_path = socket_path_from_uri(&uri).unwrap(); - assert_eq!(actual_path.as_path(), Path::new(expected_path)); - - let expected_path = "./relative/path/to/a/socket.sock".as_ref(); - let uri = socket_path_to_uri(expected_path).unwrap(); - let actual_path = socket_path_from_uri(&uri).unwrap(); - assert_eq!(actual_path.as_path(), Path::new(expected_path)); - } - - pin_project! { - #[project = ConnStreamProj] - pub(crate) enum ConnStream { - Tcp{ #[pin] transport: hyper_rustls::MaybeHttpsStream }, - Udp{ #[pin] transport: tokio::net::UnixStream }, - } - } -} - -#[cfg(unix)] -use uds::{ConnStream, ConnStreamProj}; - -#[cfg(not(unix))] -pin_project_lite::pin_project! { - #[project = ConnStreamProj] - pub(crate) enum ConnStream { - Tcp{ #[pin] transport: hyper_rustls::MaybeHttpsStream }, - } -} - -#[derive(Clone)] -pub(crate) struct Connector { - tcp: hyper_rustls::HttpsConnector, -} - -impl Connector { - pub(crate) fn new() -> Self { - Self { - tcp: hyper_rustls::HttpsConnectorBuilder::new() - .with_native_roots() - .https_or_http() - .enable_http1() - .build(), - } - } -} - -impl tokio::io::AsyncRead for ConnStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - match self.project() { - ConnStreamProj::Tcp { transport } => transport.poll_read(cx, buf), - #[cfg(unix)] - ConnStreamProj::Udp { transport } => transport.poll_read(cx, buf), - } - } -} - -impl hyper::client::connect::Connection for ConnStream { - fn connected(&self) -> hyper::client::connect::Connected { - match self { - Self::Tcp { transport } => transport.connected(), - #[cfg(unix)] - Self::Udp { transport: _ } => hyper::client::connect::Connected::new(), - } - } -} - -impl tokio::io::AsyncWrite for ConnStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.project() { - ConnStreamProj::Tcp { transport } => transport.poll_write(cx, buf), - #[cfg(unix)] - ConnStreamProj::Udp { transport } => transport.poll_write(cx, buf), - } - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.project() { - ConnStreamProj::Tcp { transport } => transport.poll_shutdown(cx), - #[cfg(unix)] - ConnStreamProj::Udp { transport } => transport.poll_shutdown(cx), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project() { - ConnStreamProj::Tcp { transport } => transport.poll_flush(cx), - #[cfg(unix)] - ConnStreamProj::Udp { transport } => transport.poll_flush(cx), - } - } -} - -impl hyper::service::Service for Connector { - type Response = ConnStream; - type Error = Box; - - // This lint gets lifted in this place in a newer version, see: - // https://github.com/rust-lang/rust-clippy/pull/8030 - #[allow(clippy::type_complexity)] - type Future = Pin> + Send>>; - - fn call(&mut self, uri: hyper::Uri) -> Self::Future { - match uri.scheme_str() { - Some("unix") => Box::pin(async move { - #[cfg(unix)] - { - let path = uds::socket_path_from_uri(&uri)?; - Ok(ConnStream::Udp { - transport: tokio::net::UnixStream::connect(path).await?, - }) - } - #[cfg(not(unix))] - { - Err(crate::errors::Error::UnixSockeUnsuported.into()) - } - }), - _ => { - let fut = self.tcp.call(uri); - Box::pin(async { - Ok(ConnStream::Tcp { - transport: fut.await?, - }) - }) - } - } - } - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.tcp.poll_ready(cx) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - /// Verify that the Connector type implements the correct bound Connect + Clone - /// to be able to use the hyper::Client - fn test_hyper_client_from_connector() { - let _: hyper::Client = hyper::Client::builder().build(Connector::new()); - } -} diff --git a/ddprof-exporter/src/connector/conn_stream.rs b/ddprof-exporter/src/connector/conn_stream.rs new file mode 100644 index 0000000..c718806 --- /dev/null +++ b/ddprof-exporter/src/connector/conn_stream.rs @@ -0,0 +1,145 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc. + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures::{future, Future, FutureExt, TryFutureExt}; +use hyper_rustls::HttpsConnector; +use pin_project::pin_project; + +#[derive(Debug)] +#[pin_project(project=ConnStreamProj)] +pub enum ConnStream { + Tcp { + #[pin] + transport: tokio::net::TcpStream, + }, + Tls { + #[pin] + transport: Box>, + }, + #[cfg(unix)] + Udp { + #[pin] + transport: tokio::net::UnixStream, + }, +} + +pub type ConnStreamError = Box; + +use hyper::{client::HttpConnector, service::Service}; +impl ConnStream { + pub async fn from_uds_uri(uri: hyper::Uri) -> Result { + #[cfg(unix)] + { + let path = super::uds::socket_path_from_uri(&uri)?; + Ok(ConnStream::Udp { + transport: tokio::net::UnixStream::connect(path).await?, + }) + } + #[cfg(not(unix))] + { + Err(crate::errors::Error::UnixSockeUnsuported.into()) + } + } + + pub fn from_http_connector_with_uri( + c: &mut HttpConnector, + uri: hyper::Uri, + ) -> impl Future> { + c.call(uri).map(|r| match r { + Ok(t) => Ok(ConnStream::Tcp { transport: t }), + Err(e) => Err(e.into()), + }) + } + + pub fn from_https_connector_with_uri( + c: &mut HttpsConnector, + uri: hyper::Uri, + require_tls: bool, + ) -> impl Future> { + c.call(uri).and_then(move |stream| match stream { + // move only require_tls + hyper_rustls::MaybeHttpsStream::Http(t) => { + if require_tls { + future::ready(Err( + crate::errors::Error::CannotEstablishTlsConnection.into() + )) + } else { + future::ready(Ok(ConnStream::Tcp { transport: t })) + } + } + hyper_rustls::MaybeHttpsStream::Https(t) => future::ready(Ok(ConnStream::Tls { + transport: Box::from(t), + })), + }) + } +} + +impl tokio::io::AsyncRead for ConnStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match self.project() { + ConnStreamProj::Tcp { transport } => transport.poll_read(cx, buf), + ConnStreamProj::Tls { transport } => transport.poll_read(cx, buf), + #[cfg(unix)] + ConnStreamProj::Udp { transport } => transport.poll_read(cx, buf), + } + } +} + +impl hyper::client::connect::Connection for ConnStream { + fn connected(&self) -> hyper::client::connect::Connected { + match self { + Self::Tcp { transport } => transport.connected(), + Self::Tls { transport } => { + let (tcp, _) = transport.get_ref(); + tcp.connected() + } + #[cfg(unix)] + Self::Udp { transport: _ } => hyper::client::connect::Connected::new(), + } + } +} + +impl tokio::io::AsyncWrite for ConnStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.project() { + ConnStreamProj::Tcp { transport } => transport.poll_write(cx, buf), + ConnStreamProj::Tls { transport } => transport.poll_write(cx, buf), + #[cfg(unix)] + ConnStreamProj::Udp { transport } => transport.poll_write(cx, buf), + } + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.project() { + ConnStreamProj::Tcp { transport } => transport.poll_shutdown(cx), + ConnStreamProj::Tls { transport } => transport.poll_shutdown(cx), + #[cfg(unix)] + ConnStreamProj::Udp { transport } => transport.poll_shutdown(cx), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + ConnStreamProj::Tcp { transport } => transport.poll_flush(cx), + ConnStreamProj::Tls { transport } => transport.poll_flush(cx), + #[cfg(unix)] + ConnStreamProj::Udp { transport } => transport.poll_flush(cx), + } + } +} diff --git a/ddprof-exporter/src/connector/mod.rs b/ddprof-exporter/src/connector/mod.rs new file mode 100644 index 0000000..b104e1d --- /dev/null +++ b/ddprof-exporter/src/connector/mod.rs @@ -0,0 +1,146 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc. + +use futures::future::BoxFuture; +use futures::{future, FutureExt}; +use hyper::client::HttpConnector; +use rustls::ClientConfig; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +#[cfg(unix)] +pub mod uds; + +mod conn_stream; +use conn_stream::{ConnStream, ConnStreamError}; + +#[derive(Clone)] +pub enum Connector { + Http(hyper::client::HttpConnector), + Https(hyper_rustls::HttpsConnector), +} + +impl Connector { + pub(crate) fn new() -> Self { + match build_https_connector() { + Ok(connector) => Connector::Https(connector), + Err(_) => Connector::Http(HttpConnector::new()), + } + } + + fn build_conn_stream<'a>( + &mut self, + uri: hyper::Uri, + require_tls: bool, + ) -> BoxFuture<'a, Result> { + match self { + Self::Http(c) => { + if require_tls { + future::err::( + crate::errors::Error::CannotEstablishTlsConnection.into(), + ) + .boxed() + } else { + ConnStream::from_http_connector_with_uri(c, uri).boxed() + } + } + Self::Https(c) => { + ConnStream::from_https_connector_with_uri(c, uri, require_tls).boxed() + } + } + } +} + +fn build_https_connector( +) -> anyhow::Result> { + let certs = load_root_certs()?; + let client_config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(certs) + .with_no_client_auth(); + Ok(hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config(client_config) + .https_or_http() + .enable_http1() + .build()) +} + +fn load_root_certs() -> anyhow::Result { + let mut roots = rustls::RootCertStore::empty(); + + for cert in rustls_native_certs::load_native_certs()? { + let cert = rustls::Certificate(cert.0); + + //TODO: log when invalid cert is loaded + roots.add(&cert).ok(); + } + if roots.is_empty() { + return Err(crate::errors::Error::NoValidCertifacteRootsFound.into()); + } + Ok(roots) +} + +impl hyper::service::Service for Connector { + type Response = ConnStream; + type Error = ConnStreamError; + + // This lint gets lifted in this place in a newer version, see: + // https://github.com/rust-lang/rust-clippy/pull/8030 + #[allow(clippy::type_complexity)] + type Future = Pin> + Send>>; + + fn call(&mut self, uri: hyper::Uri) -> Self::Future { + match uri.scheme_str() { + Some("unix") => conn_stream::ConnStream::from_uds_uri(uri).boxed(), + Some("https") => self.build_conn_stream(uri, true), + _ => self.build_conn_stream(uri, false), + } + } + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + match self { + Connector::Http(c) => c.poll_ready(cx).map_err(|e| e.into()), + Connector::Https(c) => c.poll_ready(cx), + } + } +} + +#[cfg(test)] +mod tests { + use hyper::service::Service; + use std::env; + + use super::*; + + #[test] + /// Verify that the Connector type implements the correct bound Connect + Clone + /// to be able to use the hyper::Client + fn test_hyper_client_from_connector() { + let _: hyper::Client = hyper::Client::builder().build(Connector::new()); + } + + #[tokio::test] + /// Verify that Connector will only allow non tls connections if root certificates + /// are not found + async fn test_missing_root_certificates_only_allow_http_connections() { + const ENV_SSL_CERT_FILE: &str = "SSL_CERT_FILE"; + let old_value = env::var(ENV_SSL_CERT_FILE).unwrap_or_default(); + + env::set_var(ENV_SSL_CERT_FILE, "this/folder/does/not/exist"); + let mut connector = Connector::new(); + assert!(matches!(connector, Connector::Http(_))); + + let stream = connector + .call(hyper::Uri::from_static("https://example.com")) + .await + .unwrap_err(); + + assert_eq!( + *stream.downcast::().unwrap(), + crate::errors::Error::CannotEstablishTlsConnection + ); + + env::set_var(ENV_SSL_CERT_FILE, old_value); + } +} diff --git a/ddprof-exporter/src/connector/uds.rs b/ddprof-exporter/src/connector/uds.rs new file mode 100644 index 0000000..4b2788f --- /dev/null +++ b/ddprof-exporter/src/connector/uds.rs @@ -0,0 +1,54 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc. + +use std::error::Error; +use std::ffi::OsString; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; + +/// Creates a new Uri, with the `unix` scheme, and the path to the socket +/// encoded as a hex string, to prevent special characters in the url authority +pub fn socket_path_to_uri(path: &Path) -> Result> { + let path = hex::encode(path.as_os_str().as_bytes()); + Ok(hyper::Uri::builder() + .scheme("unix") + .authority(path) + .path_and_query("") + .build()?) +} + +pub fn socket_path_from_uri(uri: &hyper::Uri) -> anyhow::Result { + if uri.scheme_str() != Some("unix") { + return Err(crate::errors::Error::InvalidUrl.into()); + } + let path = hex::decode( + uri.authority() + .ok_or(crate::errors::Error::InvalidUrl)? + .as_str(), + ) + .map_err(|_| crate::errors::Error::InvalidUrl)?; + Ok(PathBuf::from(OsString::from_vec(path))) +} + +#[test] +fn test_encode_unix_socket_path_absolute() { + let expected_path = "/path/to/a/socket.sock".as_ref(); + let uri = socket_path_to_uri(expected_path).unwrap(); + assert_eq!(uri.scheme_str(), Some("unix")); + + let actual_path = socket_path_from_uri(&uri).unwrap(); + assert_eq!(actual_path.as_path(), Path::new(expected_path)) +} + +#[test] +fn test_encode_unix_socket_relative_path() { + let expected_path = "relative/path/to/a/socket.sock".as_ref(); + let uri = socket_path_to_uri(expected_path).unwrap(); + let actual_path = socket_path_from_uri(&uri).unwrap(); + assert_eq!(actual_path.as_path(), Path::new(expected_path)); + + let expected_path = "./relative/path/to/a/socket.sock".as_ref(); + let uri = socket_path_to_uri(expected_path).unwrap(); + let actual_path = socket_path_from_uri(&uri).unwrap(); + assert_eq!(actual_path.as_path(), Path::new(expected_path)); +} diff --git a/ddprof-exporter/src/errors.rs b/ddprof-exporter/src/errors.rs index df7828f..30bc43e 100644 --- a/ddprof-exporter/src/errors.rs +++ b/ddprof-exporter/src/errors.rs @@ -4,12 +4,14 @@ use std::error; use std::fmt; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] #[allow(dead_code)] pub(crate) enum Error { InvalidUrl, OperationTimedOut, UnixSockeUnsuported, + CannotEstablishTlsConnection, + NoValidCertifacteRootsFound, } impl fmt::Display for Error { @@ -18,6 +20,12 @@ impl fmt::Display for Error { Self::InvalidUrl => "invalid url", Self::OperationTimedOut => "operation timed out", Self::UnixSockeUnsuported => "unix sockets unsuported on windows", + Self::CannotEstablishTlsConnection => { + "cannot establish requested secure TLS connection" + } + Self::NoValidCertifacteRootsFound => { + "native tls couldn't find any valid certifacte roots" + } }) } }