From 9635e05d6c942675189a4f53c78ccb3d42a0207c Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Wed, 15 Feb 2023 09:33:59 +1100 Subject: [PATCH] Support adding root cert via env `CARGO_HTTP_CAINFO`, `SSL_CERT_{FILE, PATH}` (#774) * Support for custom root cert in `binstalk_downloader::remote::Client` * Support adding root cert via env `CARGO_HTTP_CAINFO`, `SSL_CERT_{FILE, PATH}` Signed-off-by: Jiahao XU --- crates/bin/src/entry.rs | 15 ++++- crates/binstalk-downloader/src/remote.rs | 24 +++++-- .../src/remote/certificate.rs | 67 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 crates/binstalk-downloader/src/remote/certificate.rs diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs index 69da24ee..7a20e43c 100644 --- a/crates/bin/src/entry.rs +++ b/crates/bin/src/entry.rs @@ -4,7 +4,11 @@ use binstalk::{ errors::BinstallError, fetchers::{Fetcher, GhCrateMeta, QuickInstall}, get_desired_targets, - helpers::{jobserver_client::LazyJobserverClient, remote::Client, tasks::AutoAbortJoinHandle}, + helpers::{ + jobserver_client::LazyJobserverClient, + remote::{Certificate, Client}, + tasks::AutoAbortJoinHandle, + }, ops::{ self, resolve::{CrateName, Resolution, ResolutionFetch, VersionReqExt}, @@ -73,6 +77,15 @@ pub async fn install_crates(args: Args, jobserver_client: LazyJobserverClient) - args.min_tls_version.map(|v| v.into()), Duration::from_millis(rate_limit.duration.get()), rate_limit.request_count, + ["CARGO_HTTP_CAINFO", "SSL_CERT_FILE", "SSL_CERT_PATH"] + .into_iter() + .filter_map(|env_name| match Certificate::from_env(env_name) { + Ok(option) => option, + Err(err) => { + warn!("Failed to load root certificate specified by env {env_name}: {err}",); + None + } + }), ) .map_err(BinstallError::from)?; diff --git a/crates/binstalk-downloader/src/remote.rs b/crates/binstalk-downloader/src/remote.rs index d9cc7ab7..e301614b 100644 --- a/crates/binstalk-downloader/src/remote.rs +++ b/crates/binstalk-downloader/src/remote.rs @@ -22,6 +22,9 @@ pub use url::Url; mod delay_request; use delay_request::DelayRequest; +mod certificate; +pub use certificate::{Certificate, OpenCertificateError}; + const MAX_RETRY_DURATION: Duration = Duration::from_secs(120); const MAX_RETRY_COUNT: u8 = 3; const DEFAULT_RETRY_DURATION_FOR_RATE_LIMIT: Duration = Duration::from_millis(200); @@ -66,23 +69,30 @@ impl Client { min_tls: Option, per: Duration, num_request: NonZeroU64, + certificates: impl IntoIterator, ) -> Result { fn inner( user_agent: &str, min_tls: Option, per: Duration, num_request: NonZeroU64, + certificates: &mut dyn Iterator, ) -> Result { let tls_ver = min_tls .map(|tls| tls.max(DEFAULT_MIN_TLS)) .unwrap_or(DEFAULT_MIN_TLS); - let client = reqwest::ClientBuilder::new() + let mut builder = reqwest::ClientBuilder::new() .user_agent(user_agent) .https_only(true) .min_tls_version(tls_ver) - .tcp_nodelay(false) - .build()?; + .tcp_nodelay(false); + + for certificate in certificates { + builder = builder.add_root_certificate(certificate.0); + } + + let client = builder.build()?; Ok(Client(Arc::new(Inner { client: client.clone(), @@ -94,7 +104,13 @@ impl Client { }))) } - inner(user_agent.as_ref(), min_tls, per, num_request) + inner( + user_agent.as_ref(), + min_tls, + per, + num_request, + &mut certificates.into_iter(), + ) } /// Return inner reqwest client. diff --git a/crates/binstalk-downloader/src/remote/certificate.rs b/crates/binstalk-downloader/src/remote/certificate.rs new file mode 100644 index 00000000..553c5c3f --- /dev/null +++ b/crates/binstalk-downloader/src/remote/certificate.rs @@ -0,0 +1,67 @@ +use std::{env, ffi::OsStr, fs, io, path::Path}; + +use compact_str::CompactString; +use reqwest::tls; +use thiserror::Error as ThisError; + +use super::ReqwestError; + +#[derive(Debug, ThisError)] +pub enum OpenCertificateError { + #[error(transparent)] + Reqwest(#[from] ReqwestError), + + #[error(transparent)] + Io(#[from] io::Error), + + #[error("Expected extension .pem or .der, but found {0:#?}")] + UnknownExtensions(Option), +} + +#[derive(Clone, Debug)] +pub struct Certificate(pub(super) tls::Certificate); + +impl Certificate { + /// Open Certificate with path specified by the environment variable `name` + pub fn from_env(name: impl AsRef) -> Result, OpenCertificateError> { + Self::from_env_inner(name.as_ref()) + } + + fn from_env_inner(name: &OsStr) -> Result, OpenCertificateError> { + env::var_os(name) + .map(|value| Self::open_inner(Path::new(&value))) + .transpose() + } + + /// Open Certificate on disk and automatically detect its format based on + /// its extension. + pub fn open(path: impl AsRef) -> Result { + Self::open_inner(path.as_ref()) + } + + fn open_inner(path: &Path) -> Result { + let ext = path.extension(); + + let f = if ext == Some(OsStr::new("pem")) { + Self::from_pem + } else if ext == Some(OsStr::new("der")) { + Self::from_der + } else { + return Err(OpenCertificateError::UnknownExtensions( + ext.map(|os_str| os_str.to_string_lossy().into()), + )); + }; + + Ok(f(fs::read(path)?)?) + } + + /// Create a Certificate from a binary DER encoded certificate + pub fn from_der(der: impl AsRef<[u8]>) -> Result { + tls::Certificate::from_der(der.as_ref()).map(Self) + } + + /// Create a Certificate from a PEM encoded certificate + pub fn from_pem(pem: impl AsRef<[u8]>) -> Result { + tls::Certificate::from_pem(pem.as_ref()).map(Self) + } +}