1pub mod bundle;
2pub(crate) mod client_cache;
3pub mod config;
4mod header_policy;
5pub mod health;
6pub mod registry;
7pub(crate) mod rest_match;
8pub(crate) mod ssrf;
9pub mod static_config;
10pub mod static_dispatch;
11pub mod static_endpoint;
12pub(crate) mod tls;
13#[cfg(test)]
14mod tls_harness;
15pub(crate) mod tls_reload;
16use crate::config::parse_ok_status_code_range;
17pub use bundle::HttpBundle;
18pub use bundle::HttpStaticBundle;
19pub(crate) use client_cache::{
20 HttpComponentKind, PINNED_CLIENT_MAX_ENTRIES, PINNED_CLIENT_TTL, PinnedClientCache,
21};
22pub use config::HttpConfig;
23pub use health::HttpHealthCheck;
24pub use registry::HttpRouteRegistry;
25pub use static_config::HttpStaticConfig;
26pub use static_endpoint::{HttpStaticComponent, HttpStaticConsumer, HttpStaticEndpoint};
27#[cfg(test)]
28pub(crate) use tls::{
29 FORCE_FALLBACK_REBUILD_FAIL, FORCE_WEBPKI_FALLBACK, build_client_call_count,
30 build_client_fallback_count,
31};
32#[cfg(test)]
33#[allow(unused_imports)]
37pub(crate) use tls::{FallbackTrigger, webpki_fallback_client};
38pub(crate) use tls::{build_client, client_or_emergency, strict_tls_error};
39
40use std::collections::HashMap;
41use std::future::Future;
42use std::pin::Pin;
43
44use std::sync::{Arc, Mutex, OnceLock};
45use std::task::{Context, Poll};
46use std::time::Duration;
47
48use tokio::sync::OnceCell;
49use tower::Layer;
50use tower::Service;
51use tracing::debug;
52
53use axum::body::BodyDataStream;
54use camel_api::component_metadata::ComponentMetadata;
55use camel_auth::bearer_token_layer::BearerTokenLayer;
56use camel_auth::oauth2::TokenProvider;
57use camel_component_api::tls_source::ServerTlsSource;
58use camel_component_api::{Body, BoxProcessor, CamelError, Exchange, StreamBody, StreamMetadata};
59use camel_component_api::{Component, Consumer, Endpoint, ProducerContext, RuntimeObservability};
60use camel_component_api::{UriComponents, UriConfig, parse_uri, raw_query_pairs};
61use futures::StreamExt;
62use futures::TryStreamExt;
63use futures::stream::BoxStream;
64
65#[derive(Clone)]
126pub struct HttpEndpointConfig {
127 pub base_url: String,
128 pub http_method: Option<String>,
129 pub throw_exception_on_failure: bool,
130 pub ok_status_code_range: (u16, u16),
131 pub response_timeout: Option<Duration>,
132 pub query_params: Vec<(String, String)>,
136 pub raw_query: Option<String>,
141 pub allow_internal: bool,
142 pub allow_cleartext: bool,
148 pub blocked_hosts: Vec<String>,
149 pub max_body_size: usize,
150 pub read_timeout_ms: u64,
151 pub max_response_bytes: usize,
152 pub auth: HttpAuth,
153 pub token_provider: Option<Arc<dyn TokenProvider>>,
154 pub user_agent: Option<String>,
155 pub bridge_endpoint: bool,
156 pub connection_close: bool,
157 pub skip_request_headers: Vec<String>,
158 pub skip_response_headers: Vec<String>,
159 pub follow_redirects: bool,
160 pub max_redirects: usize,
161 pub allowed_uri_hosts: Option<Vec<AllowedUriHost>>,
166}
167
168impl std::fmt::Debug for HttpEndpointConfig {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 f.debug_struct("HttpEndpointConfig")
183 .field("base_url", &camel_api::redact::redact_url(&self.base_url))
184 .field("http_method", &self.http_method)
185 .field(
186 "throw_exception_on_failure",
187 &self.throw_exception_on_failure,
188 )
189 .field("ok_status_code_range", &self.ok_status_code_range)
190 .field("response_timeout", &self.response_timeout)
191 .field(
192 "query_params",
193 &self
194 .query_params
195 .iter()
196 .map(|(key, _)| (key, "***"))
197 .collect::<Vec<_>>(),
198 )
199 .field("raw_query", &self.raw_query.as_ref().map(|_| "?[redacted]"))
200 .field("allow_internal", &self.allow_internal)
201 .field("allow_cleartext", &self.allow_cleartext)
202 .field("blocked_hosts", &self.blocked_hosts)
203 .field("max_body_size", &self.max_body_size)
204 .field("read_timeout_ms", &self.read_timeout_ms)
205 .field("max_response_bytes", &self.max_response_bytes)
206 .field("auth", &self.auth)
207 .field("token_provider", &self.token_provider)
208 .field("user_agent", &self.user_agent)
209 .field("bridge_endpoint", &self.bridge_endpoint)
210 .field("connection_close", &self.connection_close)
211 .field("skip_request_headers", &self.skip_request_headers)
212 .field("skip_response_headers", &self.skip_response_headers)
213 .field("follow_redirects", &self.follow_redirects)
214 .field("max_redirects", &self.max_redirects)
215 .field("allowed_uri_hosts", &self.allowed_uri_hosts)
216 .finish()
217 }
218}
219
220#[derive(Clone, PartialEq)]
221pub enum HttpAuth {
222 None,
223 Basic { username: String, password: String },
224 Bearer { token: String },
225}
226
227impl std::fmt::Debug for HttpAuth {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 match self {
230 HttpAuth::None => f.write_str("None"),
231 HttpAuth::Basic { username, .. } => f
232 .debug_struct("Basic")
233 .field("username", username)
234 .field("password", &"***")
235 .finish(),
236 HttpAuth::Bearer { .. } => f.debug_struct("Bearer").field("token", &"***").finish(),
237 }
238 }
239}
240
241fn is_consumed_option(key: &str) -> bool {
250 HttpEndpointConfig::uri_options()
251 .iter()
252 .any(|option| option.name == key || option.aliases.iter().any(|alias| alias == key))
253}
254
255impl UriConfig for HttpEndpointConfig {
256 fn scheme() -> &'static str {
258 "http"
259 }
260
261 fn from_uri(uri: &str) -> Result<Self, CamelError> {
262 let parts = parse_uri(uri)?;
263 Self::from_components(parts)
264 }
265
266 fn from_components(parts: UriComponents) -> Result<Self, CamelError> {
267 if parts.scheme != "http" && parts.scheme != "https" {
269 return Err(CamelError::InvalidUri(format!(
270 "expected scheme 'http' or 'https', got '{}'",
271 parts.scheme
272 )));
273 }
274
275 let base_url = format!("{}:{}", parts.scheme, parts.path);
278
279 let http_method = parts.params.get("httpMethod").cloned();
280
281 let throw_exception_on_failure = match parts.params.get("throwExceptionOnFailure") {
282 Some(v) => parse_bool_param_http(v).map_err(|e| {
283 CamelError::InvalidUri(format!("invalid value for throwExceptionOnFailure: {e}"))
284 })?,
285 None => true,
286 };
287
288 let ok_status_code_range = match parts.params.get("okStatusCodeRange") {
290 Some(v) => parse_ok_status_code_range(v)?,
291 None => (200, 299),
292 };
293
294 let response_timeout = match parts.params.get("responseTimeout") {
295 Some(v) => Some(v.parse::<u64>().map(Duration::from_millis).map_err(|e| {
296 CamelError::InvalidUri(format!("invalid value for responseTimeout: {e}"))
297 })?),
298 None => None,
299 };
300
301 let allow_internal = match parts.params.get("allowInternal") {
303 Some(v) => parse_bool_param_http(v).map_err(|e| {
304 CamelError::InvalidUri(format!("invalid value for allowInternal: {e}"))
305 })?,
306 None => false, };
308
309 let allow_cleartext = match parts.params.get("allowCleartext") {
312 Some(v) => parse_bool_param_http(v).map_err(|e| {
313 CamelError::InvalidUri(format!("invalid value for allowCleartext: {e}"))
314 })?,
315 None => false, };
317
318 let blocked_hosts = parts
320 .params
321 .get("blockedHosts")
322 .map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
323 .unwrap_or_default();
324
325 let max_body_size = match parts.params.get("maxBodySize") {
326 Some(v) => v.parse::<usize>().map_err(|e| {
327 CamelError::InvalidUri(format!("invalid value for maxBodySize: {e}"))
328 })?,
329 None => 10 * 1024 * 1024, };
331
332 let read_timeout_ms = match parts.params.get("readTimeout") {
333 Some(v) => v.parse::<u64>().map_err(|e| {
334 CamelError::InvalidUri(format!("invalid value for readTimeout: {e}"))
335 })?,
336 None => 30_000, };
338
339 let max_response_bytes = match parts.params.get("maxResponseBytes") {
340 Some(v) => v.parse::<usize>().map_err(|e| {
341 CamelError::InvalidUri(format!("invalid value for maxResponseBytes: {e}"))
342 })?,
343 None => 10 * 1024 * 1024, };
345
346 let auth = parse_auth_from_params(&parts.params)?;
347
348 let user_agent = parts.params.get("userAgent").cloned();
349
350 if parts.params.contains_key("cookieHandling") {
351 return Err(CamelError::InvalidUri(
352 "cookieHandling is not supported".into(),
353 ));
354 }
355
356 let bridge_endpoint = match parts.params.get("bridgeEndpoint") {
357 Some(v) => parse_bool_param_http(v).map_err(|e| {
358 CamelError::InvalidUri(format!("invalid value for bridgeEndpoint: {e}"))
359 })?,
360 None => false,
361 };
362
363 let connection_close = match parts.params.get("connectionClose") {
364 Some(v) => parse_bool_param_http(v).map_err(|e| {
365 CamelError::InvalidUri(format!("invalid value for connectionClose: {e}"))
366 })?,
367 None => false,
368 };
369
370 let skip_request_headers = parts
371 .params
372 .get("skipRequestHeaders")
373 .map(|v| {
374 v.split(',')
375 .map(str::trim)
376 .filter(|s| !s.is_empty())
377 .map(|s| s.to_ascii_lowercase())
378 .collect::<Vec<_>>()
379 })
380 .unwrap_or_default();
381
382 let skip_response_headers = parts
383 .params
384 .get("skipResponseHeaders")
385 .map(|v| {
386 v.split(',')
387 .map(str::trim)
388 .filter(|s| !s.is_empty())
389 .map(|s| s.to_ascii_lowercase())
390 .collect::<Vec<_>>()
391 })
392 .unwrap_or_default();
393
394 let follow_redirects = match parts.params.get("followRedirects") {
395 Some(v) => parse_bool_param_http(v).map_err(|e| {
396 CamelError::InvalidUri(format!("invalid value for followRedirects: {e}"))
397 })?,
398 None => false,
399 };
400
401 let max_redirects = match parts.params.get("maxRedirects") {
402 Some(v) => v.parse::<usize>().map_err(|e| {
403 CamelError::InvalidUri(format!("invalid value for maxRedirects: {e}"))
404 })?,
405 None => 10,
406 };
407
408 let allowed_uri_hosts = match parts.params.get("allowedUriHosts") {
411 Some(v) => Some(parse_allowed_uri_hosts(v)?),
412 None => None,
413 };
414
415 let raw_query = parts.raw_query.clone();
420
421 Ok(Self {
422 base_url,
423 http_method,
424 throw_exception_on_failure,
425 ok_status_code_range,
426 response_timeout,
427 query_params: Vec::new(),
428 raw_query,
429 allow_internal,
430 allow_cleartext,
431 blocked_hosts,
432 max_body_size,
433 read_timeout_ms,
434 max_response_bytes,
435 auth,
436 token_provider: None,
437 user_agent,
438 bridge_endpoint,
439 connection_close,
440 skip_request_headers,
441 skip_response_headers,
442 follow_redirects,
443 max_redirects,
444 allowed_uri_hosts,
445 })
446 }
447}
448
449#[derive(Debug, Clone, UriConfig)]
456#[allow(dead_code)]
457#[uri_scheme = "http"]
458#[uri_config(
459 skip_impl,
460 metadata(
461 scheme = "http",
462 description = "HTTP client and server component",
463 producer,
464 consumer,
465 streaming
466 ),
467 crate = "camel_component_api"
468)]
469struct HttpEndpointUriConfig {
470 #[allow(dead_code)]
471 _base_url: String,
472
473 #[uri_param(
474 name = "httpMethod",
475 desc = "HTTP method. Defaults to CamelHttpMethod header or POST/GET"
476 )]
477 http_method: Option<String>,
478
479 #[uri_param(
480 name = "throwExceptionOnFailure",
481 default = "true",
482 desc = "Throw on non-2xx status"
483 )]
484 throw_exception_on_failure: bool,
485
486 #[uri_param(
487 name = "okStatusCodeRange",
488 default = "200-299",
489 desc = "Success status code range"
490 )]
491 ok_status_code_range: String,
492
493 #[uri_param(name = "responseTimeout", desc = "Response timeout in milliseconds")]
494 response_timeout: Option<u64>,
495
496 #[uri_param(
497 name = "connectTimeout",
498 desc = "Connection timeout in milliseconds (consumed option; effective timeout comes from the global http config)"
499 )]
500 connect_timeout: Option<u64>,
501
502 #[uri_param(
503 name = "allowInternal",
504 default = "false",
505 desc = "Allow private/internal network destinations (SSRF)"
506 )]
507 allow_internal: bool,
508
509 #[uri_param(
510 name = "allowCleartext",
511 default = "false",
512 desc = "Allow cleartext http:// to public targets (transport consent; ADR-0081)"
513 )]
514 allow_cleartext: bool,
515
516 #[uri_param(name = "blockedHosts", desc = "Comma-separated blocked host list")]
517 blocked_hosts: Option<String>,
518
519 #[uri_param(
520 name = "maxBodySize",
521 default = "10485760",
522 desc = "Max request/response body bytes"
523 )]
524 max_body_size: u64,
525
526 #[uri_param(name = "readTimeout", desc = "Socket read timeout in milliseconds")]
527 read_timeout: Option<u64>,
528
529 #[uri_param(name = "maxResponseBytes", desc = "Max response body bytes")]
530 max_response_bytes: Option<u64>,
531
532 #[uri_param(
533 name = "authMethod",
534 kind = "enum:None,Basic,Bearer",
535 desc = "Authentication method"
536 )]
537 auth_method: Option<String>,
538
539 #[uri_param(name = "authUsername", secret, desc = "Basic auth username")]
540 auth_username: Option<String>,
541
542 #[uri_param(name = "authPassword", secret, desc = "Basic auth password")]
543 auth_password: Option<String>,
544
545 #[uri_param(name = "authBearerToken", secret, desc = "Bearer auth token")]
546 auth_bearer_token: Option<String>,
547
548 #[uri_param(name = "userAgent", desc = "User-Agent header")]
549 user_agent: Option<String>,
550
551 #[uri_param(
552 name = "bridgeEndpoint",
553 default = "false",
554 desc = "Bridge endpoint mode"
555 )]
556 bridge_endpoint: bool,
557
558 #[uri_param(
559 name = "connectionClose",
560 default = "false",
561 desc = "Send Connection: close"
562 )]
563 connection_close: bool,
564
565 #[uri_param(
566 name = "skipRequestHeaders",
567 desc = "Comma-separated request headers to skip"
568 )]
569 skip_request_headers: Option<String>,
570
571 #[uri_param(
572 name = "skipResponseHeaders",
573 desc = "Comma-separated response headers to skip"
574 )]
575 skip_response_headers: Option<String>,
576
577 #[uri_param(
578 name = "followRedirects",
579 default = "false",
580 desc = "Follow HTTP redirects"
581 )]
582 follow_redirects: bool,
583
584 #[uri_param(name = "maxRedirects", default = "10", desc = "Max redirect hops")]
585 max_redirects: u64,
586
587 #[uri_param(
588 name = "allowedUriHosts",
589 desc = "Comma-separated allowlist of CamelHttpUri override hosts (host or host:port)"
590 )]
591 allowed_uri_hosts: Option<String>,
592}
593
594impl HttpEndpointConfig {
595 pub fn metadata() -> ComponentMetadata {
598 HttpEndpointUriConfig::metadata()
599 }
600
601 pub fn uri_options() -> Vec<camel_api::component_metadata::UriOption> {
603 HttpEndpointUriConfig::uri_options()
604 }
605}
606
607fn parse_auth_from_params(params: &HashMap<String, String>) -> Result<HttpAuth, CamelError> {
608 let Some(method) = params.get("authMethod") else {
609 return Ok(HttpAuth::None);
610 };
611
612 if method.eq_ignore_ascii_case("none") {
613 return Ok(HttpAuth::None);
614 }
615
616 if method.eq_ignore_ascii_case("basic") {
617 let username = params.get("authUsername").cloned().ok_or_else(|| {
618 CamelError::InvalidUri("authUsername is required for authMethod=Basic".to_string())
619 })?;
620 let password = params.get("authPassword").cloned().ok_or_else(|| {
621 CamelError::InvalidUri("authPassword is required for authMethod=Basic".to_string())
622 })?;
623 return Ok(HttpAuth::Basic { username, password });
624 }
625
626 if method.eq_ignore_ascii_case("bearer") {
627 let token = params.get("authBearerToken").cloned().ok_or_else(|| {
628 CamelError::InvalidUri("authBearerToken is required for authMethod=Bearer".to_string())
629 })?;
630 return Ok(HttpAuth::Bearer { token });
631 }
632
633 Err(CamelError::InvalidUri(format!(
634 "invalid value for authMethod: {method} (expected None, Basic, or Bearer)"
635 )))
636}
637
638fn parse_bool_param_http(value: &str) -> Result<bool, CamelError> {
639 match value.to_ascii_lowercase().as_str() {
640 "true" | "1" | "yes" => Ok(true),
641 "false" | "0" | "no" => Ok(false),
642 _ => Err(CamelError::InvalidUri(format!(
643 "invalid boolean value: '{value}'"
644 ))),
645 }
646}
647
648impl HttpEndpointConfig {
649 pub fn from_uri_with_defaults(uri: &str, config: &HttpConfig) -> Result<Self, CamelError> {
650 let parts = parse_uri(uri)?;
651 let mut endpoint = Self::from_components(parts.clone())?;
652 if endpoint.response_timeout.is_none() {
653 endpoint.response_timeout = Some(Duration::from_millis(config.response_timeout_ms));
654 }
655 if !parts.params.contains_key("allowInternal") {
656 endpoint.allow_internal = config.allow_internal;
657 }
658 if !parts.params.contains_key("blockedHosts") {
659 endpoint.blocked_hosts = config.blocked_hosts.clone();
660 }
661 if !parts.params.contains_key("maxBodySize") {
662 endpoint.max_body_size = config.max_body_size;
663 }
664 if !parts.params.contains_key("readTimeout") {
665 endpoint.read_timeout_ms = config.read_timeout_ms;
666 }
667 if !parts.params.contains_key("maxResponseBytes") {
668 endpoint.max_response_bytes = config.max_response_bytes;
669 }
670 if !parts.params.contains_key("okStatusCodeRange")
671 && let Some(range) = &config.ok_status_code_range
672 {
673 endpoint.ok_status_code_range = parse_ok_status_code_range(range)?;
674 }
675 if !parts.params.contains_key("followRedirects") {
676 endpoint.follow_redirects = config.follow_redirects;
677 }
678 if !parts.params.contains_key("maxRedirects") {
679 endpoint.max_redirects = config.max_redirects.unwrap_or(10);
680 }
681
682 Ok(endpoint)
683 }
684}
685
686#[derive(Debug, Clone)]
692pub struct HttpServerConfig {
693 pub scheme: String,
695 pub host: String,
697 pub port: u16,
699 pub path: String,
701 pub max_request_body: usize,
703 pub max_response_body: usize,
705 pub max_inflight_requests: usize,
711 pub method: Option<String>,
718 pub tls_config: Option<crate::config::ServerTlsConfig>,
721}
722
723impl UriConfig for HttpServerConfig {
724 fn scheme() -> &'static str {
726 "http"
727 }
728
729 fn from_uri(uri: &str) -> Result<Self, CamelError> {
730 let parts = parse_uri(uri)?;
731 Self::from_components(parts)
732 }
733
734 fn from_components(parts: UriComponents) -> Result<Self, CamelError> {
735 if parts.scheme != "http" && parts.scheme != "https" {
737 return Err(CamelError::InvalidUri(format!(
738 "expected scheme 'http' or 'https', got '{}'",
739 parts.scheme
740 )));
741 }
742
743 let authority_and_path = parts.path.trim_start_matches('/');
746
747 let (authority, path_suffix) = if let Some(idx) = authority_and_path.find('/') {
749 (&authority_and_path[..idx], &authority_and_path[idx..])
750 } else {
751 (authority_and_path, "/")
752 };
753
754 let path = if path_suffix.is_empty() {
755 "/"
756 } else {
757 path_suffix
758 }
759 .to_string();
760
761 let (host, port) = if let Some(colon) = authority.rfind(':') {
763 let port_str = &authority[colon + 1..];
764 match port_str.parse::<u16>() {
765 Ok(p) => (authority[..colon].to_string(), p),
766 Err(_) => {
767 return Err(CamelError::InvalidUri(format!(
768 "invalid port '{}' in authority",
769 port_str
770 )));
771 }
772 }
773 } else {
774 let default_port = if parts.scheme == "https" { 443 } else { 80 };
776 (authority.to_string(), default_port)
777 };
778
779 let max_request_body = parts
780 .params
781 .get("maxRequestBody")
782 .and_then(|v| v.parse::<usize>().ok())
783 .unwrap_or(2 * 1024 * 1024); let max_response_body = parts
786 .params
787 .get("maxResponseBody")
788 .and_then(|v| v.parse::<usize>().ok())
789 .unwrap_or(10 * 1024 * 1024); let max_inflight_requests = parts
792 .params
793 .get("maxInflightRequests")
794 .and_then(|v| v.parse::<usize>().ok())
795 .unwrap_or(1024);
796 let max_inflight_requests = max_inflight_requests_limit(max_inflight_requests)?;
800
801 let method = parts.params.get("httpMethod").map(|m| m.to_uppercase());
807
808 Ok(Self {
809 scheme: parts.scheme,
810 host,
811 port,
812 path,
813 max_request_body,
814 max_response_body,
815 max_inflight_requests,
816 method,
817 tls_config: {
818 let cert = parts.params.get("tlsCert").cloned();
819 let key = parts.params.get("tlsKey").cloned();
820 match (cert, key) {
821 (Some(c), Some(k)) => Some(crate::config::ServerTlsConfig {
822 cert_path: c,
823 key_path: k,
824 }),
825 (None, None) => None,
826 _ => None, }
828 },
829 })
830 }
831}
832
833impl HttpServerConfig {
834 pub fn from_uri_with_defaults(uri: &str, config: &HttpConfig) -> Result<Self, CamelError> {
835 let parts = parse_uri(uri)?;
836 let mut server = Self::from_components(parts.clone())?;
837 if !parts.params.contains_key("maxRequestBody") {
838 server.max_request_body = config.max_request_body;
839 }
840 if !parts.params.contains_key("maxResponseBody") {
841 server.max_response_body = config.max_body_size;
843 }
844 Ok(server)
845 }
846}
847
848pub enum HttpReplyBody {
856 Bytes(bytes::Bytes),
857 Stream(BoxStream<'static, Result<bytes::Bytes, CamelError>>),
858}
859
860pub struct RequestEnvelope {
865 pub method: String,
866 pub path: String,
867 pub query: String,
868 pub headers: http::HeaderMap,
869 pub body: StreamBody,
870 pub path_params: std::collections::HashMap<String, String>,
876 pub reply_tx: tokio::sync::oneshot::Sender<HttpReply>,
877}
878
879pub struct HttpReply {
883 pub status: u16,
884 pub headers: Vec<(String, String)>,
885 pub body: HttpReplyBody,
886}
887
888type ServerKey = (String, u16);
893
894struct ServerHandle {
896 registry: HttpRouteRegistry,
897 bound_addr: std::net::SocketAddr,
900 max_request_body: usize,
901 max_response_body: usize,
902 max_inflight_requests: usize,
903 is_tls: bool,
904 tls_cert_path: Option<String>,
905 tls_key_path: Option<String>,
906 monitor_task: tokio::task::JoinHandle<()>,
909 #[allow(dead_code)]
915 server_abort: tokio::task::AbortHandle,
916 tls_config: Option<axum_server::tls_rustls::RustlsConfig>,
919 tls_source: Option<ServerTlsSource>,
920}
921
922#[derive(Default)]
925struct RegistryState {
926 entries: HashMap<ServerKey, Arc<OnceCell<ServerHandle>>>,
927 staged: HashMap<ServerKey, tokio::net::TcpListener>,
928}
929
930pub struct ServerRegistry {
932 inner: Mutex<RegistryState>,
933}
934
935impl ServerRegistry {
936 pub fn global() -> &'static Self {
938 static INSTANCE: OnceLock<ServerRegistry> = OnceLock::new();
939 INSTANCE.get_or_init(|| ServerRegistry {
940 inner: Mutex::new(RegistryState::default()),
941 })
942 }
943
944 #[allow(clippy::too_many_arguments)]
947 pub async fn get_or_spawn(
948 &'static self,
949 host: &str,
950 port: u16,
951 max_request_body: usize,
952 max_response_body: usize,
953 max_inflight_requests: usize,
954 runtime: Arc<dyn RuntimeObservability>,
955 route_id: String,
956 tls_config: Option<crate::config::ServerTlsConfig>,
957 ) -> Result<HttpRouteRegistry, CamelError> {
958 self.get_or_spawn_internal(
959 host,
960 port,
961 max_request_body,
962 max_response_body,
963 max_inflight_requests,
964 runtime,
965 route_id,
966 tls_config,
967 None,
968 )
969 .await
970 }
971
972 #[allow(clippy::too_many_arguments)]
979 pub async fn get_or_spawn_with_listener(
980 &'static self,
981 listener: tokio::net::TcpListener,
982 max_request_body: usize,
983 max_response_body: usize,
984 max_inflight_requests: usize,
985 runtime: Arc<dyn RuntimeObservability>,
986 route_id: String,
987 tls_config: Option<crate::config::ServerTlsConfig>,
988 ) -> Result<HttpRouteRegistry, CamelError> {
989 let addr = listener
990 .local_addr()
991 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
992 self.get_or_spawn_internal(
993 &addr.ip().to_string(),
994 addr.port(),
995 max_request_body,
996 max_response_body,
997 max_inflight_requests,
998 runtime,
999 route_id,
1000 tls_config,
1001 Some(listener),
1002 )
1003 .await
1004 }
1005
1006 pub async fn stage_listener(
1013 &'static self,
1014 listener: tokio::net::TcpListener,
1015 ) -> Result<(), CamelError> {
1016 let addr = listener
1017 .local_addr()
1018 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
1019 let host = addr.ip().to_string();
1020 use std::collections::hash_map::Entry;
1021 let mut guard = self.inner.lock().map_err(|_| {
1022 CamelError::EndpointCreationFailed("ServerRegistry lock poisoned".into())
1023 })?;
1024 match guard.staged.entry((host.clone(), addr.port())) {
1025 Entry::Occupied(_) => Err(CamelError::EndpointCreationFailed(format!(
1026 "listener already staged for {host}:{}",
1027 addr.port()
1028 ))),
1029 Entry::Vacant(slot) => {
1030 slot.insert(listener);
1031 Ok(())
1032 }
1033 }
1034 }
1035
1036 pub fn bound_addr(&'static self, host: &str, port: u16) -> Option<std::net::SocketAddr> {
1039 let guard = self.inner.lock().ok()?;
1040 guard
1041 .entries
1042 .get(&(host.to_string(), port))
1043 .and_then(|cell| cell.get())
1044 .map(|handle| handle.bound_addr)
1045 }
1046
1047 #[allow(clippy::too_many_arguments)]
1048 async fn get_or_spawn_internal(
1049 &'static self,
1050 host: &str,
1051 port: u16,
1052 max_request_body: usize,
1053 max_response_body: usize,
1054 max_inflight_requests: usize,
1055 runtime: Arc<dyn RuntimeObservability>,
1056 route_id: String,
1057 tls_config: Option<crate::config::ServerTlsConfig>,
1058 provided: Option<tokio::net::TcpListener>,
1059 ) -> Result<HttpRouteRegistry, CamelError> {
1060 let host_owned = host.to_string();
1061 let key = (host.to_string(), port);
1062
1063 let cell = {
1064 let mut guard = self.inner.lock().map_err(|_| {
1065 CamelError::EndpointCreationFailed("ServerRegistry lock poisoned".into())
1066 })?;
1067 if let Some(existing) = guard.entries.get(&key)
1071 && let Some(handle) = existing.get()
1072 && handle.monitor_task.is_finished()
1073 {
1074 if handle.is_tls {
1077 let scheme = if handle.is_tls { "https" } else { "http" };
1078 camel_component_api::tls_source::TlsReloadRegistry::global()
1079 .unregister(scheme, host, port);
1080 }
1081 guard.entries.remove(&key);
1082 }
1083 guard
1084 .entries
1085 .entry(key)
1086 .or_insert_with(|| Arc::new(OnceCell::new()))
1087 .clone()
1088 };
1089
1090 if let Some(existing) = cell.get()
1091 && existing.max_request_body != max_request_body
1092 {
1093 return Err(CamelError::EndpointCreationFailed(format!(
1094 "incompatible maxRequestBody for shared server (host={host}, port={port}): {} vs {}",
1095 existing.max_request_body, max_request_body
1096 )));
1097 }
1098
1099 if let Some(existing) = cell.get()
1100 && existing.max_response_body != max_response_body
1101 {
1102 return Err(CamelError::EndpointCreationFailed(format!(
1103 "incompatible maxResponseBody for shared server (host={host}, port={port}): {} vs {}",
1104 existing.max_response_body, max_response_body
1105 )));
1106 }
1107
1108 if let Some(existing) = cell.get()
1109 && existing.max_inflight_requests != max_inflight_requests
1110 {
1111 return Err(CamelError::EndpointCreationFailed(format!(
1112 "incompatible maxInflightRequests for shared server (host={host}, port={port}): {} vs {}",
1113 existing.max_inflight_requests, max_inflight_requests
1114 )));
1115 }
1116
1117 if let Some(existing) = cell.get()
1119 && existing.is_tls != tls_config.is_some()
1120 {
1121 return Err(CamelError::EndpointCreationFailed(format!(
1122 "incompatible TLS mode for shared server (host={host}, port={port}): existing is_tls={}, new has_tls={}",
1123 existing.is_tls,
1124 tls_config.is_some()
1125 )));
1126 }
1127
1128 if let (Some(existing), Some(new_tls)) = (cell.get(), &tls_config)
1130 && (existing.tls_cert_path.as_deref() != Some(&new_tls.cert_path)
1131 || existing.tls_key_path.as_deref() != Some(&new_tls.key_path))
1132 {
1133 return Err(CamelError::EndpointCreationFailed(format!(
1134 "incompatible TLS cert/key for shared server (host={host}, port={port}): routes on the same TLS port must use the same cert and key"
1135 )));
1136 }
1137
1138 let handle = cell
1139 .get_or_try_init(|| {
1140 let rt = Arc::clone(&runtime);
1141 let rid = route_id.clone();
1142 let key = (host_owned.clone(), port);
1143 async move {
1144 let source = match provided {
1153 Some(listener) => ListenerSource::Staged(listener),
1154 None => {
1155 let mut guard = self.inner.lock().map_err(|_| {
1156 CamelError::EndpointCreationFailed(
1157 "ServerRegistry lock poisoned".into(),
1158 )
1159 })?;
1160 match guard.staged.remove(&key) {
1161 Some(listener) => ListenerSource::Staged(listener),
1162 None => {
1166 if let Some((staged_host, _)) = guard
1167 .staged
1168 .keys()
1169 .find(|(_, staged_port)| *staged_port == port)
1170 {
1171 let staged_host = staged_host.clone();
1172 return Err(CamelError::EndpointCreationFailed(
1173 format!(
1174 "staged listener conflict on port {port}: staged under host {staged_host}, requested {host_owned}"
1175 ),
1176 ));
1177 }
1178 ListenerSource::Bind
1179 }
1180 }
1181 }
1182 };
1183 spawn_entry(
1184 key,
1185 source,
1186 max_request_body,
1187 max_response_body,
1188 max_inflight_requests,
1189 rt,
1190 rid,
1191 tls_config,
1192 )
1193 .await
1194 .and_then(|handle| {
1195 Arc::try_unwrap(handle).map_err(|_| {
1199 CamelError::EndpointCreationFailed(
1200 "spawned server handle has dangling clones".into(),
1201 )
1202 })
1203 })
1204 }
1205 })
1206 .await?;
1207
1208 Ok(handle.registry.clone())
1209 }
1210
1211 pub async fn unregister(&self, host: &str, port: u16) {
1215 debug!(
1216 host = camel_api::redact::redact_host(host),
1217 port = port,
1218 "consumer unregistered from HTTP server"
1219 );
1220 }
1221
1222 #[cfg(test)]
1229 pub fn reset() {
1230 let instance = Self::global();
1231 let mut guard = instance
1232 .inner
1233 .lock()
1234 .expect("ServerRegistry lock poisoned during test reset");
1235 guard.entries.clear();
1236 guard.staged.clear();
1237 }
1238}
1239
1240enum ListenerSource {
1243 Bind,
1244 Staged(tokio::net::TcpListener),
1245}
1246
1247#[allow(clippy::too_many_arguments)]
1252async fn spawn_entry(
1253 key: ServerKey,
1254 source: ListenerSource,
1255 max_request_body: usize,
1256 max_response_body: usize,
1257 max_inflight_requests: usize,
1258 runtime: Arc<dyn RuntimeObservability>,
1259 route_id: String,
1260 tls_config: Option<crate::config::ServerTlsConfig>,
1261) -> Result<Arc<ServerHandle>, CamelError> {
1262 max_inflight_requests_limit(max_inflight_requests)?;
1268 let rt = Arc::clone(&runtime);
1269 let rid = route_id.clone();
1270 let (host_owned, port) = key;
1271 let listener = match source {
1272 ListenerSource::Bind => {
1273 let addr = format!("{host_owned}:{port}");
1274 tokio::net::TcpListener::bind(&addr).await.map_err(|e| {
1275 CamelError::EndpointCreationFailed(format!("Failed to bind {addr}: {e}"))
1276 })?
1277 }
1278 ListenerSource::Staged(listener) => listener,
1279 };
1280 let bound_addr = listener
1281 .local_addr()
1282 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
1283 let server_exited = tokio_util::sync::CancellationToken::new();
1284 let registry = HttpRouteRegistry::new_with_server_exited(server_exited.clone());
1285 let inflight = Arc::new(tokio::sync::Semaphore::new(max_inflight_requests));
1286 let tls_rustls_cfg: Option<axum_server::tls_rustls::RustlsConfig>;
1289 let tls_source: Option<ServerTlsSource>;
1290 let server_task = if let Some(ref tls) = tls_config {
1291 let rustls_config = load_tls_config(&tls.cert_path, &tls.key_path)?;
1292 let source = ServerTlsSource {
1293 cert_path: std::path::PathBuf::from(&tls.cert_path),
1294 key_path: std::path::PathBuf::from(&tls.key_path),
1295 client_ca_path: None,
1296 };
1297 let rustls_cfg =
1301 axum_server::tls_rustls::RustlsConfig::from_config(std::sync::Arc::new(rustls_config));
1302 tls_rustls_cfg = Some(rustls_cfg.clone());
1303 tls_source = Some(source);
1304 let std_listener = listener.into_std().map_err(|e| {
1306 CamelError::EndpointCreationFailed(format!("TLS listener conversion: {e}"))
1307 })?;
1308 tokio::spawn(run_axum_server_tls(
1309 std_listener,
1310 rustls_cfg,
1311 registry.clone(),
1312 max_request_body,
1313 max_response_body,
1314 Arc::clone(&inflight),
1315 Arc::clone(&rt),
1316 rid.clone(),
1317 ))
1318 } else {
1319 tls_rustls_cfg = None;
1320 tls_source = None;
1321 tokio::spawn(run_axum_server(
1322 listener,
1323 registry.clone(),
1324 max_request_body,
1325 max_response_body,
1326 Arc::clone(&inflight),
1327 Arc::clone(&rt),
1328 rid.clone(),
1329 ))
1330 };
1331 let addr_for_monitor = format!("{host_owned}:{port}");
1332 let server_abort = server_task.abort_handle();
1333 let monitor_task = tokio::spawn(monitor_axum_task(
1334 server_task,
1335 addr_for_monitor,
1336 Arc::clone(&rt),
1337 rid,
1338 server_exited,
1339 ));
1340 let handle = ServerHandle {
1341 registry,
1342 bound_addr,
1343 max_request_body,
1344 max_response_body,
1345 max_inflight_requests,
1346 is_tls: tls_config.is_some(),
1347 tls_cert_path: tls_config.as_ref().map(|t| t.cert_path.clone()),
1348 tls_key_path: tls_config.as_ref().map(|t| t.key_path.clone()),
1349 monitor_task,
1350 server_abort,
1351 tls_config: tls_rustls_cfg,
1352 tls_source,
1353 };
1354 if let (Some(tls_cfg), Some(source)) = (handle.tls_config.as_ref(), handle.tls_source.as_ref())
1359 {
1360 let handler = Arc::new(crate::tls_reload::HttpReloadHandler::new(
1361 tls_cfg.clone(),
1362 source.clone(),
1363 host_owned.clone(),
1364 port,
1365 ));
1366 camel_component_api::tls_source::TlsReloadRegistry::global().register(handler);
1367 }
1368 Ok(Arc::new(handle))
1369}
1370
1371use axum::{
1376 Router,
1377 body::Body as AxumBody,
1378 extract::{Request, State},
1379 http::{Response, StatusCode},
1380 response::IntoResponse,
1381};
1382
1383#[derive(Clone)]
1384pub(crate) struct AppState {
1385 registry: HttpRouteRegistry,
1386 max_request_body: usize,
1387 max_response_body: usize,
1388 inflight: Arc<tokio::sync::Semaphore>,
1389}
1390
1391const CONSUMER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
1398
1399async fn run_axum_server(
1400 listener: tokio::net::TcpListener,
1401 registry: HttpRouteRegistry,
1402 max_request_body: usize,
1403 max_response_body: usize,
1404 inflight: Arc<tokio::sync::Semaphore>,
1405 runtime: Arc<dyn RuntimeObservability>,
1406 route_id: String,
1407) {
1408 let state = AppState {
1409 registry,
1410 max_request_body,
1411 max_response_body,
1412 inflight,
1413 };
1414 let app = Router::new()
1415 .fallback(dispatch_handler)
1416 .with_state(state)
1417 .layer(tower_http::timeout::TimeoutLayer::with_status_code(
1418 StatusCode::REQUEST_TIMEOUT,
1419 CONSUMER_REQUEST_TIMEOUT,
1420 ));
1421
1422 axum::serve(listener, app).await.unwrap_or_else(|e| {
1423 runtime
1424 .metrics()
1425 .increment_errors(&route_id, "e:http:accept");
1426 tracing::error!(error = %e, "Axum server error");
1428 });
1429}
1430
1431#[allow(clippy::too_many_arguments)]
1432async fn run_axum_server_tls(
1433 listener: std::net::TcpListener,
1434 tls_cfg: axum_server::tls_rustls::RustlsConfig,
1435 registry: HttpRouteRegistry,
1436 max_request_body: usize,
1437 max_response_body: usize,
1438 inflight: Arc<tokio::sync::Semaphore>,
1439 runtime: Arc<dyn RuntimeObservability>,
1440 route_id: String,
1441) {
1442 let state = AppState {
1443 registry,
1444 max_request_body,
1445 max_response_body,
1446 inflight,
1447 };
1448 let app = Router::new()
1449 .fallback(dispatch_handler)
1450 .with_state(state)
1451 .layer(tower_http::timeout::TimeoutLayer::with_status_code(
1452 StatusCode::REQUEST_TIMEOUT,
1453 CONSUMER_REQUEST_TIMEOUT,
1454 ));
1455
1456 let server = match axum_server::from_tcp_rustls(listener, tls_cfg) {
1461 Ok(server) => server,
1462 Err(e) => {
1463 runtime
1464 .metrics()
1465 .increment_errors(&route_id, "e:http:accept-tls");
1466 tracing::error!(error = %e, "Axum TLS server setup error");
1468 return;
1469 }
1470 };
1471
1472 server
1473 .serve(app.into_make_service())
1474 .await
1475 .unwrap_or_else(|e| {
1476 runtime
1477 .metrics()
1478 .increment_errors(&route_id, "e:http:accept-tls");
1479 tracing::error!(error = %e, "Axum TLS server error");
1481 });
1482}
1483
1484async fn monitor_axum_task(
1494 handle: tokio::task::JoinHandle<()>,
1495 addr: String,
1496 runtime: Arc<dyn RuntimeObservability>,
1497 route_id: String,
1498 server_exited: tokio_util::sync::CancellationToken,
1499) {
1500 match handle.await {
1501 Ok(()) => {
1502 }
1504 Err(join_err) => {
1505 runtime
1506 .metrics()
1507 .increment_errors(&route_id, "e:http:server-task-exited");
1508 tracing::error!(
1510 addr = %addr,
1511 error = %join_err,
1512 "Axum server task exited unexpectedly — all routes on this port are now dead"
1513 );
1514 server_exited.cancel();
1518 }
1519 }
1520}
1521
1522fn load_tls_config(
1525 cert_path: &str,
1526 key_path: &str,
1527) -> Result<tokio_rustls::rustls::ServerConfig, CamelError> {
1528 use std::fs::File;
1529 use std::io::BufReader;
1530
1531 let cert_file = File::open(cert_path)
1532 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS cert file error: {e}")))?;
1533 let key_file = File::open(key_path)
1534 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key file error: {e}")))?;
1535
1536 let certs: Vec<_> = rustls_pemfile::certs(&mut BufReader::new(cert_file))
1537 .collect::<Result<Vec<_>, _>>()
1538 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS cert parse error: {e}")))?;
1539
1540 let key = rustls_pemfile::private_key(&mut BufReader::new(key_file))
1541 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key parse error: {e}")))?
1542 .ok_or_else(|| CamelError::EndpointCreationFailed("TLS: no private key found".into()))?;
1543
1544 tokio_rustls::rustls::ServerConfig::builder()
1545 .with_no_client_auth()
1546 .with_single_cert(certs, key)
1547 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS config error: {e}")))
1548}
1549
1550async fn dispatch_handler(State(state): State<AppState>, req: Request) -> impl IntoResponse {
1551 let path = req.uri().path().to_owned();
1552 let method = req.method().to_string();
1553
1554 let api_sender = {
1571 let inner = state.registry.inner.read().await;
1572 inner.api_routes.get(&path).cloned()
1573 }; let (rest_sender, path_params) = if api_sender.is_some() {
1576 (None, Default::default())
1578 } else {
1579 let inner = state.registry.inner.read().await;
1580 match rest_match::match_endpoint(&method, &path, &inner.rest_endpoints) {
1581 rest_match::MatchOutcome::Found(m) => (Some(m.payload), m.path_params),
1582 rest_match::MatchOutcome::Ambiguous => {
1583 tracing::warn!(
1589 method = %method,
1590 path = %path,
1591 "ambiguous REST template match — returning 500"
1592 );
1593 return Response::builder()
1594 .status(StatusCode::INTERNAL_SERVER_ERROR)
1595 .body(AxumBody::from("Internal Server Error"))
1596 .expect("infallible"); }
1598 rest_match::MatchOutcome::NotFound => (None, Default::default()),
1599 }
1600 }; let sender = api_sender.or(rest_sender);
1603
1604 if let Some(sender) = sender {
1605 let query = req.uri().query().unwrap_or("").to_string();
1606 let headers = req.headers().clone();
1607
1608 let content_length: Option<u64> = headers
1610 .get(http::header::CONTENT_LENGTH)
1611 .and_then(|v| v.to_str().ok())
1612 .and_then(|s| s.parse().ok());
1613
1614 if let Some(len) = content_length
1615 && len > state.max_request_body as u64
1616 {
1617 return Response::builder()
1618 .status(StatusCode::PAYLOAD_TOO_LARGE)
1619 .body(AxumBody::from("Request body exceeds configured limit"))
1620 .expect("infallible"); }
1622
1623 let _permit = match Arc::clone(&state.inflight).try_acquire_owned() {
1624 Ok(permit) => permit,
1625 Err(_) => {
1626 return Response::builder()
1627 .status(StatusCode::SERVICE_UNAVAILABLE)
1628 .body(AxumBody::from("Service Unavailable"))
1629 .expect("infallible"); }
1631 };
1632
1633 let content_type = headers
1639 .get(http::header::CONTENT_TYPE)
1640 .and_then(|v| v.to_str().ok())
1641 .map(|s| s.to_string());
1642
1643 let data_stream: BodyDataStream = req.into_body().into_data_stream();
1644 let max_body = state.max_request_body;
1645 let mut seen: u64 = 0;
1646 let capped_stream =
1647 data_stream
1648 .map_err(|e| CamelError::Io(e.to_string()))
1649 .map(move |chunk| match chunk {
1650 Ok(bytes) => {
1651 seen = seen.saturating_add(bytes.len() as u64);
1652 if seen > max_body as u64 {
1653 Err(CamelError::ProcessorError(format!(
1654 "Request body exceeds configured limit of {max_body} bytes"
1655 )))
1656 } else {
1657 Ok(bytes)
1658 }
1659 }
1660 Err(e) => Err(e),
1661 });
1662 let boxed: BoxStream<'static, Result<bytes::Bytes, CamelError>> = Box::pin(capped_stream);
1663
1664 let stream_body = StreamBody {
1665 stream: Arc::new(tokio::sync::Mutex::new(Some(boxed))),
1666 metadata: StreamMetadata {
1667 size_hint: content_length,
1668 content_type,
1669 origin: None,
1670 },
1671 };
1672
1673 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel::<HttpReply>();
1674 let envelope = RequestEnvelope {
1675 method,
1676 path,
1677 query,
1678 headers,
1679 body: stream_body,
1680 path_params,
1681 reply_tx,
1682 };
1683
1684 if sender.send(envelope).await.is_err() {
1685 return Response::builder()
1686 .status(StatusCode::SERVICE_UNAVAILABLE)
1687 .body(AxumBody::from("Consumer unavailable"))
1688 .expect("infallible"); }
1690
1691 match reply_rx.await {
1692 Ok(reply) => {
1693 let reply = match reply.body {
1694 HttpReplyBody::Bytes(b)
1695 if exceeds_max_response_body(b.len(), state.max_response_body) =>
1696 {
1697 HttpReply {
1698 status: 500,
1699 headers: vec![],
1700 body: HttpReplyBody::Bytes(bytes::Bytes::from(
1701 "Response body exceeds configured limit",
1702 )),
1703 }
1704 }
1705 _ => reply,
1706 };
1707
1708 let status =
1709 StatusCode::from_u16(reply.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1710 let mut builder = Response::builder().status(status);
1711 for (k, v) in &reply.headers {
1712 builder = builder.header(k.as_str(), v.as_str());
1713 }
1714 match reply.body {
1715 HttpReplyBody::Bytes(b) => {
1716 builder.body(AxumBody::from(b)).unwrap_or_else(|_| {
1717 Response::builder()
1718 .status(StatusCode::INTERNAL_SERVER_ERROR)
1719 .body(AxumBody::from("Invalid response headers from consumer"))
1720 .expect("infallible") })
1722 }
1723 HttpReplyBody::Stream(stream) => builder
1724 .body(AxumBody::from_stream(stream))
1725 .unwrap_or_else(|_| {
1726 Response::builder()
1727 .status(StatusCode::INTERNAL_SERVER_ERROR)
1728 .body(AxumBody::from("Invalid response headers from consumer"))
1729 .expect("infallible") }),
1731 }
1732 }
1733 Err(_) => Response::builder()
1734 .status(StatusCode::INTERNAL_SERVER_ERROR)
1735 .body(AxumBody::from("Pipeline error"))
1736 .expect("infallible"), }
1738 } else {
1739 static_dispatch::dispatch_static(&state, req, &path).await
1741 }
1742}
1743
1744fn exceeds_max_response_body(len: usize, max: usize) -> bool {
1745 len > max
1746}
1747
1748fn title_case_header(name: &str) -> String {
1749 name.split('-')
1750 .map(|part| {
1751 let mut chars = part.chars();
1752 match chars.next() {
1753 None => String::new(),
1754 Some(first) => first.to_uppercase().chain(chars.as_str().chars()).collect(),
1755 }
1756 })
1757 .collect::<Vec<_>>()
1758 .join("-")
1759}
1760
1761pub(crate) struct HttpKernelAuth {
1776 pub(crate) plan: camel_api::security_policy::RouteSecurityPlan,
1777 pub(crate) providers: Arc<camel_auth::ProviderRegistry>,
1778}
1779
1780impl HttpKernelAuth {
1781 pub(crate) fn from_security_context(
1786 ctx: &camel_component_api::SecurityContext,
1787 ) -> Option<Self> {
1788 Some(Self {
1789 plan: ctx.plan.clone()?,
1790 providers: ctx.providers.clone()?,
1791 })
1792 }
1793}
1794
1795fn envelope_channel_capacity(max_inflight_requests: usize) -> usize {
1809 max_inflight_requests.max(1)
1810}
1811
1812pub(crate) fn max_inflight_requests_limit(configured: usize) -> Result<usize, CamelError> {
1827 if configured > tokio::sync::Semaphore::MAX_PERMITS {
1828 return Err(CamelError::Config(format!(
1829 "maxInflightRequests {configured} exceeds the supported upper bound {} (tokio::sync::Semaphore::MAX_PERMITS)",
1830 tokio::sync::Semaphore::MAX_PERMITS
1831 )));
1832 }
1833 Ok(configured)
1834}
1835
1836pub struct HttpConsumer {
1837 config: HttpServerConfig,
1838 runtime: Arc<dyn RuntimeObservability>,
1840 kernel: Option<Arc<HttpKernelAuth>>,
1844}
1845
1846impl HttpConsumer {
1847 pub fn new(config: HttpServerConfig, runtime: Arc<dyn RuntimeObservability>) -> Self {
1848 Self {
1849 config,
1850 runtime,
1851 kernel: None,
1852 }
1853 }
1854}
1855
1856#[async_trait::async_trait]
1857impl Consumer for HttpConsumer {
1858 async fn start(&mut self, ctx: camel_component_api::ConsumerContext) -> Result<(), CamelError> {
1859 use camel_component_api::{Body, Exchange, Message};
1860
1861 max_inflight_requests_limit(self.config.max_inflight_requests)?;
1866
1867 let registry = ServerRegistry::global()
1868 .get_or_spawn(
1869 &self.config.host,
1870 self.config.port,
1871 self.config.max_request_body,
1872 self.config.max_response_body,
1873 self.config.max_inflight_requests,
1874 self.runtime.clone(),
1875 ctx.route_id().to_string(),
1876 self.config.tls_config.clone(),
1877 )
1878 .await?;
1879
1880 let (env_tx, mut env_rx) = tokio::sync::mpsc::channel::<RequestEnvelope>(
1884 envelope_channel_capacity(self.config.max_inflight_requests),
1885 );
1886 if let Some(method) = self.config.method.clone() {
1893 let segments = rest_match::parse_path_template(&self.config.path);
1894 registry
1895 .register_rest_endpoint(method, segments, env_tx)
1896 .await;
1897 } else {
1898 registry
1899 .register_api_route(self.config.path.clone(), env_tx)
1900 .await;
1901 }
1902
1903 ctx.mark_ready();
1912
1913 let in_flight = ctx.in_flight_counter();
1917
1918 let path = self.config.path.clone();
1919 let registry_for_cleanup = registry.clone();
1920 let server_exited = registry.server_exited.clone();
1921 let cancel_token = ctx.cancel_token();
1922 let kernel = self.kernel.clone();
1923 let mut server_died = false;
1928 loop {
1929 tokio::select! {
1930 _ = ctx.cancelled() => {
1931 break;
1932 }
1933 _ = server_exited.cancelled() => {
1934 server_died = true;
1939 break;
1940 }
1941 envelope = env_rx.recv() => {
1942 let Some(envelope) = envelope else { break; };
1943
1944 let claim =
1952 in_flight.as_ref().map(camel_component_api::InFlightClaim::attach);
1953
1954 let mut msg = Message::default();
1956
1957 msg.set_header("CamelHttpMethod",
1959 serde_json::Value::String(envelope.method.clone()));
1960 msg.set_header("CamelHttpPath",
1961 serde_json::Value::String(envelope.path.clone()));
1962 msg.set_header("CamelHttpQuery",
1963 serde_json::Value::String(envelope.query.clone()));
1964
1965 for (param_name, param_value) in &envelope.path_params {
1972 msg.set_header(
1973 format!("CamelHttpPath_{param_name}"),
1974 serde_json::Value::String(param_value.clone()),
1975 );
1976 }
1977
1978 for (k, v) in &envelope.headers {
1980 if let Ok(val_str) = v.to_str() {
1981 msg.set_header(
1982 title_case_header(k.as_str()),
1983 serde_json::Value::String(val_str.to_string()),
1984 );
1985 }
1986 }
1987
1988 msg.body = Body::Stream(envelope.body);
1991
1992 #[allow(unused_mut)]
1993 let mut exchange = Exchange::new(msg);
1994
1995 #[cfg(feature = "otel")]
1997 {
1998 let headers: HashMap<String, String> = envelope
1999 .headers
2000 .iter()
2001 .filter_map(|(k, v)| {
2002 Some((k.as_str().to_lowercase(), v.to_str().ok()?.to_string()))
2003 })
2004 .collect();
2005 camel_otel::extract_into_exchange(&mut exchange, &headers);
2006 }
2007
2008 let reply_tx = envelope.reply_tx;
2009 let sender = ctx.sender().clone();
2010 let path_clone = path.clone();
2011 let cancel = cancel_token.clone();
2012 let auth_headers = envelope.headers.clone();
2016 let auth_uri: http::Uri = {
2017 let full = if envelope.query.is_empty() {
2018 envelope.path.clone()
2019 } else {
2020 format!("{}?{}", envelope.path, envelope.query)
2021 };
2022 full.parse().unwrap_or_default()
2026 };
2027 let kernel = kernel.clone();
2028
2029 tokio::spawn(async move {
2049 if cancel.is_cancelled() {
2057 let _ = reply_tx.send(HttpReply {
2058 status: 503,
2059 headers: vec![],
2060 body: HttpReplyBody::Bytes(bytes::Bytes::from("Service Unavailable")),
2061 });
2062 return;
2063 }
2064
2065 if let Some(kernel) = kernel.as_ref()
2074 && !matches!(
2075 kernel.plan.access_mode,
2076 camel_api::security_policy::AccessMode::Public
2077 )
2078 {
2079 let principal = match camel_auth::extract_token_multi(
2080 &auth_headers,
2081 &auth_uri,
2082 &kernel.plan.credential_sources,
2083 ) {
2084 Some(extracted) => {
2085 match camel_auth::kernel_authenticate(
2086 &kernel.plan,
2087 &kernel.providers,
2088 &extracted,
2089 )
2090 .await
2091 {
2092 Ok(principal) => principal,
2093 Err(e) => {
2094 tracing::warn!(
2096 path = %path_clone,
2097 error = %e,
2098 "HTTP request authentication failed"
2099 );
2100 let _ = reply_tx.send(pipeline_error_to_reply(
2101 e,
2102 &path_clone,
2103 ));
2104 return;
2105 }
2106 }
2107 }
2108 None => {
2109 tracing::warn!(
2111 path = %path_clone,
2112 "HTTP request rejected: no credential found in any source"
2113 );
2114 let _ = reply_tx.send(pipeline_error_to_reply(
2115 CamelError::Unauthenticated(
2116 "no credential found in any source".to_string(),
2117 ),
2118 &path_clone,
2119 ));
2120 return;
2121 }
2122 };
2123 camel_auth::install_carrier(&mut exchange, &principal);
2124 }
2125
2126 let (tx, rx) = tokio::sync::oneshot::channel();
2128 let envelope = camel_component_api::consumer::ExchangeEnvelope {
2129 exchange,
2130 reply_tx: Some(tx),
2131 in_flight_claim: claim,
2137 };
2138
2139 let result = match sender.send(envelope).await {
2140 Ok(()) => rx.await.map_err(|_| camel_component_api::CamelError::ChannelClosed),
2141 Err(_) => Err(camel_component_api::CamelError::ChannelClosed),
2142 }
2143 .and_then(|r| r);
2144
2145 let reply = match result {
2146 Ok(out) => {
2147 let status = out
2148 .input
2149 .header("CamelHttpResponseCode")
2150 .and_then(|v| {
2151 let raw = v.as_u64()
2152 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))?;
2153 let code = raw as u16;
2154 (100..1000).contains(&code).then_some(code)
2155 })
2156 .unwrap_or(200);
2157
2158 let user_content_type = out
2159 .input
2160 .header("Content-Type")
2161 .and_then(|v| v.as_str().map(|s| s.to_string()));
2162
2163 let (reply_body, inferred_content_type): (HttpReplyBody, Option<String>) = match out.input.body {
2164 Body::Bytes(b) => (HttpReplyBody::Bytes(b), None),
2165 Body::Text(s) => (HttpReplyBody::Bytes(bytes::Bytes::from(s.into_bytes())), Some("text/plain; charset=utf-8".to_string())),
2166 Body::Xml(s) => (HttpReplyBody::Bytes(bytes::Bytes::from(s.into_bytes())), Some("application/xml".to_string())),
2167 Body::Json(v) => (HttpReplyBody::Bytes(bytes::Bytes::from(
2168 v.to_string().into_bytes(),
2169 )), Some("application/json".to_string())),
2170 Body::Stream(s) => {
2171 let ct = s.metadata.content_type.clone();
2172 match s.stream.lock().await.take() {
2173 Some(stream) => (
2174 HttpReplyBody::Stream(stream),
2175 ct,
2176 ),
2177 None => {
2178 tracing::error!(
2180 "Body::Stream already consumed before HTTP reply — returning 500"
2181 );
2182 let error_reply = HttpReply {
2183 status: 500,
2184 headers: vec![],
2185 body: HttpReplyBody::Bytes(bytes::Bytes::new()),
2186 };
2187 if reply_tx.send(error_reply).is_err() {
2188 debug!("reply_tx dropped before error reply could be sent");
2189 }
2190 return;
2191 }
2192 }
2193 }
2194 _ => (HttpReplyBody::Bytes(bytes::Bytes::new()), None),
2196 };
2197
2198 let resp_headers = select_response_headers(
2199 &out.input.headers,
2200 user_content_type,
2201 inferred_content_type,
2202 );
2203
2204 HttpReply {
2205 status,
2206 headers: resp_headers,
2207 body: reply_body,
2208 }
2209 }
2210 Err(e) => {
2211 pipeline_error_to_reply(e, &path_clone)
2212 }
2213 };
2214
2215 let _ = reply_tx.send(reply);
2217 });
2218 }
2219 }
2220 }
2221
2222 if let Some(method) = &self.config.method {
2227 registry_for_cleanup
2228 .unregister_rest_endpoint(method, &path)
2229 .await;
2230 } else {
2231 registry_for_cleanup.unregister_api_route(&path).await;
2232 }
2233
2234 ServerRegistry::global()
2240 .unregister(&self.config.host, self.config.port)
2241 .await;
2242
2243 if server_died {
2244 tracing::error!(
2246 host = %camel_api::redact::redact_host(&self.config.host),
2247 port = self.config.port,
2248 path = %path,
2249 "Shared HTTP server exited — failing consumer to engage route supervision (ADR-0007)"
2250 );
2251 return Err(CamelError::RouteError(format!(
2255 "shared HTTP server for {}:{} exited unexpectedly; route transport is dead",
2256 camel_api::redact::redact_host(&self.config.host),
2257 self.config.port
2258 )));
2259 }
2260
2261 Ok(())
2262 }
2263
2264 async fn stop(&mut self) -> Result<(), CamelError> {
2265 Ok(())
2266 }
2267
2268 fn concurrency_model(&self) -> camel_component_api::ConcurrencyModel {
2269 camel_component_api::ConcurrencyModel::Concurrent { max: None }
2270 }
2271
2272 fn startup_mode(&self) -> camel_component_api::ConsumerStartupMode {
2279 camel_component_api::ConsumerStartupMode::Explicit
2280 }
2281
2282 fn set_security_context(&mut self, ctx: camel_component_api::SecurityContext) {
2285 self.kernel = HttpKernelAuth::from_security_context(&ctx).map(Arc::new);
2286 }
2287}
2288
2289pub struct HttpComponent {
2294 config: HttpConfig,
2295 pinned_cache: std::sync::Arc<PinnedClientCache>,
2296 client: reqwest::Client,
2297 strict_tls_error: Option<CamelError>,
2301}
2302
2303impl HttpComponent {
2304 pub fn new() -> Self {
2305 let config = HttpConfig::default();
2306 let strict_err = strict_tls_error(&config);
2307 let (client, build_err) = client_or_emergency(&config);
2308 Self {
2309 client,
2310 config,
2311 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2312 PINNED_CLIENT_TTL,
2313 PINNED_CLIENT_MAX_ENTRIES,
2314 )),
2315 strict_tls_error: strict_err.or(build_err),
2316 }
2317 }
2318
2319 pub fn with_config(config: HttpConfig) -> Self {
2320 let strict_err = strict_tls_error(&config);
2321 let (client, build_err) = client_or_emergency(&config);
2322 Self {
2323 client,
2324 config,
2325 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2326 PINNED_CLIENT_TTL,
2327 PINNED_CLIENT_MAX_ENTRIES,
2328 )),
2329 strict_tls_error: strict_err.or(build_err),
2330 }
2331 }
2332
2333 pub fn with_optional_config(config: Option<HttpConfig>) -> Self {
2334 match config {
2335 Some(cfg) => Self::with_config(cfg),
2336 None => Self::new(),
2337 }
2338 }
2339}
2340
2341impl Default for HttpComponent {
2342 fn default() -> Self {
2343 Self::new()
2344 }
2345}
2346
2347impl Component for HttpComponent {
2348 fn scheme(&self) -> &str {
2349 "http"
2350 }
2351
2352 fn metadata(&self) -> ComponentMetadata {
2353 HttpEndpointConfig::metadata()
2354 }
2355
2356 fn create_endpoint(
2357 &self,
2358 uri: &str,
2359 ctx: &dyn camel_component_api::ComponentContext,
2360 ) -> Result<Box<dyn Endpoint>, CamelError> {
2361 if let Some(err) = &self.strict_tls_error {
2362 return Err(err.clone());
2363 }
2364 self.config.validate()?;
2365 let config = HttpEndpointConfig::from_uri_with_defaults(uri, &self.config)?;
2366 let server_config = HttpServerConfig::from_uri_with_defaults(uri, &self.config)?;
2367 ctx.register_current_route_health_check(Arc::new(HttpHealthCheck::new(
2368 server_config.host.clone(),
2369 server_config.port,
2370 )));
2371 self.pinned_cache
2372 .wire(HttpComponentKind::Http, ctx.metrics());
2373 Ok(Box::new(HttpEndpoint {
2374 uri: uri.to_string(),
2375 config,
2376 server_config,
2377 client: self.client.clone(),
2378 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2379 http_config: self.config.clone(),
2380 }))
2381 }
2382}
2383
2384pub struct HttpsComponent {
2385 config: HttpConfig,
2386 pinned_cache: std::sync::Arc<PinnedClientCache>,
2387 client: reqwest::Client,
2388 strict_tls_error: Option<CamelError>,
2392}
2393
2394impl HttpsComponent {
2395 pub fn new() -> Self {
2396 let config = HttpConfig::default();
2397 let strict_err = strict_tls_error(&config);
2398 let (client, build_err) = client_or_emergency(&config);
2399 Self {
2400 client,
2401 config,
2402 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2403 PINNED_CLIENT_TTL,
2404 PINNED_CLIENT_MAX_ENTRIES,
2405 )),
2406 strict_tls_error: strict_err.or(build_err),
2407 }
2408 }
2409
2410 pub fn with_config(config: HttpConfig) -> Self {
2411 let strict_err = strict_tls_error(&config);
2412 let (client, build_err) = client_or_emergency(&config);
2413 Self {
2414 client,
2415 config,
2416 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2417 PINNED_CLIENT_TTL,
2418 PINNED_CLIENT_MAX_ENTRIES,
2419 )),
2420 strict_tls_error: strict_err.or(build_err),
2421 }
2422 }
2423
2424 pub fn with_optional_config(config: Option<HttpConfig>) -> Self {
2425 match config {
2426 Some(cfg) => Self::with_config(cfg),
2427 None => Self::new(),
2428 }
2429 }
2430}
2431
2432impl Default for HttpsComponent {
2433 fn default() -> Self {
2434 Self::new()
2435 }
2436}
2437
2438impl Component for HttpsComponent {
2439 fn scheme(&self) -> &str {
2440 "https"
2441 }
2442
2443 fn metadata(&self) -> ComponentMetadata {
2444 let mut meta = HttpEndpointConfig::metadata();
2447 meta.scheme = "https".to_string();
2448 meta.description = "HTTPS client and server component (TLS over HTTP)".to_string();
2449 meta
2450 }
2451
2452 fn create_endpoint(
2453 &self,
2454 uri: &str,
2455 ctx: &dyn camel_component_api::ComponentContext,
2456 ) -> Result<Box<dyn Endpoint>, CamelError> {
2457 if let Some(err) = &self.strict_tls_error {
2458 return Err(err.clone());
2459 }
2460 self.config.validate()?;
2461 let config = HttpEndpointConfig::from_uri_with_defaults(uri, &self.config)?;
2462 let server_config = HttpServerConfig::from_uri_with_defaults(uri, &self.config)?;
2463 ctx.register_current_route_health_check(Arc::new(HttpHealthCheck::new(
2464 server_config.host.clone(),
2465 server_config.port,
2466 )));
2467 self.pinned_cache
2468 .wire(HttpComponentKind::Https, ctx.metrics());
2469 Ok(Box::new(HttpEndpoint {
2470 uri: uri.to_string(),
2471 config,
2472 server_config,
2473 client: self.client.clone(),
2474 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2475 http_config: self.config.clone(),
2476 }))
2477 }
2478}
2479
2480struct HttpEndpoint {
2485 uri: String,
2486 config: HttpEndpointConfig,
2487 server_config: HttpServerConfig,
2488 client: reqwest::Client,
2489 pinned_cache: std::sync::Arc<PinnedClientCache>,
2490 http_config: HttpConfig,
2491}
2492
2493impl Endpoint for HttpEndpoint {
2494 fn uri(&self) -> &str {
2495 &self.uri
2496 }
2497
2498 fn create_consumer(
2499 &self,
2500 rt: Arc<dyn camel_component_api::RuntimeObservability>,
2501 ) -> Result<Box<dyn Consumer>, CamelError> {
2502 let scheme_is_https = self.server_config.scheme == "https";
2505 let has_tls = self.server_config.tls_config.is_some();
2506
2507 if scheme_is_https && !has_tls {
2508 return Err(CamelError::EndpointCreationFailed(
2509 "https:// consumer requires tlsCert and tlsKey parameters".to_string(),
2510 ));
2511 }
2512 if !scheme_is_https && has_tls {
2513 return Err(CamelError::EndpointCreationFailed(
2514 "http:// is incompatible with tlsCert/tlsKey — use https:// for TLS".to_string(),
2515 ));
2516 }
2517 max_inflight_requests_limit(self.server_config.max_inflight_requests)?;
2522 Ok(Box::new(HttpConsumer::new(self.server_config.clone(), rt)))
2523 }
2524
2525 fn create_producer(
2526 &self,
2527 rt: Arc<dyn camel_component_api::RuntimeObservability>,
2528 _ctx: &ProducerContext,
2529 ) -> Result<BoxProcessor, CamelError> {
2530 let producer = HttpProducer {
2531 config: Arc::new(self.config.clone()),
2532 client: self.client.clone(),
2533 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2534 http_config: Arc::new(self.http_config.clone()),
2535 runtime: rt,
2536 };
2537 if let Some(ref provider) = self.config.token_provider {
2538 let layer = BearerTokenLayer::new(Arc::clone(provider));
2539 Ok(BoxProcessor::new(layer.layer(producer)))
2540 } else {
2541 Ok(BoxProcessor::new(producer))
2542 }
2543 }
2544}
2545
2546#[derive(Clone)]
2551struct HttpProducer {
2552 config: Arc<HttpEndpointConfig>,
2553 client: reqwest::Client,
2554 pinned_cache: std::sync::Arc<PinnedClientCache>,
2555 http_config: Arc<HttpConfig>,
2556 runtime: Arc<dyn RuntimeObservability>,
2562}
2563
2564impl HttpProducer {
2565 fn resolve_method(exchange: &Exchange, config: &HttpEndpointConfig) -> String {
2566 if let Some(ref method) = config.http_method {
2567 return method.to_uppercase();
2568 }
2569 if let Some(method) = exchange
2570 .input
2571 .header("CamelHttpMethod")
2572 .and_then(|v| v.as_str())
2573 {
2574 return method.to_uppercase();
2575 }
2576 if !exchange.input.body.is_empty() {
2577 return "POST".to_string();
2578 }
2579 "GET".to_string()
2580 }
2581
2582 fn resolve_url(exchange: &Exchange, config: &HttpEndpointConfig) -> Result<String, CamelError> {
2583 if config.bridge_endpoint {
2592 let Some(query) = resolve_endpoint_query(config)? else {
2593 return Ok(config.base_url.clone());
2594 };
2595 let _: url::Url = url::Url::parse(&config.base_url).map_err(|e| {
2603 CamelError::ProcessorError(format!(
2604 "invalid base URL '{}': {e}",
2605 redact_url_for_diagnostics(&config.base_url)
2606 ))
2607 })?;
2608 let mut url = config.base_url.clone();
2609 url.push('?');
2610 url.push_str(&query);
2611 return Ok(url);
2612 }
2613
2614 if let Some(uri) = exchange
2615 .input
2616 .header("CamelHttpUri")
2617 .and_then(|v| v.as_str())
2618 {
2619 if let Some(fence) = &config.allowed_uri_hosts
2624 && !uri_host_allowed(uri, fence)?
2625 {
2626 return Err(CamelError::ProcessorError(format!(
2627 "CamelHttpUri host not allowed by allowedUriHosts fence: {}",
2628 redact_url_for_diagnostics(uri)
2629 )));
2630 }
2631 let (base, override_query) = match uri.split_once('?') {
2638 Some((base, query)) => (base, Some(query)),
2639 None => (uri, None),
2640 };
2641 if let Some(query) = override_query {
2647 for (_key, span) in raw_query_pairs(query)? {
2648 validate_raw_query_span(span)?;
2649 }
2650 }
2651 let mut url = base.to_string();
2652 if let Some(path) = exchange
2653 .input
2654 .header("CamelHttpPath")
2655 .and_then(|v| v.as_str())
2656 {
2657 if !url.ends_with('/') && !path.starts_with('/') {
2658 url.push('/');
2659 }
2660 url.push_str(path);
2661 }
2662 if let Some(query) = exchange
2663 .input
2664 .header("CamelHttpQuery")
2665 .and_then(|v| v.as_str())
2666 {
2667 if let Some(merged) = merge_header_query(override_query, query)? {
2668 url.push('?');
2669 url.push_str(&merged);
2670 }
2671 return Ok(url);
2672 }
2673 if let Some(query) = override_query {
2674 url.push('?');
2675 url.push_str(query);
2676 }
2677 return Ok(url);
2678 }
2679
2680 let mut url = config.base_url.clone();
2681
2682 if let Some(path) = exchange
2683 .input
2684 .header("CamelHttpPath")
2685 .and_then(|v| v.as_str())
2686 {
2687 if !url.ends_with('/') && !path.starts_with('/') {
2688 url.push('/');
2689 }
2690 url.push_str(path);
2691 }
2692
2693 if let Some(query) = exchange
2694 .input
2695 .header("CamelHttpQuery")
2696 .and_then(|v| v.as_str())
2697 {
2698 if let Some(merged) =
2703 merge_header_query(resolve_endpoint_query(config)?.as_deref(), query)?
2704 {
2705 url.push('?');
2706 url.push_str(&merged);
2707 }
2708 return Ok(url);
2709 }
2710
2711 if let Some(query) = resolve_endpoint_query(config)? {
2712 url.push('?');
2713 url.push_str(&query);
2714 }
2715
2716 Ok(url)
2717 }
2718
2719 fn is_ok_status(status: u16, range: (u16, u16)) -> bool {
2720 status >= range.0 && status <= range.1
2721 }
2722}
2723
2724#[derive(Clone, Debug, PartialEq, Eq)]
2729pub struct AllowedUriHost {
2730 pub host: String,
2732 pub port: Option<u16>,
2734}
2735
2736fn parse_allowed_uri_hosts(raw: &str) -> Result<Vec<AllowedUriHost>, CamelError> {
2744 let mut entries = Vec::new();
2745 for segment in raw.split(',') {
2746 let segment = segment.trim();
2747 if segment.is_empty() {
2748 continue;
2749 }
2750 let parsed = url::Url::parse(&format!("http://{segment}"))
2751 .map_err(|_| invalid_allowed_uri_host_entry(segment))?;
2752 if parsed.path() != "/" || !parsed.username().is_empty() || parsed.password().is_some() {
2756 return Err(invalid_allowed_uri_host_entry(segment));
2757 }
2758 let Some(host) = parsed.host_str() else {
2759 return Err(invalid_allowed_uri_host_entry(segment));
2760 };
2761 entries.push(AllowedUriHost {
2762 host: host.to_string(),
2763 port: parsed.port(),
2764 });
2765 }
2766 if entries.is_empty() {
2767 return Err(CamelError::InvalidUri(
2768 "allowedUriHosts declares no valid host entries".to_string(),
2769 ));
2770 }
2771 Ok(entries)
2772}
2773
2774fn invalid_allowed_uri_host_entry(segment: &str) -> CamelError {
2775 CamelError::InvalidUri(format!("invalid allowedUriHosts entry '{segment}'"))
2776}
2777
2778pub(crate) fn uri_host_allowed(
2785 url_str: &str,
2786 fence: &[AllowedUriHost],
2787) -> Result<bool, CamelError> {
2788 let Ok(parsed) = url::Url::parse(url_str) else {
2789 return Ok(false);
2790 };
2791 let Some(host) = parsed.host_str() else {
2792 return Ok(false);
2793 };
2794 let effective_port = parsed.port().or(match parsed.scheme() {
2795 "https" => Some(443_u16),
2796 "http" => Some(80),
2797 _ => None,
2798 });
2799 Ok(fence.iter().any(|entry| {
2800 entry.host == host
2801 && match entry.port {
2802 None => true,
2803 Some(port) => effective_port == Some(port),
2804 }
2805 }))
2806}
2807
2808fn resolve_endpoint_query(config: &HttpEndpointConfig) -> Result<Option<String>, CamelError> {
2821 let mut parts: Vec<String> = Vec::new();
2822 let mut authored_keys = std::collections::HashSet::new();
2823
2824 if let Some(raw) = config.raw_query.as_deref() {
2825 for (key, span) in raw_query_pairs(raw)? {
2826 authored_keys.insert(key.clone());
2827 if is_consumed_option(&key) {
2828 continue;
2829 }
2830 validate_raw_query_span(span)?;
2831 parts.push(span.to_string());
2832 }
2833 }
2834
2835 for (key, value) in &config.query_params {
2836 if !authored_keys.contains(key.as_str()) {
2837 parts.push(format!(
2838 "{}={}",
2839 encode_query_component(key),
2840 encode_query_component(value)
2841 ));
2842 }
2843 }
2844
2845 if parts.is_empty() && config.raw_query.as_deref() != Some("") {
2846 return Ok(None);
2847 }
2848 Ok(Some(parts.join("&")))
2849}
2850
2851fn merge_header_query(
2860 higher_precedence: Option<&str>,
2861 header_query: &str,
2862) -> Result<Option<String>, CamelError> {
2863 if header_query.is_empty() {
2864 return Ok(higher_precedence.map(str::to_string));
2865 }
2866 let mut parts: Vec<String> = Vec::new();
2867 let mut higher_keys = std::collections::HashSet::new();
2868 for (key, span) in raw_query_pairs(higher_precedence.unwrap_or(""))? {
2869 higher_keys.insert(key);
2870 parts.push(span.to_string());
2871 }
2872 for (key, span) in raw_query_pairs(header_query)? {
2873 validate_raw_query_span(span)?;
2874 if !higher_keys.contains(key.as_str()) {
2875 parts.push(span.to_string());
2876 }
2877 }
2878 if parts.is_empty() {
2879 return Ok(None);
2880 }
2881 Ok(Some(parts.join("&")))
2882}
2883
2884fn is_legal_query_byte(byte: u8) -> bool {
2895 matches!(byte,
2896 b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
2897 | b'-' | b'.' | b'_' | b'~'
2898 | b'!' | b'$' | b'&' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
2899 | b':' | b'@' | b'/' | b'?'
2900 | b'%')
2901}
2902
2903fn validate_raw_query_span(span: &str) -> Result<(), CamelError> {
2909 for &byte in span.as_bytes() {
2910 if !is_legal_query_byte(byte) {
2911 return Err(CamelError::ProcessorError(format!(
2912 "raw query pair '{span}' contains byte 0x{byte:02X}, which is not legal in a URL query component"
2913 )));
2914 }
2915 }
2916 Ok(())
2917}
2918
2919fn encode_query_component(component: &str) -> String {
2923 const HEX: &[u8; 16] = b"0123456789ABCDEF";
2924 let mut out = String::with_capacity(component.len());
2925 for &byte in component.as_bytes() {
2926 match byte {
2927 b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' => {
2928 out.push(byte as char);
2929 }
2930 _ => {
2931 out.push('%');
2932 out.push(HEX[(byte >> 4) as usize] as char);
2933 out.push(HEX[(byte & 0x0f) as usize] as char);
2934 }
2935 }
2936 }
2937 out
2938}
2939
2940pub(crate) fn redact_url_for_diagnostics(raw: &str) -> String {
2968 match url::Url::parse(raw) {
2969 Ok(mut u) => {
2970 if !u.cannot_be_a_base()
2977 && u.host_str().is_none()
2978 && camel_api::redact::window_has_at_sign(raw)
2979 {
2980 return "[redacted]".to_string();
2981 }
2982 if !u.username().is_empty() || u.password().is_some() {
2983 let _ = u.set_username("***");
2984 let _ = u.set_password(None);
2985 }
2986 let s = u.to_string();
2989 camel_api::redact::redact_url(&s)
2990 }
2991 Err(_) => camel_api::redact::redact_url_fail_closed(raw),
2992 }
2993}
2994
2995const MAX_ERROR_RESPONSE_BODY_BYTES: usize = 4096;
3000
3001fn truncate_error_body(body: &[u8]) -> String {
3002 if body.len() <= MAX_ERROR_RESPONSE_BODY_BYTES {
3003 String::from_utf8_lossy(body).into_owned()
3004 } else {
3005 let mut s = String::from_utf8_lossy(&body[..MAX_ERROR_RESPONSE_BODY_BYTES]).into_owned();
3006 s.push_str("...[truncated]");
3007 s
3008 }
3009}
3010
3011impl HttpProducer {
3012 fn is_entity_enclosing(method: &str) -> bool {
3017 matches!(method, "POST" | "PUT" | "PATCH")
3018 }
3019}
3020
3021impl Service<Exchange> for HttpProducer {
3022 type Response = Exchange;
3023 type Error = CamelError;
3024 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
3025
3026 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3027 Poll::Ready(Ok(()))
3028 }
3029
3030 fn call(&mut self, exchange: Exchange) -> Self::Future {
3031 let config = self.config.clone();
3032 let shared_client = self.client.clone();
3033 let pinned_cache = std::sync::Arc::clone(&self.pinned_cache);
3034 let http_config = self.http_config.clone();
3035 let component_metrics = self.runtime.component_metrics();
3036
3037 Box::pin(async move {
3038 let mut exchange = exchange;
3039 let outcome = async {
3040 let method_str = HttpProducer::resolve_method(&exchange, &config);
3041 let suppress_body = !HttpProducer::is_entity_enclosing(&method_str);
3046 let url = HttpProducer::resolve_url(&exchange, &config)?;
3047
3048 ssrf::validate_url_for_ssrf(&url, &config)?;
3050
3051 let resolved = ssrf::resolve_initial_url_for_ssrf(
3059 &url,
3060 config.allow_internal,
3061 config.allow_cleartext,
3062 )
3063 .await?;
3064 let client: reqwest::Client = if let Some((ref host, ref addrs)) = resolved {
3065 pinned_cache
3069 .get_or_build(host.as_str(), addrs, || {
3070 build_client(&http_config, Some((host.as_str(), addrs)))
3071 })
3072 .await?
3073 } else {
3074 shared_client.clone()
3075 };
3076
3077 debug!(
3078 correlation_id = %exchange.correlation_id(),
3079 method = %method_str,
3080 url = %redact_url_for_diagnostics(&url),
3081 "HTTP request"
3082 );
3083
3084 let method = method_str.parse::<reqwest::Method>().map_err(|e| {
3085 CamelError::ProcessorError(format!(
3086 "Invalid HTTP method '{}': {}",
3087 method_str, e
3088 ))
3089 })?;
3090
3091 let mut collected_headers: Vec<(
3093 reqwest::header::HeaderName,
3094 reqwest::header::HeaderValue,
3095 )> = Vec::new();
3096
3097 if let Some(user_agent) = &config.user_agent
3098 && !config.bridge_endpoint
3099 {
3100 match constructed_header("user-agent", user_agent) {
3101 Ok((_, val)) => {
3102 collected_headers.push((reqwest::header::USER_AGENT, val));
3103 }
3104 Err(drop) => debug!(
3105 correlation_id = %exchange.correlation_id(),
3106 header = %drop.name,
3107 "outbound header dropped: {}",
3108 drop.reason
3109 ),
3110 }
3111 }
3112
3113 #[cfg(feature = "otel")]
3115 let should_inject_otel = !config.bridge_endpoint;
3116 #[cfg(feature = "otel")]
3117 if should_inject_otel {
3118 let mut otel_headers = HashMap::new();
3119 camel_otel::inject_from_exchange(&exchange, &mut otel_headers);
3120 for (k, v) in otel_headers {
3121 match constructed_header(&k, &v) {
3122 Ok((name, val)) => collected_headers.push((name, val)),
3123 Err(drop) => debug!(
3124 correlation_id = %exchange.correlation_id(),
3125 header = %drop.name,
3126 "outbound header dropped: {}",
3127 drop.reason
3128 ),
3129 }
3130 }
3131 }
3132
3133 let conn_tokens = header_policy::connection_tokens(
3134 exchange
3135 .input
3136 .headers
3137 .iter()
3138 .filter(|(k, _)| k.eq_ignore_ascii_case("connection"))
3139 .filter_map(|(_, v)| v.as_str()),
3140 );
3141
3142 let outbound = select_outbound_headers(
3143 &exchange.input.headers,
3144 &config.skip_request_headers,
3145 &conn_tokens,
3146 );
3147 for drop in &outbound.drops {
3148 if let Some(value_kind) = drop.value_kind {
3149 debug!(
3150 correlation_id = %exchange.correlation_id(),
3151 header = %drop.name,
3152 value_kind = value_kind,
3153 "outbound header dropped: {}",
3154 drop.reason
3155 );
3156 } else {
3157 debug!(
3158 correlation_id = %exchange.correlation_id(),
3159 header = %drop.name,
3160 "outbound header dropped: {}",
3161 drop.reason
3162 );
3163 }
3164 }
3165 collected_headers.extend(outbound.accepted);
3166
3167 if !config.bridge_endpoint {
3169 match &config.auth {
3170 HttpAuth::None => {}
3171 HttpAuth::Basic { username, password } => {
3172 use base64::Engine;
3173 let credentials = format!("{username}:{password}");
3175 let encoded =
3176 base64::engine::general_purpose::STANDARD.encode(credentials);
3177 match constructed_header("authorization", &format!("Basic {encoded}")) {
3180 Ok((_, val)) => {
3181 collected_headers.push((reqwest::header::AUTHORIZATION, val));
3182 }
3183 Err(drop) => debug!(
3184 correlation_id = %exchange.correlation_id(),
3185 header = %drop.name,
3186 "outbound header dropped: {}",
3187 drop.reason
3188 ),
3189 }
3190 }
3191 HttpAuth::Bearer { token } => {
3192 let bearer = format!("Bearer {token}");
3194 match constructed_header("authorization", &bearer) {
3195 Ok((_, val)) => {
3196 collected_headers.push((reqwest::header::AUTHORIZATION, val));
3197 }
3198 Err(drop) => debug!(
3199 correlation_id = %exchange.correlation_id(),
3200 header = %drop.name,
3201 "outbound header dropped: {}",
3202 drop.reason
3203 ),
3204 }
3205 }
3206 }
3207
3208 if config.connection_close {
3209 collected_headers.push((
3210 reqwest::header::CONNECTION,
3211 reqwest::header::HeaderValue::from_static("close"),
3212 ));
3213 }
3214 }
3215
3216 let is_stream_body = matches!(exchange.input.body, Body::Stream(_));
3218 let materialized_body: Option<Vec<u8>> = if is_stream_body {
3219 if suppress_body {
3220 std::mem::take(&mut exchange.input.body);
3228 tracing::warn!(
3230 correlation_id = %exchange.correlation_id(),
3231 method = %method_str,
3232 "dropping request body for non-entity-enclosing HTTP method"
3233 );
3234 }
3235 None } else {
3237 let body = std::mem::take(&mut exchange.input.body);
3238 let bytes = body.into_bytes(config.max_body_size).await?;
3239 if bytes.is_empty() {
3240 None
3242 } else if suppress_body {
3243 tracing::warn!(
3245 correlation_id = %exchange.correlation_id(),
3246 method = %method_str,
3247 "dropping request body for non-entity-enclosing HTTP method"
3248 );
3249 None
3250 } else {
3251 Some(bytes.to_vec())
3252 }
3253 };
3254
3255 let response = if config.follow_redirects && !is_stream_body {
3256 ssrf::send_with_ssrf_safe_redirects(
3262 &client,
3263 &shared_client,
3264 &pinned_cache,
3265 &http_config,
3266 &config,
3267 method,
3268 &url,
3269 collected_headers,
3270 materialized_body,
3271 config.max_redirects,
3272 config.response_timeout,
3273 )
3274 .await?
3275 } else {
3276 let mut request = client.request(method, &url);
3278
3279 if let Some(timeout) = config.response_timeout {
3280 request = request.timeout(timeout);
3281 }
3282
3283 for (name, value) in &collected_headers {
3284 request = request.header(name, value);
3285 }
3286
3287 if is_stream_body {
3288 if let Body::Stream(ref s) = exchange.input.body {
3289 let mut stream_lock = s.stream.lock().await;
3290 if let Some(stream) = stream_lock.take() {
3291 request = request.body(reqwest::Body::wrap_stream(stream));
3292 } else {
3293 return Err(CamelError::AlreadyConsumed);
3294 }
3295 }
3296 } else if let Some(ref body_bytes) = materialized_body {
3297 request = request.body(body_bytes.clone());
3298 }
3299
3300 request.send().await.map_err(|e| {
3301 CamelError::ProcessorError(format!("HTTP request failed: {e}"))
3302 })?
3303 };
3304
3305 let status_code = response.status().as_u16();
3306 let status_text = response
3307 .status()
3308 .canonical_reason()
3309 .unwrap_or("Unknown")
3310 .to_string();
3311
3312 for (key, value) in response.headers() {
3313 if config
3314 .skip_response_headers
3315 .iter()
3316 .any(|h| h.eq_ignore_ascii_case(key.as_str()))
3317 {
3318 continue;
3319 }
3320 if let Ok(val_str) = value.to_str() {
3321 exchange.input.set_header(
3322 title_case_header(key.as_str()),
3323 serde_json::Value::String(val_str.to_string()),
3324 );
3325 }
3326 }
3327
3328 exchange.input.set_header(
3329 "CamelHttpResponseCode",
3330 serde_json::Value::Number(status_code.into()),
3331 );
3332 exchange.input.set_header(
3333 "CamelHttpResponseText",
3334 serde_json::Value::String(status_text.clone()),
3335 );
3336
3337 let read_timeout = Duration::from_millis(config.read_timeout_ms);
3339 let response_body = tokio::time::timeout(read_timeout, async {
3340 if let Some(content_len) = response.content_length()
3342 && content_len > config.max_response_bytes as u64
3343 {
3344 return Err(CamelError::ProcessorError(format!(
3345 "Response body too large: {} bytes exceeds limit of {} bytes",
3346 content_len, config.max_response_bytes
3347 )));
3348 }
3349 use futures::TryStreamExt;
3351 let mut stream = response.bytes_stream();
3352 let mut total: usize = 0;
3353 let mut collected = Vec::new();
3354 while let Some(chunk) = stream.try_next().await.map_err(|e| {
3355 CamelError::ProcessorError(format!("Failed to read response body: {e}"))
3356 })? {
3357 total += chunk.len();
3358 if total > config.max_response_bytes {
3359 return Err(CamelError::ProcessorError(format!(
3360 "Response body too large: {} bytes exceeds limit of {} bytes",
3361 total, config.max_response_bytes
3362 )));
3363 }
3364 collected.push(chunk);
3365 }
3366 let mut result = bytes::BytesMut::with_capacity(total);
3367 for chunk in collected {
3368 result.extend_from_slice(&chunk);
3369 }
3370 Ok::<bytes::Bytes, CamelError>(result.freeze())
3371 })
3372 .await
3373 .map_err(|_| {
3374 CamelError::ProcessorError(format!(
3375 "Read timeout after {}ms",
3376 config.read_timeout_ms
3377 ))
3378 })??;
3379
3380 if config.throw_exception_on_failure
3381 && !HttpProducer::is_ok_status(status_code, config.ok_status_code_range)
3382 {
3383 return Err(CamelError::HttpOperationFailed {
3384 method: method_str,
3385 url: redact_url_for_diagnostics(&url),
3388 status_code,
3389 status_text,
3390 response_body: Some(truncate_error_body(&response_body)),
3391 });
3392 }
3393
3394 if !response_body.is_empty() {
3395 exchange.input.body = Body::Bytes(bytes::Bytes::from(response_body.to_vec()));
3396 }
3397
3398 debug!(
3399 correlation_id = %exchange.correlation_id(),
3400 status = status_code,
3401 url = %redact_url_for_diagnostics(&url),
3402 "HTTP response"
3403 );
3404 Ok(exchange)
3405 }
3406 .await;
3407 component_metrics.observe("http", "request", outcome.is_err());
3414 outcome
3415 })
3416 }
3417}
3418
3419#[cfg(test)]
3433pub(crate) static REGISTRY_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3434
3435#[cfg(test)]
3443pub(crate) fn lock_registry_test_mutex() -> std::sync::MutexGuard<'static, ()> {
3444 REGISTRY_TEST_MUTEX
3445 .lock()
3446 .unwrap_or_else(|poisoned| poisoned.into_inner())
3447}
3448
3449#[cfg(test)]
3453static CA_STORE_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3454
3455#[cfg(test)]
3456pub(crate) fn lock_ca_store_test_mutex() -> std::sync::MutexGuard<'static, ()> {
3457 CA_STORE_TEST_MUTEX
3458 .lock()
3459 .unwrap_or_else(|poisoned| poisoned.into_inner())
3460}
3461
3462fn pipeline_error_to_reply(e: CamelError, path: &str) -> HttpReply {
3472 match e {
3473 CamelError::Unauthenticated(msg) => {
3474 tracing::warn!(error = %msg, path = %path, "Authentication failed");
3475 HttpReply {
3476 status: 401,
3477 headers: vec![("WWW-Authenticate".to_string(), "Bearer".to_string())],
3478 body: HttpReplyBody::Bytes(bytes::Bytes::from("Unauthorized")),
3479 }
3480 }
3481 CamelError::Unauthorized(msg) => {
3482 tracing::warn!(error = %msg, path = %path, "Authorization failed");
3483 HttpReply {
3484 status: 403,
3485 headers: vec![],
3486 body: HttpReplyBody::Bytes(bytes::Bytes::from("Forbidden")),
3487 }
3488 }
3489 CamelError::TypeConversionFailed(msg) => {
3490 tracing::warn!(error = %msg, path = %path, "Type conversion failed (bad request)");
3491 json_error_reply(400, "bad_request", msg)
3492 }
3493 CamelError::ValidationError(msg) => {
3494 tracing::warn!(error = %msg, path = %path, "Schema validation failed (bad request)");
3495 json_error_reply(400, "validation_error", msg)
3496 }
3497 CamelError::ConsumerStopping => {
3498 tracing::debug!(path = %path, "Pipeline aborted during route shutdown");
3499 HttpReply {
3500 status: 503,
3501 headers: vec![],
3502 body: HttpReplyBody::Bytes(bytes::Bytes::from("Service Unavailable")),
3503 }
3504 }
3505 CamelError::UnsupportedMediaType { consumed, declared } => {
3506 tracing::warn!(error = %consumed, declared = %declared, path = %path, "Unsupported media type (bad request)");
3507 json_error_reply(
3508 415,
3509 "unsupported_media_type",
3510 format!("consumed {consumed}, declared {declared}"),
3511 )
3512 }
3513 CamelError::NotAcceptable { accept, produced } => {
3514 tracing::warn!(error = %accept, produced = %produced, path = %path, "Not acceptable (bad request)");
3515 json_error_reply(
3516 406,
3517 "not_acceptable",
3518 format!("accept {accept}, produced {produced}"),
3519 )
3520 }
3521 e => {
3522 tracing::warn!(error = %e, path = %path, "Pipeline error processing HTTP request");
3524 HttpReply {
3525 status: 500,
3526 headers: vec![],
3527 body: HttpReplyBody::Bytes(bytes::Bytes::from("Internal Server Error")),
3528 }
3529 }
3530 }
3531}
3532
3533fn json_error_reply(status: u16, code: &str, message: String) -> HttpReply {
3541 let body = serde_json::to_string(&serde_json::json!({
3542 "error": code,
3543 "message": message,
3544 }))
3545 .unwrap_or_else(|_| "{}".to_string()); HttpReply {
3547 status,
3548 headers: vec![("Content-Type".to_string(), "application/json".to_string())],
3549 body: HttpReplyBody::Bytes(bytes::Bytes::from(body)),
3550 }
3551}
3552
3553const fn json_value_kind(v: &serde_json::Value) -> &'static str {
3557 match v {
3558 serde_json::Value::Null => "null",
3559 serde_json::Value::Bool(_) => "bool",
3560 serde_json::Value::Number(_) => "number",
3561 serde_json::Value::String(_) => "string",
3562 serde_json::Value::Array(_) => "array",
3563 serde_json::Value::Object(_) => "object",
3564 }
3565}
3566
3567fn scalar_string_form(v: &serde_json::Value) -> Option<String> {
3572 match v {
3573 serde_json::Value::String(s) => Some(s.clone()),
3574 serde_json::Value::Number(n) => Some(n.to_string()),
3575 serde_json::Value::Bool(b) => Some(b.to_string()),
3576 _ => None,
3577 }
3578}
3579
3580fn select_response_headers(
3595 headers: &HashMap<String, serde_json::Value>,
3596 user_content_type: Option<String>,
3597 inferred_content_type: Option<String>,
3598) -> Vec<(String, String)> {
3599 let conn_tokens = header_policy::connection_tokens(
3600 headers
3601 .iter()
3602 .filter(|(k, _)| k.eq_ignore_ascii_case("connection"))
3603 .filter_map(|(_, v)| v.as_str()),
3604 );
3605 let mut selected: Vec<(String, String)> = Vec::new();
3606 for (k, v) in headers {
3607 if k.starts_with("Camel") {
3608 debug!(header = %k, "reply header dropped: Camel namespace");
3609 continue;
3610 }
3611 if header_policy::excluded_response(k, &conn_tokens) {
3612 debug!(header = %k, "reply header dropped: emission policy");
3613 continue;
3614 }
3615 match scalar_string_form(v) {
3616 Some(s) => selected.push((k.clone(), s)),
3617 None => debug!(
3618 header = %k,
3619 value_kind = json_value_kind(v),
3620 "reply header dropped: no scalar string form"
3621 ),
3622 }
3623 }
3624 if let Some(ct) = user_content_type.or(inferred_content_type) {
3625 selected.push(("Content-Type".to_string(), ct));
3626 }
3627 selected
3628}
3629
3630#[derive(Debug)]
3635struct OutboundHeaderDrop<'a> {
3636 name: &'a str,
3637 reason: &'static str,
3638 value_kind: Option<&'static str>,
3639}
3640
3641struct OutboundHeaderSelection<'a> {
3644 accepted: Vec<(reqwest::header::HeaderName, reqwest::header::HeaderValue)>,
3645 drops: Vec<OutboundHeaderDrop<'a>>,
3646}
3647
3648fn select_outbound_headers<'a>(
3662 headers: &'a HashMap<String, serde_json::Value>,
3663 skip_request_headers: &[String],
3664 conn_tokens: &[String],
3665) -> OutboundHeaderSelection<'a> {
3666 let mut accepted = Vec::new();
3667 let mut drops = Vec::new();
3668 for (key, value) in headers {
3669 if key.starts_with("Camel") {
3670 drops.push(OutboundHeaderDrop {
3671 name: key,
3672 reason: "Camel namespace",
3673 value_kind: None,
3674 });
3675 continue;
3676 }
3677 if skip_request_headers
3678 .iter()
3679 .any(|h| h.eq_ignore_ascii_case(key))
3680 {
3681 drops.push(OutboundHeaderDrop {
3682 name: key,
3683 reason: "skip_request_headers",
3684 value_kind: None,
3685 });
3686 continue;
3687 }
3688 if header_policy::excluded_outbound(key, conn_tokens) {
3689 drops.push(OutboundHeaderDrop {
3690 name: key,
3691 reason: "outbound emission policy",
3692 value_kind: None,
3693 });
3694 continue;
3695 }
3696 let Some(val_str) = scalar_string_form(value) else {
3697 drops.push(OutboundHeaderDrop {
3698 name: key,
3699 reason: "no scalar string form",
3700 value_kind: Some(json_value_kind(value)),
3701 });
3702 continue;
3703 };
3704 match constructed_header(key, &val_str) {
3705 Ok((name, val)) => accepted.push((name, val)),
3706 Err(drop) => drops.push(drop),
3707 }
3708 }
3709 OutboundHeaderSelection { accepted, drops }
3710}
3711
3712fn constructed_header<'a>(
3717 name: &'a str,
3718 value: &str,
3719) -> Result<(reqwest::header::HeaderName, reqwest::header::HeaderValue), OutboundHeaderDrop<'a>> {
3720 let header_name = match reqwest::header::HeaderName::from_bytes(name.as_bytes()) {
3721 Ok(header_name) => header_name,
3722 Err(_) => {
3723 return Err(OutboundHeaderDrop {
3724 name,
3725 reason: "invalid header name",
3726 value_kind: None,
3727 });
3728 }
3729 };
3730 let header_value = match reqwest::header::HeaderValue::from_str(value) {
3731 Ok(header_value) => header_value,
3732 Err(_) => {
3733 return Err(OutboundHeaderDrop {
3734 name,
3735 reason: "invalid header value",
3736 value_kind: None,
3737 });
3738 }
3739 };
3740 Ok((header_name, header_value))
3741}
3742
3743#[cfg(test)]
3744#[path = "lib_tests.rs"]
3745mod tests;