Skip to main content

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