claude_agent/client/
network.rs

1//! Network configuration for proxy, TLS, certificate, and connection pool settings.
2
3use std::env;
4use std::path::PathBuf;
5use std::time::Duration;
6
7/// Connection pool configuration.
8#[derive(Clone, Debug)]
9pub struct PoolConfig {
10    pub idle_timeout: Duration,
11    pub max_idle_per_host: usize,
12    pub tcp_keepalive: Option<Duration>,
13    pub http2_keep_alive: Option<Duration>,
14}
15
16impl Default for PoolConfig {
17    fn default() -> Self {
18        Self {
19            idle_timeout: Duration::from_secs(90),
20            max_idle_per_host: 32,
21            tcp_keepalive: Some(Duration::from_secs(60)),
22            http2_keep_alive: Some(Duration::from_secs(30)),
23        }
24    }
25}
26
27impl PoolConfig {
28    pub fn minimal() -> Self {
29        Self {
30            idle_timeout: Duration::from_secs(30),
31            max_idle_per_host: 2,
32            tcp_keepalive: None,
33            http2_keep_alive: None,
34        }
35    }
36
37    pub fn high_throughput() -> Self {
38        Self {
39            idle_timeout: Duration::from_secs(120),
40            max_idle_per_host: 64,
41            tcp_keepalive: Some(Duration::from_secs(30)),
42            http2_keep_alive: Some(Duration::from_secs(15)),
43        }
44    }
45}
46
47/// Network configuration for HTTP client.
48#[derive(Clone, Debug, Default)]
49pub struct NetworkConfig {
50    /// Proxy configuration
51    pub proxy: Option<ProxyConfig>,
52    /// Custom CA certificate file path
53    pub ca_cert: Option<PathBuf>,
54    /// Client certificate for mTLS
55    pub client_cert: Option<ClientCertConfig>,
56    /// Connection pool settings
57    pub pool: Option<PoolConfig>,
58}
59
60/// Proxy server configuration.
61#[derive(Clone, Debug)]
62pub struct ProxyConfig {
63    /// HTTPS proxy URL
64    pub https: Option<String>,
65    /// HTTP proxy URL
66    pub http: Option<String>,
67    /// No-proxy patterns (space or comma separated)
68    pub no_proxy: Vec<String>,
69}
70
71/// Client certificate configuration for mTLS.
72#[derive(Clone, Debug)]
73pub struct ClientCertConfig {
74    /// Path to client certificate (PEM)
75    pub cert_path: PathBuf,
76    /// Path to client private key (PEM)
77    pub key_path: PathBuf,
78    /// Optional passphrase for encrypted key
79    pub key_passphrase: Option<String>,
80}
81
82impl NetworkConfig {
83    /// Create from environment variables.
84    pub fn from_env() -> Self {
85        Self {
86            proxy: ProxyConfig::from_env(),
87            ca_cert: env::var("SSL_CERT_FILE")
88                .ok()
89                .or_else(|| env::var("REQUESTS_CA_BUNDLE").ok())
90                .map(PathBuf::from),
91            client_cert: ClientCertConfig::from_env(),
92            pool: None,
93        }
94    }
95
96    /// Set proxy configuration.
97    pub fn with_proxy(mut self, proxy: ProxyConfig) -> Self {
98        self.proxy = Some(proxy);
99        self
100    }
101
102    /// Set CA certificate path.
103    pub fn with_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
104        self.ca_cert = Some(path.into());
105        self
106    }
107
108    /// Set client certificate for mTLS.
109    pub fn with_client_cert(mut self, cert: ClientCertConfig) -> Self {
110        self.client_cert = Some(cert);
111        self
112    }
113
114    /// Set connection pool configuration.
115    pub fn with_pool(mut self, pool: PoolConfig) -> Self {
116        self.pool = Some(pool);
117        self
118    }
119
120    /// Check if any network configuration is set.
121    pub fn is_configured(&self) -> bool {
122        self.proxy.is_some()
123            || self.ca_cert.is_some()
124            || self.client_cert.is_some()
125            || self.pool.is_some()
126    }
127
128    /// Apply configuration to reqwest ClientBuilder.
129    pub fn apply_to_builder(
130        &self,
131        mut builder: reqwest::ClientBuilder,
132    ) -> Result<reqwest::ClientBuilder, std::io::Error> {
133        if let Some(ref proxy) = self.proxy {
134            builder = proxy.apply_to_builder(builder)?;
135        }
136
137        if let Some(ref ca_path) = self.ca_cert {
138            let cert_data = std::fs::read(ca_path)?;
139            if let Ok(cert) = reqwest::Certificate::from_pem(&cert_data) {
140                builder = builder.add_root_certificate(cert);
141            }
142        }
143
144        if let Some(ref client_cert) = self.client_cert {
145            builder = client_cert.apply_to_builder(builder)?;
146        }
147
148        if let Some(ref pool) = self.pool {
149            builder = builder
150                .pool_idle_timeout(pool.idle_timeout)
151                .pool_max_idle_per_host(pool.max_idle_per_host);
152
153            if let Some(keepalive) = pool.tcp_keepalive {
154                builder = builder.tcp_keepalive(keepalive);
155            }
156
157            if let Some(interval) = pool.http2_keep_alive {
158                builder = builder
159                    .http2_keep_alive_interval(interval)
160                    .http2_keep_alive_while_idle(true);
161            }
162        }
163
164        Ok(builder)
165    }
166}
167
168impl ProxyConfig {
169    /// Create from environment variables.
170    pub fn from_env() -> Option<Self> {
171        let https = env::var("HTTPS_PROXY")
172            .ok()
173            .or_else(|| env::var("https_proxy").ok());
174        let http = env::var("HTTP_PROXY")
175            .ok()
176            .or_else(|| env::var("http_proxy").ok());
177
178        if https.is_none() && http.is_none() {
179            return None;
180        }
181
182        let no_proxy = env::var("NO_PROXY")
183            .ok()
184            .or_else(|| env::var("no_proxy").ok())
185            .map(|s| {
186                s.split([',', ' '])
187                    .map(|p| p.trim().to_string())
188                    .filter(|p| !p.is_empty())
189                    .collect()
190            })
191            .unwrap_or_default();
192
193        Some(Self {
194            https,
195            http,
196            no_proxy,
197        })
198    }
199
200    /// Create with HTTPS proxy.
201    pub fn https(url: impl Into<String>) -> Self {
202        Self {
203            https: Some(url.into()),
204            http: None,
205            no_proxy: Vec::new(),
206        }
207    }
208
209    /// Add HTTP proxy.
210    pub fn with_http(mut self, url: impl Into<String>) -> Self {
211        self.http = Some(url.into());
212        self
213    }
214
215    /// Add no-proxy patterns.
216    pub fn with_no_proxy(mut self, patterns: impl IntoIterator<Item = String>) -> Self {
217        self.no_proxy.extend(patterns);
218        self
219    }
220
221    /// Apply to reqwest ClientBuilder.
222    pub fn apply_to_builder(
223        &self,
224        mut builder: reqwest::ClientBuilder,
225    ) -> Result<reqwest::ClientBuilder, std::io::Error> {
226        if let Some(ref https_url) = self.https
227            && let Ok(proxy) = reqwest::Proxy::https(https_url)
228        {
229            builder = builder.proxy(proxy);
230        }
231        if let Some(ref http_url) = self.http
232            && let Ok(proxy) = reqwest::Proxy::http(http_url)
233        {
234            builder = builder.proxy(proxy);
235        }
236        // Note: no_proxy is typically handled by the proxy itself or system config
237        Ok(builder)
238    }
239}
240
241impl ClientCertConfig {
242    /// Create from environment variables.
243    pub fn from_env() -> Option<Self> {
244        let cert_path = env::var("CLAUDE_CODE_CLIENT_CERT").ok()?;
245        let key_path = env::var("CLAUDE_CODE_CLIENT_KEY").ok()?;
246        let key_passphrase = env::var("CLAUDE_CODE_CLIENT_KEY_PASSPHRASE").ok();
247
248        Some(Self {
249            cert_path: PathBuf::from(cert_path),
250            key_path: PathBuf::from(key_path),
251            key_passphrase,
252        })
253    }
254
255    /// Create with certificate paths.
256    pub fn new(cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
257        Self {
258            cert_path: cert_path.into(),
259            key_path: key_path.into(),
260            key_passphrase: None,
261        }
262    }
263
264    /// Set key passphrase.
265    pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
266        self.key_passphrase = Some(passphrase.into());
267        self
268    }
269
270    /// Apply to reqwest ClientBuilder.
271    pub fn apply_to_builder(
272        &self,
273        builder: reqwest::ClientBuilder,
274    ) -> Result<reqwest::ClientBuilder, std::io::Error> {
275        let cert_data = std::fs::read(&self.cert_path)?;
276        let key_data = std::fs::read(&self.key_path)?;
277
278        let mut pem_data = cert_data;
279        pem_data.extend_from_slice(b"\n");
280        pem_data.extend_from_slice(&key_data);
281
282        let identity = reqwest::Identity::from_pem(&pem_data)
283            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
284
285        Ok(builder.identity(identity))
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_proxy_config_builder() {
295        let proxy = ProxyConfig::https("https://proxy.example.com:8080")
296            .with_http("http://proxy.example.com:8080")
297            .with_no_proxy(vec!["localhost".to_string(), "*.internal".to_string()]);
298
299        assert!(proxy.https.is_some());
300        assert!(proxy.http.is_some());
301        assert_eq!(proxy.no_proxy.len(), 2);
302    }
303
304    #[test]
305    fn test_network_config_builder() {
306        let config = NetworkConfig::default()
307            .with_proxy(ProxyConfig::https("https://proxy.com"))
308            .with_ca_cert("/path/to/ca.pem");
309
310        assert!(config.proxy.is_some());
311        assert!(config.ca_cert.is_some());
312        assert!(config.is_configured());
313    }
314}