diff --git a/litellm-rust/crates/core/tests/ocr.rs b/litellm-rust/crates/core/tests/ocr.rs index a6b26bd8a27..e7a8fc0abc1 100644 --- a/litellm-rust/crates/core/tests/ocr.rs +++ b/litellm-rust/crates/core/tests/ocr.rs @@ -6,7 +6,7 @@ use litellm_host::{ host::{Host, HostOp, HostResult}, machine::{HostFailure, Machine, MachineStep}, }; -use litellm_http::{HttpClientConfig, HttpClientPool, HttpSettings}; +use litellm_http::{HttpClientPool, HttpSettings, Resolution}; use litellm_llms::{ base_llm::ocr::{ error::Error as OcrError, @@ -184,7 +184,7 @@ async fn ocr_client_uses_the_injected_http_pool_configuration() { }; let client = OcrClient::new( &HttpClientPool::new(Arc::new(PublicDnsResolver)), - &HttpClientConfig::resolve(&settings).config, + &Resolution::from(&settings).config, UrlPolicy::default(), VertexAuth::default(), ) diff --git a/litellm-rust/crates/http/AGENTS.md b/litellm-rust/crates/http/AGENTS.md new file mode 100644 index 00000000000..08fa34bd799 --- /dev/null +++ b/litellm-rust/crates/http/AGENTS.md @@ -0,0 +1 @@ +- https://github.com/BerriAI/litellm-docs/blob/main/docs/guides/security_settings.md diff --git a/litellm-rust/crates/http/src/config.rs b/litellm-rust/crates/http/src/config.rs index 30216405fc8..a6cc08c210d 100644 --- a/litellm-rust/crates/http/src/config.rs +++ b/litellm-rust/crates/http/src/config.rs @@ -38,84 +38,80 @@ pub struct Resolution { pub unsupported: Vec, } -impl HttpClientConfig { - pub fn resolve(settings: &HttpSettings) -> Resolution { - let (key_exchange_group, unsupported_curve) = match settings - .ssl_ecdh_curve - .as_deref() - .map(str::parse::) - { - None => (None, None), - Some(Ok(group)) => (Some(group), None), - Some(Err(unsupported)) => (None, Some(unsupported)), - }; - let ciphers = settings - .ssl_security_level - .as_deref() - .map(CipherSelection::from); - let verify = match &settings.ssl_verify { - Some(SslVerify::Disabled) => Verify::Disabled, - Some(SslVerify::CaBundle(path)) => Verify::CaBundle(path.clone()), +impl From<&HttpSettings> for Verify { + fn from(settings: &HttpSettings) -> Self { + match &settings.ssl_verify { + Some(SslVerify::Disabled) => Self::Disabled, + Some(SslVerify::CaBundle(path)) => Self::CaBundle(path.clone()), Some(SslVerify::Enabled) | None => settings .ssl_cert_file .clone() - .map_or(Verify::BuiltInRoots, Verify::CaBundle), - }; - let (tls12_cipher_suites, unsupported_ciphers) = ciphers - .map_or((None, Vec::new()), |ciphers| { - (ciphers.tls12_cipher_suites, ciphers.unsupported) - }); - Resolution { - config: Self { - verify, + .map_or(Self::BuiltInRoots, Self::CaBundle), + } + } +} + +impl From<&HttpSettings> for Resolution { + fn from(settings: &HttpSettings) -> Self { + let curve = settings + .ssl_ecdh_curve + .as_deref() + .map(str::parse::) + .transpose(); + let ciphers = settings + .ssl_security_level + .as_deref() + .map(CipherSelection::from) + .unwrap_or_default(); + Self { + config: HttpClientConfig { + verify: Verify::from(settings), client_certificate: settings.ssl_certificate.clone(), - key_exchange_group, - tls12_cipher_suites, + key_exchange_group: curve.clone().ok().flatten(), + tls12_cipher_suites: ciphers.tls12_cipher_suites, force_ipv4: settings.force_ipv4, http2: settings.http2, user_agent: settings.user_agent.clone(), - trust_proxy_env: !settings.ignore_proxy_env - || settings.trust_proxy_env - || settings.http2 - || settings.httpx_transport, + trust_proxy_env: settings.trusts_proxy_env(), connect_timeout: settings.connect_timeout, tcp_keepalive: settings.tcp_keepalive, pool_idle_timeout: settings.pool_idle_timeout, }, - unsupported: unsupported_curve - .into_iter() - .chain(unsupported_ciphers) - .collect(), + unsupported: curve.err().into_iter().chain(ciphers.unsupported).collect(), } } +} - pub fn client_builder(&self) -> Result { +impl TryFrom<&HttpClientConfig> for reqwest::ClientBuilder { + type Error = Error; + + fn try_from(config: &HttpClientConfig) -> Result { let base = reqwest::Client::builder() - .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 { + .use_preconfigured_tls(rustls::ClientConfig::try_from(config)?) + .connect_timeout(config.connect_timeout) + .pool_idle_timeout(config.pool_idle_timeout); + let with_keepalive = match config.tcp_keepalive { None => base, Some(keepalive) => base .tcp_keepalive(keepalive.idle) .tcp_keepalive_interval(keepalive.interval) .tcp_keepalive_retries(keepalive.retries), }; - let with_address = if self.force_ipv4 { + let with_address = if config.force_ipv4 { with_keepalive.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) } else { with_keepalive }; - let with_protocol = if self.http2 { + let with_protocol = if config.http2 { with_address } else { with_address.http1_only() }; - let with_agent = match &self.user_agent { + let with_agent = match &config.user_agent { Some(agent) => with_protocol.user_agent(agent), None => with_protocol, }; - Ok(if self.trust_proxy_env { + Ok(if config.trust_proxy_env { with_agent } else { with_agent.no_proxy() @@ -161,7 +157,7 @@ mod tests { #[case] settings: HttpSettings, #[case] expected: Verify, ) { - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!(config.verify, expected); } @@ -172,7 +168,7 @@ mod tests { ..HttpSettings::default() } .with_environment(&|name: &str| (name == "SSL_VERIFY").then(|| "true".to_string())); - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!(config.verify, Verify::BuiltInRoots); } @@ -188,7 +184,7 @@ mod tests { ssl_ecdh_curve: Some(curve.into()), ..HttpSettings::default() }; - let resolution = HttpClientConfig::resolve(&settings); + let resolution = Resolution::from(&settings); assert_eq!(resolution.config.key_exchange_group, expected); assert_eq!(resolution.unsupported, []); } @@ -199,7 +195,7 @@ mod tests { ssl_ecdh_curve: Some("secp521r1".into()), ..HttpSettings::default() }; - let resolution = HttpClientConfig::resolve(&settings); + let resolution = Resolution::from(&settings); assert_eq!(resolution.config.key_exchange_group, None); assert_eq!( resolution.unsupported, @@ -213,7 +209,7 @@ mod tests { ssl_security_level: Some("DEFAULT@SECLEVEL=1".into()), ..HttpSettings::default() }; - let resolution = HttpClientConfig::resolve(&settings); + let resolution = Resolution::from(&settings); assert_eq!(resolution.config.tls12_cipher_suites, None); assert_eq!( resolution.unsupported, @@ -230,7 +226,7 @@ mod tests { ), ..HttpSettings::default() }; - let resolution = HttpClientConfig::resolve(&settings); + let resolution = Resolution::from(&settings); assert_eq!( resolution.config.tls12_cipher_suites, Some(vec![ @@ -265,7 +261,7 @@ mod tests { pool_idle_timeout: Duration::from_secs(45), ..HttpSettings::default() }; - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!( config, HttpClientConfig { @@ -303,7 +299,7 @@ mod tests { #[case] settings: HttpSettings, #[case] expected: bool, ) { - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!(config.trust_proxy_env, expected); } @@ -312,10 +308,10 @@ mod tests { let path = std::env::temp_dir().join("litellm-http-missing-bundle.pem"); let config = HttpClientConfig { verify: Verify::CaBundle(path.clone()), - ..HttpClientConfig::resolve(&HttpSettings::default()).config + ..Resolution::from(&HttpSettings::default()).config }; assert!(matches!( - config.client_builder(), + reqwest::ClientBuilder::try_from(&config), Err(Error::Read { path: reported, .. }) if reported == path )); } @@ -327,9 +323,9 @@ mod tests { std::fs::write(&path, b"not a certificate").unwrap(); let config = HttpClientConfig { verify: Verify::CaBundle(path.clone()), - ..HttpClientConfig::resolve(&HttpSettings::default()).config + ..Resolution::from(&HttpSettings::default()).config }; - let result = config.client_builder().map(drop); + let result = reqwest::ClientBuilder::try_from(&config).map(drop); std::fs::remove_file(&path).unwrap(); assert!(matches!( result, diff --git a/litellm-rust/crates/http/src/pool.rs b/litellm-rust/crates/http/src/pool.rs index e6e0de9bc5f..330d6de29e8 100644 --- a/litellm-rust/crates/http/src/pool.rs +++ b/litellm-rust/crates/http/src/pool.rs @@ -67,7 +67,9 @@ impl HttpClientPool { { return Ok(pooled.client.clone()); } - let client = self.apply(variant, key.0.client_builder()?).build()?; + let client = self + .apply(variant, reqwest::ClientBuilder::try_from(&key.0)?) + .build()?; self.lock().insert( key, PooledClient { @@ -114,7 +116,7 @@ mod tests { }; use super::*; - use crate::{HttpSettings, Verify}; + use crate::{HttpSettings, Resolution, Verify}; struct FixedResolver(SocketAddr); @@ -132,7 +134,7 @@ mod tests { fn config(user_agent: &str) -> HttpClientConfig { HttpClientConfig { user_agent: Some(user_agent.into()), - ..HttpClientConfig::resolve(&HttpSettings::default()).config + ..Resolution::from(&HttpSettings::default()).config } } diff --git a/litellm-rust/crates/http/src/settings.rs b/litellm-rust/crates/http/src/settings.rs index be2f4f42fb4..6d7bf934e4b 100644 --- a/litellm-rust/crates/http/src/settings.rs +++ b/litellm-rust/crates/http/src/settings.rs @@ -111,6 +111,10 @@ impl HttpSettings { } } + pub fn trusts_proxy_env(&self) -> bool { + !self.ignore_proxy_env || self.trust_proxy_env || self.http2 || self.httpx_transport + } + pub fn without_missing_files(self, exists: &dyn Fn(&Path) -> bool) -> Self { Self { ssl_verify: match self.ssl_verify { diff --git a/litellm-rust/crates/http/src/tls.rs b/litellm-rust/crates/http/src/tls.rs index 49405b97366..aaae2b659e3 100644 --- a/litellm-rust/crates/http/src/tls.rs +++ b/litellm-rust/crates/http/src/tls.rs @@ -97,6 +97,7 @@ pub enum Unsupported { CipherToken(String), } +#[derive(Default)] pub(crate) struct CipherSelection { pub(crate) tls12_cipher_suites: Option>, pub(crate) unsupported: Vec, @@ -304,10 +305,10 @@ mod tests { use rustls::NamedGroup; use super::*; - use crate::HttpSettings; + use crate::{HttpSettings, Resolution}; fn config(settings: HttpSettings) -> HttpClientConfig { - HttpClientConfig::resolve(&settings).config + Resolution::from(&settings).config } fn offered_groups(tls: &ClientConfig) -> Vec { diff --git a/litellm-rust/crates/llms/src/custom_httpx/media.rs b/litellm-rust/crates/llms/src/custom_httpx/media.rs index 02d152d3ef1..572e7f12e54 100644 --- a/litellm-rust/crates/llms/src/custom_httpx/media.rs +++ b/litellm-rust/crates/llms/src/custom_httpx/media.rs @@ -350,7 +350,7 @@ impl Resolve for PublicDnsResolver { mod tests { use std::collections::HashSet; - use litellm_http::HttpSettings; + use litellm_http::{HttpSettings, Resolution}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, @@ -445,7 +445,7 @@ mod tests { ) -> MediaFetcher { let direct = HttpClientConfig { trust_proxy_env: false, - ..HttpClientConfig::resolve(&HttpSettings::default()).config + ..Resolution::from(&HttpSettings::default()).config }; MediaFetcher::with_resolution( &HttpClientPool::new(Arc::new(LoopbackDnsResolver(pinned_address))), @@ -636,7 +636,7 @@ mod tests { async fn rejects_url_credentials_before_network_access() { let fetcher = MediaFetcher::new( &HttpClientPool::new(Arc::new(PublicDnsResolver)), - &HttpClientConfig::resolve(&HttpSettings::default()).config, + &Resolution::from(&HttpSettings::default()).config, UrlPolicy::default(), ) .expect("media fetcher builds"); diff --git a/litellm-rust/crates/python-bridge/src/http.rs b/litellm-rust/crates/python-bridge/src/http.rs index 118f4669b62..385f78be9fa 100644 --- a/litellm-rust/crates/python-bridge/src/http.rs +++ b/litellm-rust/crates/python-bridge/src/http.rs @@ -4,7 +4,9 @@ use std::{ sync::{Arc, LazyLock, Mutex, PoisonError}, }; -use litellm_http::{HttpClientConfig, HttpClientPool, HttpSettings, SslVerify, Unsupported}; +use litellm_http::{ + HttpClientConfig, HttpClientPool, HttpSettings, Resolution, SslVerify, Unsupported, +}; use litellm_llms::custom_httpx::media::{PublicDnsResolver, UrlPolicy}; use pyo3::{prelude::*, types::PyDict}; @@ -28,7 +30,7 @@ pub(crate) fn call_config( .with_environment(&|name| std::env::var(name).ok()); let settings = for_call(configured, call_ssl_verify(kwargs)?, asynchronous) .without_missing_files(&|path: &Path| path.exists()); - let resolution = HttpClientConfig::resolve(&settings); + let resolution = Resolution::from(&settings); for unsupported in unreported(&REPORTED_UNSUPPORTED, resolution.unsupported) { PythonSettings::warn(py, &unsupported.to_string())?; } @@ -251,7 +253,7 @@ user_agent='litellm/9.9.9', Python::initialize(); Python::attach(|py| { let settings = settings(&python_settings(py, overrides)).unwrap(); - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!(config.verify, expected); }); } @@ -345,7 +347,7 @@ user_agent='litellm/9.9.9', ..HttpSettings::default() }; let settings = for_call(opted_out, None, asynchronous); - let config = HttpClientConfig::resolve(&settings).config; + let config = Resolution::from(&settings).config; assert_eq!(config.trust_proxy_env, expected); } }