Skip to main content

rama/cli/service/
echo.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
8use crate::{
9    Layer, Service,
10    cli::ForwardKind,
11    combinators::{Either, Either3},
12    error::{BoxError, BoxErrorExt, ErrorContext},
13    extensions::ExtensionsRef,
14    http::{
15        BodyLimitLayer, Request, Response, Version,
16        body::util::BodyExt,
17        convert::curl,
18        core::h2::frame::EarlyFrameCapture,
19        fingerprint::{AkamaiH2, Ja4H},
20        header::USER_AGENT,
21        headers::exotic::XClacksOverhead,
22        layer::set_header::SetResponseHeaderLayer,
23        layer::{required_header::AddRequiredResponseHeadersLayer, trace::TraceLayer},
24        proto::h2::PseudoHeaderOrder,
25        server::HttpServer,
26        service::web::{extract::Json, response::IntoResponse},
27        ws::handshake::{
28            matcher::WebSocketMatcher,
29            server::{WebSocketAcceptor, WebSocketEchoService},
30        },
31    },
32    layer::limit::policy::UnlimitedPolicy,
33    layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy},
34    net::address::ip::geo::IpGeoDb,
35    net::forwarded::Forwarded,
36    net::stream::SocketInfo,
37    net::{AuthorityInputExt, Protocol, ProtocolInputExt},
38    proxy::haproxy::server::HaProxyLayer,
39    rt::Executor,
40    tcp::TcpStream,
41    telemetry::tracing,
42    ua::{UserAgent, layer::classifier::UserAgentClassifierLayer, profile::UserAgentDatabase},
43    utils::octets::mib,
44};
45
46use rama_core::error::ErrorExt as _;
47use rama_http::layer::upgrade::UpgradeLayer;
48use serde::Serialize;
49use serde_json::json;
50use std::{convert::Infallible, sync::Arc, time::Duration};
51
52core::cfg_select! {
53    feature = "boring" => {
54        use crate::tls::boring::server::TlsAcceptorLayer;
55    }
56    feature = "rustls" => {
57        use crate::tls::rustls::server::TlsAcceptorLayer;
58    }
59    _ => {}
60}
61
62#[cfg(any(feature = "rustls", feature = "boring"))]
63use crate::{
64    tls::fingerprint::{Ja3, Ja4, PeetPrint},
65    tls::{
66        SecureTransport,
67        client::ClientHelloExtension,
68        client::{ECHClientHello, NegotiatedTlsParameters},
69        server::TlsServerConfig,
70    },
71};
72
73#[derive(Debug, Clone)]
74/// Builder that can be used to run your own echo [`Service`],
75/// echo'ing back information about that request and its underlying transport / presentation layers.
76pub struct EchoServiceBuilder<H> {
77    concurrent_limit: usize,
78    body_limit: usize,
79    timeout: Duration,
80    forward: Option<ForwardKind>,
81
82    #[cfg(any(feature = "rustls", feature = "boring"))]
83    tls_server_config: Option<TlsServerConfig>,
84
85    http_version: Option<Version>,
86
87    ws_support: bool,
88
89    http_service_builder: H,
90
91    uadb: Option<std::sync::Arc<UserAgentDatabase>>,
92
93    geo_db: Option<std::sync::Arc<IpGeoDb>>,
94}
95
96impl Default for EchoServiceBuilder<()> {
97    fn default() -> Self {
98        Self {
99            concurrent_limit: 0,
100            body_limit: mib(1),
101            timeout: Duration::ZERO,
102            forward: None,
103
104            #[cfg(any(feature = "rustls", feature = "boring"))]
105            tls_server_config: None,
106
107            http_version: None,
108
109            ws_support: false,
110
111            http_service_builder: (),
112
113            uadb: None,
114
115            geo_db: None,
116        }
117    }
118}
119
120impl EchoServiceBuilder<()> {
121    /// Create a new [`EchoServiceBuilder`].
122    #[must_use]
123    pub fn new() -> Self {
124        Self::default()
125    }
126}
127
128impl<H> EchoServiceBuilder<H> {
129    crate::utils::macros::generate_set_and_with! {
130        /// set the number of concurrent connections to allow
131        ///
132        /// (0 = no limit)
133        pub fn concurrent(mut self, limit: usize) -> Self {
134            self.concurrent_limit = limit;
135            self
136        }
137    }
138
139    crate::utils::macros::generate_set_and_with! {
140        /// set the body limit in bytes for each request
141        pub fn body_limit(mut self, limit: usize) -> Self {
142            self.body_limit = limit;
143            self
144        }
145    }
146
147    crate::utils::macros::generate_set_and_with! {
148        /// set the timeout in seconds for each connection
149        ///
150        /// (0 = no timeout)
151        pub fn timeout(mut self, timeout: Duration) -> Self {
152            self.timeout = timeout;
153            self
154        }
155    }
156
157    crate::utils::macros::generate_set_and_with! {
158        /// enable support for one of the following "forward" headers or protocols
159        ///
160        /// Supported headers:
161        ///
162        /// Forwarded ("for="), X-Forwarded-For
163        ///
164        /// X-Client-IP Client-IP, X-Real-IP
165        ///
166        /// CF-Connecting-IP, True-Client-IP
167        ///
168        /// Or using HaProxy protocol.
169        pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
170            self.forward = kind;
171            self
172        }
173    }
174
175    crate::utils::macros::generate_set_and_with! {
176        #[cfg(any(feature = "rustls", feature = "boring"))]
177        /// define a tls server cert config to be used for tls terminaton
178        /// by the echo service.
179        pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
180            self.tls_server_config = cfg;
181            self
182        }
183    }
184
185    crate::utils::macros::generate_set_and_with! {
186        /// set the http version to use for the http server (auto by default)
187        pub fn http_version(mut self, version: Option<Version>) -> Self {
188            self.http_version = version;
189            self
190        }
191    }
192
193    /// add a custom http layer which will be applied to the existing http layers
194    pub fn with_http_layer<H2>(self, layer: H2) -> EchoServiceBuilder<(H, H2)> {
195        EchoServiceBuilder {
196            concurrent_limit: self.concurrent_limit,
197            body_limit: self.body_limit,
198            timeout: self.timeout,
199            forward: self.forward,
200
201            #[cfg(any(feature = "rustls", feature = "boring"))]
202            tls_server_config: self.tls_server_config,
203
204            http_version: self.http_version,
205
206            ws_support: self.ws_support,
207
208            http_service_builder: (self.http_service_builder, layer),
209
210            uadb: self.uadb,
211
212            geo_db: self.geo_db,
213        }
214    }
215
216    crate::utils::macros::generate_set_and_with! {
217        /// maybe set the user agent datasbase that if set would be used to look up
218        /// a user agent (by ua header string) to see if we have a ja3/ja4 hash.
219        pub fn user_agent_database(
220            mut self,
221            db: Option<std::sync::Arc<UserAgentDatabase>>,
222        ) -> Self {
223            self.uadb = db;
224            self
225        }
226    }
227
228    crate::utils::macros::generate_set_and_with! {
229        /// attach an IP geolocation database, enabling geo enrichment of the
230        /// echoed JSON. Typically built from `RAMA_IP_GEO_DB`.
231        pub fn geo_db(mut self, db: Option<std::sync::Arc<IpGeoDb>>) -> Self {
232            self.geo_db = db;
233            self
234        }
235    }
236
237    crate::utils::macros::generate_set_and_with! {
238        /// define whether or not WS support is enabled
239        pub fn ws_support(
240            mut self,
241            support: bool,
242        ) -> Self {
243            self.ws_support = support;
244            self
245        }
246    }
247}
248
249impl<H> EchoServiceBuilder<H>
250where
251    H: Layer<EchoService, Service: Service<Request, Output = Response, Error = BoxError>>,
252{
253    #[expect(unused_mut)]
254    /// build a tcp service ready to echo http traffic back
255    pub fn build(
256        mut self,
257        exec: Executor,
258    ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
259        let tcp_forwarded_layer = match &self.forward {
260            Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
261            _ => None,
262        };
263
264        let http_service = Arc::new(self.build_http(exec.clone()));
265
266        let tcp_service_builder = (
267            ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
268            LimitLayer::new(if self.concurrent_limit > 0 {
269                Either::A(ConcurrentPolicy::max(self.concurrent_limit))
270            } else {
271                Either::B(UnlimitedPolicy::new())
272            }),
273            if !self.timeout.is_zero() {
274                TimeoutLayer::new(self.timeout)
275            } else {
276                TimeoutLayer::never()
277            },
278            tcp_forwarded_layer,
279            BodyLimitLayer::request_only(self.body_limit),
280            #[cfg(any(feature = "rustls", feature = "boring"))]
281            self.tls_server_config
282                .map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
283        );
284
285        let http_transport_service = match self.http_version {
286            Some(Version::HTTP_2) => Either3::A({
287                let mut http = HttpServer::new_h2(exec);
288                if self.ws_support {
289                    http.h2_mut().set_enable_connect_protocol();
290                }
291                http.service(http_service)
292            }),
293            Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
294                Either3::B(HttpServer::new_http1(exec).service(http_service))
295            }
296            Some(version) => {
297                return Err(BoxError::from_static_str("unsupported http version")
298                    .context_debug_field("version", version));
299            }
300            None => Either3::C({
301                let mut http = HttpServer::auto(exec);
302                if self.ws_support {
303                    http.h2_mut().set_enable_connect_protocol();
304                }
305                http.service(http_service)
306            }),
307        };
308
309        Ok(tcp_service_builder.into_layer(http_transport_service))
310    }
311
312    /// build an http service ready to echo http traffic back
313    pub fn build_http(
314        &self,
315        exec: Executor,
316    ) -> impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H> {
317        let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
318
319        // Attribution header, derived from the loaded databases' notices.
320        let geo_attribution = self.geo_db.as_ref().and_then(|db| {
321            let notices: Vec<_> = db.attributions().collect();
322            (!notices.is_empty()).then(|| crate::cli::service::geo::geo_attribution_layer(notices))
323        });
324
325        (
326            TraceLayer::new_for_http(),
327            SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
328            AddRequiredResponseHeadersLayer::default(),
329            geo_attribution,
330            UserAgentClassifierLayer::new(),
331            ConsumeErrLayer::default(),
332            http_forwarded_layer,
333            self.ws_support.then(|| {
334                UpgradeLayer::new(
335                    exec,
336                    WebSocketMatcher::default(),
337                    {
338                        let acceptor = WebSocketAcceptor::default()
339                            .with_protocols_flex(true)
340                            .with_echo_protocols();
341
342                        #[cfg(feature = "compression")]
343                        {
344                            acceptor.with_per_message_deflate_overwrite_extensions()
345                        }
346                        #[cfg(not(feature = "compression"))]
347                        {
348                            acceptor
349                        }
350                    },
351                    ConsumeErrLayer::trace_as(tracing::Level::DEBUG)
352                        .into_layer(WebSocketEchoService::default()),
353                )
354            }),
355        )
356            .into_layer(self.http_service_builder.layer(EchoService {
357                uadb: self.uadb.clone(),
358                geo_db: self.geo_db.clone(),
359            }))
360    }
361}
362
363#[derive(Debug, Clone)]
364#[non_exhaustive]
365/// The inner echo-service used by the [`EchoServiceBuilder`].
366pub struct EchoService {
367    uadb: Option<std::sync::Arc<UserAgentDatabase>>,
368    geo_db: Option<std::sync::Arc<IpGeoDb>>,
369}
370
371impl Service<Request> for EchoService {
372    type Output = Response;
373    type Error = BoxError;
374
375    async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
376        let user_agent_info = req
377            .extensions()
378            .get_ref()
379            .map(|ua: &UserAgent| {
380                json!({
381                    "user_agent": ua.header_str().to_owned(),
382                    "kind": ua.info().map(|info| info.kind.to_string()),
383                    "version": ua.info().and_then(|info| info.version),
384                    "platform": ua.platform().map(|v| v.to_string()),
385                })
386            })
387            .unwrap_or_default();
388
389        let authority = req
390            .authority()
391            .context("echo: resolve request authority")?
392            .to_string();
393        let scheme = req.protocol().unwrap_or(&Protocol::HTTP).to_string();
394
395        let ua_str = req
396            .headers()
397            .get(USER_AGENT)
398            .and_then(|h| h.to_str().ok())
399            .map(ToOwned::to_owned);
400        tracing::debug!(
401            user_agent.original = ua_str,
402            "echo request received from ua with ua header",
403        );
404
405        #[derive(Debug, Serialize)]
406        struct FingerprintProfileData {
407            hash: String,
408            verbose: String,
409            matched: bool,
410        }
411
412        let ja4h = Ja4H::compute(&req)
413            .inspect_err(|err| tracing::error!("ja4h compute failure: {err:?}"))
414            .ok()
415            .map(|ja4h| {
416                let mut profile_ja4h: Option<FingerprintProfileData> = None;
417
418                if let Some(uadb) = self.uadb.as_deref()
419                    && let Some(profile) =
420                        ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
421                    {
422                        let matched_ja4h = match req.version() {
423                            Version::HTTP_10 | Version::HTTP_11 => profile
424                                .http
425                                .ja4h_h1_navigate(Some(req.method().clone()))
426                                .inspect_err(|err| {
427                                    tracing::trace!(
428                                        "ja4h computation of matched profile for incoming h1 req: {err:?}"
429                                    )
430                                })
431                                .ok(),
432                            Version::HTTP_2 => profile
433                                .http
434                                .ja4h_h2_navigate(Some(req.method().clone()))
435                                .inspect_err(|err| {
436                                    tracing::trace!(
437                                        "ja4h computation of matched profile for incoming h2 req: {err:?}"
438                                    )
439                                })
440                                .ok(),
441                            _ => None,
442                        };
443                        if let Some(tgt) = matched_ja4h {
444                            let hash = format!("{tgt}");
445                            let matched = format!("{ja4h}") == hash;
446                            profile_ja4h = Some(FingerprintProfileData {
447                                hash,
448                                verbose: format!("{tgt:?}"),
449                                matched,
450                            });
451                        }
452                    }
453
454                json!({
455                    "hash": format!("{ja4h}"),
456                    "verbose": format!("{ja4h:?}"),
457                    "profile": profile_ja4h,
458                })
459            });
460
461        let (parts, body) = req.into_parts();
462
463        let body = body
464            .collect()
465            .await
466            .context("collect request body for echo purposes")?
467            .to_bytes();
468
469        let curl_request = curl::cmd_string_for_request_parts_and_payload(&parts, &body);
470
471        let headers: Vec<_> = parts
472            .headers
473            .into_ordered_iter()
474            .map(|(name, value)| {
475                (
476                    name,
477                    std::str::from_utf8(value.as_bytes())
478                        .map(|s| s.to_owned())
479                        .unwrap_or_else(|_| format!("0x{:x?}", value.as_bytes())),
480                )
481            })
482            .collect();
483
484        let body = hex::encode(body.as_ref());
485
486        #[cfg(any(feature = "rustls", feature = "boring"))]
487        let tls_info = parts
488            .extensions
489            .get_ref::<SecureTransport>()
490            .and_then(|st| st.client_hello())
491            .map(|hello| {
492                let ja4 = Ja4::compute(parts.extensions.extensions())
493                    .inspect_err(|err| tracing::trace!("ja4 computation: {err:?}"))
494                    .ok();
495
496                let mut profile_ja4: Option<FingerprintProfileData> = None;
497
498                if let Some(uadb) = self.uadb.as_deref()
499                    && let Some(profile) =
500                        ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
501                {
502                    let matched_ja4 = profile
503                        .tls
504                        .compute_ja4(
505                            parts
506                                .extensions
507                                .get_ref::<NegotiatedTlsParameters>()
508                                .map(|param| param.protocol_version),
509                        )
510                        .inspect_err(|err| {
511                            tracing::trace!("ja4 computation of matched profile: {err:?}")
512                        })
513                        .ok();
514                    if let (Some(src), Some(tgt)) = (ja4.as_ref(), matched_ja4) {
515                        let hash = format!("{tgt}");
516                        let matched = format!("{src}") == hash;
517                        profile_ja4 = Some(FingerprintProfileData {
518                            hash,
519                            verbose: format!("{tgt:?}"),
520                            matched,
521                        });
522                    }
523                }
524
525                let ja4 = ja4.map(|ja4| {
526                    json!({
527                        "hash": format!("{ja4}"),
528                        "verbose": format!("{ja4:?}"),
529                        "profile": profile_ja4,
530                    })
531                });
532
533                let ja3 = Ja3::compute(parts.extensions.extensions())
534                    .inspect_err(|err| tracing::trace!("ja3 computation: {err:?}"))
535                    .ok();
536
537                let mut profile_ja3: Option<FingerprintProfileData> = None;
538
539                if let Some(uadb) = self.uadb.as_deref()
540                    && let Some(profile) =
541                        ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
542                {
543                    let matched_ja3 = profile
544                        .tls
545                        .compute_ja3(
546                            parts
547                                .extensions
548                                .get_ref::<NegotiatedTlsParameters>()
549                                .map(|param| param.protocol_version),
550                        )
551                        .inspect_err(|err| {
552                            tracing::trace!("ja3 computation of matched profile: {err:?}")
553                        })
554                        .ok();
555                    if let (Some(src), Some(tgt)) = (ja3.as_ref(), matched_ja3) {
556                        let hash = format!("{tgt:x}");
557                        let matched = format!("{src:x}") == hash;
558                        profile_ja3 = Some(FingerprintProfileData {
559                            hash,
560                            verbose: format!("{tgt}"),
561                            matched,
562                        });
563                    }
564                }
565
566                let ja3 = ja3.map(|ja3| {
567                    json!({
568                        "hash": format!("{ja3:x}"),
569                        "verbose": format!("{ja3}"),
570                        "profile": profile_ja3,
571                    })
572                });
573
574                let peet = PeetPrint::compute(parts.extensions.extensions())
575                    .inspect_err(|err| tracing::trace!("peet computation: {err:?}"))
576                    .ok();
577
578                let mut profile_peet: Option<FingerprintProfileData> = None;
579
580                if let Some(uadb) = self.uadb.as_deref()
581                    && let Some(profile) =
582                        ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
583                {
584                    let matched_peet = profile
585                        .tls
586                        .compute_peet()
587                        .inspect_err(|err| {
588                            tracing::trace!("peetprint computation of matched profile: {err:?}")
589                        })
590                        .ok();
591                    if let (Some(src), Some(tgt)) = (peet.as_ref(), matched_peet) {
592                        let hash = format!("{tgt}");
593                        let matched = format!("{src}") == hash;
594                        profile_peet = Some(FingerprintProfileData {
595                            hash,
596                            verbose: format!("{tgt:?}"),
597                            matched,
598                        });
599                    }
600                }
601
602                let peet = peet.map(|peet| {
603                    json!({
604                        "hash": format!("{peet}"),
605                        "verbose": format!("{peet:?}"),
606                        "profile": profile_peet,
607                    })
608                });
609
610                json!({
611                    "header": {
612                        "version": hello.protocol_version().to_string(),
613                        "cipher_suites": hello
614                        .cipher_suites().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
615                        "compression_algorithms": hello
616                        .compression_algorithms().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
617                    },
618                    "extensions": hello.extensions().iter().map(|extension| match extension {
619                        ClientHelloExtension::ServerName(domain) => json!({
620                            "id": extension.id().to_string(),
621                            "data": domain,
622                        }),
623                        ClientHelloExtension::SignatureAlgorithms(v) => json!({
624                            "id": extension.id().to_string(),
625                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
626                        }),
627                        ClientHelloExtension::SupportedVersions(v) => json!({
628                            "id": extension.id().to_string(),
629                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
630                        }),
631                        ClientHelloExtension::ApplicationLayerProtocolNegotiation(v) => json!({
632                            "id": extension.id().to_string(),
633                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
634                        }),
635                        ClientHelloExtension::ApplicationSettings{ protocols, .. } => json!({
636                            "id": extension.id().to_string(),
637                            "data": protocols.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
638                        }),
639                        ClientHelloExtension::SupportedGroups(v) => json!({
640                            "id": extension.id().to_string(),
641                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
642                        }),
643                        ClientHelloExtension::ECPointFormats(v) => json!({
644                            "id": extension.id().to_string(),
645                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
646                        }),
647                        ClientHelloExtension::CertificateCompression(v) => json!({
648                            "id": extension.id().to_string(),
649                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
650                        }),
651                        ClientHelloExtension::DelegatedCredentials(v) => json!({
652                            "id": extension.id().to_string(),
653                            "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
654                        }),
655                        ClientHelloExtension::RecordSizeLimit(v) => json!({
656                            "id": extension.id().to_string(),
657                            "data": v.to_string(),
658                        }),
659                        ClientHelloExtension::EncryptedClientHello(ech) => match ech {
660                            ECHClientHello::Outer(ech) => json!({
661                                "id": extension.id().to_string(),
662                                "data": {
663                                    "type": "outer",
664                                    "cipher_suite": {
665                                        "aead_id": ech.cipher_suite.aead_id.to_string(),
666                                        "kdf_id": ech.cipher_suite.kdf_id.to_string(),
667                                    },
668                                    "config_id": ech.config_id,
669                                    "enc":  format!("0x{}", hex::encode(&ech.enc)),
670                                    "payload": format!("0x{}", hex::encode(&ech.payload)),
671                                },
672                            }),
673                            ECHClientHello::Inner => json!({
674                                "id": extension.id().to_string(),
675                                "data": {
676                                    "type": "inner",
677                                },
678                            })
679
680                        }
681                        ClientHelloExtension::Opaque { id, data } => if data.is_empty() {
682                            json!({
683                                "id": id.to_string()
684                            })
685                        } else {
686                            json!({
687                                "id": id.to_string(),
688                                "data": format!("0x{}", hex::encode(data))
689                            })
690                        },
691                    }).collect::<Vec<_>>(),
692                    "ja3": ja3,
693                    "ja4": ja4,
694                    "peet": peet
695                })
696            });
697
698        #[cfg(not(any(feature = "rustls", feature = "boring")))]
699        let tls_info: Option<()> = None;
700
701        let mut h2 = None;
702        if parts.version == Version::HTTP_2 {
703            let early_frames = parts.extensions.get_ref::<EarlyFrameCapture>();
704            let pseudo_headers = parts.extensions.get_ref::<PseudoHeaderOrder>();
705            let akamai_h2 = AkamaiH2::compute(&parts.extensions)
706                .inspect_err(|err| tracing::trace!("akamai h2 compute failure: {err:?}"))
707                .ok()
708                .map(|akamai| {
709                    json!({
710                        "hash": format!("{akamai}"),
711                        "verbose": format!("{akamai:?}"),
712                    })
713                });
714
715            h2 = Some(json!({
716                "early_frames": early_frames,
717                "pseudo_headers": pseudo_headers,
718                "akamai_h2": akamai_h2,
719            }));
720        }
721
722        // attribution rides in the x-geo-attribution response header, not the body
723        let geo = self
724            .geo_db
725            .as_ref()
726            .and_then(|db| {
727                parts
728                    .extensions
729                    .get_ref::<Forwarded>()
730                    .and_then(|f| f.client_ip())
731                    .or_else(|| {
732                        parts
733                            .extensions
734                            .get_ref::<SocketInfo>()
735                            .map(|s| s.peer_addr().ip_addr)
736                    })
737                    .and_then(|ip| db.resolve(ip))
738            })
739            .map(|info| serde_json::to_value(&info).unwrap_or_default())
740            .unwrap_or(serde_json::Value::Null);
741
742        Ok(Json(json!({
743            "ua": user_agent_info,
744            "geo": geo,
745            "http": {
746                "version": format!("{:?}", parts.version),
747                "scheme": scheme,
748                "method": format!("{:?}", parts.method),
749                "authority": authority,
750                "path": parts.uri.path_or_root().into_owned(),
751                "query": parts.uri.query().map(|q| q.as_encoded_str().into_owned()),
752                "h2": h2,
753                "headers": headers,
754                "payload": body,
755                "ja4h": ja4h,
756                "curl": curl_request,
757            },
758            "tls": tls_info,
759            "socket_addr": parts.extensions.get_ref::<Forwarded>()
760                .and_then(|f|
761                        f.client_socket_addr().map(|addr| addr.to_string())
762                            .or_else(|| f.client_ip().map(|ip| ip.to_string()))
763                ).or_else(|| parts.extensions.get_ref::<SocketInfo>().map(|v| v.peer_addr().to_string())),
764        }))
765        .into_response())
766    }
767}