diff --git a/Cargo.lock b/Cargo.lock index 6a3a8150..5e662470 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,7 @@ dependencies = [ "crates_io_api", "dirs", "embed-resource", + "file-format", "fs-lock", "log", "miette", @@ -727,6 +728,12 @@ dependencies = [ "instant", ] +[[package]] +name = "file-format" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d645737d3dda11cbf14905e9b943a1bd578cdcb751709b581a924ea9b9b18b5c" + [[package]] name = "filetime" version = "0.2.20" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml index 71d2aabf..c5d45739 100644 --- a/crates/bin/Cargo.toml +++ b/crates/bin/Cargo.toml @@ -24,9 +24,10 @@ pkg-fmt = "zip" [dependencies] binstalk = { path = "../binstalk", version = "0.8.0", default-features = false } binstalk-manifests = { path = "../binstalk-manifests", version = "0.3.0" } -clap = { version = "4.1.6", features = ["derive"] } +clap = { version = "4.1.6", features = ["derive", "env"] } crates_io_api = { version = "0.8.1", default-features = false } dirs = "4.0.0" +file-format = { version = "0.14.0", default-features = false } fs-lock = { version = "0.1.0", path = "../fs-lock" } log = { version = "0.4.17", features = ["std"] } miette = "5.5.0" diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs index 7fe87a27..19b18b77 100644 --- a/crates/bin/src/args.rs +++ b/crates/bin/src/args.rs @@ -203,6 +203,11 @@ pub struct Args { #[clap(help_heading = "Options", long, value_enum, value_name = "VERSION")] pub min_tls_version: Option, + /// Specify the root certificates to use for https connnections, + /// in addition to default system-wide ones. + #[clap(help_heading = "Options", long, env = "BINSTALL_HTTPS_ROOT_CERTS")] + pub root_certificates: Vec, + /// Print logs in json format to be parsable. #[clap(help_heading = "Options", long)] pub json_output: bool, @@ -313,7 +318,7 @@ pub fn parse() -> Args { // Filter extraneous arg when invoked by cargo // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] - let mut args: Vec = std::env::args_os().collect(); + let mut args: Vec = env::args_os().collect(); let args = if args.get(1).map(|arg| arg == "binstall").unwrap_or_default() { // Equivalent to // diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs index 7a20e43c..3e9f30cb 100644 --- a/crates/bin/src/entry.rs +++ b/crates/bin/src/entry.rs @@ -1,4 +1,9 @@ -use std::{fs, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + fs, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use binstalk::{ errors::BinstallError, @@ -17,6 +22,7 @@ use binstalk::{ }; use binstalk_manifests::cargo_toml_binstall::PkgOverride; use crates_io_api::AsyncClient as CratesIoApiClient; +use file_format::FileFormat; use log::LevelFilter; use miette::{miette, Result, WrapErr}; use tokio::task::block_in_place; @@ -77,15 +83,7 @@ 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 - } - }), + read_root_certs(args.root_certificates), ) .map_err(BinstallError::from)?; @@ -180,6 +178,49 @@ pub async fn install_crates(args: Args, jobserver_client: LazyJobserverClient) - Ok(()) } +fn do_read_root_cert(path: &Path) -> Result, BinstallError> { + use std::io::{Read, Seek}; + + let mut file = fs::File::open(path)?; + let file_format = FileFormat::from_reader(&mut file)?; + + let open_cert = match file_format { + FileFormat::PemCertificate => Certificate::from_pem, + FileFormat::DerCertificate => Certificate::from_der, + _ => { + warn!( + "Unable to load {}: Expected pem or der ceritificate but found {file_format}", + path.display() + ); + + return Ok(None); + } + }; + + // Move file back to its head + file.rewind()?; + + let mut buffer = Vec::with_capacity(200); + file.read_to_end(&mut buffer)?; + + open_cert(&buffer).map_err(From::from).map(Some) +} + +fn read_root_certs(root_certificate_paths: Vec) -> impl Iterator { + root_certificate_paths + .into_iter() + .filter_map(|path| match do_read_root_cert(&path) { + Ok(optional_cert) => optional_cert, + Err(err) => { + warn!( + "Failed to load root certificate at {}: {err}", + path.display() + ); + None + } + }) +} + /// Return (install_path, manifests, temp_dir) fn compute_paths_and_load_manifests( roots: Option, diff --git a/crates/binstalk-downloader/src/remote.rs b/crates/binstalk-downloader/src/remote.rs index e301614b..049ccb38 100644 --- a/crates/binstalk-downloader/src/remote.rs +++ b/crates/binstalk-downloader/src/remote.rs @@ -23,7 +23,7 @@ mod delay_request; use delay_request::DelayRequest; mod certificate; -pub use certificate::{Certificate, OpenCertificateError}; +pub use certificate::Certificate; const MAX_RETRY_DURATION: Duration = Duration::from_secs(120); const MAX_RETRY_COUNT: u8 = 3; diff --git a/crates/binstalk-downloader/src/remote/certificate.rs b/crates/binstalk-downloader/src/remote/certificate.rs index 553c5c3f..3c59eac5 100644 --- a/crates/binstalk-downloader/src/remote/certificate.rs +++ b/crates/binstalk-downloader/src/remote/certificate.rs @@ -1,60 +1,11 @@ -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)