From 4aa5d8c79bd4f556aa5ee982b0d89b3795f8105a Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Fri, 25 Sep 2026 22:01:03 -0400 Subject: [PATCH 1/2] build(deps): upgrade reqwest to 0.13 Moves http-client-proxy, devolutions-gateway, devolutions-agent and the testsuite to reqwest 0.13, keeping TLS and proxy behavior unchanged. - TLS stays rustls with ring and no OpenSSL. reqwest 0.13 dropped rustls-tls-native-roots, so clients now use rustls-no-provider and http_client_proxy::with_native_roots, which trusts exactly the native root certificates, like before, instead of rustls-platform-verifier. - Proxy behavior is unchanged: system-proxy stays disabled and the Off/System/Manual handling in http-client-proxy is the same. Checked with cargo +nightly fmt, cargo clippy --workspace --tests -D warnings, and cargo test --workspace. Co-Authored-By: Claude Opus 5.5 (1M context) --- Cargo.lock | 83 ++++++++++++++++++++--------- crates/http-client-proxy/Cargo.toml | 4 +- crates/http-client-proxy/src/lib.rs | 50 ++++++++++++++++- devolutions-agent/Cargo.toml | 4 +- devolutions-agent/src/enrollment.rs | 4 +- devolutions-gateway/Cargo.toml | 2 +- devolutions-gateway/src/api/fwd.rs | 6 ++- testsuite/Cargo.toml | 2 +- testsuite/tests/cli/agent/tunnel.rs | 11 ++-- 9 files changed, 129 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e668cbb0..8681b7744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,7 +1585,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.118", ] [[package]] @@ -1761,7 +1761,7 @@ dependencies = [ "quinn", "rand 0.8.7", "rcgen", - "reqwest", + "reqwest 0.13.5", "rustls 0.23.43", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -1872,7 +1872,7 @@ dependencies = [ "pin-project-lite 0.2.17", "proptest", "rand 0.10.2", - "reqwest", + "reqwest 0.13.5", "rstest", "rustls-cng", "rustls-native-certs", @@ -2356,7 +2356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3024,8 +3024,10 @@ dependencies = [ "ipnet", "parking_lot", "proxy_cfg", - "reqwest", + "reqwest 0.13.5", "rstest", + "rustls 0.23.43", + "rustls-native-certs", "tracing", "url", ] @@ -3127,7 +3129,6 @@ dependencies = [ "hyper 1.10.1", "hyper-util", "rustls 0.23.43", - "rustls-native-certs", "tokio 1.52.3", "tokio-rustls", "tower-service", @@ -3188,12 +3189,12 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite 0.2.17", - "socket2 0.5.10", + "socket2 0.6.5", "system-configuration", "tokio 1.52.3", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry 0.6.1", ] [[package]] @@ -3714,7 +3715,7 @@ checksum = "7692ac83a98e4b3ac01405e311bbbd5a33683447032986ed92a4a44d46ad6344" dependencies = [ "ironrdp-async", "ironrdp-connector", - "reqwest", + "reqwest 0.12.28", "tokio 1.52.3", "url", ] @@ -3727,7 +3728,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4913,7 +4914,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6060,7 +6061,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.3", "rustls 0.23.43", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.20", "tokio 1.52.3", "tracing", @@ -6100,9 +6101,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6374,6 +6375,40 @@ dependencies = [ "base64 0.22.1", "bytes 1.12.1", "futures-core", + "h2 0.4.15", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "hyper 1.10.1", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite 0.2.17", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio 1.52.3", + "tower 0.5.3", + "tower-http 0.6.11", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes 1.12.1", + "futures-core", "futures-util", "h2 0.4.15", "http 1.4.2", @@ -6386,13 +6421,11 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite 0.2.17", - "quinn", "rustls 0.23.43", - "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper 1.0.2", "tokio 1.52.3", "tokio-rustls", @@ -6619,7 +6652,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6719,7 +6752,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7274,7 +7307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7578,7 +7611,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7638,7 +7671,7 @@ dependencies = [ "proxy-socks", "quinn", "rcgen", - "reqwest", + "reqwest 0.13.5", "rstest", "rustls 0.23.43", "rustls-pemfile 2.2.0", @@ -8890,9 +8923,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -9040,7 +9073,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/http-client-proxy/Cargo.toml b/crates/http-client-proxy/Cargo.toml index cb2509256..6324a58d4 100644 --- a/crates/http-client-proxy/Cargo.toml +++ b/crates/http-client-proxy/Cargo.toml @@ -10,7 +10,9 @@ workspace = true [dependencies] proxy_cfg = "0.4" -reqwest = { version = "0.12", default-features = false } +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +rustls-native-certs = "0.8" anyhow = "1.0" url = { version = "2.5", features = ["serde"] } tracing = "0.1" diff --git a/crates/http-client-proxy/src/lib.rs b/crates/http-client-proxy/src/lib.rs index a981e4c5d..c4fd795c8 100644 --- a/crates/http-client-proxy/src/lib.rs +++ b/crates/http-client-proxy/src/lib.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::Context as _; use ipnet::IpNet; use parking_lot::RwLock; -use tracing::warn; +use tracing::{debug, warn}; use url::Url; /// Manual proxy configuration with protocol-specific URLs and exclude list. @@ -223,6 +223,49 @@ fn detect_system_proxy_for_url(url: &Url) -> anyhow::Result> { Ok(Some(proxy_url)) } +/// Makes the client trust exactly the platform's native root certificates, verified by rustls with `ring`. +/// +/// This keeps the trust behavior of reqwest 0.12's `rustls-tls-native-roots` feature. +/// reqwest 0.13 otherwise uses `rustls-platform-verifier`, which delegates verification to the OS. +/// Like reqwest 0.12, native certificates that rustls cannot parse are skipped. +/// The `ring` provider is installed as the process default if no provider is installed yet. +pub fn with_native_roots(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder { + let _ = rustls::crypto::ring::default_provider().install_default(); + + let result = rustls_native_certs::load_native_certs(); + + for error in result.errors { + debug!(%error, "Error when loading native certificate"); + } + + let mut store = rustls::RootCertStore::empty(); + let mut certs = Vec::with_capacity(result.certs.len()); + let mut invalid_count = 0usize; + + for cert in result.certs { + let parsed = store + .add(cert.clone()) + .ok() + .and_then(|()| reqwest::Certificate::from_der(&cert).ok()); + + match parsed { + Some(cert) => certs.push(cert), + None => invalid_count += 1, + } + } + + debug!( + valid_count = certs.len(), + invalid_count, "Loaded native root certificates" + ); + + if certs.is_empty() { + warn!("No valid certificates found in platform native certificate store"); + } + + builder.tls_certs_only(certs) +} + /// Builds a reqwest client with proxy configuration for a specific target URL. /// /// This function uses the provided configuration to determine the appropriate @@ -230,7 +273,8 @@ fn detect_system_proxy_for_url(url: &Url) -> anyhow::Result> { /// /// # Arguments /// -/// * `builder` - A reqwest::ClientBuilder to start with (may have timeout, TLS config, etc.) +/// * `builder` - A reqwest::ClientBuilder to start with (may have timeout, etc.); its root certificates are +/// replaced by [`with_native_roots`] /// * `url` - The URL that the client will connect to (used for proxy selection) /// * `config` - Proxy configuration (mode, manual URLs, exclude list) /// @@ -261,6 +305,8 @@ pub fn build_client_with_proxy( } }; + builder = with_native_roots(builder); + if let Some(proxy_url) = proxy_url { // Create reqwest::Proxy from the proxy URL. let proxy = reqwest::Proxy::all(proxy_url.clone()).inspect_err(|error| { diff --git a/devolutions-agent/Cargo.toml b/devolutions-agent/Cargo.toml index 2be961bb6..065fc7f52 100644 --- a/devolutions-agent/Cargo.toml +++ b/devolutions-agent/Cargo.toml @@ -42,7 +42,7 @@ prost-types = "0.13" quinn = "0.11" rand = "0.8" # FIXME(@CBenoit): maybe we don't need this crate rcgen = { version = "0.13", features = ["pem"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "socks", "json"] } +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "http2", "socks", "json"] } rustls = { version = "0.23", default-features = false, features = ["std", "ring"] } rustls-pemfile = "2.2" rustls-pki-types = "1" @@ -89,7 +89,7 @@ time = { version = "0.3", features = ["local-offset", "macros", "parsing"] } devolutions-pedm = { path = "../crates/devolutions-pedm" } notify-debouncer-mini = "0.6" now-package-broker = { path = "../crates/now-package-broker" } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "socks"] } +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "http2", "socks"] } thiserror = "2" uuid = { version = "1.17", features = ["v4"] } win-api-wrappers = { path = "../crates/win-api-wrappers" } diff --git a/devolutions-agent/src/enrollment.rs b/devolutions-agent/src/enrollment.rs index e623e5c07..c9871944e 100644 --- a/devolutions-agent/src/enrollment.rs +++ b/devolutions-agent/src/enrollment.rs @@ -157,7 +157,9 @@ fn generate_key_and_csr(agent_name: &str) -> Result<(String, String)> { } async fn request_enrollment(gateway_url: &str, enrollment_token: &str, csr_pem: &str) -> Result { - let client = reqwest::Client::new(); + let client = http_client_proxy::with_native_roots(reqwest::Client::builder()) + .build() + .context("build HTTP client")?; let enroll_url = format!("{}/jet/tunnel/enroll", gateway_url.trim_end_matches('/')); let response = client diff --git a/devolutions-gateway/Cargo.toml b/devolutions-gateway/Cargo.toml index 6d68ec6e8..d21f8af49 100644 --- a/devolutions-gateway/Cargo.toml +++ b/devolutions-gateway/Cargo.toml @@ -91,7 +91,7 @@ tracing = "0.1" # Async, futures… tokio = { version = "1.52", features = ["signal", "net", "io-util", "time", "rt", "rt-multi-thread", "sync", "macros", "parking_lot", "fs"] } tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "json", "stream", "http2", "socks"] } +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "json", "stream", "http2", "socks"] } http-client-proxy = { path = "../crates/http-client-proxy" } futures = "0.3" async-trait = "0.1" diff --git a/devolutions-gateway/src/api/fwd.rs b/devolutions-gateway/src/api/fwd.rs index b2099519a..707268a24 100644 --- a/devolutions-gateway/src/api/fwd.rs +++ b/devolutions-gateway/src/api/fwd.rs @@ -325,7 +325,11 @@ async fn fwd_http( use tokio_tungstenite::connect_async_tls_with_config; // Default HTTP client for typical usage. - static CLIENT: LazyLock = LazyLock::new(reqwest::Client::new); + static CLIENT: LazyLock = LazyLock::new(|| { + http_client_proxy::with_native_roots(reqwest::Client::builder()) + .build() + .expect("parameters known to be valid only") + }); // Dangerous HTTP client, only to be used when absolutely necessary. // E.g.: VMware services are often using untrusted self-signed certificates. diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml index 88e163f60..ebe6f6c1d 100644 --- a/testsuite/Cargo.toml +++ b/testsuite/Cargo.toml @@ -50,7 +50,7 @@ picky = { version = "7.0.0-rc.25", default-features = false, features = ["jose"] proxy-socks = { path = "../crates/proxy-socks" } quinn = "0.11" rcgen = { version = "0.13", features = ["pem", "x509-parser"] } -reqwest = { version = "0.12", default-features = false, features = ["json"] } +reqwest = { version = "0.13", default-features = false, features = ["json"] } rstest = "0.25" rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } rustls-pemfile = "2" diff --git a/testsuite/tests/cli/agent/tunnel.rs b/testsuite/tests/cli/agent/tunnel.rs index 2c53b788f..3c3f0671a 100644 --- a/testsuite/tests/cli/agent/tunnel.rs +++ b/testsuite/tests/cli/agent/tunnel.rs @@ -423,7 +423,7 @@ async fn wait_for_registered_agent( timeout: Duration, container_logs: impl Fn() -> String, ) { - let client = reqwest::Client::new(); + let client = http_client(); let deadline = Instant::now() + timeout; let mut last_response = serde_json::Value::Null; @@ -562,13 +562,18 @@ async fn assert_explicit_ip_is_refused(http_port: u16, key: &PrivateKey, agent_i ); } +fn http_client() -> reqwest::Client { + let _ = rustls::crypto::ring::default_provider().install_default(); + reqwest::Client::new() +} + async fn delete_agent(http_port: u16, key: &PrivateKey, agent_id: Uuid) { let claims = ScopeTokenClaims { scope: AccessScope::AgentDelete, exp: unix_timestamp() + 60, jti: Uuid::new_v4(), }; - let response = reqwest::Client::new() + let response = http_client() .delete(format!("http://127.0.0.1:{http_port}/jet/tunnel/agents/{agent_id}")) .bearer_auth(sign(key, "SCOPE", &claims)) .send() @@ -583,7 +588,7 @@ async fn list_agents(http_port: u16, key: &PrivateKey) -> Vec exp: unix_timestamp() + 60, jti: Uuid::new_v4(), }; - reqwest::Client::new() + http_client() .get(format!("http://127.0.0.1:{http_port}/jet/tunnel/agents")) .bearer_auth(sign(key, "SCOPE", &claims)) .send() From 3d90e1eff32b1482caf435ee2ef1bcf4bd606d7b Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Sun, 27 Sep 2026 13:47:09 -0400 Subject: [PATCH 2/2] fix(http-client-proxy): match reqwest 0.12 native-roots errors and move provider install to main Follow-up to the reqwest 0.13 upgrade. - with_native_roots now returns an error when the native store has certificates but none of them is valid. The message and source match what reqwest 0.12 returned from ClientBuilder::build ("builder error: zero valid certificates found in native root store"). An empty store is still not an error. - with_native_roots no longer installs the ring provider. The agent now installs it at the top of main, like the gateway already does, and the function docs state the precondition. - build_client_with_proxy and get_or_create_cached_client return anyhow::Result, and UpdaterError::FileDownload carries anyhow::Error. - Added a FIXME about ironrdp-tokio 0.10 still pulling reqwest 0.12 into the agent, and comments on why invalid certificates are filtered and why the forwarding client panics like reqwest 0.12's Client::new. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/http-client-proxy/src/lib.rs | 74 +++++++++++++++---- .../http-client-proxy/tests/proxy_config.rs | 11 ++- devolutions-agent/Cargo.toml | 1 + devolutions-agent/src/enrollment.rs | 1 + devolutions-agent/src/main.rs | 3 + devolutions-agent/src/updater/error.rs | 2 +- devolutions-agent/src/updater/io.rs | 2 +- devolutions-gateway/src/api/fwd.rs | 2 + 8 files changed, 79 insertions(+), 17 deletions(-) diff --git a/crates/http-client-proxy/src/lib.rs b/crates/http-client-proxy/src/lib.rs index c4fd795c8..fe51a7563 100644 --- a/crates/http-client-proxy/src/lib.rs +++ b/crates/http-client-proxy/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, VecDeque}; +use std::fmt::Write as _; use std::net::IpAddr; use std::str::FromStr; @@ -223,21 +224,26 @@ fn detect_system_proxy_for_url(url: &Url) -> anyhow::Result> { Ok(Some(proxy_url)) } -/// Makes the client trust exactly the platform's native root certificates, verified by rustls with `ring`. +/// Makes the client trust exactly the platform's native root certificates, verified by rustls. /// /// This keeps the trust behavior of reqwest 0.12's `rustls-tls-native-roots` feature. /// reqwest 0.13 otherwise uses `rustls-platform-verifier`, which delegates verification to the OS. -/// Like reqwest 0.12, native certificates that rustls cannot parse are skipped. -/// The `ring` provider is installed as the process default if no provider is installed yet. -pub fn with_native_roots(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder { - let _ = rustls::crypto::ring::default_provider().install_default(); - - let result = rustls_native_certs::load_native_certs(); +/// Like reqwest 0.12, native certificates that rustls cannot parse are skipped, +/// and an error is returned when the store has certificates but none of them is valid. +/// +/// The caller must install a process-wide default rustls crypto provider (e.g. `ring`) beforehand, +/// otherwise reqwest panics when the client is built. +pub fn with_native_roots(builder: reqwest::ClientBuilder) -> anyhow::Result { + let certs = native_root_certs(rustls_native_certs::load_native_certs())?; + Ok(builder.tls_certs_only(certs)) +} - for error in result.errors { +fn native_root_certs(result: rustls_native_certs::CertificateResult) -> anyhow::Result> { + for error in &result.errors { debug!(%error, "Error when loading native certificate"); } + // reqwest 0.13 fails the whole build on the first certificate rustls rejects, so filter them out first. let mut store = rustls::RootCertStore::empty(); let mut certs = Vec::with_capacity(result.certs.len()); let mut invalid_count = 0usize; @@ -259,11 +265,26 @@ pub fn with_native_roots(builder: reqwest::ClientBuilder) -> reqwest::ClientBuil invalid_count, "Loaded native root certificates" ); + // Same error, message and source as reqwest 0.12 returned from `ClientBuilder::build`. + if certs.is_empty() && invalid_count > 0 { + let source = if result.errors.is_empty() { + anyhow::Error::msg("zero valid certificates found in native root store") + } else { + let mut acc = String::new(); + for error in &result.errors { + let _ = writeln!(&mut acc, "{error}"); + } + anyhow::Error::msg(acc) + }; + + return Err(source.context("builder error")); + } + if certs.is_empty() { - warn!("No valid certificates found in platform native certificate store"); + warn!("No certificates found in platform native certificate store"); } - builder.tls_certs_only(certs) + Ok(certs) } /// Builds a reqwest client with proxy configuration for a specific target URL. @@ -285,7 +306,7 @@ pub fn build_client_with_proxy( mut builder: reqwest::ClientBuilder, url: &Url, config: &ProxyConfig, -) -> reqwest::Result { +) -> anyhow::Result { let proxy_url = match config { ProxyConfig::Off => { // No proxy mode - never use a proxy. @@ -305,7 +326,7 @@ pub fn build_client_with_proxy( } }; - builder = with_native_roots(builder); + builder = with_native_roots(builder)?; if let Some(proxy_url) = proxy_url { // Create reqwest::Proxy from the proxy URL. @@ -316,7 +337,7 @@ pub fn build_client_with_proxy( builder = builder.proxy(proxy); } - builder.build() + Ok(builder.build()?) } /// Gets or creates a cached HTTP client with proxy configuration. @@ -341,7 +362,7 @@ pub fn get_or_create_cached_client( builder: reqwest::ClientBuilder, url: &Url, config: &ProxyConfig, -) -> reqwest::Result { +) -> anyhow::Result { /// Global cache for HTTP clients. static CLIENT_CACHE: std::sync::LazyLock> = std::sync::LazyLock::new(|| RwLock::new(ClientCache::new())); @@ -372,3 +393,28 @@ pub fn get_or_create_cached_client( Ok(client) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_valid_native_roots_fails_like_reqwest_0_12() { + let mut result = rustls_native_certs::CertificateResult::default(); + result.certs.push(vec![0xDE, 0xAD].into()); + + let error = native_root_certs(result).err().map(|error| format!("{error:#}")); + + assert_eq!( + error.as_deref(), + Some("builder error: zero valid certificates found in native root store") + ); + } + + #[test] + fn empty_native_store_is_not_an_error() { + let certs = native_root_certs(rustls_native_certs::CertificateResult::default()); + + assert!(certs.is_ok_and(|certs| certs.is_empty())); + } +} diff --git a/crates/http-client-proxy/tests/proxy_config.rs b/crates/http-client-proxy/tests/proxy_config.rs index 6e106ad2f..aaaf7e02f 100644 --- a/crates/http-client-proxy/tests/proxy_config.rs +++ b/crates/http-client-proxy/tests/proxy_config.rs @@ -1,9 +1,18 @@ #![allow(clippy::unwrap_used, reason = "test code can panic on errors")] -use http_client_proxy::{ManualProxyConfig, ProxyConfig, build_client_with_proxy}; +use http_client_proxy::{ManualProxyConfig, ProxyConfig}; use rstest::rstest; use url::Url; +fn build_client_with_proxy( + builder: reqwest::ClientBuilder, + url: &Url, + config: &ProxyConfig, +) -> anyhow::Result { + let _ = rustls::crypto::ring::default_provider().install_default(); + http_client_proxy::build_client_with_proxy(builder, url, config) +} + #[rstest] #[case("http://example.com", true)] #[case("https://example.com", true)] diff --git a/devolutions-agent/Cargo.toml b/devolutions-agent/Cargo.toml index 065fc7f52..9e4198abc 100644 --- a/devolutions-agent/Cargo.toml +++ b/devolutions-agent/Cargo.toml @@ -59,6 +59,7 @@ url = { version = "2.5", features = ["serde"] } x509-parser = "0.16" uuid = { version = "1.23", features = ["v4", "serde"] } +# FIXME: "server" still pulls reqwest 0.12 via ironrdp-tokio 0.10; drop it once ironrdp-tokio uses reqwest 0.13. [dependencies.ironrdp] version = "0.17" default-features = false diff --git a/devolutions-agent/src/enrollment.rs b/devolutions-agent/src/enrollment.rs index c9871944e..553c5e052 100644 --- a/devolutions-agent/src/enrollment.rs +++ b/devolutions-agent/src/enrollment.rs @@ -158,6 +158,7 @@ fn generate_key_and_csr(agent_name: &str) -> Result<(String, String)> { async fn request_enrollment(gateway_url: &str, enrollment_token: &str, csr_pem: &str) -> Result { let client = http_client_proxy::with_native_roots(reqwest::Client::builder()) + .context("load native root certificates")? .build() .context("build HTTP client")?; let enroll_url = format!("{}/jet/tunnel/enroll", gateway_url.trim_end_matches('/')); diff --git a/devolutions-agent/src/main.rs b/devolutions-agent/src/main.rs index f8c9a048f..7ffcae6b7 100644 --- a/devolutions-agent/src/main.rs +++ b/devolutions-agent/src/main.rs @@ -207,6 +207,9 @@ fn parse_up_command_args_with_reader(args: &[String], mut stdin_read } fn main() { + // reqwest is built with `rustls-no-provider` and panics on client build without a default provider. + let _ = rustls::crypto::ring::default_provider().install_default(); + let mut controller = Controller::new(SERVICE_NAME, DISPLAY_NAME, DESCRIPTION); if let Some(cmd) = env::args().nth(1) { diff --git a/devolutions-agent/src/updater/error.rs b/devolutions-agent/src/updater/error.rs index fa67bc38f..05cf7257c 100644 --- a/devolutions-agent/src/updater/error.rs +++ b/devolutions-agent/src/updater/error.rs @@ -47,7 +47,7 @@ pub(crate) enum UpdaterError { #[error("missing registry value")] MissingRegistryValue, #[error("failed to download file at {url}")] - FileDownload { source: reqwest::Error, url: String }, + FileDownload { source: anyhow::Error, url: String }, #[error("invalid UTF-8")] Utf8, #[error("IO error")] diff --git a/devolutions-agent/src/updater/io.rs b/devolutions-agent/src/updater/io.rs index 43c7a973f..a0c0908e7 100644 --- a/devolutions-agent/src/updater/io.rs +++ b/devolutions-agent/src/updater/io.rs @@ -53,7 +53,7 @@ pub(crate) async fn download_binary(url: &str, proxy_conf: &ProxyConf) -> Result .send() .and_then(|response| response.bytes()) .map_err(|source| UpdaterError::FileDownload { - source, + source: source.into(), url: url.to_owned(), }) .await?; diff --git a/devolutions-gateway/src/api/fwd.rs b/devolutions-gateway/src/api/fwd.rs index 707268a24..32865f80a 100644 --- a/devolutions-gateway/src/api/fwd.rs +++ b/devolutions-gateway/src/api/fwd.rs @@ -325,8 +325,10 @@ async fn fwd_http( use tokio_tungstenite::connect_async_tls_with_config; // Default HTTP client for typical usage. + // Panics like reqwest 0.12's `Client::new` did when the native store holds no valid certificate. static CLIENT: LazyLock = LazyLock::new(|| { http_client_proxy::with_native_roots(reqwest::Client::builder()) + .expect("native root store holds at least one valid certificate") .build() .expect("parameters known to be valid only") });