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::{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)]
74pub 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 #[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 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 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 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 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 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 pub fn http_version(mut self, version: Option<Version>) -> Self {
188 self.http_version = version;
189 self
190 }
191 }
192
193 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 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 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 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 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 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 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]
365pub 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 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}