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
38#[derive(Clone, Debug, Default)]
40pub struct NetworkConfig {
41 pub proxy: Option<ProxyConfig>,
43 pub ca_cert: Option<PathBuf>,
45 pub client_cert: Option<ClientCertConfig>,
47 pub pool: Option<PoolConfig>,
49}
50
51#[derive(Clone, Debug)]
53pub struct ProxyConfig {
54 pub https: Option<String>,
56 pub http: Option<String>,
58 pub no_proxy: Vec<String>,
60}
61
62#[derive(Clone, Debug)]
64pub struct ClientCertConfig {
65 pub cert_path: PathBuf,
67 pub key_path: PathBuf,
69 pub key_passphrase: Option<String>,
71}
72
73impl NetworkConfig {
74 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 pub fn with_proxy(mut self, proxy: ProxyConfig) -> Self {
89 self.proxy = Some(proxy);
90 self
91 }
92
93 pub fn with_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
95 self.ca_cert = Some(path.into());
96 self
97 }
98
99 pub fn with_client_cert(mut self, cert: ClientCertConfig) -> Self {
101 self.client_cert = Some(cert);
102 self
103 }
104
105 pub fn with_pool(mut self, pool: PoolConfig) -> Self {
107 self.pool = Some(pool);
108 self
109 }
110
111 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 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 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 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 pub fn with_http(mut self, url: impl Into<String>) -> Self {
202 self.http = Some(url.into());
203 self
204 }
205
206 pub fn with_no_proxy(mut self, patterns: impl IntoIterator<Item = String>) -> Self {
208 self.no_proxy.extend(patterns);
209 self
210 }
211
212 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 Ok(builder)
229 }
230}
231
232impl ClientCertConfig {
233 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 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 pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
257 self.key_passphrase = Some(passphrase.into());
258 self
259 }
260
261 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}