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
78fn default_connect_timeout_ms() -> u64 {
79    5_000
80}
81
82fn default_pool_max_idle_per_host() -> usize {
83    100
84}
85
86fn default_pool_idle_timeout_ms() -> u64 {
87    90_000
88}
89
90fn default_response_timeout_ms() -> u64 {
91    30_000
92}
93
94fn default_read_timeout_ms() -> u64 {
95    30_000
96}
97
98fn default_max_body_size() -> usize {
99    10_485_760
100}
101
102fn default_max_response_bytes() -> usize {
103    10_485_760
104}
105
106fn default_max_request_body() -> usize {
107    2_097_152
108}
109
110impl Default for HttpConfig {
111    fn default() -> Self {
112        Self {
113            connect_timeout_ms: default_connect_timeout_ms(),
114            pool_max_idle_per_host: default_pool_max_idle_per_host(),
115            pool_idle_timeout_ms: default_pool_idle_timeout_ms(),
116            follow_redirects: false,
117            max_redirects: None,
118            response_timeout_ms: default_response_timeout_ms(),
119            read_timeout_ms: default_read_timeout_ms(),
120            max_body_size: default_max_body_size(),
121            max_response_bytes: default_max_response_bytes(),
122            max_request_body: default_max_request_body(),
123            allow_private_ips: false,
124            blocked_hosts: Vec::new(),
125            ok_status_code_range: None,
126            tls: None,
127            proxy_url: None,
128        }
129    }
130}
131
132impl HttpConfig {
133    pub fn validate(&self) -> Result<(), CamelError> {
134        if let Some(max_redirects) = self.max_redirects
135            && max_redirects > 20
136        {
137            return Err(CamelError::Config(
138                "max_redirects must be <= 20".to_string(),
139            ));
140        }
141
142        if let Some(range) = &self.ok_status_code_range {
143            parse_ok_status_code_range(range)?;
144        }
145
146        if let Some(proxy_url) = &self.proxy_url {
147            reqwest::Proxy::all(proxy_url)
148                .map_err(|e| CamelError::Config(format!("invalid proxy_url: {e}")))?;
149        }
150
151        Ok(())
152    }
153
154    pub fn with_connect_timeout_ms(mut self, ms: u64) -> Self {
155        self.connect_timeout_ms = ms;
156        self
157    }
158    pub fn with_pool_max_idle_per_host(mut self, n: usize) -> Self {
159        self.pool_max_idle_per_host = n;
160        self
161    }
162    pub fn with_pool_idle_timeout_ms(mut self, ms: u64) -> Self {
163        self.pool_idle_timeout_ms = ms;
164        self
165    }
166    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
167        self.follow_redirects = follow;
168        self
169    }
170    pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
171        self.max_redirects = max_redirects;
172        self
173    }
174    pub fn with_response_timeout_ms(mut self, ms: u64) -> Self {
175        self.response_timeout_ms = ms;
176        self
177    }
178    pub fn with_read_timeout_ms(mut self, ms: u64) -> Self {
179        self.read_timeout_ms = ms;
180        self
181    }
182    pub fn with_max_body_size(mut self, n: usize) -> Self {
183        self.max_body_size = n;
184        self
185    }
186    pub fn with_max_response_bytes(mut self, n: usize) -> Self {
187        self.max_response_bytes = n;
188        self
189    }
190    pub fn with_max_request_body(mut self, n: usize) -> Self {
191        self.max_request_body = n;
192        self
193    }
194    pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
195        self.allow_private_ips = allow;
196        self
197    }
198    pub fn with_blocked_hosts(mut self, hosts: Vec<String>) -> Self {
199        self.blocked_hosts = hosts;
200        self
201    }
202    pub fn with_ok_status_code_range(mut self, range: Option<String>) -> Self {
203        self.ok_status_code_range = range;
204        self
205    }
206    pub fn with_tls(mut self, tls: Option<TlsConfig>) -> Self {
207        self.tls = tls;
208        self
209    }
210    pub fn with_proxy_url(mut self, proxy_url: Option<String>) -> Self {
211        self.proxy_url = proxy_url;
212        self
213    }
214}
215
216pub(crate) fn parse_ok_status_code_range(range: &str) -> Result<(u16, u16), CamelError> {
217    let (start_str, end_str) = range.split_once('-').ok_or_else(|| {
218        CamelError::Config("ok_status_code_range must be in NNN-NNN format".to_string())
219    })?;
220
221    if start_str.len() != 3
222        || end_str.len() != 3
223        || !start_str.chars().all(|c| c.is_ascii_digit())
224        || !end_str.chars().all(|c| c.is_ascii_digit())
225    {
226        return Err(CamelError::Config(
227            "ok_status_code_range must be in NNN-NNN format".to_string(),
228        ));
229    }
230
231    let start = start_str
232        .parse::<u16>()
233        .map_err(|_| CamelError::Config("ok_status_code_range start is invalid".to_string()))?;
234    let end = end_str
235        .parse::<u16>()
236        .map_err(|_| CamelError::Config("ok_status_code_range end is invalid".to_string()))?;
237
238    if start > end {
239        return Err(CamelError::Config(
240            "ok_status_code_range start must be <= end".to_string(),
241        ));
242    }
243
244    Ok((start, end))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_http_config_defaults() {
253        let cfg = HttpConfig::default();
254        assert_eq!(cfg.connect_timeout_ms, 5_000);
255        assert_eq!(cfg.pool_max_idle_per_host, 100);
256        assert_eq!(cfg.pool_idle_timeout_ms, 90_000);
257        assert!(!cfg.follow_redirects);
258        assert_eq!(cfg.max_redirects, None);
259        assert_eq!(cfg.response_timeout_ms, 30_000);
260        assert_eq!(cfg.max_body_size, 10_485_760);
261        assert_eq!(cfg.max_request_body, 2_097_152);
262        assert!(!cfg.allow_private_ips);
263        assert!(cfg.blocked_hosts.is_empty());
264        assert!(cfg.tls.is_none());
265        assert!(cfg.proxy_url.is_none());
266    }
267
268    #[test]
269    fn test_http_config_builder() {
270        let cfg = HttpConfig::default()
271            .with_connect_timeout_ms(1_000)
272            .with_pool_max_idle_per_host(50)
273            .with_follow_redirects(true)
274            .with_allow_private_ips(true)
275            .with_blocked_hosts(vec!["evil.com".to_string()]);
276        assert_eq!(cfg.connect_timeout_ms, 1_000);
277        assert_eq!(cfg.pool_max_idle_per_host, 50);
278        assert!(cfg.follow_redirects);
279        assert!(cfg.allow_private_ips);
280        assert_eq!(cfg.blocked_hosts, vec!["evil.com".to_string()]);
281        assert_eq!(cfg.response_timeout_ms, 30_000);
282    }
283
284    #[test]
285    fn test_rejects_max_redirects_over_limit() {
286        let cfg = HttpConfig {
287            max_redirects: Some(21),
288            ..HttpConfig::default()
289        };
290        assert!(cfg.validate().is_err());
291    }
292
293    #[test]
294    fn test_accepts_valid_max_redirects() {
295        let cfg = HttpConfig {
296            max_redirects: Some(10),
297            ..HttpConfig::default()
298        };
299        assert!(cfg.validate().is_ok());
300    }
301
302    #[test]
303    fn test_rejects_malformed_status_range() {
304        let cfg = HttpConfig {
305            ok_status_code_range: Some("abc-xyz".into()),
306            ..HttpConfig::default()
307        };
308        assert!(cfg.validate().is_err());
309    }
310
311    #[test]
312    fn test_accepts_valid_status_range() {
313        let cfg = HttpConfig {
314            ok_status_code_range: Some("200-299".into()),
315            ..HttpConfig::default()
316        };
317        assert!(cfg.validate().is_ok());
318    }
319
320    #[test]
321    fn test_rejects_invalid_proxy_url() {
322        let cfg = HttpConfig {
323            proxy_url: Some("::not-a-proxy::".into()),
324            ..HttpConfig::default()
325        };
326        assert!(cfg.validate().is_err());
327    }
328}