Skip to main content

camel_component_http/
config.rs

1use serde::Deserialize;
2
3use camel_component_api::CamelError;
4
5#[derive(Debug, Clone, PartialEq, Deserialize)]
6pub struct HttpConfig {
7    #[serde(default = "default_connect_timeout_ms")]
8    pub connect_timeout_ms: u64,
9    #[serde(default = "default_pool_max_idle_per_host")]
10    pub pool_max_idle_per_host: usize,
11    #[serde(default = "default_pool_idle_timeout_ms")]
12    pub pool_idle_timeout_ms: u64,
13    #[serde(default)]
14    pub follow_redirects: bool,
15    #[serde(default)]
16    pub max_redirects: Option<usize>,
17    #[serde(default = "default_response_timeout_ms")]
18    pub response_timeout_ms: u64,
19    #[serde(default = "default_read_timeout_ms")]
20    pub read_timeout_ms: u64,
21    #[serde(default = "default_max_body_size")]
22    pub max_body_size: usize,
23    #[serde(default = "default_max_response_bytes")]
24    pub max_response_bytes: usize,
25    #[serde(default = "default_max_request_body")]
26    pub max_request_body: usize,
27    #[serde(default)]
28    pub allow_private_ips: bool,
29    #[serde(default)]
30    pub blocked_hosts: Vec<String>,
31    #[serde(default)]
32    pub ok_status_code_range: Option<String>,
33    #[serde(default)]
34    pub tls: Option<TlsConfig>,
35    #[serde(default)]
36    pub proxy_url: Option<String>,
37}
38
39/// TLS configuration for HTTP/HTTPS client connections.
40#[derive(Debug, Clone, PartialEq, Deserialize)]
41pub struct TlsConfig {
42    /// Enables TLS customization for client connections.
43    pub enabled: bool,
44    /// Verifies peer certificates when true. Defaults to true.
45    #[serde(default = "default_verify_peer")]
46    pub verify_peer: bool,
47    /// Optional path to custom CA certificate bundle (PEM or DER).
48    #[serde(default)]
49    pub ca_cert_path: Option<String>,
50    /// Optional path to client certificate for mTLS (PEM).
51    #[serde(default)]
52    pub client_cert_path: Option<String>,
53    /// Optional path to client private key for mTLS (PEM).
54    #[serde(default)]
55    pub client_key_path: Option<String>,
56    /// If true, skips certificate verification (discouraged).
57    #[serde(default)]
58    pub insecure: bool,
59}
60
61fn default_verify_peer() -> bool {
62    true
63}
64
65impl Default for TlsConfig {
66    fn default() -> Self {
67        Self {
68            enabled: false,
69            verify_peer: default_verify_peer(),
70            ca_cert_path: None,
71            client_cert_path: None,
72            client_key_path: None,
73            insecure: false,
74        }
75    }
76}
77
78/// Server-side TLS configuration for HTTP consumer endpoints.
79///
80/// When present, the HTTP server binds with TLS via `axum_server::from_tcp_rustls`.
81/// When absent, the server binds plain HTTP via `axum::serve`.
82#[derive(Clone)]
83pub struct ServerTlsConfig {
84    /// Path to the PEM-encoded server certificate chain.
85    pub cert_path: String,
86    /// Path to the PEM-encoded server private key.
87    pub key_path: String,
88}
89
90impl std::fmt::Debug for ServerTlsConfig {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ServerTlsConfig")
93            .field("cert_path", &"[REDACTED]")
94            .field("key_path", &"[REDACTED]")
95            .finish()
96    }
97}
98
99fn default_connect_timeout_ms() -> u64 {
100    5_000
101}
102
103fn default_pool_max_idle_per_host() -> usize {
104    100
105}
106
107fn default_pool_idle_timeout_ms() -> u64 {
108    90_000
109}
110
111fn default_response_timeout_ms() -> u64 {
112    30_000
113}
114
115fn default_read_timeout_ms() -> u64 {
116    30_000
117}
118
119fn default_max_body_size() -> usize {
120    10_485_760
121}
122
123fn default_max_response_bytes() -> usize {
124    10_485_760
125}
126
127fn default_max_request_body() -> usize {
128    2_097_152
129}
130
131impl Default for HttpConfig {
132    fn default() -> Self {
133        Self {
134            connect_timeout_ms: default_connect_timeout_ms(),
135            pool_max_idle_per_host: default_pool_max_idle_per_host(),
136            pool_idle_timeout_ms: default_pool_idle_timeout_ms(),
137            follow_redirects: false,
138            max_redirects: None,
139            response_timeout_ms: default_response_timeout_ms(),
140            read_timeout_ms: default_read_timeout_ms(),
141            max_body_size: default_max_body_size(),
142            max_response_bytes: default_max_response_bytes(),
143            max_request_body: default_max_request_body(),
144            allow_private_ips: false,
145            blocked_hosts: Vec::new(),
146            ok_status_code_range: None,
147            tls: None,
148            proxy_url: None,
149        }
150    }
151}
152
153impl HttpConfig {
154    pub fn validate(&self) -> Result<(), CamelError> {
155        if let Some(max_redirects) = self.max_redirects
156            && max_redirects > 20
157        {
158            return Err(CamelError::Config(
159                "max_redirects must be <= 20".to_string(),
160            ));
161        }
162
163        if let Some(range) = &self.ok_status_code_range {
164            parse_ok_status_code_range(range)?;
165        }
166
167        if let Some(proxy_url) = &self.proxy_url {
168            reqwest::Proxy::all(proxy_url)
169                .map_err(|e| CamelError::Config(format!("invalid proxy_url: {e}")))?;
170        }
171
172        Ok(())
173    }
174
175    pub fn with_connect_timeout_ms(mut self, ms: u64) -> Self {
176        self.connect_timeout_ms = ms;
177        self
178    }
179    pub fn with_pool_max_idle_per_host(mut self, n: usize) -> Self {
180        self.pool_max_idle_per_host = n;
181        self
182    }
183    pub fn with_pool_idle_timeout_ms(mut self, ms: u64) -> Self {
184        self.pool_idle_timeout_ms = ms;
185        self
186    }
187    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
188        self.follow_redirects = follow;
189        self
190    }
191    pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
192        self.max_redirects = max_redirects;
193        self
194    }
195    pub fn with_response_timeout_ms(mut self, ms: u64) -> Self {
196        self.response_timeout_ms = ms;
197        self
198    }
199    pub fn with_read_timeout_ms(mut self, ms: u64) -> Self {
200        self.read_timeout_ms = ms;
201        self
202    }
203    pub fn with_max_body_size(mut self, n: usize) -> Self {
204        self.max_body_size = n;
205        self
206    }
207    pub fn with_max_response_bytes(mut self, n: usize) -> Self {
208        self.max_response_bytes = n;
209        self
210    }
211    pub fn with_max_request_body(mut self, n: usize) -> Self {
212        self.max_request_body = n;
213        self
214    }
215    pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
216        self.allow_private_ips = allow;
217        self
218    }
219    pub fn with_blocked_hosts(mut self, hosts: Vec<String>) -> Self {
220        self.blocked_hosts = hosts;
221        self
222    }
223    pub fn with_ok_status_code_range(mut self, range: Option<String>) -> Self {
224        self.ok_status_code_range = range;
225        self
226    }
227    pub fn with_tls(mut self, tls: Option<TlsConfig>) -> Self {
228        self.tls = tls;
229        self
230    }
231    pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
232        self.proxy_url = proxy_url;
233        self
234    }
235}
236
237pub(crate) fn parse_ok_status_code_range(range: &str) -> Result<(u16, u16), CamelError> {
238    let (start_str, end_str) = range.split_once('-').ok_or_else(|| {
239        CamelError::Config("ok_status_code_range must be in NNN-NNN format".to_string())
240    })?;
241
242    if start_str.len() != 3
243        || end_str.len() != 3
244        || !start_str.chars().all(|c| c.is_ascii_digit())
245        || !end_str.chars().all(|c| c.is_ascii_digit())
246    {
247        return Err(CamelError::Config(
248            "ok_status_code_range must be in NNN-NNN format".to_string(),
249        ));
250    }
251
252    let start = start_str
253        .parse::<u16>()
254        .map_err(|_| CamelError::Config("ok_status_code_range start is invalid".to_string()))?;
255    let end = end_str
256        .parse::<u16>()
257        .map_err(|_| CamelError::Config("ok_status_code_range end is invalid".to_string()))?;
258
259    if start > end {
260        return Err(CamelError::Config(
261            "ok_status_code_range start must be <= end".to_string(),
262        ));
263    }
264
265    Ok((start, end))
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_http_config_defaults() {
274        let cfg = HttpConfig::default();
275        assert_eq!(cfg.connect_timeout_ms, 5_000);
276        assert_eq!(cfg.pool_max_idle_per_host, 100);
277        assert_eq!(cfg.pool_idle_timeout_ms, 90_000);
278        assert!(!cfg.follow_redirects);
279        assert_eq!(cfg.max_redirects, None);
280        assert_eq!(cfg.response_timeout_ms, 30_000);
281        assert_eq!(cfg.max_body_size, 10_485_760);
282        assert_eq!(cfg.max_request_body, 2_097_152);
283        assert!(!cfg.allow_private_ips);
284        assert!(cfg.blocked_hosts.is_empty());
285        assert!(cfg.tls.is_none());
286        assert!(cfg.proxy_url.is_none());
287    }
288
289    #[test]
290    fn test_http_config_builder() {
291        let cfg = HttpConfig::default()
292            .with_connect_timeout_ms(1_000)
293            .with_pool_max_idle_per_host(50)
294            .with_follow_redirects(true)
295            .with_allow_private_ips(true)
296            .with_blocked_hosts(vec!["evil.com".to_string()]);
297        assert_eq!(cfg.connect_timeout_ms, 1_000);
298        assert_eq!(cfg.pool_max_idle_per_host, 50);
299        assert!(cfg.follow_redirects);
300        assert!(cfg.allow_private_ips);
301        assert_eq!(cfg.blocked_hosts, vec!["evil.com".to_string()]);
302        assert_eq!(cfg.response_timeout_ms, 30_000);
303    }
304
305    #[test]
306    fn test_rejects_max_redirects_over_limit() {
307        let cfg = HttpConfig {
308            max_redirects: Some(21),
309            ..HttpConfig::default()
310        };
311        assert!(cfg.validate().is_err());
312    }
313
314    #[test]
315    fn test_accepts_valid_max_redirects() {
316        let cfg = HttpConfig {
317            max_redirects: Some(10),
318            ..HttpConfig::default()
319        };
320        assert!(cfg.validate().is_ok());
321    }
322
323    #[test]
324    fn test_rejects_malformed_status_range() {
325        let cfg = HttpConfig {
326            ok_status_code_range: Some("abc-xyz".into()),
327            ..HttpConfig::default()
328        };
329        assert!(cfg.validate().is_err());
330    }
331
332    #[test]
333    fn test_accepts_valid_status_range() {
334        let cfg = HttpConfig {
335            ok_status_code_range: Some("200-299".into()),
336            ..HttpConfig::default()
337        };
338        assert!(cfg.validate().is_ok());
339    }
340
341    #[test]
342    fn test_rejects_invalid_proxy_url() {
343        let cfg = HttpConfig {
344            proxy_url: Some("::not-a-proxy::".into()),
345            ..HttpConfig::default()
346        };
347        assert!(cfg.validate().is_err());
348    }
349
350    #[test]
351    fn server_tls_config_debug_redacts_paths() {
352        let cfg = super::ServerTlsConfig {
353            cert_path: "/secret/cert.pem".to_string(),
354            key_path: "/secret/key.pem".to_string(),
355        };
356        let debug = format!("{:?}", cfg);
357        assert!(
358            !debug.contains("/secret"),
359            "paths must be redacted: {debug}"
360        );
361        assert!(debug.contains("REDACTED"), "must show REDACTED: {debug}");
362    }
363}