claude_agent/client/
network.rs1use std::env;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[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#[derive(Clone, Debug, Default)]
49pub struct NetworkConfig {
50 pub proxy: Option<ProxyConfig>,
52 pub ca_cert: Option<PathBuf>,
54 pub client_cert: Option<ClientCertConfig>,
56 pub pool: Option<PoolConfig>,
58}
59
60#[derive(Clone, Debug)]
62pub struct ProxyConfig {
63 pub https: Option<String>,
65 pub http: Option<String>,
67 pub no_proxy: Vec<String>,
69}
70
71#[derive(Clone, Debug)]
73pub struct ClientCertConfig {
74 pub cert_path: PathBuf,
76 pub key_path: PathBuf,
78 pub key_passphrase: Option<String>,
80}
81
82impl NetworkConfig {
83 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 pub fn with_proxy(mut self, proxy: ProxyConfig) -> Self {
98 self.proxy = Some(proxy);
99 self
100 }
101
102 pub fn with_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
104 self.ca_cert = Some(path.into());
105 self
106 }
107
108 pub fn with_client_cert(mut self, cert: ClientCertConfig) -> Self {
110 self.client_cert = Some(cert);
111 self
112 }
113
114 pub fn with_pool(mut self, pool: PoolConfig) -> Self {
116 self.pool = Some(pool);
117 self
118 }
119
120 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 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 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 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 pub fn with_http(mut self, url: impl Into<String>) -> Self {
211 self.http = Some(url.into());
212 self
213 }
214
215 pub fn with_no_proxy(mut self, patterns: impl IntoIterator<Item = String>) -> Self {
217 self.no_proxy.extend(patterns);
218 self
219 }
220
221 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 Ok(builder)
238 }
239}
240
241impl ClientCertConfig {
242 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 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 pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
266 self.key_passphrase = Some(passphrase.into());
267 self
268 }
269
270 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}