Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ serde_json = { version = "1.0.132", features = ["preserve_order", "unbounded_dep
serde_stacker = { version = "0.1.12" }
shlex = { version = "2.0.0" }
simdutf8 = { version = "0.1.5" }
ssl-certs = { version = "0.1.1" }
ssl-certs = { version = "0.2.0" }
strum = { version = "0.28.0", features = ["derive"] }
target-lexicon = { version = "0.13.0" }
tempfile = { version = "3.25.0" }
Expand Down
5 changes: 3 additions & 2 deletions crates/prek-consts/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl EnvVars {
pub const PREK_NO_FAST_PATH: &'static str = "PREK_NO_FAST_PATH";
pub const PREK_UV_SOURCE: &'static str = "PREK_UV_SOURCE";
pub const PREK_UV_VERSION: &'static str = "PREK_UV_VERSION";
pub const PREK_SYSTEM_CERTS: &'static str = "PREK_SYSTEM_CERTS";
pub const PREK_NATIVE_TLS: &'static str = "PREK_NATIVE_TLS";
pub const PREK_DOWNLOAD_CHECKSUM_POLICY: &'static str = "PREK_DOWNLOAD_CHECKSUM_POLICY";
pub const SSL_CERT_FILE: &'static str = "SSL_CERT_FILE";
Expand Down Expand Up @@ -307,15 +308,15 @@ mod tests {

let env_vars = EnvVars::from_map(&[
(EnvVars::PREK_DOCKER_NO_INIT, "yes"),
(EnvVars::PREK_NATIVE_TLS, "0"),
(EnvVars::PREK_SYSTEM_CERTS, "0"),
(EnvVars::PREK_ALLOW_NO_CONFIG, "maybe"),
]);
assert_eq!(
env_vars.var_as_bool(EnvVars::PREK_DOCKER_NO_INIT),
Ok(Some(true))
);
assert_eq!(
env_vars.var_as_bool(EnvVars::PREK_NATIVE_TLS),
env_vars.var_as_bool(EnvVars::PREK_SYSTEM_CERTS),
Ok(Some(false))
);
assert_eq!(
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use anyhow::{Context, Result, bail};
use aws_lc_rs::digest::{Context as Sha256Context, SHA256};
use tokio::io::{AsyncRead, ReadBuf};

use crate::http::REQWEST_CLIENT;
use crate::http::reqwest_client;

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) struct Sha256Digest([u8; 32]);
Expand Down Expand Up @@ -98,7 +98,7 @@ pub(crate) async fn fetch_checksum_with(
checksum_url: &str,
customize_request: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
) -> Result<Option<String>> {
let response = customize_request(REQWEST_CLIENT.get(checksum_url))
let response = customize_request(reqwest_client()?.get(checksum_url))
.send()
.await
.with_context(|| format!("Failed to fetch checksum from {checksum_url}"))?;
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/cli/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use prek_update::{UpdateRequest, Updater};
use tracing::{debug, enabled};

use crate::cli::ExitStatus;
use crate::http::REQWEST_CLIENT;
use crate::http::reqwest_client;
use crate::install_source::InstallSource;
use crate::printer::Printer;

Expand Down Expand Up @@ -106,7 +106,7 @@ pub(crate) async fn self_update(
// available version of prek.
match updater
.run(
&REQWEST_CLIENT,
reqwest_client()?,
update_request,
token.as_deref(),
enabled!(tracing::Level::DEBUG),
Expand Down
126 changes: 94 additions & 32 deletions crates/prek/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async fn download_to_temp_file(

debug!(%url, "Downloading");

let response = customize_request(REQWEST_CLIENT.get(url))
let response = customize_request(reqwest_client()?.get(url))
.send()
.await
.and_then(reqwest::Response::error_for_status)
Expand Down Expand Up @@ -145,33 +145,56 @@ async fn download_to_temp_file(
))
}

pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> =
LazyLock::new(|| reqwest_client_from_env(&EnvVars));
#[derive(Debug, thiserror::Error)]
pub(crate) enum ClientError {
#[error("Failed to initialize HTTP client")]
Build(#[source] reqwest::Error),

fn reqwest_client_from_env(env_vars: &impl EnvVarsRead) -> reqwest::Client {
let native_tls = env_vars
.var_as_bool(EnvVars::PREK_NATIVE_TLS)
#[error(
"Failed to initialize HTTP client with system certificates. Ensure system CA certificates are installed, or set `PREK_SYSTEM_CERTS=false` to use bundled certificates"
)]
SystemCertificates(#[source] reqwest::Error),
}

pub(crate) fn reqwest_client() -> Result<&'static reqwest::Client, &'static ClientError> {
static CLIENT: LazyLock<Result<reqwest::Client, ClientError>> =
LazyLock::new(reqwest_client_from_env);

CLIENT.as_ref()
}

fn reqwest_client_from_env() -> Result<reqwest::Client, ClientError> {
// TODO: Remove PREK_NATIVE_TLS compatibility in the next breaking release.
let system_certs_var = if EnvVars.is_set(EnvVars::PREK_SYSTEM_CERTS) {
EnvVars::PREK_SYSTEM_CERTS
} else {
EnvVars::PREK_NATIVE_TLS
};
let system_certs = EnvVars
.var_as_bool(system_certs_var)
.unwrap_or_else(|value| {
warn_user!(
"Invalid value for {}: {:?}. Expected a boolean value; using default ({:?})",
EnvVars::PREK_NATIVE_TLS,
system_certs_var,
value,
"false",
);
Some(false)
})
.unwrap_or(false);

let certs = ssl_certs::load_certs_from_env();
for error in certs.errors {
warn_user!("Failed to load certificates: {error}");
}
let certs = certs
.certs
.iter()
.filter_map(|cert| Certificate::from_der(cert).ok())
.collect();
create_reqwest_client(native_tls, certs)
// An explicitly configured source replaces default roots even when empty.
let certs = ssl_certs::load_certs_from_env().map(|certs| {
for error in certs.errors {
warn_user!("Failed to load certificates: {error}");
}
certs
.certs
.iter()
.filter_map(|cert| Certificate::from_der(cert).ok())
.collect()
});
create_reqwest_client(system_certs, certs)
}

// Bound the connect phase, and fail a download that stalls (no bytes received for this long)
Expand All @@ -180,29 +203,40 @@ fn reqwest_client_from_env(env_vars: &impl EnvVarsRead) -> reqwest::Client {
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const READ_TIMEOUT: Duration = Duration::from_secs(30);

fn create_reqwest_client(native_tls: bool, custom_certs: Vec<Certificate>) -> reqwest::Client {
fn create_reqwest_client(
system_certs: bool,
custom_certs: Option<Vec<Certificate>>,
) -> Result<reqwest::Client, ClientError> {
let system_certs = system_certs && custom_certs.is_none();
let builder = reqwest::ClientBuilder::new()
.tls_backend_rustls()
.user_agent(format!("prek/{}", crate::version::version()))
.connect_timeout(CONNECT_TIMEOUT)
.read_timeout(READ_TIMEOUT);

let builder = if native_tls {
debug!("Using native TLS for reqwest client");
// Use rustls with rustls-platform-verifier which uses the platform's native certificate facilities.
builder.tls_backend_rustls().tls_certs_merge(custom_certs)
let builder = if let Some(custom_certs) = custom_certs {
debug!("Using custom certificates for reqwest client");
builder.tls_certs_only(custom_certs)
} else if system_certs {
debug!("Using system certificates for reqwest client");
// With no explicit roots, reqwest uses rustls-platform-verifier.
builder
} else {
debug!("Using bundled certificates for reqwest client");
let root_certs = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|cert_der| Certificate::from_der(cert_der).ok());

// Merge custom certificates on top of webpki-root-certs
builder
.tls_backend_rustls()
.tls_certs_only(custom_certs)
.tls_certs_merge(root_certs)
builder.tls_certs_only(root_certs)
};

builder.build().expect("Failed to build reqwest client")
builder.build().map_err(|error| {
if system_certs {
ClientError::SystemCertificates(error)
} else {
ClientError::Build(error)
}
})
}

#[cfg(test)]
Expand Down Expand Up @@ -305,11 +339,39 @@ mod tests {
);
}

/// Default roots trust public HTTPS. Unrelated or empty custom roots replace
/// them and fail during certificate verification.
#[tokio::test]
async fn test_native_tls() {
let client = super::create_reqwest_client(true, vec![]);
let resp = client.get("https://github.com").send().await;
assert!(resp.is_ok(), "Failed to send request with native TLS");
async fn test_tls_certificates() -> Result<()> {
let custom_cert = reqwest::Certificate::from_pem(indoc::indoc! {b"
-----BEGIN CERTIFICATE-----
MIIBdDCCARqgAwIBAgIUZ1kmigC8aj49VPno0N1d7vKCDbswCgYIKoZIzj0EAwIw
FzEVMBMGA1UEAwwMcHJlayB0ZXN0IENBMCAXDTI2MTAyNjExNTM1OFoYDzIxMjYx
MDAyMTE1MzU4WjAXMRUwEwYDVQQDDAxwcmVrIHRlc3QgQ0EwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAATnHgUgx6Q0+Oeri/poUmyQLJiXk09eceAySysTGV+SI5wc
dxLzmCZvsolKlQ4A66QRleU3emn0GBKgnz2uqxFYo0IwQDAdBgNVHQ4EFgQU53Go
+jl4xGYHpgHFQpoYrttAbsQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AQYwCgYIKoZIzj0EAwIDSAAwRQIhALvrJ3U2Yk1k+iLH+rjj9jSnpt3bv5bhhKxK
WMKctiPPAiAcPHp1Si2wO726t4qJK5lX9cnTC3Azy/ZASjAzf8Dx1g==
-----END CERTIFICATE-----
"})?;

for system_certs in [false, true] {
let client = super::create_reqwest_client(system_certs, None)?;
client.get("https://example.com").send().await?;

for certs in [vec![], vec![custom_cert.clone()]] {
let client = super::create_reqwest_client(system_certs, Some(certs))?;
let Err(error) = client.get("https://example.com").send().await else {
anyhow::bail!("Custom certificates must replace default roots");
};
assert!(
format!("{error:?}").contains("UnknownIssuer"),
"Expected an untrusted issuer with system_certs={system_certs}: {error:?}"
);
}
}
Ok(())
}

#[tokio::test]
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/languages/deno/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tracing::{debug, trace, warn};
use crate::archive;
use crate::checksum::{Sha256Digest, digest_from_sha256sums, fetch_checksum};
use crate::fs::LockedFile;
use crate::http::{REQWEST_CLIENT, download_artifact};
use crate::http::{download_artifact, reqwest_client};
use crate::languages::deno::DenoRequest;
use crate::languages::deno::version::DenoVersion;
use crate::languages::version::{ToolchainPolicy, ToolchainSource, find_system_executables};
Expand Down Expand Up @@ -187,7 +187,7 @@ impl DenoInstaller {
}

let url = "https://deno.com/versions.json";
let response: VersionsResponse = REQWEST_CLIENT.get(url).send().await?.json().await?;
let response: VersionsResponse = reqwest_client()?.get(url).send().await?.json().await?;

// Versions are already sorted in descending order (newest first)
let versions: Vec<DenoVersion> = response
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/languages/dotnet/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tracing::{debug, trace, warn};

use super::version::DotnetVersion;
use crate::fs::LockedFile;
use crate::http::REQWEST_CLIENT;
use crate::http::reqwest_client;
use crate::languages::dotnet::DotnetRequest;
use crate::languages::version::{ToolchainPolicy, ToolchainSource, find_system_executables};
use crate::process::Cmd;
Expand Down Expand Up @@ -239,7 +239,7 @@ impl DotnetInstaller {
let script_dir = tempfile::tempdir()?;
let script_path = script_dir.path().join(script_name);

let response = REQWEST_CLIENT
let response = reqwest_client()?
.get(script_url)
.send()
.await?
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/languages/golang/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::archive;
use crate::checksum::Sha256Digest;
use crate::fs::LockedFile;
use crate::git;
use crate::http::{REQWEST_CLIENT, download_artifact};
use crate::http::{download_artifact, reqwest_client};
use crate::languages::golang::GoRequest;
use crate::languages::golang::golang::bin_dir;
use crate::languages::golang::version::GoVersion;
Expand Down Expand Up @@ -256,7 +256,7 @@ impl GoInstaller {

async fn fetch_checksum(version: &str, filename: &str) -> Result<Option<Sha256Digest>> {
let url = "https://go.dev/dl/?mode=json&include=all";
let response = REQWEST_CLIENT
let response = reqwest_client()?
.get(url)
.send()
.await
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/languages/node/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tracing::{debug, trace, warn};
use crate::archive;
use crate::checksum::{digest_from_sha256sums, fetch_checksum};
use crate::fs::{LockedFile, is_executable};
use crate::http::{REQWEST_CLIENT, download_artifact};
use crate::http::{download_artifact, reqwest_client};
use crate::languages::node::NodeRequest;
use crate::languages::node::version::NodeVersion;
use crate::languages::version::{ToolchainPolicy, ToolchainSource, find_system_executables};
Expand Down Expand Up @@ -191,7 +191,7 @@ impl NodeInstaller {
/// List all versions of Node.js available on the Node.js website.
async fn list_remote_versions(&self) -> Result<Vec<NodeVersion>> {
let url = "https://nodejs.org/dist/index.json";
let versions: Vec<NodeVersion> = REQWEST_CLIENT.get(url).send().await?.json().await?;
let versions: Vec<NodeVersion> = reqwest_client()?.get(url).send().await?.json().await?;
Ok(versions)
}

Expand Down
6 changes: 3 additions & 3 deletions crates/prek/src/languages/python/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use crate::archive;
use crate::checksum::{digest_from_sha256sums, fetch_checksum};
use crate::fs::LockedFile;
use crate::http::{REQWEST_CLIENT, TempDownload, download_artifact};
use crate::http::{TempDownload, download_artifact, reqwest_client};
use crate::process::Cmd;
use crate::store::{CacheBucket, Store};
use crate::warn_user;
Expand Down Expand Up @@ -381,7 +381,7 @@ impl InstallSource {
};

debug!("Fetching uv metadata from: {}", api_url);
let response = REQWEST_CLIENT
let response = reqwest_client()?
.get(&api_url)
.header("Accept", "*/*")
.send()
Expand Down Expand Up @@ -426,7 +426,7 @@ impl InstallSource {
let simple_url = format!("{}uv/", source.url());

debug!("Fetching from simple API: {}", simple_url);
let response = REQWEST_CLIENT
let response = reqwest_client()?
.get(&simple_url)
.header(ACCEPT, "*/*")
.send()
Expand Down
4 changes: 2 additions & 2 deletions crates/prek/src/languages/ruby/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use tracing::{debug, trace, warn};
use crate::archive;
use crate::checksum::{digest_from_sha256sums, fetch_checksum_with};
use crate::fs::LockedFile;
use crate::http::{DownloadChecksumPolicy, REQWEST_CLIENT, download_artifact_with};
use crate::http::{DownloadChecksumPolicy, download_artifact_with, reqwest_client};
use crate::languages::ruby::RubyRequest;
use crate::languages::version::{ToolchainPolicy, ToolchainSource, find_system_executables};
use crate::process::Cmd;
Expand Down Expand Up @@ -290,7 +290,7 @@ impl RubyInstaller {
let (api_url, is_github) = rv_ruby_api_url(&EnvVars);
let suffix = format!(".{platform}.tar.gz");

let req = REQWEST_CLIENT
let req = reqwest_client()?
.get(&api_url)
.header("Accept", "application/vnd.github+json");
let req = maybe_add_github_auth(req, is_github, &EnvVars);
Expand Down
Loading
Loading