claude_agent/client/
network.rs

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