use std::sync::{Arc, LazyLock};
static CLIENT_CONFIG: LazyLock<rustls::ClientConfig> = LazyLock::new(|| {
let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.unwrap_or_else(|err| panic!("ring supports the default TLS versions: {err}"))
.with_root_certificates(root_store())
.with_no_client_auth();
config.alpn_protocols = vec![b"http/1.1".to_vec()];
config
});
fn root_store() -> rustls::RootCertStore {
let mut roots = rustls::RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
roots
}
pub fn client_config() -> rustls::ClientConfig {
CLIENT_CONFIG.clone()
}
pub fn client_builder() -> reqwest::ClientBuilder {
reqwest::Client::builder().tls_backend_preconfigured(client_config())
}
#[cfg(all(feature = "db", feature = "http-client"))]
pub fn blocking_client_builder() -> reqwest::blocking::ClientBuilder {
reqwest::blocking::Client::builder().tls_backend_preconfigured(client_config())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_config_uses_ring_and_webpki_roots() {
let config = client_config();
assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]);
let groups = |provider: &rustls::crypto::CryptoProvider| {
provider
.kx_groups
.iter()
.map(|group| group.name())
.collect::<Vec<_>>()
};
assert_eq!(
groups(config.crypto_provider()),
groups(&rustls::crypto::ring::default_provider()),
"framework clients must use the ring provider"
);
assert_eq!(root_store().len(), webpki_roots::TLS_SERVER_ROOTS.len());
}
#[test]
fn client_builder_builds_without_a_process_provider() {
let client = client_builder().build();
assert!(client.is_ok(), "{client:?}");
}
#[test]
fn framework_sources_build_clients_through_this_module() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders = Vec::new();
visit(&src, &mut offenders);
assert!(
offenders.is_empty(),
"bare reqwest client construction: {offenders:#?}"
);
}
fn visit(dir: &std::path::Path, offenders: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).expect("read src dir").flatten() {
let path = entry.path();
if path.is_dir() {
visit(&path, offenders);
continue;
}
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
let is_rust = path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("rs"));
if !is_rust || name == "reqwest_tls.rs" || name == "tests.rs" {
continue;
}
let text = std::fs::read_to_string(&path).expect("read source");
for line in bare_client_lines(&text) {
offenders.push(format!("{}:{line}", path.display()));
}
}
}
fn bare_client_lines(text: &str) -> Vec<usize> {
let text = text.replace("\r\n", "\n");
let production = text
.match_indices("\n#[cfg(test)]\nmod ")
.find(|(at, needle)| {
text[at + needle.len()..]
.lines()
.next()
.is_some_and(|line| line.trim_end().ends_with('{'))
})
.map_or(text.as_str(), |(at, _)| &text[..at]);
let mut found = Vec::new();
for (index, line) in production.lines().enumerate() {
let code = line.trim_start();
if code.starts_with("//") {
continue;
}
let imports_client = code.starts_with("use reqwest::")
&& code
.split(|c: char| !c.is_alphanumeric() && c != '_')
.any(|token| ["Client", "ClientBuilder", "get"].contains(&token));
if imports_client
|| [
"reqwest::Client::new()",
"reqwest::Client::builder()",
"reqwest::Client::default()",
"reqwest::ClientBuilder::new()",
"reqwest::ClientBuilder::default()",
"reqwest::get(",
"reqwest::blocking::Client::new()",
"reqwest::blocking::Client::builder()",
"reqwest::blocking::Client::default()",
"reqwest::blocking::ClientBuilder::new()",
"reqwest::blocking::get(",
]
.iter()
.any(|needle| code.contains(needle))
{
found.push(index + 1);
}
}
found
}
#[test]
fn the_scan_skips_the_test_module_with_either_line_ending() {
let lf = "fn prod() {\n let c = reqwest::Client::new();\n}\n\
#[cfg(test)]\nmod tests {\n fn t() { reqwest::Client::new(); }\n}\n";
let crlf = lf.replace('\n', "\r\n");
assert_eq!(bare_client_lines(lf), vec![2]);
assert_eq!(bare_client_lines(&crlf), vec![2]);
}
}