use std::io::Cursor;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::task::JoinHandle;
use crate::config::HttpConfig;
pub(crate) const HARNESS_IO_TIMEOUT: Duration = Duration::from_secs(10);
pub(crate) const HARNESS_MAX_CONNECTIONS: usize = 4;
pub(crate) const HARNESS_READ_CAP: usize = 8 * 1024;
pub(crate) const HARNESS_OK_REPLY: &str =
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok";
pub(crate) struct TestCa {
pub(crate) ca_params: rcgen::CertificateParams,
pub(crate) ca_key: rcgen::KeyPair,
pub(crate) ca_pem: String,
}
pub(crate) fn gen_test_ca() -> TestCa {
let ca_key = rcgen::KeyPair::generate().expect("ca keygen"); let mut ca_params = rcgen::CertificateParams::default();
ca_params
.distinguished_name
.push(rcgen::DnType::CommonName, "camel-http test ca");
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params
.key_usages
.push(rcgen::KeyUsagePurpose::KeyCertSign);
let ca_cert = ca_params.self_signed(&ca_key).expect("ca self-sign"); TestCa {
ca_pem: ca_cert.pem(),
ca_params,
ca_key,
}
}
pub(crate) fn gen_leaf_signed_by_ca(ca: &TestCa, san_ip: IpAddr) -> (String, String) {
let leaf_key = rcgen::KeyPair::generate().expect("leaf keygen"); let mut leaf_params = rcgen::CertificateParams::default();
leaf_params.subject_alt_names = vec![rcgen::SanType::IpAddress(san_ip)];
let issuer = rcgen::Issuer::from_params(&ca.ca_params, &ca.ca_key);
let leaf_cert = leaf_params
.signed_by(&leaf_key, &issuer)
.expect("leaf sign"); (leaf_cert.pem(), leaf_key.serialize_pem())
}
pub(crate) fn gen_client_identity(ca: &TestCa) -> (String, String) {
let id_key = rcgen::KeyPair::generate().expect("identity keygen"); let mut id_params = rcgen::CertificateParams::default();
id_params
.distinguished_name
.push(rcgen::DnType::CommonName, "camel-http test client");
let issuer = rcgen::Issuer::from_params(&ca.ca_params, &ca.ca_key);
let id_cert = id_params
.signed_by(&id_key, &issuer)
.expect("identity sign"); (id_cert.pem(), id_key.serialize_pem())
}
pub(crate) fn gen_self_signed_server_cert() -> (String, String) {
let server_key = rcgen::KeyPair::generate().expect("server keygen"); let mut server_params = rcgen::CertificateParams::default();
server_params.subject_alt_names =
vec![rcgen::SanType::IpAddress(IpAddr::V4([127, 0, 0, 1].into()))];
let server_cert = server_params
.self_signed(&server_key)
.expect("self-signed server cert"); (server_cert.pem(), server_key.serialize_pem())
}
pub(crate) async fn spawn_tls_server(
server_cert_pem: &str,
server_key_pem: &str,
client_ca_pem: Option<&str>,
) -> (SocketAddr, JoinHandle<()>) {
let chain: Vec<_> = rustls_pemfile::certs(&mut Cursor::new(server_cert_pem.as_bytes()))
.collect::<Result<_, _>>()
.expect("server cert pem parses"); let key = rustls_pemfile::private_key(&mut Cursor::new(server_key_pem.as_bytes()))
.expect("server key pem parses") .expect("server key pem carries a private key section");
let provider: Arc<rustls::crypto::CryptoProvider> =
rustls::crypto::CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| Arc::new(rustls::crypto::aws_lc_rs::default_provider()));
let versions_builder = rustls::ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.expect("stock rustls provider supports the safe default protocol versions");
let server_config = match client_ca_pem {
None => versions_builder
.with_no_client_auth()
.with_single_cert(chain, key),
Some(ca_pem) => {
let client_ca: Vec<_> = rustls_pemfile::certs(&mut Cursor::new(ca_pem.as_bytes()))
.collect::<Result<_, _>>()
.expect("client CA pem parses"); let mut roots = rustls::RootCertStore::empty();
roots.add_parsable_certificates(client_ca);
let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
Arc::new(roots),
provider,
)
.build()
.expect("client cert verifier builds"); versions_builder
.with_client_cert_verifier(verifier)
.with_single_cert(chain, key)
}
}
.expect("server TLS config builds");
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("loopback listener binds"); let addr = listener
.local_addr()
.expect("loopback listener yields a local addr");
let server = tokio::spawn(async move {
for _ in 0..HARNESS_MAX_CONNECTIONS {
let tcp = match tokio::time::timeout(HARNESS_IO_TIMEOUT, listener.accept()).await {
Ok(Ok((stream, _))) => stream,
_ => break,
};
let mut tls_stream =
match tokio::time::timeout(HARNESS_IO_TIMEOUT, acceptor.accept(tcp)).await {
Ok(Ok(stream)) => stream,
_ => continue,
};
let mut buf = vec![0u8; HARNESS_READ_CAP];
let mut seen = 0usize;
loop {
if seen == buf.len() || buf[..seen].ends_with(b"\r\n\r\n") {
break;
}
match tokio::time::timeout(HARNESS_IO_TIMEOUT, tls_stream.read(&mut buf[seen..]))
.await
{
Ok(Ok(0)) | Ok(Err(_)) | Err(_) => break,
Ok(Ok(n)) => seen += n,
}
}
let _ = tokio::time::timeout(
HARNESS_IO_TIMEOUT,
tls_stream.write_all(HARNESS_OK_REPLY.as_bytes()),
)
.await;
let _ = tokio::time::timeout(HARNESS_IO_TIMEOUT, tls_stream.shutdown()).await;
}
});
(addr, server)
}
pub(crate) fn forced_fallback_env<R>(body: impl FnOnce() -> R) -> R {
let _ca_guard = crate::lock_ca_store_test_mutex();
let empty_ca_dir = tempfile::tempdir().expect("tempdir"); let empty_ca_file = empty_ca_dir.path().join("empty-ca-bundle.pem");
std::fs::write(&empty_ca_file, b"").expect("write empty CA file");
let (prev_file, prev_dir) = unsafe {
let prev = (
std::env::var_os("SSL_CERT_FILE"),
std::env::var_os("SSL_CERT_DIR"),
);
std::env::set_var("SSL_CERT_FILE", &empty_ca_file);
std::env::set_var("SSL_CERT_DIR", empty_ca_dir.path());
prev
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(body));
unsafe {
match prev_file {
Some(v) => std::env::set_var("SSL_CERT_FILE", v),
None => std::env::remove_var("SSL_CERT_FILE"),
}
match prev_dir {
Some(v) => std::env::set_var("SSL_CERT_DIR", v),
None => std::env::remove_var("SSL_CERT_DIR"),
}
}
result.unwrap_or_else(|payload| std::panic::resume_unwind(payload))
}
pub(crate) fn force_webpki_fallback<R>(body: impl FnOnce() -> R) -> R {
struct ResetFlag;
impl Drop for ResetFlag {
fn drop(&mut self) {
crate::FORCE_WEBPKI_FALLBACK.with(|c| c.set(false));
}
}
let already_armed = crate::FORCE_WEBPKI_FALLBACK.with(|c| c.replace(true));
assert!(
!already_armed,
"force_webpki_fallback must not nest — the seam flag was already armed"
);
let _reset = ResetFlag;
body()
}
pub(crate) fn force_webpki_fallback_rebuild_failure<R>(body: impl FnOnce() -> R) -> R {
struct ResetBoth;
impl Drop for ResetBoth {
fn drop(&mut self) {
crate::FORCE_WEBPKI_FALLBACK.with(|c| c.set(false));
crate::FORCE_FALLBACK_REBUILD_FAIL.with(|c| c.set(false));
}
}
let rebuild_already_armed = crate::FORCE_FALLBACK_REBUILD_FAIL.with(std::cell::Cell::get);
let fallback_already_armed = crate::FORCE_WEBPKI_FALLBACK.with(std::cell::Cell::get);
assert!(
!rebuild_already_armed && !fallback_already_armed,
"force_webpki_fallback_rebuild_failure must not nest — a seam flag was already armed"
);
crate::FORCE_FALLBACK_REBUILD_FAIL.with(|c| c.set(true));
crate::FORCE_WEBPKI_FALLBACK.with(|c| c.set(true));
let _reset = ResetBoth;
body()
}
pub(crate) fn plain_http_test_client() -> reqwest::Client {
let _ca_guard = crate::lock_ca_store_test_mutex();
reqwest::Client::new()
}
pub(crate) fn build_under_seam(config: &HttpConfig) -> reqwest::Client {
let fallbacks_before = crate::build_client_fallback_count();
let client = force_webpki_fallback(|| {
crate::build_client(config, None).expect("client must build") });
let fallbacks_taken = crate::build_client_fallback_count() - fallbacks_before;
assert_eq!(
fallbacks_taken, 1,
"the force seam must route the build through the webpki fallback"
);
client
}