Skip to main content

rama/cli/service/echo/
mod.rs

1//! Echo '[`Service`] that echos the [`http`] [`Request`] and [`tls`] client config.
2//!
3//! [`Service`]: crate::Service
4//! [`http`]: crate::http
5//! [`Request`]: crate::http::Request
6//! [`tls`]: crate::tls
7
8#[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)]
98/// Builder that can be used to run your own echo [`Service`],
99/// echo'ing back information about that request and its underlying transport / presentation layers.
100pub 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    /// Create a new [`EchoServiceBuilder`].
162    #[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        /// set the number of concurrent connections to allow
171        ///
172        /// (0 = no limit)
173        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        /// rate limit the service in requests per second
181        /// (rejected with a 429 + Retry-After response)
182        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        /// throttle each connection at the given byte rate
190        /// (both directions, each with its own budget)
191        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        /// set the body limit in bytes for each request
199        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        /// set the age at which each connection starts draining, give or take a tenth; it is
207        /// dropped [`with_timeout_grace`](Self::with_timeout_grace) later
208        ///
209        /// (0 = no limit)
210        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        /// set at most how long a draining connection may still run before it is dropped
218        ///
219        /// Also bounds the drain when the server shuts down; the server's own scope can end it
220        /// sooner. Default: 5 seconds.
221        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        /// enable support for one of the following "forward" headers or protocols
229        ///
230        /// Supported headers:
231        ///
232        /// Forwarded ("for="), X-Forwarded-For
233        ///
234        /// X-Client-IP Client-IP, X-Real-IP
235        ///
236        /// CF-Connecting-IP, True-Client-IP
237        ///
238        /// Or using HaProxy protocol.
239        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        /// define a tls server cert config to be used for tls terminaton
248        /// by the echo service.
249        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        /// set the http version to use for the http server over TCP (auto by default)
257        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        /// advertise alternative services, such as HTTP/3, on every response
265        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        /// Retain bounded HTTP/3 metadata for echo responses. Disabled by default.
274        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        /// Retain bounded HTTP/2 early frames for echo responses. Disabled by default.
282        pub fn http2_observation(mut self, enabled: bool) -> Self {
283            self.http2_observation = enabled;
284            self
285        }
286    }
287
288    /// add a custom http layer which will be applied to the existing http layers
289    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        /// maybe set the user agent datasbase that if set would be used to look up
321        /// a user agent (by ua header string) to see if we have a ja3/ja4 hash.
322        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        /// attach an IP geolocation database, enabling geo enrichment of the
333        /// echoed JSON. Typically built from `RAMA_IP_GEO_DB`.
334        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        /// define whether or not WS support is enabled
342        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    /// build a tcp service ready to echo http traffic back
357    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    /// build a tcp service and an HTTP/3 service, for accepted QUIC connections, ready to
366    /// echo http traffic back
367    ///
368    /// Both serve one http service: they share its rate limit, the connection limit,
369    /// the connection timeout and the per-connection throttle. TLS for QUIC is
370    /// configured on its endpoint.
371    #[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        // One layer for both: QUIC paces all streams of a connection against one budget.
412        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            // RFC 9220: WebSockets over HTTP/3 use Extended CONNECT.
459            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    /// build an http service ready to echo http traffic back
486    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        // Attribution header, derived from the loaded databases' notices.
493        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]
552/// The inner echo-service used by the [`EchoServiceBuilder`].
553pub 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
602/// The recorded fingerprint matching the observed one, else the first recorded one.
603fn 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        // attribution rides in the x-geo-attribution response header, not the body
938        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}