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