mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
refactor(rust): parse TLS settings through FromStr, From and TryFrom
KeyExchangeGroup and Tls12CipherSuite parse with FromStr and fail with Unsupported, so a setting rustls cannot honor is a typed error instead of a missing value. The cipher string conversions cannot fail and use From. The rustls ClientConfig is built with TryFrom<&HttpClientConfig>, and the built-in root store is constructed in one expression
This commit is contained in:
parent
51010ea486
commit
ffbfe7205f
3 changed files with 113 additions and 105 deletions
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
use crate::{
|
||||
error::Error,
|
||||
settings::{HttpSettings, SslVerify, TcpKeepalive},
|
||||
tls::{self, KeyExchangeGroup, Tls12CipherSuite, Unsupported},
|
||||
tls::{CipherSelection, KeyExchangeGroup, Tls12CipherSuite, Unsupported},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
|
@ -43,7 +43,7 @@ impl HttpClientConfig {
|
|||
let (key_exchange_group, unsupported_curve) = match settings
|
||||
.ssl_ecdh_curve
|
||||
.as_deref()
|
||||
.map(KeyExchangeGroup::from_openssl_name)
|
||||
.map(str::parse::<KeyExchangeGroup>)
|
||||
{
|
||||
None => (None, None),
|
||||
Some(Ok(group)) => (Some(group), None),
|
||||
|
|
@ -52,7 +52,7 @@ impl HttpClientConfig {
|
|||
let ciphers = settings
|
||||
.ssl_security_level
|
||||
.as_deref()
|
||||
.map(tls::parse_cipher_string);
|
||||
.map(CipherSelection::from);
|
||||
let verify = match &settings.ssl_verify {
|
||||
Some(SslVerify::Disabled) => Verify::Disabled,
|
||||
Some(SslVerify::CaBundle(path)) => Verify::CaBundle(path.clone()),
|
||||
|
|
@ -91,7 +91,7 @@ impl HttpClientConfig {
|
|||
|
||||
pub fn client_builder(&self) -> Result<reqwest::ClientBuilder, Error> {
|
||||
let base = reqwest::Client::builder()
|
||||
.use_preconfigured_tls(tls::client_config(self)?)
|
||||
.use_preconfigured_tls(rustls::ClientConfig::try_from(self)?)
|
||||
.connect_timeout(self.connect_timeout)
|
||||
.pool_idle_timeout(self.pool_idle_timeout);
|
||||
let with_keepalive = match self.tcp_keepalive {
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ pub use error::Error;
|
|||
pub use pool::{ClientVariant, HttpClientPool};
|
||||
pub use proxy::EnvironmentProxies;
|
||||
pub use settings::{HttpSettings, SslVerify, TcpKeepalive};
|
||||
pub use tls::{KeyExchangeGroup, Tls12CipherSuite, Unsupported, client_config};
|
||||
pub use tls::{KeyExchangeGroup, Tls12CipherSuite, Unsupported};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fmt, path::Path, sync::Arc};
|
||||
use std::{fmt, path::Path, str::FromStr, sync::Arc};
|
||||
|
||||
use rustls::{
|
||||
CipherSuite, ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
|
||||
|
|
@ -19,8 +19,10 @@ pub enum KeyExchangeGroup {
|
|||
Secp384r1,
|
||||
}
|
||||
|
||||
impl KeyExchangeGroup {
|
||||
pub(crate) fn from_openssl_name(name: &str) -> Result<Self, Unsupported> {
|
||||
impl FromStr for KeyExchangeGroup {
|
||||
type Err = Unsupported;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
match name.trim().to_ascii_lowercase().as_str() {
|
||||
"x25519" => Ok(Self::X25519),
|
||||
"prime256v1" | "secp256r1" | "p-256" => Ok(Self::Secp256r1),
|
||||
|
|
@ -28,7 +30,9 @@ impl KeyExchangeGroup {
|
|||
_ => Err(Unsupported::EcdhCurve(name.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyExchangeGroup {
|
||||
fn supported(self) -> &'static dyn SupportedKxGroup {
|
||||
match self {
|
||||
Self::X25519 => ring::kx_group::X25519,
|
||||
|
|
@ -48,19 +52,23 @@ pub enum Tls12CipherSuite {
|
|||
EcdheRsaChacha20,
|
||||
}
|
||||
|
||||
impl Tls12CipherSuite {
|
||||
fn from_openssl_name(name: &str) -> Option<Self> {
|
||||
impl FromStr for Tls12CipherSuite {
|
||||
type Err = Unsupported;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
match name {
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256" => Some(Self::EcdheEcdsaAes128Gcm),
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384" => Some(Self::EcdheEcdsaAes256Gcm),
|
||||
"ECDHE-ECDSA-CHACHA20-POLY1305" => Some(Self::EcdheEcdsaChacha20),
|
||||
"ECDHE-RSA-AES128-GCM-SHA256" => Some(Self::EcdheRsaAes128Gcm),
|
||||
"ECDHE-RSA-AES256-GCM-SHA384" => Some(Self::EcdheRsaAes256Gcm),
|
||||
"ECDHE-RSA-CHACHA20-POLY1305" => Some(Self::EcdheRsaChacha20),
|
||||
_ => None,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256" => Ok(Self::EcdheEcdsaAes128Gcm),
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384" => Ok(Self::EcdheEcdsaAes256Gcm),
|
||||
"ECDHE-ECDSA-CHACHA20-POLY1305" => Ok(Self::EcdheEcdsaChacha20),
|
||||
"ECDHE-RSA-AES128-GCM-SHA256" => Ok(Self::EcdheRsaAes128Gcm),
|
||||
"ECDHE-RSA-AES256-GCM-SHA384" => Ok(Self::EcdheRsaAes256Gcm),
|
||||
"ECDHE-RSA-CHACHA20-POLY1305" => Ok(Self::EcdheRsaChacha20),
|
||||
_ => Err(Unsupported::CipherToken(name.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tls12CipherSuite {
|
||||
fn suite(self) -> CipherSuite {
|
||||
match self {
|
||||
Self::EcdheEcdsaAes128Gcm => CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
|
|
@ -101,46 +109,47 @@ enum CipherToken {
|
|||
Unsupported(Unsupported),
|
||||
}
|
||||
|
||||
fn cipher_token(token: &str) -> CipherToken {
|
||||
if let Some(suite) = Tls12CipherSuite::from_openssl_name(token) {
|
||||
return CipherToken::Suite(suite);
|
||||
}
|
||||
match token {
|
||||
"DEFAULT" | "ALL" | "HIGH" => CipherToken::EverySuite,
|
||||
"@STRENGTH" | "@SECLEVEL=2" => CipherToken::Ordering,
|
||||
level if level.starts_with("@SECLEVEL=") => {
|
||||
CipherToken::Unsupported(Unsupported::SecurityLevel(level.to_owned()))
|
||||
impl From<&str> for CipherToken {
|
||||
fn from(token: &str) -> Self {
|
||||
match token {
|
||||
"DEFAULT" | "ALL" | "HIGH" => Self::EverySuite,
|
||||
"@STRENGTH" | "@SECLEVEL=2" => Self::Ordering,
|
||||
level if level.starts_with("@SECLEVEL=") => {
|
||||
Self::Unsupported(Unsupported::SecurityLevel(level.to_owned()))
|
||||
}
|
||||
name => name.parse().map_or_else(Self::Unsupported, Self::Suite),
|
||||
}
|
||||
other => CipherToken::Unsupported(Unsupported::CipherToken(other.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_cipher_string(value: &str) -> CipherSelection {
|
||||
let tokens: Vec<CipherToken> = tokenize(value)
|
||||
.iter()
|
||||
.map(|token| cipher_token(token))
|
||||
.collect();
|
||||
let every_suite = tokens
|
||||
.iter()
|
||||
.any(|token| matches!(token, CipherToken::EverySuite));
|
||||
let mut suites: Vec<Tls12CipherSuite> = tokens
|
||||
.iter()
|
||||
.filter_map(|token| match token {
|
||||
CipherToken::Suite(suite) => Some(*suite),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
suites.sort_unstable();
|
||||
suites.dedup();
|
||||
CipherSelection {
|
||||
tls12_cipher_suites: (!every_suite && !suites.is_empty()).then_some(suites),
|
||||
unsupported: tokens
|
||||
.into_iter()
|
||||
impl From<&str> for CipherSelection {
|
||||
fn from(value: &str) -> Self {
|
||||
let tokens: Vec<CipherToken> = tokenize(value)
|
||||
.iter()
|
||||
.map(|token| CipherToken::from(token.as_str()))
|
||||
.collect();
|
||||
let every_suite = tokens
|
||||
.iter()
|
||||
.any(|token| matches!(token, CipherToken::EverySuite));
|
||||
let mut suites: Vec<Tls12CipherSuite> = tokens
|
||||
.iter()
|
||||
.filter_map(|token| match token {
|
||||
CipherToken::Unsupported(unsupported) => Some(unsupported),
|
||||
CipherToken::Suite(suite) => Some(*suite),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
.collect();
|
||||
suites.sort_unstable();
|
||||
suites.dedup();
|
||||
CipherSelection {
|
||||
tls12_cipher_suites: (!every_suite && !suites.is_empty()).then_some(suites),
|
||||
unsupported: tokens
|
||||
.into_iter()
|
||||
.filter_map(|token| match token {
|
||||
CipherToken::Unsupported(unsupported) => Some(unsupported),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,57 +164,56 @@ fn tokenize(value: &str) -> Vec<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn client_config(config: &HttpClientConfig) -> Result<ClientConfig, Error> {
|
||||
let base = ring::default_provider();
|
||||
let provider = Arc::new(CryptoProvider {
|
||||
kx_groups: config
|
||||
.key_exchange_group
|
||||
.map_or_else(|| base.kx_groups.clone(), |group| vec![group.supported()]),
|
||||
cipher_suites: base
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|suite| {
|
||||
suite.tls13().is_some()
|
||||
|| config
|
||||
.tls12_cipher_suites
|
||||
.as_ref()
|
||||
.is_none_or(|allowed| allowed.iter().any(|a| a.suite() == suite.suite()))
|
||||
})
|
||||
.collect(),
|
||||
..base
|
||||
});
|
||||
let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(|error| Error::Client(error.to_string()))?;
|
||||
let verified = match &config.verify {
|
||||
Verify::Disabled => builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerification(provider))),
|
||||
Verify::BuiltInRoots => builder.with_root_certificates(built_in_roots()),
|
||||
Verify::CaBundle(path) => builder.with_root_certificates(bundle_roots(path)?),
|
||||
};
|
||||
let mut tls = match &config.client_certificate {
|
||||
None => verified.with_no_client_auth(),
|
||||
Some(path) => {
|
||||
let (chain, key) = identity(path)?;
|
||||
verified
|
||||
.with_client_auth_cert(chain, key)
|
||||
.map_err(|error| invalid_pem(path, error))?
|
||||
}
|
||||
};
|
||||
tls.alpn_protocols = if config.http2 {
|
||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||
} else {
|
||||
vec![b"http/1.1".to_vec()]
|
||||
};
|
||||
Ok(tls)
|
||||
}
|
||||
impl TryFrom<&HttpClientConfig> for ClientConfig {
|
||||
type Error = Error;
|
||||
|
||||
fn built_in_roots() -> RootCertStore {
|
||||
let mut store = RootCertStore::empty();
|
||||
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
store
|
||||
fn try_from(config: &HttpClientConfig) -> Result<Self, Self::Error> {
|
||||
let base = ring::default_provider();
|
||||
let provider = Arc::new(CryptoProvider {
|
||||
kx_groups: config
|
||||
.key_exchange_group
|
||||
.map_or_else(|| base.kx_groups.clone(), |group| vec![group.supported()]),
|
||||
cipher_suites: base
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|suite| {
|
||||
suite.tls13().is_some()
|
||||
|| config.tls12_cipher_suites.as_ref().is_none_or(|allowed| {
|
||||
allowed.iter().any(|a| a.suite() == suite.suite())
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
..base
|
||||
});
|
||||
let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(|error| Error::Client(error.to_string()))?;
|
||||
let verified = match &config.verify {
|
||||
Verify::Disabled => builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerification(provider))),
|
||||
Verify::BuiltInRoots => builder.with_root_certificates(RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
}),
|
||||
Verify::CaBundle(path) => builder.with_root_certificates(bundle_roots(path)?),
|
||||
};
|
||||
let mut tls = match &config.client_certificate {
|
||||
None => verified.with_no_client_auth(),
|
||||
Some(path) => {
|
||||
let (chain, key) = identity(path)?;
|
||||
verified
|
||||
.with_client_auth_cert(chain, key)
|
||||
.map_err(|error| invalid_pem(path, error))?
|
||||
}
|
||||
};
|
||||
tls.alpn_protocols = if config.http2 {
|
||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||
} else {
|
||||
vec![b"http/1.1".to_vec()]
|
||||
};
|
||||
Ok(tls)
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_roots(path: &Path) -> Result<RootCertStore, Error> {
|
||||
|
|
@ -327,7 +335,7 @@ mod tests {
|
|||
#[case] curve: &str,
|
||||
#[case] expected: NamedGroup,
|
||||
) {
|
||||
let tls = client_config(&config(HttpSettings {
|
||||
let tls = ClientConfig::try_from(&config(HttpSettings {
|
||||
ssl_ecdh_curve: Some(curve.into()),
|
||||
..HttpSettings::default()
|
||||
}))
|
||||
|
|
@ -337,7 +345,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn default_settings_offer_every_group_and_suite_of_the_provider() {
|
||||
let tls = client_config(&config(HttpSettings::default())).unwrap();
|
||||
let tls = ClientConfig::try_from(&config(HttpSettings::default())).unwrap();
|
||||
let provider = ring::default_provider();
|
||||
assert_eq!(offered_groups(&tls).len(), provider.kx_groups.len());
|
||||
assert_eq!(
|
||||
|
|
@ -348,7 +356,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn named_suites_are_the_only_tls12_suites_offered_and_tls13_stays() {
|
||||
let tls = client_config(&config(HttpSettings {
|
||||
let tls = ClientConfig::try_from(&config(HttpSettings {
|
||||
ssl_security_level: Some("ECDHE-RSA-AES256-GCM-SHA384".into()),
|
||||
..HttpSettings::default()
|
||||
}))
|
||||
|
|
@ -369,7 +377,7 @@ mod tests {
|
|||
#[case(true, &[b"h2".as_slice(), b"http/1.1".as_slice()])]
|
||||
#[case(false, &[b"http/1.1".as_slice()])]
|
||||
fn alpn_offers_h2_only_when_http2_is_on(#[case] http2: bool, #[case] expected: &[&[u8]]) {
|
||||
let tls = client_config(&config(HttpSettings {
|
||||
let tls = ClientConfig::try_from(&config(HttpSettings {
|
||||
http2,
|
||||
..HttpSettings::default()
|
||||
}))
|
||||
|
|
@ -388,7 +396,7 @@ mod tests {
|
|||
b"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n",
|
||||
)
|
||||
.unwrap();
|
||||
let result = client_config(&HttpClientConfig {
|
||||
let result = ClientConfig::try_from(&HttpClientConfig {
|
||||
client_certificate: Some(path.clone()),
|
||||
..config(HttpSettings::default())
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue