1#[cfg(feature = "quic")]
9mod quic_view;
10
11use std::{convert::Infallible, sync::Arc, time::Duration};
12
13use rama_core::error::ErrorExt as _;
14use rama_http::layer::upgrade::UpgradeLayer;
15use serde::Serialize;
16use serde_json::json;
17
18#[cfg(feature = "quic")]
19use self::quic_view::{EchoH3, EchoQuic, EchoQuicConnection};
20use crate::{
21 Layer, Service,
22 cli::{DEFAULT_CONNECTION_GRACE, ForwardKind, connection_scope},
23 combinators::{Either, Either3},
24 error::{BoxError, BoxErrorExt, ErrorContext},
25 extensions::ExtensionsRef,
26 graceful::WeakScope,
27 http::{
28 BodyLimitLayer, Request, Response, Version,
29 body::util::BodyExt,
30 convert::curl,
31 core::h2::frame::EarlyFrameCapture,
32 fingerprint::{AkamaiH2, Ja4H},
33 header::USER_AGENT,
34 headers::{AltSvc, exotic::XClacksOverhead},
35 layer::{
36 required_header::AddRequiredResponseHeadersLayer, set_header::SetResponseHeaderLayer,
37 trace::TraceLayer,
38 },
39 proto::h2::PseudoHeaderOrder,
40 server::HttpServer,
41 service::web::{extract::Json, response::IntoResponse},
42 ws::handshake::{
43 matcher::WebSocketMatcher,
44 server::{WebSocketAcceptor, WebSocketEchoService},
45 },
46 },
47 layer::{
48 ConsumeErrLayer, LimitLayer,
49 limit::policy::{ConcurrentPolicy, RateLimitReached, RatePolicy, UnlimitedPolicy},
50 },
51 net::{
52 AuthorityInputExt, Protocol, ProtocolInputExt,
53 address::ip::geo::IpGeoDb,
54 forwarded::ForwardedClientExt as _,
55 stream::{
56 SocketInfo,
57 layer::{ThrottleLayer, ThrottleMode},
58 },
59 },
60 proxy::haproxy::server::HaProxyLayer,
61 tcp::TcpStream,
62 telemetry::tracing,
63 ua::{
64 UserAgent,
65 layer::classifier::UserAgentClassifierLayer,
66 profile::{
67 analysis::fingerprint::{RecordedFingerprintCache, RecordedFingerprints},
68 selection::database::UserAgentDatabase,
69 },
70 },
71 utils::{octets::mib, rate::Rate},
72};
73#[cfg(feature = "quic")]
74use crate::{
75 http::proto::h3::observation::{H3RequestObservation, ObservationLimits, SettingsStatus},
76 quic::{self, QuicConnectionInfo, proto::capture::QuicObservation},
77};
78
79core::cfg_select! {
80 feature = "boring" => {
81 use crate::tls::boring::server::TlsAcceptorLayer;
82 }
83 feature = "rustls" => {
84 use crate::tls::rustls::server::TlsAcceptorLayer;
85 }
86 _ => {}
87}
88
89#[cfg(any(feature = "rustls", feature = "boring"))]
90use crate::tls::{
91 SecureTransport,
92 client::{ClientHelloExtension, ECHClientHello},
93 fingerprint::{Ja3, Ja4, PeetPrint},
94 server::TlsServerConfig,
95};
96
97#[derive(Debug, Clone)]
98pub struct EchoServiceBuilder<H> {
101 concurrent_limit: usize,
102 rate_limit: Option<Rate>,
103 throttle: Option<Rate>,
104 body_limit: usize,
105 timeout: Duration,
106 timeout_grace: Duration,
107 forward: Option<ForwardKind>,
108
109 #[cfg(any(feature = "rustls", feature = "boring"))]
110 tls_server_config: Option<TlsServerConfig>,
111
112 http_version: Option<Version>,
113 http2_observation: bool,
114 #[cfg(feature = "quic")]
115 http3_observation: Option<ObservationLimits>,
116
117 ws_support: bool,
118
119 alt_svc: Option<AltSvc>,
120
121 http_service_builder: H,
122
123 uadb: Option<std::sync::Arc<UserAgentDatabase>>,
124
125 geo_db: Option<std::sync::Arc<IpGeoDb>>,
126}
127
128impl Default for EchoServiceBuilder<()> {
129 fn default() -> Self {
130 Self {
131 concurrent_limit: 0,
132 rate_limit: None,
133 throttle: None,
134 body_limit: mib(1),
135 timeout: Duration::ZERO,
136 timeout_grace: DEFAULT_CONNECTION_GRACE,
137 forward: None,
138
139 #[cfg(any(feature = "rustls", feature = "boring"))]
140 tls_server_config: None,
141
142 http_version: None,
143 http2_observation: false,
144 #[cfg(feature = "quic")]
145 http3_observation: None,
146
147 ws_support: false,
148
149 alt_svc: None,
150
151 http_service_builder: (),
152
153 uadb: None,
154
155 geo_db: None,
156 }
157 }
158}
159
160impl EchoServiceBuilder<()> {
161 #[must_use]
163 pub fn new() -> Self {
164 Self::default()
165 }
166}
167
168impl<H> EchoServiceBuilder<H> {
169 crate::utils::macros::generate_set_and_with! {
170 pub fn concurrent(mut self, limit: usize) -> Self {
174 self.concurrent_limit = limit;
175 self
176 }
177 }
178
179 crate::utils::macros::generate_set_and_with! {
180 pub fn rate_limit(mut self, rate: Option<Rate>) -> Self {
183 self.rate_limit = rate;
184 self
185 }
186 }
187
188 crate::utils::macros::generate_set_and_with! {
189 pub fn throttle(mut self, rate: Option<Rate>) -> Self {
192 self.throttle = rate;
193 self
194 }
195 }
196
197 crate::utils::macros::generate_set_and_with! {
198 pub fn body_limit(mut self, limit: usize) -> Self {
200 self.body_limit = limit;
201 self
202 }
203 }
204
205 crate::utils::macros::generate_set_and_with! {
206 pub fn timeout(mut self, timeout: Duration) -> Self {
211 self.timeout = timeout;
212 self
213 }
214 }
215
216 crate::utils::macros::generate_set_and_with! {
217 pub fn timeout_grace(mut self, timeout_grace: Duration) -> Self {
222 self.timeout_grace = timeout_grace;
223 self
224 }
225 }
226
227 crate::utils::macros::generate_set_and_with! {
228 pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
240 self.forward = kind;
241 self
242 }
243 }
244
245 crate::utils::macros::generate_set_and_with! {
246 #[cfg(any(feature = "rustls", feature = "boring"))]
247 pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
250 self.tls_server_config = cfg;
251 self
252 }
253 }
254
255 crate::utils::macros::generate_set_and_with! {
256 pub fn http_version(mut self, version: Option<Version>) -> Self {
258 self.http_version = version;
259 self
260 }
261 }
262
263 crate::utils::macros::generate_set_and_with! {
264 pub fn alt_svc(mut self, alt_svc: Option<AltSvc>) -> Self {
266 self.alt_svc = alt_svc;
267 self
268 }
269 }
270
271 #[cfg(feature = "quic")]
272 crate::utils::macros::generate_set_and_with! {
273 pub fn http3_observation(mut self, limits: Option<ObservationLimits>) -> Self {
275 self.http3_observation = limits;
276 self
277 }
278 }
279
280 crate::utils::macros::generate_set_and_with! {
281 pub fn http2_observation(mut self, enabled: bool) -> Self {
283 self.http2_observation = enabled;
284 self
285 }
286 }
287
288 pub fn with_http_layer<H2>(self, layer: H2) -> EchoServiceBuilder<(H, H2)> {
290 EchoServiceBuilder {
291 concurrent_limit: self.concurrent_limit,
292 rate_limit: self.rate_limit,
293 throttle: self.throttle,
294 body_limit: self.body_limit,
295 timeout: self.timeout,
296 timeout_grace: self.timeout_grace,
297 forward: self.forward,
298
299 #[cfg(any(feature = "rustls", feature = "boring"))]
300 tls_server_config: self.tls_server_config,
301
302 http_version: self.http_version,
303 http2_observation: self.http2_observation,
304 #[cfg(feature = "quic")]
305 http3_observation: self.http3_observation,
306
307 ws_support: self.ws_support,
308
309 alt_svc: self.alt_svc,
310
311 http_service_builder: (self.http_service_builder, layer),
312
313 uadb: self.uadb,
314
315 geo_db: self.geo_db,
316 }
317 }
318
319 crate::utils::macros::generate_set_and_with! {
320 pub fn user_agent_database(
323 mut self,
324 db: Option<std::sync::Arc<UserAgentDatabase>>,
325 ) -> Self {
326 self.uadb = db;
327 self
328 }
329 }
330
331 crate::utils::macros::generate_set_and_with! {
332 pub fn geo_db(mut self, db: Option<std::sync::Arc<IpGeoDb>>) -> Self {
335 self.geo_db = db;
336 self
337 }
338 }
339
340 crate::utils::macros::generate_set_and_with! {
341 pub fn ws_support(
343 mut self,
344 support: bool,
345 ) -> Self {
346 self.ws_support = support;
347 self
348 }
349 }
350}
351
352impl<H> EchoServiceBuilder<H>
353where
354 H: Layer<EchoService, Service: Service<Request, Output = Response, Error = BoxError>>,
355{
356 pub fn build(
358 self,
359 scope: impl Into<WeakScope>,
360 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
361 let scope = scope.into();
362 self.build_services(scope).map(|(tcp, _)| tcp)
363 }
364
365 #[cfg(feature = "quic")]
372 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
373 pub fn build_with_http3(
374 self,
375 scope: impl Into<WeakScope>,
376 ) -> Result<
377 (
378 impl Service<TcpStream, Output = (), Error = Infallible>,
379 impl Service<quic::Connection, Output = (), Error = Infallible>,
380 ),
381 BoxError,
382 > {
383 let scope = scope.into();
384 self.build_services(scope)
385 }
386
387 fn build_services(
388 self,
389 scope: WeakScope,
390 ) -> Result<
391 (
392 impl Service<TcpStream, Output = (), Error = Infallible>,
393 impl super::MaybeHttp3Service,
394 ),
395 BoxError,
396 > {
397 let tcp_forwarded_layer = match &self.forward {
398 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
399 _ => None,
400 };
401
402 let http_service = Arc::new(self.build_http(scope.clone()));
403
404 let connection_limit = LimitLayer::new(if self.concurrent_limit > 0 {
405 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
406 } else {
407 Either::B(UnlimitedPolicy::new())
408 });
409 let connection_timeout = connection_scope(&scope, self.timeout, self.timeout_grace);
410
411 let throttle = self
413 .throttle
414 .map(|rate| ThrottleLayer::symmetric(ThrottleMode::per_conn(rate)));
415 let tcp_service_builder = (
416 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
417 connection_limit.clone(),
418 connection_timeout.clone(),
419 throttle.clone(),
420 tcp_forwarded_layer,
421 BodyLimitLayer::request_only(self.body_limit),
422 #[cfg(any(feature = "rustls", feature = "boring"))]
423 self.tls_server_config
424 .map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
425 );
426
427 let http_transport_service = match self.http_version {
428 Some(Version::HTTP_2) => Either3::A({
429 let mut http = HttpServer::new_h2(scope.clone());
430 http.h2_mut()
431 .set_observe_early_frames(self.http2_observation);
432 if self.ws_support {
433 http.h2_mut().set_enable_connect_protocol();
434 }
435 http.service(http_service.clone())
436 }),
437 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
438 Either3::B(HttpServer::new_http1(scope.clone()).service(http_service.clone()))
439 }
440 Some(version) => {
441 return Err(BoxError::from_static_str("unsupported http version")
442 .context_debug_field("version", version));
443 }
444 None => Either3::C({
445 let mut http = HttpServer::auto(scope.clone());
446 http.h2_mut()
447 .set_observe_early_frames(self.http2_observation);
448 if self.ws_support {
449 http.h2_mut().set_enable_connect_protocol();
450 }
451 http.service(http_service.clone())
452 }),
453 };
454
455 #[cfg(feature = "quic")]
456 let http3_service = {
457 let mut http3 = HttpServer::new_http3(scope);
458 http3.http3_mut().observation = self.http3_observation.map(Box::new);
460 http3.http3_mut().extended_connect = self.ws_support;
461 (
462 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
463 connection_limit,
464 connection_timeout,
465 throttle,
466 BodyLimitLayer::request_only(self.body_limit),
467 )
468 .into_layer(http3.service(http_service))
469 };
470 #[cfg(not(feature = "quic"))]
471 let http3_service = (
472 connection_limit,
473 connection_timeout,
474 throttle,
475 http_service,
476 scope,
477 );
478
479 Ok((
480 tcp_service_builder.into_layer(http_transport_service),
481 http3_service,
482 ))
483 }
484
485 pub fn build_http(
487 &self,
488 scope: WeakScope,
489 ) -> impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H> {
490 let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
491
492 let geo_attribution = self.geo_db.as_ref().and_then(|db| {
494 let notices: Vec<_> = db.attributions().collect();
495 (!notices.is_empty()).then(|| crate::cli::service::geo::geo_attribution_layer(notices))
496 });
497
498 (
499 TraceLayer::new_for_http(),
500 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
501 self.alt_svc
502 .clone()
503 .map(SetResponseHeaderLayer::if_not_present_typed),
504 AddRequiredResponseHeadersLayer::default(),
505 self.rate_limit.map(|rate| {
506 LimitLayer::new(RatePolicy::abort(rate)).with_error_into_response_fn(
507 |err: RateLimitReached| Ok::<_, Infallible>(err.into_response()),
508 )
509 }),
510 geo_attribution,
511 UserAgentClassifierLayer::new(),
512 ConsumeErrLayer::default(),
513 http_forwarded_layer,
514 self.ws_support.then(|| {
515 UpgradeLayer::new_with_services(
516 scope,
517 WebSocketMatcher::default(),
518 {
519 let acceptor = WebSocketAcceptor::default()
520 .with_protocols_flex(true)
521 .with_echo_protocols();
522
523 #[cfg(feature = "compression")]
524 {
525 acceptor.with_per_message_deflate_overwrite_extensions()
526 }
527
528 #[cfg(not(feature = "compression"))]
529 {
530 acceptor
531 }
532 },
533 ConsumeErrLayer::trace_as(tracing::Level::DEBUG)
534 .into_layer(WebSocketEchoService::default()),
535 )
536 }),
537 )
538 .into_layer(self.http_service_builder.layer(EchoService {
539 profiles: self.uadb.clone().map(|database| {
540 Arc::new(EchoProfiles {
541 fingerprints: RecordedFingerprintCache::new(&database),
542 database,
543 })
544 }),
545 geo_db: self.geo_db.clone(),
546 }))
547 }
548}
549
550#[derive(Debug, Clone)]
551#[non_exhaustive]
552pub struct EchoService {
554 profiles: Option<Arc<EchoProfiles>>,
555 geo_db: Option<std::sync::Arc<IpGeoDb>>,
556}
557
558#[derive(Debug)]
559struct EchoProfiles {
560 database: Arc<UserAgentDatabase>,
561 fingerprints: RecordedFingerprintCache,
562}
563
564impl EchoProfiles {
565 fn recorded(&self, ua: &str) -> Option<&RecordedFingerprints> {
566 self.fingerprints.get(&self.database, ua)
567 }
568}
569
570#[derive(Debug, Serialize)]
571struct CurlScripts {
572 unix: String,
573 powershell: String,
574}
575
576fn curl_scripts(
577 parts: &crate::http::request::Parts,
578 payload: &rama_core::bytes::Bytes,
579) -> Result<CurlScripts, curl::CurlPayloadRequiresStdin> {
580 let render = |compatibility| {
581 curl::try_cmd_string_for_request_parts_and_payload_with_options(
582 parts,
583 payload,
584 curl::CurlExportOptions::default().with_script_compatibility(compatibility),
585 &curl::CurlScriptPayloadMode::Inline,
586 )
587 };
588
589 Ok(CurlScripts {
590 unix: render(curl::CurlScriptCompatibility::Unix)?,
591 powershell: render(curl::CurlScriptCompatibility::PowerShell)?,
592 })
593}
594
595#[derive(Debug, Serialize)]
596struct FingerprintProfileData {
597 hash: String,
598 verbose: String,
599 matched: bool,
600}
601
602fn profile_fingerprint<'a, T: 'a>(
604 candidates: impl IntoIterator<Item = &'a T>,
605 is_match: impl Fn(&T) -> bool,
606 hash: impl Fn(&T) -> String,
607 verbose: impl Fn(&T) -> String,
608) -> Option<FingerprintProfileData> {
609 let mut first = None;
610 for candidate in candidates {
611 if is_match(candidate) {
612 return Some(FingerprintProfileData {
613 hash: hash(candidate),
614 verbose: verbose(candidate),
615 matched: true,
616 });
617 }
618 first.get_or_insert(candidate);
619 }
620 first.map(|candidate| FingerprintProfileData {
621 hash: hash(candidate),
622 verbose: verbose(candidate),
623 matched: false,
624 })
625}
626
627impl Service<Request> for EchoService {
628 type Output = Response;
629 type Error = BoxError;
630
631 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
632 let user_agent_info = req
633 .extensions()
634 .get_ref()
635 .map(|ua: &UserAgent| {
636 json!({
637 "user_agent": ua.header_str().to_owned(),
638 "kind": ua.info().map(|info| info.kind.to_string()),
639 "version": ua.info().and_then(|info| info.version),
640 "platform": ua.platform().map(|v| v.to_string()),
641 })
642 })
643 .unwrap_or_default();
644
645 let authority = req
646 .authority()
647 .context("echo: resolve request authority")?
648 .to_string();
649 let scheme = req.protocol().unwrap_or(&Protocol::HTTP).to_string();
650
651 let ua_str = req
652 .headers()
653 .get(USER_AGENT)
654 .and_then(|h| h.to_str().ok())
655 .map(ToOwned::to_owned);
656 tracing::debug!(
657 user_agent.original = ua_str,
658 "echo request received from ua with ua header",
659 );
660
661 let recorded = self
662 .profiles
663 .as_deref()
664 .zip(ua_str.as_deref())
665 .and_then(|(profiles, ua)| profiles.recorded(ua));
666
667 let ja4h = Ja4H::compute(&req)
668 .inspect_err(|err| tracing::error!("ja4h compute failure: {err:?}"))
669 .ok()
670 .map(|ja4h| {
671 let profile_ja4h = recorded.and_then(|recorded| {
672 profile_fingerprint(
673 recorded.http.iter(),
674 |tgt| tgt.matches_client_shape(&ja4h),
675 |tgt| format!("{tgt}"),
676 |tgt| format!("{tgt:?}"),
677 )
678 });
679
680 json!({
681 "hash": format!("{ja4h}"),
682 "verbose": format!("{ja4h:?}"),
683 "profile": profile_ja4h,
684 })
685 });
686
687 let (parts, body) = req.into_parts();
688
689 let body = body
690 .collect()
691 .await
692 .context("collect request body for echo purposes")?
693 .to_bytes();
694
695 let curl_request =
696 curl_scripts(&parts, &body).context("create curl command for echo response")?;
697
698 let headers: Vec<_> = parts
699 .headers
700 .into_ordered_iter()
701 .map(|(name, value)| {
702 (
703 name,
704 std::str::from_utf8(value.as_bytes())
705 .map(|s| s.to_owned())
706 .unwrap_or_else(|_| format!("0x{:x?}", value.as_bytes())),
707 )
708 })
709 .collect();
710
711 let body = crate::utils::fmt::hex(&body).to_string();
712
713 #[cfg(all(feature = "quic", any(feature = "rustls", feature = "boring")))]
714 let quic_completeness = parts
715 .extensions
716 .get_ref::<QuicObservation>()
717 .map(|value| value.completeness);
718 #[cfg(all(not(feature = "quic"), any(feature = "rustls", feature = "boring")))]
719 let quic_completeness = serde_json::Value::Null;
720
721 #[cfg(any(feature = "rustls", feature = "boring"))]
722 let tls_info = parts
723 .extensions
724 .get_ref::<SecureTransport>()
725 .and_then(|st| st.client_hello())
726 .map(|hello| {
727 let recorded_tls = recorded.map_or(&[][..], |recorded| match parts.version {
728 Version::HTTP_3 => &recorded.tls_quic[..],
729 _ => &recorded.tls[..],
730 });
731 let ja4 = Ja4::compute(parts.extensions.extensions())
732 .inspect_err(|err| tracing::trace!("ja4 computation: {err:?}"))
733 .ok();
734
735 let profile_ja4 = ja4.as_ref().and_then(|src| {
736 profile_fingerprint(
737 recorded_tls.iter().filter_map(|tgt| tgt.ja4.as_ref()),
738 |tgt| tgt == src,
739 |tgt| format!("{tgt}"),
740 |tgt| format!("{tgt:?}"),
741 )
742 });
743
744 let ja4 = ja4.map(|ja4| {
745 json!({
746 "hash": format!("{ja4}"),
747 "verbose": format!("{ja4:?}"),
748 "profile": profile_ja4,
749 })
750 });
751
752 let ja3 = Ja3::compute(parts.extensions.extensions())
753 .inspect_err(|err| tracing::trace!("ja3 computation: {err:?}"))
754 .ok();
755
756 let profile_ja3 = ja3.as_ref().and_then(|src| {
757 profile_fingerprint(
758 recorded_tls.iter().filter_map(|tgt| tgt.ja3.as_ref()),
759 |tgt| tgt == src,
760 |tgt| format!("{tgt:x}"),
761 |tgt| format!("{tgt}"),
762 )
763 });
764
765 let ja3 = ja3.map(|ja3| {
766 json!({
767 "hash": format!("{ja3:x}"),
768 "verbose": format!("{ja3}"),
769 "profile": profile_ja3,
770 })
771 });
772
773 let peet = PeetPrint::compute(parts.extensions.extensions())
774 .inspect_err(|err| tracing::trace!("peet computation: {err:?}"))
775 .ok();
776
777 let profile_peet = peet.as_ref().and_then(|src| {
778 profile_fingerprint(
779 recorded_tls.iter().filter_map(|tgt| tgt.peetprint.as_ref()),
780 |tgt| tgt == src,
781 |tgt| format!("{tgt}"),
782 |tgt| format!("{tgt:?}"),
783 )
784 });
785
786 let peet = peet.map(|peet| {
787 json!({
788 "hash": format!("{peet}"),
789 "verbose": format!("{peet:?}"),
790 "profile": profile_peet,
791 })
792 });
793
794 json!({
795 "transport": if parts.version == Version::HTTP_3 { "quic" } else { "tcp" },
796 "completeness": quic_completeness,
797 "header": {
798 "version": hello.protocol_version().to_string(),
799 "cipher_suites": hello
800 .cipher_suites().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
801 "compression_algorithms": hello
802 .compression_algorithms().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
803 },
804 "extensions": hello.extensions().iter().map(|extension| match extension {
805 ClientHelloExtension::ServerName(domain) => json!({
806 "id": extension.id().to_string(),
807 "data": domain,
808 }),
809 ClientHelloExtension::SignatureAlgorithms(v) => json!({
810 "id": extension.id().to_string(),
811 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
812 }),
813 ClientHelloExtension::SupportedVersions(v) => json!({
814 "id": extension.id().to_string(),
815 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
816 }),
817 ClientHelloExtension::ApplicationLayerProtocolNegotiation(v) => json!({
818 "id": extension.id().to_string(),
819 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
820 }),
821 ClientHelloExtension::ApplicationSettings{ protocols, .. } => json!({
822 "id": extension.id().to_string(),
823 "data": protocols.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
824 }),
825 ClientHelloExtension::SupportedGroups(v) => json!({
826 "id": extension.id().to_string(),
827 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
828 }),
829 ClientHelloExtension::ECPointFormats(v) => json!({
830 "id": extension.id().to_string(),
831 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
832 }),
833 ClientHelloExtension::CertificateCompression(v) => json!({
834 "id": extension.id().to_string(),
835 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
836 }),
837 ClientHelloExtension::DelegatedCredentials(v) => json!({
838 "id": extension.id().to_string(),
839 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
840 }),
841 ClientHelloExtension::RecordSizeLimit(v)
842 | ClientHelloExtension::ServerPaddingRequest(v) => json!({
843 "id": extension.id().to_string(),
844 "data": v.to_string(),
845 }),
846 ClientHelloExtension::EncryptedClientHello(ech) => match ech {
847 ECHClientHello::Outer(ech) => json!({
848 "id": extension.id().to_string(),
849 "data": {
850 "type": "outer",
851 "cipher_suite": {
852 "aead_id": ech.cipher_suite.aead_id.to_string(),
853 "kdf_id": ech.cipher_suite.kdf_id.to_string(),
854 },
855 "config_id": ech.config_id,
856 "enc": crate::utils::fmt::hex(&ech.enc).with_prefix(true).to_string(),
857 "payload": crate::utils::fmt::hex(&ech.payload).with_prefix(true).to_string(),
858 },
859 }),
860 ECHClientHello::Inner => json!({
861 "id": extension.id().to_string(),
862 "data": {
863 "type": "inner",
864 },
865 })
866
867 }
868 ClientHelloExtension::Opaque { id, data } => if data.is_empty() {
869 json!({
870 "id": id.to_string()
871 })
872 } else {
873 json!({
874 "id": id.to_string(),
875 "data": crate::utils::fmt::hex(data).with_prefix(true).to_string()
876 })
877 },
878 }).collect::<Vec<_>>(),
879 "ja3": ja3,
880 "ja4": ja4,
881 "peet": peet
882 })
883 });
884
885 #[cfg(not(any(feature = "rustls", feature = "boring")))]
886 let tls_info: Option<()> = None;
887
888 let mut h2 = None;
889 if parts.version == Version::HTTP_2 {
890 let early_frames = parts.extensions.get_ref::<EarlyFrameCapture>();
891 let pseudo_headers = parts.extensions.get_ref::<PseudoHeaderOrder>();
892 let akamai_h2 = AkamaiH2::compute(&parts.extensions)
893 .inspect_err(|err| tracing::trace!("akamai h2 compute failure: {err:?}"))
894 .ok()
895 .map(|akamai| {
896 json!({
897 "hash": format!("{akamai}"),
898 "verbose": format!("{akamai:?}"),
899 })
900 });
901
902 h2 = Some(json!({
903 "early_frames": early_frames,
904 "pseudo_headers": pseudo_headers,
905 "akamai_h2": akamai_h2,
906 }));
907 }
908
909 #[cfg(feature = "quic")]
910 let h3 = parts
911 .extensions
912 .get_ref::<H3RequestObservation>()
913 .map(|observation| {
914 EchoH3::from(observation.view(
915 &observation.connection,
916 SettingsStatus::Pending,
917 parts.extensions.get_ref::<PseudoHeaderOrder>(),
918 ))
919 });
920 #[cfg(feature = "quic")]
921 let quic = parts
922 .extensions
923 .get_ref::<QuicObservation>()
924 .map(EchoQuic::from);
925 #[cfg(feature = "quic")]
926 let quic_connection = parts
927 .extensions
928 .get_ref::<QuicConnectionInfo>()
929 .map(EchoQuicConnection::from);
930 #[cfg(not(feature = "quic"))]
931 let (h3, quic, quic_connection) = (
932 serde_json::Value::Null,
933 serde_json::Value::Null,
934 serde_json::Value::Null,
935 );
936
937 let geo = self
939 .geo_db
940 .as_ref()
941 .and_then(|db| {
942 crate::net::client_ip::client_ip(&parts.extensions).and_then(|ip| db.resolve(ip))
943 })
944 .map(|info| serde_json::to_value(&info).unwrap_or_default())
945 .unwrap_or(serde_json::Value::Null);
946
947 Ok(Json(json!({
948 "ua": user_agent_info,
949 "geo": geo,
950 "http": {
951 "version": format!("{:?}", parts.version),
952 "scheme": scheme,
953 "method": format!("{:?}", parts.method),
954 "authority": authority,
955 "path": parts.uri.path_or_root().into_owned(),
956 "query": parts.uri.query().map(|q| q.as_encoded_str().into_owned()),
957 "h2": h2,
958 "h3": h3,
959 "headers": headers,
960 "payload": body,
961 "ja4h": ja4h,
962 "curl": curl_request,
963 },
964 "tls": tls_info,
965 "quic": quic,
966 "quic_connection": quic_connection,
967 "socket_addr": parts.extensions.forwarded_client_socket_addr()
968 .map(|addr| addr.to_string())
969 .or_else(|| parts.extensions.forwarded_client_ip().map(|ip| ip.to_string()))
970 .or_else(|| SocketInfo::ingress(&parts.extensions).map(|v| v.peer_addr().to_string())),
971 }))
972 .into_response())
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use crate::http::{Body, StatusCode};
980 #[cfg(feature = "quic")]
981 use crate::quic::proto::Version as QuicVersion;
982
983 #[tokio::test]
984 async fn binary_request_body_is_echoed_with_lossless_platform_curl_scripts() {
985 let payload = rama_core::bytes::Bytes::from_static(b"\0\xfftail\n");
986 let request = Request::builder()
987 .method("POST")
988 .uri("http://example.com/upload")
989 .body(Body::from(payload))
990 .unwrap();
991
992 let response = EchoService {
993 profiles: None,
994 geo_db: None,
995 }
996 .serve(request)
997 .await
998 .expect("echo binary request");
999 assert_eq!(response.status(), StatusCode::OK);
1000
1001 let body = response
1002 .into_body()
1003 .collect()
1004 .await
1005 .expect("collect echo response")
1006 .to_bytes();
1007 let json: serde_json::Value = serde_json::from_slice(&body).expect("parse echo response");
1008
1009 assert_eq!(json["http"]["payload"], "00ff7461696c0a");
1010 let unix = json["http"]["curl"]["unix"]
1011 .as_str()
1012 .expect("Unix curl script");
1013 assert!(unix.contains("base64 -d"));
1014 assert!(unix.contains("--data-binary '@-'"));
1015 let powershell = json["http"]["curl"]["powershell"]
1016 .as_str()
1017 .expect("PowerShell curl script");
1018 assert!(powershell.contains("[Convert]::FromBase64String"));
1019 assert!(powershell.contains("--data-binary ('@' + $__ramaCurlPayload)"));
1020 }
1021
1022 #[cfg(feature = "quic")]
1023 #[tokio::test]
1024 async fn negotiated_quic_versions_are_echoed_without_capture() {
1025 let request = Request::builder()
1026 .version(Version::HTTP_3)
1027 .uri("https://example.test/")
1028 .body(Body::empty())
1029 .unwrap();
1030 request.extensions().insert(QuicConnectionInfo {
1031 version: QuicVersion::V2,
1032 original_version: QuicVersion::V1,
1033 early_data_keys_enabled: false,
1034 handshake_rtt: Duration::from_millis(10),
1035 handshake_mtu: 1200,
1036 });
1037 let response = EchoService {
1038 profiles: None,
1039 geo_db: None,
1040 }
1041 .serve(request)
1042 .await
1043 .unwrap();
1044 assert_eq!(response.status(), StatusCode::OK);
1045 let bytes = response.into_body().collect().await.unwrap().to_bytes();
1046 let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1047 assert_eq!(
1048 value["quic_connection"],
1049 json!({
1050 "version": {"raw": "0x6b3343cf", "name": "v2"},
1051 "original_version": {"raw": "0x00000001", "name": "v1"},
1052 })
1053 );
1054 assert!(value["quic"].is_null());
1055 }
1056
1057 #[cfg(feature = "quic")]
1058 #[tokio::test]
1059 async fn observation_schema_distinguishes_absent_and_incomplete_h3() {
1060 for captured in [false, true] {
1061 let request = Request::builder()
1062 .version(Version::HTTP_3)
1063 .uri("https://example.test/")
1064 .header("user-agent", "custom/1")
1065 .body(Body::empty())
1066 .unwrap();
1067 if captured {
1068 request.extensions().insert(H3RequestObservation {
1069 stream_id: 0,
1070 connection: Arc::new(Default::default()),
1071 first_request: Arc::new(Default::default()),
1072 fields: Vec::new(),
1073 fields_complete: false,
1074 priority: None,
1075 });
1076 }
1077 let response = EchoService {
1078 profiles: None,
1079 geo_db: None,
1080 }
1081 .serve(request)
1082 .await
1083 .unwrap();
1084 let bytes = response.into_body().collect().await.unwrap().to_bytes();
1085 let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1086 assert_eq!(value["http"]["version"], "HTTP/3.0");
1087 assert!(value["http"]["h2"].is_null());
1088 assert!(value["quic"].is_null());
1089 assert!(value["quic_connection"].is_null());
1090 assert!(value["tls"].is_null());
1091 let h3 = &value["http"]["h3"];
1092 if captured {
1093 assert_eq!(h3["settings_status"], "unavailable");
1094 assert!(h3["settings"].is_null());
1095 assert_eq!(h3["completeness"]["snapshot"], false);
1096 assert_eq!(h3["completeness"]["fields"], false);
1097 assert_eq!(h3["request_fields"], json!([]));
1098 } else {
1099 assert!(h3.is_null());
1100 }
1101 assert!(
1102 value["http"]["ja4h"]["verbose"]
1103 .as_str()
1104 .unwrap()
1105 .starts_with("ge30")
1106 );
1107 }
1108 }
1109}