diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml index 15074082981..2e2e8809b7f 100644 --- a/litellm-rust/Cargo.toml +++ b/litellm-rust/Cargo.toml @@ -26,7 +26,7 @@ pythonize = "0.29.0" rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "http2", "stream"] } rstest = "0.26.1" -rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10" diff --git a/litellm-rust/crates/ai-gateway/src/io/tls.rs b/litellm-rust/crates/ai-gateway/src/io/tls.rs index e3754e44002..96544adffb7 100644 --- a/litellm-rust/crates/ai-gateway/src/io/tls.rs +++ b/litellm-rust/crates/ai-gateway/src/io/tls.rs @@ -8,7 +8,7 @@ //! Installing from the dial rather than from a `main` also covers the `cdylib` //! the Python bridge loads, the tests, and the benches, none of which have one. //! ring is what reqwest already falls back to, so installing it changes no -//! working path, and an embedder that installed its own provider first keeps it. +//! working path, and whoever installs into this rustls build first still wins. use std::sync::Once; @@ -38,13 +38,32 @@ where #[cfg(test)] mod tests { + use rustls::crypto::CryptoProvider; + use super::ensure_crypto_provider; + fn fingerprint( + provider: &CryptoProvider, + ) -> (Vec, Vec) { + ( + provider + .cipher_suites + .iter() + .map(|suite| suite.suite()) + .collect(), + provider + .kx_groups + .iter() + .map(|group| group.name()) + .collect(), + ) + } + #[test] fn client_config_builder_works_with_both_provider_features_enabled() { ensure_crypto_provider(); - assert!(rustls::crypto::CryptoProvider::get_default().is_some()); + assert!(CryptoProvider::get_default().is_some()); let config = rustls::ClientConfig::builder() .with_root_certificates(rustls::RootCertStore::empty()) @@ -53,13 +72,29 @@ mod tests { assert!(!config.crypto_provider().cipher_suites.is_empty()); } + #[test] + fn installs_ring_rather_than_aws_lc_rs() { + ensure_crypto_provider(); + + let installed = CryptoProvider::get_default().expect("a provider is installed"); + + assert_eq!( + fingerprint(installed), + fingerprint(&rustls::crypto::ring::default_provider()) + ); + assert_ne!( + fingerprint(installed), + fingerprint(&rustls::crypto::aws_lc_rs::default_provider()) + ); + } + #[test] fn ensure_crypto_provider_is_idempotent() { ensure_crypto_provider(); - let first = rustls::crypto::CryptoProvider::get_default().cloned(); + let first = CryptoProvider::get_default().cloned(); ensure_crypto_provider(); - let second = rustls::crypto::CryptoProvider::get_default().cloned(); + let second = CryptoProvider::get_default().cloned(); assert!(first.is_some()); assert!(std::sync::Arc::ptr_eq(&first.unwrap(), &second.unwrap())); diff --git a/litellm-rust/crates/ai-gateway/tests/crypto_provider_wiring.rs b/litellm-rust/crates/ai-gateway/tests/crypto_provider_wiring.rs new file mode 100644 index 00000000000..db5698f8460 --- /dev/null +++ b/litellm-rust/crates/ai-gateway/tests/crypto_provider_wiring.rs @@ -0,0 +1,41 @@ +//! Guards the wiring, not just the helper: the dial itself has to install the +//! rustls provider, in a test binary where nothing else has installed one. + +use std::collections::HashMap; +use std::time::Duration; + +use litellm_ai_gateway::io::responses_ws::ResponsesWebSocketConnection; +use tokio::net::TcpListener; + +#[tokio::test] +async fn dialing_wss_returns_an_error_instead_of_panicking() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind a loopback port"); + let port = listener + .local_addr() + .expect("read the bound address") + .port(); + + tokio::spawn(async move { + while let Ok((stream, _peer)) = listener.accept().await { + drop(stream); + } + }); + + let result = ResponsesWebSocketConnection::connect_url( + &format!("wss://127.0.0.1:{port}/"), + &HashMap::new(), + Some(Duration::from_secs(10)), + ) + .await; + + assert!( + result.is_err(), + "a plain TCP server cannot finish a TLS handshake" + ); + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "the dial is what installs the process-wide provider" + ); +}