Skip to content

Commit

Permalink
feat: DPRINT_TLS_CA_STORE and DPRINT_CERT (#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed May 25, 2024
1 parent 544247d commit bdfcef1
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 18 deletions.
5 changes: 4 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion crates/dprint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/dprint/build.rs
Original file line number Diff line number Diff line change
@@ -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());
Expand Down
1 change: 1 addition & 0 deletions crates/dprint/clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
16 changes: 10 additions & 6 deletions crates/dprint/src/arg_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions crates/dprint/src/environment/real_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down Expand Up @@ -455,6 +456,7 @@ fn canonicalize_path(path: impl AsRef<Path>) -> Result<CanonicalizedPathBuf> {
const CACHE_DIR_ENV_VAR_NAME: &str = "DPRINT_CACHE_DIR";

static CACHE_DIR: Lazy<Result<CanonicalizedPathBuf>> = 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)?;
Expand Down
3 changes: 2 additions & 1 deletion crates/dprint/src/plugins/resolver.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use dprint_core::async_runtime::future;
use dprint_core::communication::IdGenerator;
Expand Down Expand Up @@ -121,7 +122,7 @@ impl<TEnvironment: Environment> PluginResolver<TEnvironment> {
)
}
}
bail!("Error resolving plugin {}: {:#}", plugin_reference.display(), err);
Err(err).with_context(|| format!("Error resolving plugin {}", plugin_reference.display()))
}
}
})
Expand Down
16 changes: 10 additions & 6 deletions crates/dprint/src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,16 @@ OPTIONS:
-L, --log-level <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.
Expand Down
201 changes: 201 additions & 0 deletions crates/dprint/src/utils/certs.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
read_file_bytes: &impl Fn(&str) -> Result<Vec<u8>, std::io::Error>,
) -> Result<RootCertStore, RootCertStoreLoadError> {
let cert_info = load_cert_info(read_env_var, read_file_bytes)?;
Ok(create_root_cert_store(cert_info))
}

struct CertInfo {
ca_stores: Vec<CaStore>,
ca_file: Option<Vec<Vec<u8>>>,
}

fn load_cert_info(
read_env_var: &impl Fn(&str) -> Option<String>,
read_file_bytes: &impl Fn(&str) -> Result<Vec<u8>, std::io::Error>,
) -> Result<CertInfo, RootCertStoreLoadError> {
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<String>) -> Result<Vec<CaStore>, 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<Vec<u8>, std::io::Error>) -> Result<Vec<Vec<u8>>, 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);
}
}
1 change: 1 addition & 0 deletions crates/dprint/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod cached_downloader;
mod certs;
mod checksums;
mod error_count_logger;
mod extract_zip;
Expand Down
11 changes: 11 additions & 0 deletions crates/dprint/src/utils/url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,6 +111,15 @@ enum AgentKind {

fn build_agent(kind: AgentKind) -> Result<ureq::Agent> {
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)?);
}
Expand All @@ -126,6 +136,7 @@ fn get_proxy_url(kind: AgentKind) -> Option<String> {
fn read_proxy_env_var(env_var_name: &str) -> Option<String> {
// 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())
Expand Down
6 changes: 3 additions & 3 deletions dprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}

0 comments on commit bdfcef1

Please sign in to comment.