1use 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::{
34 ConsumeErrLayer, LimitLayer, TimeoutLayer,
35 limit::policy::{ConcurrentPolicy, RateLimitReached, RatePolicy},
36 },
37 net::address::ip::geo::IpGeoDb,
38 net::forwarded::Forwarded,
39 net::stream::SocketInfo,
40 net::stream::layer::{ThrottleLayer, ThrottleMode},
41 net::{AuthorityInputExt, Protocol, ProtocolInputExt},
42 proxy::haproxy::server::HaProxyLayer,
43 rt::Executor,
44 tcp::TcpStream,
45 telemetry::tracing,
46 ua::{UserAgent, layer::classifier::UserAgentClassifierLayer, profile::UserAgentDatabase},
47 utils::{octets::mib, rate::Rate},
48};
49
50use rama_core::error::ErrorExt as _;
51use rama_http::layer::upgrade::UpgradeLayer;
52use serde::Serialize;
53use serde_json::json;
54use std::{convert::Infallible, sync::Arc, time::Duration};
55
56core::cfg_select! {
57 feature = "boring" => {
58 use crate::tls::boring::server::TlsAcceptorLayer;
59 }
60 feature = "rustls" => {
61 use crate::tls::rustls::server::TlsAcceptorLayer;
62 }
63 _ => {}
64}
65
66#[cfg(any(feature = "rustls", feature = "boring"))]
67use crate::{
68 tls::fingerprint::{Ja3, Ja4, PeetPrint},
69 tls::{
70 SecureTransport,
71 client::ClientHelloExtension,
72 client::{ECHClientHello, NegotiatedTlsParameters},
73 server::TlsServerConfig,
74 },
75};
76
77#[derive(Debug, Clone)]
78pub struct EchoServiceBuilder<H> {
81 concurrent_limit: usize,
82 rate_limit: Option<Rate>,
83 throttle: Option<Rate>,
84 body_limit: usize,
85 timeout: Duration,
86 forward: Option<ForwardKind>,
87
88 #[cfg(any(feature = "rustls", feature = "boring"))]
89 tls_server_config: Option<TlsServerConfig>,
90
91 http_version: Option<Version>,
92
93 ws_support: bool,
94
95 http_service_builder: H,
96
97 uadb: Option<std::sync::Arc<UserAgentDatabase>>,
98
99 geo_db: Option<std::sync::Arc<IpGeoDb>>,
100}
101
102impl Default for EchoServiceBuilder<()> {
103 fn default() -> Self {
104 Self {
105 concurrent_limit: 0,
106 rate_limit: None,
107 throttle: None,
108 body_limit: mib(1),
109 timeout: Duration::ZERO,
110 forward: None,
111
112 #[cfg(any(feature = "rustls", feature = "boring"))]
113 tls_server_config: None,
114
115 http_version: None,
116
117 ws_support: false,
118
119 http_service_builder: (),
120
121 uadb: None,
122
123 geo_db: None,
124 }
125 }
126}
127
128impl EchoServiceBuilder<()> {
129 #[must_use]
131 pub fn new() -> Self {
132 Self::default()
133 }
134}
135
136impl<H> EchoServiceBuilder<H> {
137 crate::utils::macros::generate_set_and_with! {
138 pub fn concurrent(mut self, limit: usize) -> Self {
142 self.concurrent_limit = limit;
143 self
144 }
145 }
146
147 crate::utils::macros::generate_set_and_with! {
148 pub fn rate_limit(mut self, rate: Option<Rate>) -> Self {
151 self.rate_limit = rate;
152 self
153 }
154 }
155
156 crate::utils::macros::generate_set_and_with! {
157 pub fn throttle(mut self, rate: Option<Rate>) -> Self {
160 self.throttle = rate;
161 self
162 }
163 }
164
165 crate::utils::macros::generate_set_and_with! {
166 pub fn body_limit(mut self, limit: usize) -> Self {
168 self.body_limit = limit;
169 self
170 }
171 }
172
173 crate::utils::macros::generate_set_and_with! {
174 pub fn timeout(mut self, timeout: Duration) -> Self {
178 self.timeout = timeout;
179 self
180 }
181 }
182
183 crate::utils::macros::generate_set_and_with! {
184 pub fn forward(mut self, kind: Option<ForwardKind>) -> Self {
196 self.forward = kind;
197 self
198 }
199 }
200
201 crate::utils::macros::generate_set_and_with! {
202 #[cfg(any(feature = "rustls", feature = "boring"))]
203 pub fn tls_server_config(mut self, cfg: Option<TlsServerConfig>) -> Self {
206 self.tls_server_config = cfg;
207 self
208 }
209 }
210
211 crate::utils::macros::generate_set_and_with! {
212 pub fn http_version(mut self, version: Option<Version>) -> Self {
214 self.http_version = version;
215 self
216 }
217 }
218
219 pub fn with_http_layer<H2>(self, layer: H2) -> EchoServiceBuilder<(H, H2)> {
221 EchoServiceBuilder {
222 concurrent_limit: self.concurrent_limit,
223 rate_limit: self.rate_limit,
224 throttle: self.throttle,
225 body_limit: self.body_limit,
226 timeout: self.timeout,
227 forward: self.forward,
228
229 #[cfg(any(feature = "rustls", feature = "boring"))]
230 tls_server_config: self.tls_server_config,
231
232 http_version: self.http_version,
233
234 ws_support: self.ws_support,
235
236 http_service_builder: (self.http_service_builder, layer),
237
238 uadb: self.uadb,
239
240 geo_db: self.geo_db,
241 }
242 }
243
244 crate::utils::macros::generate_set_and_with! {
245 pub fn user_agent_database(
248 mut self,
249 db: Option<std::sync::Arc<UserAgentDatabase>>,
250 ) -> Self {
251 self.uadb = db;
252 self
253 }
254 }
255
256 crate::utils::macros::generate_set_and_with! {
257 pub fn geo_db(mut self, db: Option<std::sync::Arc<IpGeoDb>>) -> Self {
260 self.geo_db = db;
261 self
262 }
263 }
264
265 crate::utils::macros::generate_set_and_with! {
266 pub fn ws_support(
268 mut self,
269 support: bool,
270 ) -> Self {
271 self.ws_support = support;
272 self
273 }
274 }
275}
276
277impl<H> EchoServiceBuilder<H>
278where
279 H: Layer<EchoService, Service: Service<Request, Output = Response, Error = BoxError>>,
280{
281 #[expect(unused_mut)]
282 pub fn build(
284 mut self,
285 exec: Executor,
286 ) -> Result<impl Service<TcpStream, Output = (), Error = Infallible>, BoxError> {
287 let tcp_forwarded_layer = match &self.forward {
288 Some(ForwardKind::HaProxy) => Some(HaProxyLayer::default()),
289 _ => None,
290 };
291
292 let http_service = Arc::new(self.build_http(exec.clone()));
293
294 let tcp_service_builder = (
295 ConsumeErrLayer::trace_as(tracing::Level::DEBUG),
296 LimitLayer::new(if self.concurrent_limit > 0 {
297 Either::A(ConcurrentPolicy::max(self.concurrent_limit))
298 } else {
299 Either::B(UnlimitedPolicy::new())
300 }),
301 if !self.timeout.is_zero() {
302 TimeoutLayer::new(self.timeout)
303 } else {
304 TimeoutLayer::never()
305 },
306 self.throttle
307 .map(|rate| ThrottleLayer::symmetric(ThrottleMode::per_conn(rate))),
308 tcp_forwarded_layer,
309 BodyLimitLayer::request_only(self.body_limit),
310 #[cfg(any(feature = "rustls", feature = "boring"))]
311 self.tls_server_config
312 .map(|cfg| TlsAcceptorLayer::new(cfg).with_store_client_hello(true)),
313 );
314
315 let http_transport_service = match self.http_version {
316 Some(Version::HTTP_2) => Either3::A({
317 let mut http = HttpServer::new_h2(exec);
318 if self.ws_support {
319 http.h2_mut().set_enable_connect_protocol();
320 }
321 http.service(http_service)
322 }),
323 Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => {
324 Either3::B(HttpServer::new_http1(exec).service(http_service))
325 }
326 Some(version) => {
327 return Err(BoxError::from_static_str("unsupported http version")
328 .context_debug_field("version", version));
329 }
330 None => Either3::C({
331 let mut http = HttpServer::auto(exec);
332 if self.ws_support {
333 http.h2_mut().set_enable_connect_protocol();
334 }
335 http.service(http_service)
336 }),
337 };
338
339 Ok(tcp_service_builder.into_layer(http_transport_service))
340 }
341
342 pub fn build_http(
344 &self,
345 exec: Executor,
346 ) -> impl Service<Request, Output: IntoResponse, Error = Infallible> + use<H> {
347 let http_forwarded_layer = super::http_forwarded_layer(self.forward.as_ref());
348
349 let geo_attribution = self.geo_db.as_ref().and_then(|db| {
351 let notices: Vec<_> = db.attributions().collect();
352 (!notices.is_empty()).then(|| crate::cli::service::geo::geo_attribution_layer(notices))
353 });
354
355 (
356 TraceLayer::new_for_http(),
357 SetResponseHeaderLayer::<XClacksOverhead>::if_not_present_default_typed(),
358 AddRequiredResponseHeadersLayer::default(),
359 self.rate_limit.map(|rate| {
360 LimitLayer::new(RatePolicy::abort(rate)).with_error_into_response_fn(
361 |err: RateLimitReached| Ok::<_, Infallible>(err.into_response()),
362 )
363 }),
364 geo_attribution,
365 UserAgentClassifierLayer::new(),
366 ConsumeErrLayer::default(),
367 http_forwarded_layer,
368 self.ws_support.then(|| {
369 UpgradeLayer::new_with_services(
370 exec,
371 WebSocketMatcher::default(),
372 {
373 let acceptor = WebSocketAcceptor::default()
374 .with_protocols_flex(true)
375 .with_echo_protocols();
376
377 #[cfg(feature = "compression")]
378 {
379 acceptor.with_per_message_deflate_overwrite_extensions()
380 }
381 #[cfg(not(feature = "compression"))]
382 {
383 acceptor
384 }
385 },
386 ConsumeErrLayer::trace_as(tracing::Level::DEBUG)
387 .into_layer(WebSocketEchoService::default()),
388 )
389 }),
390 )
391 .into_layer(self.http_service_builder.layer(EchoService {
392 uadb: self.uadb.clone(),
393 geo_db: self.geo_db.clone(),
394 }))
395 }
396}
397
398#[derive(Debug, Clone)]
399#[non_exhaustive]
400pub struct EchoService {
402 uadb: Option<std::sync::Arc<UserAgentDatabase>>,
403 geo_db: Option<std::sync::Arc<IpGeoDb>>,
404}
405
406#[derive(Debug, Serialize)]
407struct CurlScripts {
408 unix: String,
409 powershell: String,
410}
411
412fn curl_scripts(
413 parts: &crate::http::request::Parts,
414 payload: &rama_core::bytes::Bytes,
415) -> Result<CurlScripts, curl::CurlPayloadRequiresStdin> {
416 let render = |compatibility| {
417 curl::try_cmd_string_for_request_parts_and_payload_with_options(
418 parts,
419 payload,
420 curl::CurlExportOptions::default().with_script_compatibility(compatibility),
421 &curl::CurlScriptPayloadMode::Inline,
422 )
423 };
424
425 Ok(CurlScripts {
426 unix: render(curl::CurlScriptCompatibility::Unix)?,
427 powershell: render(curl::CurlScriptCompatibility::PowerShell)?,
428 })
429}
430
431impl Service<Request> for EchoService {
432 type Output = Response;
433 type Error = BoxError;
434
435 async fn serve(&self, req: Request) -> Result<Self::Output, Self::Error> {
436 let user_agent_info = req
437 .extensions()
438 .get_ref()
439 .map(|ua: &UserAgent| {
440 json!({
441 "user_agent": ua.header_str().to_owned(),
442 "kind": ua.info().map(|info| info.kind.to_string()),
443 "version": ua.info().and_then(|info| info.version),
444 "platform": ua.platform().map(|v| v.to_string()),
445 })
446 })
447 .unwrap_or_default();
448
449 let authority = req
450 .authority()
451 .context("echo: resolve request authority")?
452 .to_string();
453 let scheme = req.protocol().unwrap_or(&Protocol::HTTP).to_string();
454
455 let ua_str = req
456 .headers()
457 .get(USER_AGENT)
458 .and_then(|h| h.to_str().ok())
459 .map(ToOwned::to_owned);
460 tracing::debug!(
461 user_agent.original = ua_str,
462 "echo request received from ua with ua header",
463 );
464
465 #[derive(Debug, Serialize)]
466 struct FingerprintProfileData {
467 hash: String,
468 verbose: String,
469 matched: bool,
470 }
471
472 let ja4h = Ja4H::compute(&req)
473 .inspect_err(|err| tracing::error!("ja4h compute failure: {err:?}"))
474 .ok()
475 .map(|ja4h| {
476 let mut profile_ja4h: Option<FingerprintProfileData> = None;
477
478 if let Some(uadb) = self.uadb.as_deref()
479 && let Some(profile) =
480 ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
481 {
482 let matched_ja4h = match req.version() {
483 Version::HTTP_10 | Version::HTTP_11 => profile
484 .http
485 .ja4h_h1_navigate(Some(req.method().clone()))
486 .inspect_err(|err| {
487 tracing::trace!(
488 "ja4h computation of matched profile for incoming h1 req: {err:?}"
489 )
490 })
491 .ok(),
492 Version::HTTP_2 => profile
493 .http
494 .ja4h_h2_navigate(Some(req.method().clone()))
495 .inspect_err(|err| {
496 tracing::trace!(
497 "ja4h computation of matched profile for incoming h2 req: {err:?}"
498 )
499 })
500 .ok(),
501 _ => None,
502 };
503 if let Some(tgt) = matched_ja4h {
504 let hash = format!("{tgt}");
505 let matched = format!("{ja4h}") == hash;
506 profile_ja4h = Some(FingerprintProfileData {
507 hash,
508 verbose: format!("{tgt:?}"),
509 matched,
510 });
511 }
512 }
513
514 json!({
515 "hash": format!("{ja4h}"),
516 "verbose": format!("{ja4h:?}"),
517 "profile": profile_ja4h,
518 })
519 });
520
521 let (parts, body) = req.into_parts();
522
523 let body = body
524 .collect()
525 .await
526 .context("collect request body for echo purposes")?
527 .to_bytes();
528
529 let curl_request =
530 curl_scripts(&parts, &body).context("create curl command for echo response")?;
531
532 let headers: Vec<_> = parts
533 .headers
534 .into_ordered_iter()
535 .map(|(name, value)| {
536 (
537 name,
538 std::str::from_utf8(value.as_bytes())
539 .map(|s| s.to_owned())
540 .unwrap_or_else(|_| format!("0x{:x?}", value.as_bytes())),
541 )
542 })
543 .collect();
544
545 let body = hex::encode(body.as_ref());
546
547 #[cfg(any(feature = "rustls", feature = "boring"))]
548 let tls_info = parts
549 .extensions
550 .get_ref::<SecureTransport>()
551 .and_then(|st| st.client_hello())
552 .map(|hello| {
553 let ja4 = Ja4::compute(parts.extensions.extensions())
554 .inspect_err(|err| tracing::trace!("ja4 computation: {err:?}"))
555 .ok();
556
557 let mut profile_ja4: Option<FingerprintProfileData> = None;
558
559 if let Some(uadb) = self.uadb.as_deref()
560 && let Some(profile) =
561 ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
562 {
563 let matched_ja4 = profile
564 .tls
565 .compute_ja4(
566 parts
567 .extensions
568 .get_ref::<NegotiatedTlsParameters>()
569 .map(|param| param.protocol_version),
570 )
571 .inspect_err(|err| {
572 tracing::trace!("ja4 computation of matched profile: {err:?}")
573 })
574 .ok();
575 if let (Some(src), Some(tgt)) = (ja4.as_ref(), matched_ja4) {
576 let hash = format!("{tgt}");
577 let matched = format!("{src}") == hash;
578 profile_ja4 = Some(FingerprintProfileData {
579 hash,
580 verbose: format!("{tgt:?}"),
581 matched,
582 });
583 }
584 }
585
586 let ja4 = ja4.map(|ja4| {
587 json!({
588 "hash": format!("{ja4}"),
589 "verbose": format!("{ja4:?}"),
590 "profile": profile_ja4,
591 })
592 });
593
594 let ja3 = Ja3::compute(parts.extensions.extensions())
595 .inspect_err(|err| tracing::trace!("ja3 computation: {err:?}"))
596 .ok();
597
598 let mut profile_ja3: Option<FingerprintProfileData> = None;
599
600 if let Some(uadb) = self.uadb.as_deref()
601 && let Some(profile) =
602 ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
603 {
604 let matched_ja3 = profile
605 .tls
606 .compute_ja3(
607 parts
608 .extensions
609 .get_ref::<NegotiatedTlsParameters>()
610 .map(|param| param.protocol_version),
611 )
612 .inspect_err(|err| {
613 tracing::trace!("ja3 computation of matched profile: {err:?}")
614 })
615 .ok();
616 if let (Some(src), Some(tgt)) = (ja3.as_ref(), matched_ja3) {
617 let hash = format!("{tgt:x}");
618 let matched = format!("{src:x}") == hash;
619 profile_ja3 = Some(FingerprintProfileData {
620 hash,
621 verbose: format!("{tgt}"),
622 matched,
623 });
624 }
625 }
626
627 let ja3 = ja3.map(|ja3| {
628 json!({
629 "hash": format!("{ja3:x}"),
630 "verbose": format!("{ja3}"),
631 "profile": profile_ja3,
632 })
633 });
634
635 let peet = PeetPrint::compute(parts.extensions.extensions())
636 .inspect_err(|err| tracing::trace!("peet computation: {err:?}"))
637 .ok();
638
639 let mut profile_peet: Option<FingerprintProfileData> = None;
640
641 if let Some(uadb) = self.uadb.as_deref()
642 && let Some(profile) =
643 ua_str.as_deref().and_then(|s| uadb.get_exact_header_str(s))
644 {
645 let matched_peet = profile
646 .tls
647 .compute_peet()
648 .inspect_err(|err| {
649 tracing::trace!("peetprint computation of matched profile: {err:?}")
650 })
651 .ok();
652 if let (Some(src), Some(tgt)) = (peet.as_ref(), matched_peet) {
653 let hash = format!("{tgt}");
654 let matched = format!("{src}") == hash;
655 profile_peet = Some(FingerprintProfileData {
656 hash,
657 verbose: format!("{tgt:?}"),
658 matched,
659 });
660 }
661 }
662
663 let peet = peet.map(|peet| {
664 json!({
665 "hash": format!("{peet}"),
666 "verbose": format!("{peet:?}"),
667 "profile": profile_peet,
668 })
669 });
670
671 json!({
672 "header": {
673 "version": hello.protocol_version().to_string(),
674 "cipher_suites": hello
675 .cipher_suites().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
676 "compression_algorithms": hello
677 .compression_algorithms().iter().map(|s| s.to_string()).collect::<Vec<_>>(),
678 },
679 "extensions": hello.extensions().iter().map(|extension| match extension {
680 ClientHelloExtension::ServerName(domain) => json!({
681 "id": extension.id().to_string(),
682 "data": domain,
683 }),
684 ClientHelloExtension::SignatureAlgorithms(v) => json!({
685 "id": extension.id().to_string(),
686 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
687 }),
688 ClientHelloExtension::SupportedVersions(v) => json!({
689 "id": extension.id().to_string(),
690 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
691 }),
692 ClientHelloExtension::ApplicationLayerProtocolNegotiation(v) => json!({
693 "id": extension.id().to_string(),
694 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
695 }),
696 ClientHelloExtension::ApplicationSettings{ protocols, .. } => json!({
697 "id": extension.id().to_string(),
698 "data": protocols.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
699 }),
700 ClientHelloExtension::SupportedGroups(v) => json!({
701 "id": extension.id().to_string(),
702 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
703 }),
704 ClientHelloExtension::ECPointFormats(v) => json!({
705 "id": extension.id().to_string(),
706 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
707 }),
708 ClientHelloExtension::CertificateCompression(v) => json!({
709 "id": extension.id().to_string(),
710 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
711 }),
712 ClientHelloExtension::DelegatedCredentials(v) => json!({
713 "id": extension.id().to_string(),
714 "data": v.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
715 }),
716 ClientHelloExtension::RecordSizeLimit(v) => json!({
717 "id": extension.id().to_string(),
718 "data": v.to_string(),
719 }),
720 ClientHelloExtension::EncryptedClientHello(ech) => match ech {
721 ECHClientHello::Outer(ech) => json!({
722 "id": extension.id().to_string(),
723 "data": {
724 "type": "outer",
725 "cipher_suite": {
726 "aead_id": ech.cipher_suite.aead_id.to_string(),
727 "kdf_id": ech.cipher_suite.kdf_id.to_string(),
728 },
729 "config_id": ech.config_id,
730 "enc": format!("0x{}", hex::encode(&ech.enc)),
731 "payload": format!("0x{}", hex::encode(&ech.payload)),
732 },
733 }),
734 ECHClientHello::Inner => json!({
735 "id": extension.id().to_string(),
736 "data": {
737 "type": "inner",
738 },
739 })
740
741 }
742 ClientHelloExtension::Opaque { id, data } => if data.is_empty() {
743 json!({
744 "id": id.to_string()
745 })
746 } else {
747 json!({
748 "id": id.to_string(),
749 "data": format!("0x{}", hex::encode(data))
750 })
751 },
752 }).collect::<Vec<_>>(),
753 "ja3": ja3,
754 "ja4": ja4,
755 "peet": peet
756 })
757 });
758
759 #[cfg(not(any(feature = "rustls", feature = "boring")))]
760 let tls_info: Option<()> = None;
761
762 let mut h2 = None;
763 if parts.version == Version::HTTP_2 {
764 let early_frames = parts.extensions.get_ref::<EarlyFrameCapture>();
765 let pseudo_headers = parts.extensions.get_ref::<PseudoHeaderOrder>();
766 let akamai_h2 = AkamaiH2::compute(&parts.extensions)
767 .inspect_err(|err| tracing::trace!("akamai h2 compute failure: {err:?}"))
768 .ok()
769 .map(|akamai| {
770 json!({
771 "hash": format!("{akamai}"),
772 "verbose": format!("{akamai:?}"),
773 })
774 });
775
776 h2 = Some(json!({
777 "early_frames": early_frames,
778 "pseudo_headers": pseudo_headers,
779 "akamai_h2": akamai_h2,
780 }));
781 }
782
783 let geo = self
785 .geo_db
786 .as_ref()
787 .and_then(|db| {
788 parts
789 .extensions
790 .get_ref::<Forwarded>()
791 .and_then(|f| f.client_ip())
792 .or_else(|| {
793 parts
794 .extensions
795 .get_ref::<SocketInfo>()
796 .map(|s| s.peer_addr().ip_addr)
797 })
798 .and_then(|ip| db.resolve(ip))
799 })
800 .map(|info| serde_json::to_value(&info).unwrap_or_default())
801 .unwrap_or(serde_json::Value::Null);
802
803 Ok(Json(json!({
804 "ua": user_agent_info,
805 "geo": geo,
806 "http": {
807 "version": format!("{:?}", parts.version),
808 "scheme": scheme,
809 "method": format!("{:?}", parts.method),
810 "authority": authority,
811 "path": parts.uri.path_or_root().into_owned(),
812 "query": parts.uri.query().map(|q| q.as_encoded_str().into_owned()),
813 "h2": h2,
814 "headers": headers,
815 "payload": body,
816 "ja4h": ja4h,
817 "curl": curl_request,
818 },
819 "tls": tls_info,
820 "socket_addr": parts.extensions.get_ref::<Forwarded>()
821 .and_then(|f|
822 f.client_socket_addr().map(|addr| addr.to_string())
823 .or_else(|| f.client_ip().map(|ip| ip.to_string()))
824 ).or_else(|| parts.extensions.get_ref::<SocketInfo>().map(|v| v.peer_addr().to_string())),
825 }))
826 .into_response())
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833 use crate::http::{Body, StatusCode};
834
835 #[tokio::test]
836 async fn binary_request_body_is_echoed_with_lossless_platform_curl_scripts() {
837 let payload = rama_core::bytes::Bytes::from_static(b"\0\xfftail\n");
838 let request = Request::builder()
839 .method("POST")
840 .uri("http://example.com/upload")
841 .body(Body::from(payload))
842 .unwrap();
843
844 let response = EchoService {
845 uadb: None,
846 geo_db: None,
847 }
848 .serve(request)
849 .await
850 .expect("echo binary request");
851 assert_eq!(response.status(), StatusCode::OK);
852
853 let body = response
854 .into_body()
855 .collect()
856 .await
857 .expect("collect echo response")
858 .to_bytes();
859 let json: serde_json::Value = serde_json::from_slice(&body).expect("parse echo response");
860
861 assert_eq!(json["http"]["payload"], "00ff7461696c0a");
862 let unix = json["http"]["curl"]["unix"]
863 .as_str()
864 .expect("Unix curl script");
865 assert!(unix.contains("base64 -d"));
866 assert!(unix.contains("--data-binary '@-'"));
867 let powershell = json["http"]["curl"]["powershell"]
868 .as_str()
869 .expect("PowerShell curl script");
870 assert!(powershell.contains("[Convert]::FromBase64String"));
871 assert!(powershell.contains("--data-binary ('@' + $__ramaCurlPayload)"));
872 }
873}