From bdfcef1389d2d7264e86a6fa6d6f48fea756cd18 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sat, 25 May 2024 10:41:25 -0400 Subject: [PATCH] feat: `DPRINT_TLS_CA_STORE` and `DPRINT_CERT` (#850) --- Cargo.lock | 5 +- crates/dprint/Cargo.toml | 8 +- crates/dprint/build.rs | 1 + crates/dprint/clippy.toml | 1 + crates/dprint/src/arg_parser.rs | 16 +- .../src/environment/real_environment.rs | 2 + crates/dprint/src/plugins/resolver.rs | 3 +- crates/dprint/src/test_helpers.rs | 16 +- crates/dprint/src/utils/certs.rs | 201 ++++++++++++++++++ crates/dprint/src/utils/mod.rs | 1 + crates/dprint/src/utils/url.rs | 11 + dprint.json | 6 +- website/src/setup.md | 11 + 13 files changed, 264 insertions(+), 18 deletions(-) create mode 100644 crates/dprint/src/utils/certs.rs diff --git a/Cargo.lock b/Cargo.lock index 37d1158f4..3d500bcaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,9 @@ dependencies = [ "pretty_assertions", "rand", "rkyv", + "rustls", + "rustls-native-certs", + "rustls-pemfile", "serde", "serde_json", "sha2", @@ -761,6 +764,7 @@ dependencies = [ "url", "wasmer", "wasmer-compiler", + "webpki-roots", "winreg", "zip", ] @@ -2459,7 +2463,6 @@ dependencies = [ "log", "once_cell", "rustls", - "rustls-native-certs", "rustls-webpki", "socks", "url", diff --git a/crates/dprint/Cargo.toml b/crates/dprint/Cargo.toml index 0dde2e9f0..d28cec933 100644 --- a/crates/dprint/Cargo.toml +++ b/crates/dprint/Cargo.toml @@ -40,10 +40,16 @@ tokio = { version = "1", features = ["rt", "time", "macros", "rt-multi-thread", tokio-util = { version = "0.7.10" } tower-lsp = "0.20.0" twox-hash = "1.6.3" -ureq = { version = "2.9.1", features = ["socks-proxy", "tls", "native-certs"] } url = "2.5.0" zip = "0.6.6" +# keep these in sync +rustls = "0.21.8" +rustls-native-certs = "0.6.2" +rustls-pemfile = "1.0.3" +ureq = { version = "2.9.1", features = ["socks-proxy"] } +webpki-roots = "0.25.2" + # pin these to prevent them changing with `cargo install` because # patch version increases of rkyv may cause panics when deserializing # data serialized with older versions diff --git a/crates/dprint/build.rs b/crates/dprint/build.rs index 3d45c27a5..2513c88a9 100644 --- a/crates/dprint/build.rs +++ b/crates/dprint/build.rs @@ -1,3 +1,4 @@ +#[allow(clippy::disallowed_methods)] fn main() { println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap()); println!("cargo:rustc-env=RUSTC_VERSION_TEXT={}", get_rustc_version()); diff --git a/crates/dprint/clippy.toml b/crates/dprint/clippy.toml index cf47601c6..f29f49e95 100644 --- a/crates/dprint/clippy.toml +++ b/crates/dprint/clippy.toml @@ -37,4 +37,5 @@ disallowed-methods = [ { path = "std::fs::set_permissions", reason = "File system operations should be done using an environment" }, { path = "std::fs::symlink_metadata", reason = "File system operations should be done using an environment" }, { path = "std::fs::write", reason = "File system operations should be done using an environment" }, + { path = "std::env::var", reason = "Reading from the env should be done using an environment" }, ] diff --git a/crates/dprint/src/arg_parser.rs b/crates/dprint/src/arg_parser.rs index 72f77a8da..5fbb46278 100644 --- a/crates/dprint/src/arg_parser.rs +++ b/crates/dprint/src/arg_parser.rs @@ -381,12 +381,16 @@ OPTIONS: {options} ENVIRONMENT VARIABLES: - DPRINT_CACHE_DIR Directory to store the dprint cache. Note that this - directory may be periodically deleted by the CLI. - DPRINT_MAX_THREADS Limit the number of threads dprint uses for - formatting (ex. DPRINT_MAX_THREADS=4). - HTTPS_PROXY Proxy to use when downloading plugins or configuration - files (set HTTP_PROXY for HTTP).{after-help}"#) + DPRINT_CACHE_DIR Directory to store the dprint cache. Note that this + directory may be periodically deleted by the CLI. + DPRINT_MAX_THREADS Limit the number of threads dprint uses for + formatting (ex. DPRINT_MAX_THREADS=4). + DPRINT_CERT Load certificate authority from PEM encoded file. + DPRINT_TLS_CA_STORE Comma-separated list of order dependent certificate stores. + Possible values: "mozilla" and "system". + Defaults to "mozilla,system". + HTTPS_PROXY Proxy to use when downloading plugins or configuration + files (set HTTP_PROXY for HTTP).{after-help}"#) .after_help( r#"GETTING STARTED: 1. Navigate to the root directory of a code repository. diff --git a/crates/dprint/src/environment/real_environment.rs b/crates/dprint/src/environment/real_environment.rs index 23bcc6585..4bc5901da 100644 --- a/crates/dprint/src/environment/real_environment.rs +++ b/crates/dprint/src/environment/real_environment.rs @@ -272,6 +272,7 @@ impl Environment for RealEnvironment { } fn max_threads(&self) -> usize { + #[allow(clippy::disallowed_methods)] resolve_max_threads(std::env::var("DPRINT_MAX_THREADS").ok(), std::thread::available_parallelism().ok()) } @@ -455,6 +456,7 @@ fn canonicalize_path(path: impl AsRef) -> Result { const CACHE_DIR_ENV_VAR_NAME: &str = "DPRINT_CACHE_DIR"; static CACHE_DIR: Lazy> = Lazy::new(|| { + #[allow(clippy::disallowed_methods)] let cache_dir = get_cache_dir_internal(|var_name| std::env::var(var_name).ok())?; #[allow(clippy::disallowed_methods)] std::fs::create_dir_all(&cache_dir)?; diff --git a/crates/dprint/src/plugins/resolver.rs b/crates/dprint/src/plugins/resolver.rs index d74371083..de1e88e71 100644 --- a/crates/dprint/src/plugins/resolver.rs +++ b/crates/dprint/src/plugins/resolver.rs @@ -1,4 +1,5 @@ use anyhow::bail; +use anyhow::Context; use anyhow::Result; use dprint_core::async_runtime::future; use dprint_core::communication::IdGenerator; @@ -121,7 +122,7 @@ impl PluginResolver { ) } } - bail!("Error resolving plugin {}: {:#}", plugin_reference.display(), err); + Err(err).with_context(|| format!("Error resolving plugin {}", plugin_reference.display())) } } }) diff --git a/crates/dprint/src/test_helpers.rs b/crates/dprint/src/test_helpers.rs index 28db5f93a..ffd05a331 100644 --- a/crates/dprint/src/test_helpers.rs +++ b/crates/dprint/src/test_helpers.rs @@ -274,12 +274,16 @@ OPTIONS: -L, --log-level Set log level [default: info] [possible values: debug, info, warn, error, silent] ENVIRONMENT VARIABLES: - DPRINT_CACHE_DIR Directory to store the dprint cache. Note that this - directory may be periodically deleted by the CLI. - DPRINT_MAX_THREADS Limit the number of threads dprint uses for - formatting (ex. DPRINT_MAX_THREADS=4). - HTTPS_PROXY Proxy to use when downloading plugins or configuration - files (set HTTP_PROXY for HTTP). + DPRINT_CACHE_DIR Directory to store the dprint cache. Note that this + directory may be periodically deleted by the CLI. + DPRINT_MAX_THREADS Limit the number of threads dprint uses for + formatting (ex. DPRINT_MAX_THREADS=4). + DPRINT_CERT Load certificate authority from PEM encoded file. + DPRINT_TLS_CA_STORE Comma-separated list of order dependent certificate stores. + Possible values: "mozilla" and "system". + Defaults to "mozilla,system". + HTTPS_PROXY Proxy to use when downloading plugins or configuration + files (set HTTP_PROXY for HTTP). GETTING STARTED: 1. Navigate to the root directory of a code repository. diff --git a/crates/dprint/src/utils/certs.rs b/crates/dprint/src/utils/certs.rs new file mode 100644 index 000000000..5f22a8be4 --- /dev/null +++ b/crates/dprint/src/utils/certs.rs @@ -0,0 +1,201 @@ +use std::io::Cursor; + +use rustls::RootCertStore; +use thiserror::Error; + +/// Much of this code lifted and adapted from https://github.com/denoland/deno/blob/5de30c53239ac74843725d981afc0bb8c45bdf16/cli/args/mod.rs#L600 +/// Copyright the Deno authors. MIT License. + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum RootCertStoreLoadError { + #[error("Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")")] + UnknownStore(String), + #[error("Unable to add pem file to certificate store: {0}")] + FailedAddPemFile(String), + #[error("Failed opening CA file: {0}")] + CaFileOpenError(String), +} + +pub fn get_root_cert_store( + read_env_var: &impl Fn(&str) -> Option, + read_file_bytes: &impl Fn(&str) -> Result, std::io::Error>, +) -> Result { + let cert_info = load_cert_info(read_env_var, read_file_bytes)?; + Ok(create_root_cert_store(cert_info)) +} + +struct CertInfo { + ca_stores: Vec, + ca_file: Option>>, +} + +fn load_cert_info( + read_env_var: &impl Fn(&str) -> Option, + read_file_bytes: &impl Fn(&str) -> Result, std::io::Error>, +) -> Result { + Ok(CertInfo { + ca_stores: parse_ca_stores(read_env_var)?, + ca_file: match read_env_var("DPRINT_CERT") { + Some(ca_file) if !ca_file.trim().is_empty() => { + let certs = load_certs_from_file(&ca_file, read_file_bytes)?; + Some(certs) + } + _ => None, + }, + }) +} + +fn create_root_cert_store(info: CertInfo) -> RootCertStore { + let mut root_cert_store = RootCertStore::empty(); + + for store in info.ca_stores { + load_store(store, &mut root_cert_store); + } + + if let Some(ca_file) = info.ca_file { + root_cert_store.add_parsable_certificates(&ca_file); + } + + root_cert_store +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum CaStore { + System, + Mozilla, +} + +fn load_store(store: CaStore, root_cert_store: &mut RootCertStore) { + match store { + CaStore::Mozilla => { + root_cert_store.add_trust_anchors( + webpki_roots::TLS_SERVER_ROOTS + .iter() + .map(|ta| rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)), + ); + } + CaStore::System => { + let roots = rustls_native_certs::load_native_certs().expect("could not load platform certs"); + for root in roots { + root_cert_store + .add(&rustls::Certificate(root.0)) + .expect("Failed to add platform cert to root cert store"); + } + } + } +} + +fn parse_ca_stores(read_env_var: &impl Fn(&str) -> Option) -> Result, RootCertStoreLoadError> { + let Some(env_ca_store) = read_env_var("DPRINT_TLS_CA_STORE") else { + return Ok(vec![CaStore::Mozilla, CaStore::System]); + }; + + let mut values = Vec::with_capacity(2); + for value in env_ca_store.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { + match value { + "system" => { + values.push(CaStore::System); + } + "mozilla" => { + values.push(CaStore::Mozilla); + } + _ => { + return Err(RootCertStoreLoadError::UnknownStore(value.to_string())); + } + } + } + Ok(values) +} + +fn load_certs_from_file(file_path: &str, read_file_bytes: &impl Fn(&str) -> Result, std::io::Error>) -> Result>, RootCertStoreLoadError> { + let certfile = read_file_bytes(file_path).map_err(|err| RootCertStoreLoadError::CaFileOpenError(err.to_string()))?; + let mut reader = Cursor::new(certfile); + rustls_pemfile::certs(&mut reader).map_err(|e| RootCertStoreLoadError::FailedAddPemFile(e.to_string())) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parses_ca_stores() { + let test_cases = vec![ + ("mozilla", Ok(vec![CaStore::Mozilla])), + ("system", Ok(vec![CaStore::System])), + ("mozilla,system", Ok(vec![CaStore::Mozilla, CaStore::System])), + ("system,mozilla", Ok(vec![CaStore::System, CaStore::Mozilla])), + (" system , mozilla, , ,,", Ok(vec![CaStore::System, CaStore::Mozilla])), + ("system,mozilla,other", Err(RootCertStoreLoadError::UnknownStore("other".to_string()))), + ]; + for (input, expected) in test_cases { + let actual = parse_ca_stores(&move |var_name| { + assert_eq!(var_name, "DPRINT_TLS_CA_STORE"); + Some(input.to_string()) + }); + assert_eq!(actual, expected); + } + } + + const ROOT_CA: &[u8] = b"-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMKPPW4tsOymMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV +BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwIBcNMTkxMDIxMTYyODIy +WhgPMjExODA5MjcxNjI4MjJaMCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQDDA9FeGFt +cGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMH/IO +2qtHfyBKwANNPB4K0q5JVSg8XxZdRpTTlz0CwU0oRO3uHrI52raCCfVeiQutyZop +eFZTDWeXGudGAFA2B5m3orWt0s+touPi8MzjsG2TQ+WSI66QgbXTNDitDDBtTVcV +5G3Ic+3SppQAYiHSekLISnYWgXLl+k5CnEfTowg6cjqjVr0KjL03cTN3H7b+6+0S +ws4rYbW1j4ExR7K6BFNH6572yq5qR20E6GqlY+EcOZpw4CbCk9lS8/CWuXze/vMs +OfDcc6K+B625d27wyEGZHedBomT2vAD7sBjvO8hn/DP1Qb46a8uCHR6NSfnJ7bXO +G1igaIbgY1zXirNdAgMBAAGjUDBOMB0GA1UdDgQWBBTzut+pwwDfqmMYcI9KNWRD +hxcIpTAfBgNVHSMEGDAWgBTzut+pwwDfqmMYcI9KNWRDhxcIpTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB9AqSbZ+hEglAgSHxAMCqRFdhVu7MvaQM0 +P090mhGlOCt3yB7kdGfsIrUW6nQcTz7PPQFRaJMrFHPvFvPootkBUpTYR4hTkdce +H6RCRu2Jxl4Y9bY/uezd9YhGCYfUtfjA6/TH9FcuZfttmOOlxOt01XfNvVMIR6RM +z/AYhd+DeOXjr35F/VHeVpnk+55L0PYJsm1CdEbOs5Hy1ecR7ACuDkXnbM4fpz9I +kyIWJwk2zJReKcJMgi1aIinDM9ao/dca1G99PHOw8dnr4oyoTiv8ao6PWiSRHHMi +MNf4EgWfK+tZMnuqfpfO9740KzfcVoMNo4QJD4yn5YxroUOO/Azi +-----END CERTIFICATE-----"; + + #[test] + fn load_cert_file_success() { + let result = load_certs_from_file("path.pem", &|path| { + assert_eq!(path, "path.pem"); + Ok(ROOT_CA.to_vec()) + }) + .unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 807); + } + + #[test] + fn load_cert_file_not_found() { + let err = load_certs_from_file("not_found.pem", &|path| { + Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("not found '{}'", path))) + }) + .err() + .unwrap(); + let err = match err { + RootCertStoreLoadError::CaFileOpenError(e) => e, + _ => unreachable!(), + }; + assert_eq!(err, "not found 'not_found.pem'"); + } + + #[test] + fn loads_cert_info() { + let info = load_cert_info( + &|var| match var { + "DPRINT_TLS_CA_STORE" => Some("mozilla".to_string()), + "DPRINT_CERT" => Some("path.pem".to_string()), + _ => None, + }, + &|path| { + assert_eq!(path, "path.pem"); + Ok(ROOT_CA.to_vec()) + }, + ) + .unwrap(); + assert_eq!(info.ca_stores, vec![CaStore::Mozilla]); + assert_eq!(info.ca_file.unwrap()[0].len(), 807); + } +} diff --git a/crates/dprint/src/utils/mod.rs b/crates/dprint/src/utils/mod.rs index 26fc0f700..e0f298670 100644 --- a/crates/dprint/src/utils/mod.rs +++ b/crates/dprint/src/utils/mod.rs @@ -1,4 +1,5 @@ mod cached_downloader; +mod certs; mod checksums; mod error_count_logger; mod extract_zip; diff --git a/crates/dprint/src/utils/url.rs b/crates/dprint/src/utils/url.rs index af789f388..113107cce 100644 --- a/crates/dprint/src/utils/url.rs +++ b/crates/dprint/src/utils/url.rs @@ -5,6 +5,7 @@ use anyhow::bail; use anyhow::Result; use once_cell::sync::OnceCell; +use super::certs::get_root_cert_store; use super::logging::ProgressBarStyle; use super::logging::ProgressBars; use super::Logger; @@ -110,6 +111,15 @@ enum AgentKind { fn build_agent(kind: AgentKind) -> Result { let mut agent = ureq::AgentBuilder::new(); + if kind == AgentKind::Https { + #[allow(clippy::disallowed_methods)] + let root_store = get_root_cert_store(&|env_var| std::env::var(env_var).ok(), &|file_path| std::fs::read(file_path))?; + let config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + agent = agent.tls_config(Arc::new(config)); + } if let Some(proxy_url) = get_proxy_url(kind) { agent = agent.proxy(ureq::Proxy::new(proxy_url)?); } @@ -126,6 +136,7 @@ fn get_proxy_url(kind: AgentKind) -> Option { fn read_proxy_env_var(env_var_name: &str) -> Option { // too much of a hassle to create a seam for the env var reading // and this struct is created before an env is created anyway + #[allow(clippy::disallowed_methods)] std::env::var(env_var_name.to_uppercase()) .ok() .or_else(|| std::env::var(env_var_name.to_lowercase()).ok()) diff --git a/dprint.json b/dprint.json index b9e141aab..13c437850 100644 --- a/dprint.json +++ b/dprint.json @@ -16,11 +16,11 @@ "**/*-lock.json" ], "plugins": [ - "https://plugins.dprint.dev/typescript-0.89.3.wasm", + "https://plugins.dprint.dev/typescript-0.90.5.wasm", "https://plugins.dprint.dev/json-0.19.2.wasm", - "https://plugins.dprint.dev/markdown-0.16.4.wasm", + "https://plugins.dprint.dev/markdown-0.17.0.wasm", "https://plugins.dprint.dev/toml-0.6.1.wasm", - "https://plugins.dprint.dev/prettier-0.39.0.json@896b70f29ef8213c1b0ba81a93cee9c2d4f39ac2194040313cd433906db7bc7c", + "https://plugins.dprint.dev/prettier-0.40.0.json@68c668863ec834d4be0f6f5ccaab415df75336a992aceb7eeeb14fdf096a9e9c", "https://plugins.dprint.dev/exec-0.4.4.json@c207bf9b9a4ee1f0ecb75c594f774924baf62e8e53a2ce9d873816a408cecbf7" ] } diff --git a/website/src/setup.md b/website/src/setup.md index f4586c370..591be8c0f 100644 --- a/website/src/setup.md +++ b/website/src/setup.md @@ -55,6 +55,17 @@ By default, dprint stores information in the current system user's cache directo You may specify a proxy for dprint to use when downloading plugins or configuration files by setting the `HTTPS_PROXY` and `HTTP_PROXY` environment variables. +## TLS Certificates + +dprint downloads plugins via HTTPS. In some cases you may wish to configure this. This is possible via the following environment variables: + +- `DPRINT_CERT` - Load certificate authority from PEM encoded file. +- `DPRINT_TLS_CA_STORE` - Comma-separated list of order dependent certificate stores. + - Possible values: `mozilla` and `system` + - Defaults to `mozilla,system` + +Requires dprint >= 0.46.0 + ## Limiting Parallelism By default, dprint only runs for a short period of time and so it will try to take advantage of as many CPU cores as it can. This might be an issue in some scenarios, and so you can limit the amount of parallelism by setting the `DPRINT_MAX_THREADS` environment variable in version 0.32 and up (ex. `DPRINT_MAX_THREADS=4`).