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#[derive(Debug, Clone, PartialEq, Deserialize)]
41pub struct TlsConfig {
42 pub enabled: bool,
44 #[serde(default = "default_verify_peer")]
46 pub verify_peer: bool,
47 #[serde(default)]
49 pub ca_cert_path: Option<String>,
50 #[serde(default)]
52 pub client_cert_path: Option<String>,
53 #[serde(default)]
55 pub client_key_path: Option<String>,
56 #[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}