1use rama_core::error::BoxErrorExt as _;
2use std::borrow::Cow;
3
4use rama_core::{
5 Layer, Service,
6 error::{BoxError, ErrorContext},
7 extensions::{Extension, ExtensionsRef},
8 telemetry::tracing,
9};
10use rama_http::headers::{ClientHint, all_client_hints};
11use rama_http::{
12 HeaderMap, HeaderName, HeaderValue, Method, Request, Version,
13 conn::{H2ClientContextParams, Http1ClientContextParams},
14 header::{CONTENT_TYPE, REFERER, SEC_WEBSOCKET_VERSION, USER_AGENT},
15};
16use rama_net::{
17 AuthorityInputExt, Protocol, ProtocolInputExt,
18 address::{Host, HostWithOptPort},
19 client::{ConnectorService, EstablishedClientConnection},
20 uri::Uri,
21};
22use rama_utils::str::{starts_with_ignore_ascii_case, submatch_ignore_ascii_case};
23
24use crate::{
25 HttpAgent, UserAgent,
26 profile::{
27 CUSTOM_HEADER_MARKER, HttpHeadersProfile, HttpProfile, PreserveHeaderUserAgent,
28 RequestClientHints, RequestInitiator,
29 },
30};
31
32use super::{SelectedUserAgentProfile, UserAgentProvider, UserAgentSelectFallback};
33
34#[derive(Debug, Clone)]
48pub struct UserAgentEmulateService<S, P> {
49 inner: S,
50 provider: P,
51 optional: bool,
52 try_auto_detect_user_agent: bool,
53 input_header_order: Option<HeaderName>,
54 select_fallback: Option<UserAgentSelectFallback>,
55}
56
57impl<S, P> UserAgentEmulateService<S, P> {
58 pub fn new(inner: S, provider: P) -> Self {
62 Self {
63 inner,
64 provider,
65 optional: false,
66 try_auto_detect_user_agent: false,
67 input_header_order: None,
68 select_fallback: None,
69 }
70 }
71
72 rama_utils::macros::generate_set_and_with! {
73 pub fn is_optional(mut self, optional: bool) -> Self {
77 self.optional = optional;
78 self
79 }
80 }
81
82 rama_utils::macros::generate_set_and_with! {
83 pub fn try_auto_detect_user_agent(mut self, try_auto_detect_user_agent: bool) -> Self {
86 self.try_auto_detect_user_agent = try_auto_detect_user_agent;
87 self
88 }
89 }
90
91 rama_utils::macros::generate_set_and_with! {
92 pub fn input_header_order(mut self, name: Option<HeaderName>) -> Self {
104 self.input_header_order = name;
105 self
106 }
107 }
108
109 rama_utils::macros::generate_set_and_with! {
110 pub fn select_fallback(mut self, fb: Option<UserAgentSelectFallback>) -> Self {
113 self.select_fallback = fb;
114 self
115 }
116 }
117}
118
119impl<Body, S, P> Service<Request<Body>> for UserAgentEmulateService<S, P>
120where
121 Body: Send + Sync + 'static,
122 S: Service<Request<Body>, Error: Into<BoxError>>,
123 P: UserAgentProvider,
124{
125 type Output = S::Output;
126 type Error = BoxError;
127
128 async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
129 if let Some(fallback) = self.select_fallback {
130 req.extensions().insert(fallback);
131 }
132
133 if self.try_auto_detect_user_agent && !req.extensions().contains::<UserAgent>() {
134 match req
135 .headers()
136 .get(USER_AGENT)
137 .and_then(|ua| ua.to_str().ok())
138 {
139 Some(ua_str) => {
140 let user_agent = UserAgent::new(ua_str);
141 tracing::trace!(
142 user_agent.original = %ua_str,
143 "user agent {user_agent} auto-detected from request"
144 );
145 req.extensions().insert(user_agent);
146 }
147 None => {
148 tracing::debug!(
149 "user agent auto-detection not possible: no user agent header present"
150 );
151 }
152 }
153 }
154
155 if let Some(header) = self
156 .input_header_order
157 .as_ref()
158 .and_then(|name| req.headers().get(name))
159 {
160 let s = header.to_str().context("interpret header as a utf-8 str")?;
161 let headers = s
162 .split(',')
163 .map(str::trim)
164 .filter(|s| !s.is_empty())
165 .map(|s| s.parse().context("parse header part as h1 header name"))
166 .collect::<Result<Vec<HeaderName>, BoxError>>()?;
167 req.extensions().insert(InputHeaderOrder(headers));
168 }
169
170 let Some(profile) = self.provider.select_user_agent_profile(req.extensions()) else {
171 return if self.optional {
172 Ok(self.inner.serve(req).await.into_box_error()?)
173 } else {
174 Err(BoxError::from_static_str(
175 "requirement not fulfilled: user agent profile could not be selected",
176 ))
177 };
178 };
179
180 tracing::debug!(
181 user_agent.kind = %profile.ua_kind,
182 user_agent.version = ?profile.ua_version,
183 user_agent.platform = ?profile.platform,
184 "user agent profile selected for emulation"
185 );
186
187 let preserve_http = matches!(
188 req.extensions()
189 .get_ref::<UserAgent>()
190 .and_then(|ua| ua.http_agent()),
191 Some(HttpAgent::Preserve),
192 );
193
194 if preserve_http {
195 tracing::trace!(
196 user_agent.kind = %profile.ua_kind,
197 user_agent.version = ?profile.ua_version,
198 user_agent.platform = ?profile.platform,
199 "user agent emulation: skip http settings as http is instructed to be preserved"
200 );
201 } else {
202 tracing::trace!(
203 user_agent.kind = %profile.ua_kind,
204 user_agent.version = ?profile.ua_version,
205 user_agent.platform = ?profile.platform,
206 "user agent emulation: inject http context data to prepare for HTTP emulation"
207 );
208 req.extensions().insert_arc(profile.http.clone());
209 }
210
211 #[cfg(feature = "tls")]
212 {
213 use crate::TlsAgent;
214
215 let preserve_tls = matches!(
216 req.extensions()
217 .get_ref::<UserAgent>()
218 .and_then(|ua| ua.tls_agent()),
219 Some(TlsAgent::Preserve),
220 );
221 if preserve_tls {
222 tracing::trace!(
223 user_agent.kind = %profile.ua_kind,
224 user_agent.version = ?profile.ua_version,
225 user_agent.platform = ?profile.platform,
226 "user agent emulation: skip tls settings as tls is instructed to be preserved"
227 );
228 } else {
229 req.extensions().insert_arc(profile.tls.clone());
230 tracing::trace!(
231 user_agent.kind = %profile.ua_kind,
232 user_agent.version = ?profile.ua_version,
233 user_agent.platform = ?profile.platform,
234 "user agent emulation: tls profile injected in ctx"
235 );
236 }
237 }
238
239 req.extensions()
241 .insert(SelectedUserAgentProfile::from(profile));
242
243 self.inner.serve(req).await.into_box_error()
245 }
246}
247
248#[derive(Debug, Clone, Default)]
249pub struct UserAgentEmulateHttpConnectModifier<S> {
253 inner: S,
254}
255
256impl<S> UserAgentEmulateHttpConnectModifier<S> {
257 #[inline]
258 #[must_use]
260 pub fn new(inner: S) -> Self {
261 Self { inner }
262 }
263}
264
265impl<S, ReqBody> Service<Request<ReqBody>> for UserAgentEmulateHttpConnectModifier<S>
266where
267 S: ConnectorService<Request<ReqBody>, Error: Into<BoxError>>,
268 ReqBody: Send + 'static,
269{
270 type Error = BoxError;
271 type Output = EstablishedClientConnection<S::Connection, Request<ReqBody>>;
272
273 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
274 let EstablishedClientConnection { conn, input: req } =
275 self.inner.connect(req).await.into_box_error()?;
276
277 match req
278 .extensions()
279 .clone_to_if_absent::<HttpProfile>(conn.extensions())
280 {
281 Some(http_profile) => {
282 tracing::trace!(
283 http.version = ?req.version(),
284 "http profile found in context to use for http connection emulation, proceed",
285 );
286 emulate_http_connect_settings(&req, &http_profile);
287 }
288 None => {
289 tracing::trace!(
290 http.version = ?req.version(),
291 "no http profile found in context to use for http connection emulation, request is passed through as-is",
292 );
293 }
294 }
295 Ok(EstablishedClientConnection { input: req, conn })
296 }
297}
298
299#[non_exhaustive]
300#[derive(Debug, Default, Clone)]
301pub struct UserAgentEmulateHttpConnectModifierLayer;
302
303impl<S> Layer<S> for UserAgentEmulateHttpConnectModifierLayer {
304 type Service = UserAgentEmulateHttpConnectModifier<S>;
305
306 fn layer(&self, inner: S) -> Self::Service {
307 UserAgentEmulateHttpConnectModifier::new(inner)
308 }
309}
310
311fn emulate_http_connect_settings<Body>(req: &Request<Body>, profile: &HttpProfile) {
312 match req.version() {
313 Version::HTTP_09 | Version::HTTP_10 | Version::HTTP_11 => {
314 tracing::trace!("UA emulation add http1-specific settings",);
315 req.extensions().insert(Http1ClientContextParams {
316 title_header_case: profile.h1.settings.title_case_headers,
317 });
318 }
319 Version::HTTP_2 => {
320 let pseudo_headers = profile.h2.settings.http_pseudo_headers.clone();
321 let early_frames = profile.h2.settings.early_frames.clone();
322
323 if pseudo_headers.is_some() || early_frames.is_some() {
324 tracing::trace!(
325 "user agent emulation: insert h2 settings into extensions: (pseudo headers = {:?} ; early frames: {:?})",
326 pseudo_headers,
327 early_frames,
328 );
329 req.extensions().insert(H2ClientContextParams {
330 headers_pseudo_order: pseudo_headers,
331 early_frames,
332 ..Default::default()
333 });
334 }
335 }
336 Version::HTTP_3 => tracing::debug!(
337 "UA emulation not yet supported for h3: not applying anything h3-specific"
338 ),
339 #[expect(
341 unreachable_patterns,
342 reason = "forward-compat fallback for future Version variants"
343 )]
344 _ => tracing::debug!(
345 http.version = ?req.version(),
346 "UA emulation not supported for unknown http version: not applying anything version-specific",
347 ),
348 }
349}
350
351#[derive(Debug, Clone, Default)]
352#[non_exhaustive]
353pub struct UserAgentEmulateHttpRequestModifier<S> {
357 inner: S,
358}
359
360impl<S> UserAgentEmulateHttpRequestModifier<S> {
361 #[inline]
362 #[must_use]
363 pub fn new(inner: S) -> Self {
365 Self { inner }
366 }
367}
368
369impl<S, ReqBody> Service<Request<ReqBody>> for UserAgentEmulateHttpRequestModifier<S>
370where
371 S: Service<Request<ReqBody>, Error: Into<BoxError>>,
372 ReqBody: Send + 'static,
373{
374 type Error = BoxError;
375 type Output = S::Output;
376
377 async fn serve(&self, mut req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
378 match req.extensions().get_arc() {
379 Some(http_profile) => {
380 tracing::trace!(
381 http.version = ?req.version(),
382 "http profile found in context to use for emulation, proceed",
383 );
384
385 match get_base_http_headers(&req, &http_profile) {
386 Some(base_http_headers) => {
387 let original_http_header_order =
388 req.extensions().get_ref::<InputHeaderOrder>().cloned();
389 let original_headers = req.headers().clone();
390
391 let preserve_ua_header =
392 req.extensions().contains::<PreserveHeaderUserAgent>();
393
394 let authority = req.authority().map(Cow::Owned);
395 let protocol = req.protocol().map(Cow::Borrowed);
396
397 let output_headers = merge_http_headers(
398 base_http_headers,
399 original_http_header_order,
400 original_headers,
401 preserve_ua_header,
402 authority,
403 protocol,
404 Some(req.method()),
405 req.extensions()
406 .get_ref::<RequestClientHints>()
407 .map(AsRef::as_ref),
408 );
409
410 tracing::trace!("user agent emulation: http emulated");
411 *req.headers_mut() = output_headers;
412 }
413 None => {
414 tracing::debug!(
415 "user agent emulation: no http headers to emulate: no base http headers found"
416 );
417 }
418 }
419
420 if req.version() == Version::HTTP_2 {
421 let pseudo_headers = http_profile.h2.settings.http_pseudo_headers.clone();
422
423 tracing::trace!(
424 "user agent emulation: insert h2 pseudo headers into request extensions: {pseudo_headers:?}"
425 );
426
427 if let Some(pseudo_headers) = pseudo_headers {
428 req.extensions().insert(pseudo_headers);
429 }
430 }
431 }
432 None => {
433 tracing::trace!(
434 http.version = ?req.version(),
435 "no http profile found in context to use for emulation, request is passed through as-is",
436 );
437 }
438 }
439
440 let resp = self.inner.serve(req).await.into_box_error()?;
441 Ok(resp)
442 }
443}
444
445#[derive(Debug, Clone, Default)]
446#[non_exhaustive]
447pub struct UserAgentEmulateHttpRequestModifierLayer;
451
452impl<S> Layer<S> for UserAgentEmulateHttpRequestModifierLayer {
453 type Service = UserAgentEmulateHttpRequestModifier<S>;
454
455 fn layer(&self, inner: S) -> Self::Service {
456 UserAgentEmulateHttpRequestModifier::new(inner)
457 }
458}
459
460fn get_base_http_headers<'a, Body>(
461 req: &Request<Body>,
462 profile: &'a HttpProfile,
463) -> Option<&'a HeaderMap> {
464 let headers_profile = match req.version() {
465 Version::HTTP_09 | Version::HTTP_10 | Version::HTTP_11 => &profile.h1.headers,
466 Version::HTTP_2 => &profile.h2.headers,
467 _ => {
468 tracing::debug!(
469 http.version = ?req.version(),
470 "UA emulation not supported for unknown http version: not applying anything version-specific",
471 );
472 return None;
473 }
474 };
475 match req.extensions().get_ref::<RequestInitiator>().copied() {
476 Some(req_init) => {
477 tracing::trace!(
478 "base http headers defined based on hint from UserAgent (overwrite): {req_init}"
479 );
480 get_base_http_headers_from_req_init(req_init, headers_profile)
481 }
482 None => match *req.method() {
486 Method::GET => {
487 let req_init = if headers_contains_partial_value(
488 req.headers(),
489 &X_REQUESTED_WITH,
490 "XmlHttpRequest",
491 ) {
492 RequestInitiator::Xhr
493 } else if headers_contains_partial_value(
494 req.headers(),
495 &SEC_WEBSOCKET_VERSION,
496 "13",
497 ) {
498 RequestInitiator::Ws
499 } else {
500 RequestInitiator::Navigate
501 };
502 tracing::trace!(
503 "base http headers defined based on Get=XhrOrWsOrNavigate assumption: {req_init}"
504 );
505 get_base_http_headers_from_req_init(req_init, headers_profile)
506 }
507 Method::POST => {
508 let req_init = if headers_contains_partial_value(
509 req.headers(),
510 &X_REQUESTED_WITH,
511 "XmlHttpRequest",
512 ) {
513 RequestInitiator::Xhr
514 } else if headers_contains_partial_value(req.headers(), &CONTENT_TYPE, "form-") {
515 RequestInitiator::Form
516 } else {
517 RequestInitiator::Fetch
518 };
519 tracing::trace!(
520 "base http headers defined based on Post=FormOrFetch assumption: {req_init}"
521 );
522 get_base_http_headers_from_req_init(req_init, headers_profile)
523 }
524 _ => {
525 let req_init = if headers_contains_partial_value(
526 req.headers(),
527 &X_REQUESTED_WITH,
528 "XmlHttpRequest",
529 ) {
530 RequestInitiator::Xhr
531 } else if req.version() == Version::HTTP_2
532 && req.method() == Method::CONNECT
533 && headers_contains_partial_value(req.headers(), &SEC_WEBSOCKET_VERSION, "13")
534 {
535 RequestInitiator::Ws
536 } else {
537 RequestInitiator::Fetch
538 };
539 tracing::trace!(
540 "base http headers defined based on XhrOrWsOrFetch assumption: {req_init}"
541 );
542 get_base_http_headers_from_req_init(req_init, headers_profile)
543 }
544 },
545 }
546}
547
548static X_REQUESTED_WITH: HeaderName = HeaderName::from_static("x-requested-with");
549
550fn headers_contains_partial_value(headers: &HeaderMap, name: &HeaderName, value: &str) -> bool {
551 headers
552 .get(name)
553 .and_then(|value| value.to_str().ok())
554 .map(|s| submatch_ignore_ascii_case(s, value))
555 .unwrap_or_default()
556}
557
558fn get_base_http_headers_from_req_init(
559 req_init: RequestInitiator,
560 headers: &HttpHeadersProfile,
561) -> Option<&HeaderMap> {
562 match req_init {
563 RequestInitiator::Navigate => Some(&headers.navigate),
564 RequestInitiator::Form => Some(headers.form.as_ref().unwrap_or(&headers.navigate)),
565 RequestInitiator::Xhr => Some(
566 headers
567 .xhr
568 .as_ref()
569 .or(headers.fetch.as_ref())
570 .unwrap_or(&headers.navigate),
571 ),
572 RequestInitiator::Fetch => Some(
573 headers
574 .fetch
575 .as_ref()
576 .or(headers.xhr.as_ref())
577 .unwrap_or(&headers.navigate),
578 ),
579 RequestInitiator::Ws => headers.ws.as_ref(),
580 }
581}
582
583const SEC_FETCH_SITE: HeaderName = HeaderName::from_static("sec-fetch-site");
584
585#[derive(Debug, Clone, Extension)]
586#[extension(tags(http))]
587struct InputHeaderOrder(Vec<HeaderName>);
588
589#[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
590fn merge_http_headers<'a>(
591 base_http_headers: &HeaderMap,
592 original_http_header_order: Option<InputHeaderOrder>,
593 original_headers: HeaderMap,
594 preserve_ua_header: bool,
595 request_authority: Option<Cow<'a, HostWithOptPort>>,
596 protocol: Option<Cow<'a, Protocol>>,
597 method: Option<&Method>,
598 requested_client_hints: Option<&[ClientHint]>,
599) -> HeaderMap {
600 let original_header_referer_value = original_headers.get(&REFERER).cloned();
601 let is_secure_request = protocol.as_ref().map(|p| p.is_secure()).unwrap_or_default();
602
603 let mut original_headers: Vec<_> = original_headers.into_ordered_iter().collect();
604
605 let remove_original_header = |headers: &mut Vec<(HeaderName, HeaderValue)>,
606 name: &HeaderName| {
607 headers
608 .iter()
609 .position(|(header_name, _)| header_name == name)
610 .map(|index| headers.remove(index).1)
611 };
612
613 let original_client_hints: Vec<_> = all_client_hints()
616 .filter(|p| {
617 p.iter_header_names()
618 .filter_map(|name| remove_original_header(&mut original_headers, &name).map(|_| 1))
619 .sum::<u16>()
620 > 0
621 })
622 .collect();
623
624 let mut output_headers_a = Vec::new();
625 let mut output_headers_b = Vec::new();
626
627 let mut output_headers_ref = &mut output_headers_a;
628
629 let is_header_allowed = |header_name: &HeaderName| {
630 if let Some(hint) = ClientHint::match_header_name(header_name) {
631 is_secure_request
632 && (hint.is_low_entropy()
633 || requested_client_hints
634 .map(|hints| hints.contains(&hint))
635 .unwrap_or_default()
636 || original_client_hints.contains(&hint))
637 } else {
638 is_secure_request || !starts_with_ignore_ascii_case(header_name.as_str(), "sec-fetch")
639 }
640 };
641
642 for (base_name, base_value) in base_http_headers.clone().into_ordered_iter() {
644 let base_header_name = &base_name;
645 let original_value = remove_original_header(&mut original_headers, base_header_name);
646 match base_header_name.standard() {
647 Some(
648 rama_http::header::StandardHeader::Accept
649 | rama_http::header::StandardHeader::AcceptLanguage
650 | rama_http::header::StandardHeader::SecWebSocketExtensions,
651 ) => {
652 let value = original_value.unwrap_or(base_value);
653 output_headers_ref.push((base_name, value));
654 }
655 Some(
656 rama_http::header::StandardHeader::Referer
657 | rama_http::header::StandardHeader::Cookie
658 | rama_http::header::StandardHeader::Authorization
659 | rama_http::header::StandardHeader::Host
660 | rama_http::header::StandardHeader::Origin
661 | rama_http::header::StandardHeader::ContentLength
662 | rama_http::header::StandardHeader::ContentType
663 | rama_http::header::StandardHeader::SecWebSocketProtocol
664 | rama_http::header::StandardHeader::SecWebSocketKey,
665 ) => {
666 if let Some(value) = original_value {
667 output_headers_ref.push((base_name, value));
668 }
669 }
670 Some(rama_http::header::StandardHeader::UserAgent) => {
671 if preserve_ua_header {
672 let value = original_value.unwrap_or(base_value);
673 output_headers_ref.push((base_name, value));
674 } else {
675 output_headers_ref.push((base_name, base_value));
676 }
677 }
678 _ => {
679 if base_header_name == CUSTOM_HEADER_MARKER {
680 output_headers_ref = &mut output_headers_b;
681 } else if is_header_allowed(base_header_name) {
682 if base_header_name == SEC_FETCH_SITE {
683 let value = compute_sec_fetch_site_value(
686 original_header_referer_value.as_ref(),
687 method,
688 protocol.as_deref(),
689 request_authority.as_deref(),
690 );
691 output_headers_ref.push((base_name, value));
692 } else {
693 output_headers_ref.push((base_name, base_value));
694 }
695 }
696 }
697 }
698 }
699
700 for header_name in original_http_header_order
702 .into_iter()
703 .flat_map(|order| order.0)
704 {
705 if let Some(value) = remove_original_header(&mut original_headers, &header_name)
706 && is_header_allowed(&header_name)
707 && ClientHint::match_header_name(&header_name).is_none()
708 {
709 output_headers_a.push((header_name, value));
710 }
711 }
712
713 let original_headers_iter = original_headers
714 .into_iter()
715 .filter(|(header_name, _)| is_header_allowed(header_name));
716
717 HeaderMap::from_iter(
718 output_headers_a
719 .into_iter()
720 .chain(original_headers_iter) .chain(output_headers_b),
722 )
723}
724
725fn compute_sec_fetch_site_value(
726 original_header_referer_value: Option<&HeaderValue>,
727 _method: Option<&Method>,
728 protocol: Option<&Protocol>,
729 request_authority: Option<&HostWithOptPort>,
730) -> HeaderValue {
731 match &original_header_referer_value {
732 Some(referer_value) => {
733 match referer_value
734 .to_str()
735 .context("turn referer into str")
736 .and_then(|s| {
737 Uri::parse(s)
740 .or_else(|_| Uri::parse_authority_form(s))
741 .context("turn referer header value str into http Uri")
742 }) {
743 Ok(uri) => {
744 let referer_protocol = uri.scheme().cloned();
745
746 let default_port = uri
747 .port_u16()
748 .or_else(|| referer_protocol.as_ref().and_then(|p| p.default_port()));
749
750 let maybe_authority = uri.host().map(|h| {
751 let h = h.into_owned();
752 if let Some(default_port) = default_port {
753 tracing::trace!(url.full = %uri, "detected host {h} from (abs) referer uri");
754 HostWithOptPort::new_with_port(h, default_port)
755 } else {
756 tracing::trace!(url.full = %uri, "detected host {h} from (abs) referer uri: but no (default) port available");
757 HostWithOptPort::new(h)
758 }
759 });
760
761 if let Some(authority) = maybe_authority {
762 if referer_protocol.as_ref() == protocol {
763 if Some(&authority) == request_authority {
764 HeaderValue::from_static("same-origin")
765 } else if let Some(request_host) =
766 request_authority.as_ref().map(|a| &a.host)
767 {
768 let is_same_registrable_domain =
769 match (&authority.host, request_host) {
770 (Host::Name(a), Host::Name(b)) => {
771 a.have_same_registrable_domain(b)
772 }
773 (Host::Address(a), Host::Address(b)) => a == b,
774 _ => false,
775 };
776 if is_same_registrable_domain {
777 HeaderValue::from_static("same-site")
778 } else {
779 HeaderValue::from_static("cross-site")
780 }
781 } else {
782 tracing::debug!(
783 http.request.header.referer = ?referer_value,
784 "missing request authority, returning none as default",
785 );
786 HeaderValue::from_static("none")
787 }
788 } else {
789 HeaderValue::from_static("cross-site")
790 }
791 } else {
792 tracing::debug!(
793 http.request.header.referer = ?referer_value,
794 "invalid referer value (failed to extract authority from uri value)",
795 );
796 HeaderValue::from_static("none")
797 }
798 }
799 Err(err) => {
800 tracing::debug!(
801 http.request.header.referer = ?referer_value,
802 "invalid referer value (expected a valid uri, defaulting to none): {err:?}",
803 );
804 HeaderValue::from_static("none")
805 }
806 }
807 }
808 None => HeaderValue::from_static("none"),
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 use std::{convert::Infallible, str::FromStr, sync::Arc};
817
818 use itertools::Itertools as _;
819 use rama_core::extensions::Extensions;
820 use rama_core::{Layer, service::service_fn};
821 use rama_http::{Body, HeaderMap, HeaderName, HeaderValue, header::ETAG};
822 use rama_net::address::{Domain, Host};
823
824 use crate::layer::emulate::UserAgentEmulateLayer;
825 use crate::profile::{
826 Http1Profile, Http1Settings, Http2Profile, Http2Settings, HttpHeadersProfile, HttpProfile,
827 UserAgentProfile,
828 };
829
830 #[test]
831 fn test_merge_http_headers() {
832 struct TestCase {
833 description: &'static str,
834 base_http_headers: Vec<(&'static str, &'static str)>,
835 original_http_header_order: Option<Vec<&'static str>>,
836 original_headers: Vec<(&'static str, &'static str)>,
837 preserve_ua_header: bool,
838 request_authority: HostWithOptPort,
839 protocol: Protocol,
840 requested_client_hints: Option<Vec<&'static str>>,
841 expected: Vec<(&'static str, &'static str)>,
842 }
843
844 let test_cases = [
845 TestCase {
846 description: "empty",
847 base_http_headers: vec![],
848 original_http_header_order: None,
849 original_headers: vec![],
850 preserve_ua_header: false,
851 request_authority: HostWithOptPort::example_domain_http(),
852 protocol: Protocol::HTTP,
853 requested_client_hints: None,
854 expected: vec![],
855 },
856 TestCase {
857 description: "base headers only",
858 base_http_headers: vec![
859 ("Accept", "text/html"),
860 ("Content-Type", "application/json"),
861 ],
862 original_http_header_order: None,
863 original_headers: vec![],
864 preserve_ua_header: false,
865 request_authority: HostWithOptPort::example_domain_http(),
866 protocol: Protocol::HTTP,
867 requested_client_hints: None,
868 expected: vec![("Accept", "text/html")],
869 },
870 TestCase {
871 description: "base headers only with content-type",
872 base_http_headers: vec![
873 ("Accept", "text/html"),
874 ("Content-Type", "application/json"),
875 ],
876 original_http_header_order: None,
877 original_headers: vec![("content-type", "text/xml")],
878 preserve_ua_header: false,
879 request_authority: HostWithOptPort::example_domain_http(),
880 protocol: Protocol::HTTP,
881 requested_client_hints: None,
882 expected: vec![("Accept", "text/html"), ("Content-Type", "text/xml")],
883 },
884 TestCase {
885 description: "original headers only",
886 base_http_headers: vec![],
887 original_http_header_order: None,
888 original_headers: vec![("accept", "text/html")],
889 preserve_ua_header: false,
890 request_authority: HostWithOptPort::example_domain_http(),
891 protocol: Protocol::HTTP,
892 requested_client_hints: None,
893 expected: vec![("accept", "text/html")],
894 },
895 TestCase {
896 description: "original and base headers, no conflicts",
897 base_http_headers: vec![("accept", "text/html"), ("user-agent", "python/3.10")],
898 original_http_header_order: None,
899 original_headers: vec![("content-type", "application/json")],
900 preserve_ua_header: false,
901 request_authority: HostWithOptPort::example_domain_http(),
902 protocol: Protocol::HTTP,
903 requested_client_hints: None,
904 expected: vec![
905 ("accept", "text/html"),
906 ("user-agent", "python/3.10"),
907 ("content-type", "application/json"),
908 ],
909 },
910 TestCase {
911 description: "original and base headers, with conflicts",
912 base_http_headers: vec![
913 ("accept", "text/html"),
914 ("content-type", "text/html"),
915 ("user-agent", "python/3.10"),
916 ],
917 original_http_header_order: Some(vec!["content-type", "user-agent"]),
918 original_headers: vec![
919 ("content-type", "application/json"),
920 ("user-agent", "php/8.0"),
921 ],
922 preserve_ua_header: false,
923 request_authority: HostWithOptPort::example_domain_http(),
924 protocol: Protocol::HTTP,
925 requested_client_hints: None,
926 expected: vec![
927 ("accept", "text/html"),
928 ("content-type", "application/json"),
929 ("user-agent", "python/3.10"),
930 ],
931 },
932 TestCase {
933 description: "original and base headers, with conflicts, preserve ua header",
934 base_http_headers: vec![
935 ("accept", "text/html"),
936 ("content-type", "text/html"),
937 ("user-agent", "python/3.10"),
938 ],
939 original_http_header_order: Some(vec!["content-type", "user-agent"]),
940 original_headers: vec![
941 ("content-type", "application/json"),
942 ("user-agent", "php/8.0"),
943 ],
944 preserve_ua_header: true,
945 request_authority: HostWithOptPort::example_domain_http(),
946 protocol: Protocol::HTTP,
947 requested_client_hints: None,
948 expected: vec![
949 ("accept", "text/html"),
950 ("content-type", "application/json"),
951 ("user-agent", "php/8.0"),
952 ],
953 },
954 TestCase {
955 description: "no opt-in base headers defined",
956 base_http_headers: vec![
957 ("accept", "text/html"),
958 ("authorization", "Bearer 1234567890"),
959 ("cookie", "session=1234567890"),
960 ("referer", "https://example.com"),
961 ],
962 original_http_header_order: Some(vec!["content-type", "user-agent"]),
963 original_headers: vec![
964 ("content-type", "application/json"),
965 ("user-agent", "php/8.0"),
966 ],
967 preserve_ua_header: false,
968 request_authority: HostWithOptPort::example_domain_http(),
969 protocol: Protocol::HTTP,
970 requested_client_hints: None,
971 expected: vec![
972 ("accept", "text/html"),
973 ("content-type", "application/json"),
974 ("user-agent", "php/8.0"),
975 ],
976 },
977 TestCase {
978 description: "some opt-in base headers defined",
979 base_http_headers: vec![
980 ("accept", "text/html"),
981 ("authorization", "Bearer 1234567890"),
982 ("cookie", "session=1234567890"),
983 ("referer", "https://example.com"),
984 ],
985 original_http_header_order: Some(vec![
986 "content-type",
987 "cookie",
988 "user-agent",
989 "referer",
990 ]),
991 original_headers: vec![
992 ("content-type", "application/json"),
993 ("cookie", "foo=bar"),
994 ("user-agent", "php/8.0"),
995 ("referer", "https://ramaproxy.org"),
996 ],
997 preserve_ua_header: false,
998 request_authority: HostWithOptPort::new_with_port(
999 Host::Name(Domain::from_static("ramaproxy.org")),
1000 Protocol::HTTPS_DEFAULT_PORT,
1001 ),
1002 protocol: Protocol::HTTPS,
1003 requested_client_hints: None,
1004 expected: vec![
1005 ("accept", "text/html"),
1006 ("cookie", "foo=bar"),
1007 ("referer", "https://ramaproxy.org"),
1008 ("content-type", "application/json"),
1009 ("user-agent", "php/8.0"),
1010 ],
1011 },
1012 TestCase {
1013 description: "all opt-in base headers defined",
1014 base_http_headers: vec![
1015 ("accept", "text/html"),
1016 ("authorization", "Bearer 1234567890"),
1017 ("cookie", "session=1234567890"),
1018 ("referer", "https://example.com"),
1019 ],
1020 original_http_header_order: Some(vec![
1021 "content-type",
1022 "cookie",
1023 "user-agent",
1024 "referer",
1025 "authorization",
1026 ]),
1027 original_headers: vec![
1028 ("content-type", "application/json"),
1029 ("cookie", "foo=bar"),
1030 ("user-agent", "php/8.0"),
1031 ("referer", "https://ramaproxy.org"),
1032 ("authorization", "Bearer 42"),
1033 ],
1034 preserve_ua_header: false,
1035 request_authority: HostWithOptPort::example_domain_https(),
1036 protocol: Protocol::HTTPS,
1037 requested_client_hints: None,
1038 expected: vec![
1039 ("accept", "text/html"),
1040 ("authorization", "Bearer 42"),
1041 ("cookie", "foo=bar"),
1042 ("referer", "https://ramaproxy.org"),
1043 ("content-type", "application/json"),
1044 ("user-agent", "php/8.0"),
1045 ],
1046 },
1047 TestCase {
1048 description: "all opt-in base headers defined, with custom header marker",
1049 base_http_headers: vec![
1050 ("accept", "text/html"),
1051 ("authorization", "Bearer 1234567890"),
1052 ("x-rama-custom-header-marker", "1"),
1053 ("cookie", "session=1234567890"),
1054 ("referer", "https://example.com"),
1055 ],
1056 original_http_header_order: Some(vec![
1057 "content-type",
1058 "cookie",
1059 "user-agent",
1060 "referer",
1061 "authorization",
1062 ]),
1063 original_headers: vec![
1064 ("content-type", "application/json"),
1065 ("cookie", "foo=bar"),
1066 ("user-agent", "php/8.0"),
1067 ("referer", "https://ramaproxy.org"),
1068 ("authorization", "Bearer 42"),
1069 ],
1070 preserve_ua_header: false,
1071 request_authority: HostWithOptPort::new_with_port(
1072 Host::Name(Domain::from_static("ramaproxy.org")),
1073 Protocol::HTTPS_DEFAULT_PORT,
1074 ),
1075 protocol: Protocol::HTTPS,
1076 requested_client_hints: None,
1077 expected: vec![
1078 ("accept", "text/html"),
1079 ("authorization", "Bearer 42"),
1080 ("content-type", "application/json"),
1081 ("user-agent", "php/8.0"),
1082 ("cookie", "foo=bar"),
1083 ("referer", "https://ramaproxy.org"),
1084 ],
1085 },
1086 TestCase {
1087 description: "realistic browser example",
1088 base_http_headers: vec![
1089 ("Host", "www.google.com"),
1090 (
1091 "User-Agent",
1092 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1093 ),
1094 (
1095 "Accept",
1096 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1097 ),
1098 ("Accept-Language", "en-US,en;q=0.9"),
1099 ("Accept-Encoding", "gzip, deflate, br"),
1100 ("Connection", "keep-alive"),
1101 ("Referer", "https://www.google.com/"),
1102 ("Upgrade-Insecure-Requests", "1"),
1103 ("x-rama-custom-header-marker", "1"),
1104 ("Cookie", "rama-ua-test=1"),
1105 ("Sec-Fetch-Dest", "document"),
1106 ("Sec-Fetch-Mode", "navigate"),
1107 ("Sec-Fetch-Site", "cross-site"),
1108 ("Sec-Fetch-User", "?1"),
1109 ("DNT", "1"),
1110 ("Sec-GPC", "1"),
1111 ("Priority", "u=0, i"),
1112 ],
1113 original_http_header_order: Some(vec![
1114 "x-show-price",
1115 "x-show-price-currency",
1116 "accept-language",
1117 "cookie",
1118 ]),
1119 original_headers: vec![
1120 ("x-show-price", "true"),
1121 ("x-show-price-currency", "USD"),
1122 ("accept-language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1123 ("cookie", "session=on; foo=bar"),
1124 ("x-requested-with", "XMLHttpRequest"),
1125 ("host", "www.example.com"),
1126 ],
1127 preserve_ua_header: false,
1128 request_authority: HostWithOptPort::new_with_port(
1129 Host::Name(Domain::from_static("www.google.com")),
1130 Protocol::HTTP_DEFAULT_PORT,
1131 ),
1132 protocol: Protocol::HTTP,
1133 requested_client_hints: None,
1134 expected: vec![
1135 ("Host", "www.example.com"),
1136 (
1137 "User-Agent",
1138 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1139 ),
1140 (
1141 "Accept",
1142 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1143 ),
1144 ("Accept-Language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1145 ("Accept-Encoding", "gzip, deflate, br"),
1146 ("Connection", "keep-alive"),
1147 ("Upgrade-Insecure-Requests", "1"),
1148 ("x-show-price", "true"),
1149 ("x-show-price-currency", "USD"),
1150 ("x-requested-with", "XMLHttpRequest"),
1151 ("Cookie", "session=on; foo=bar"),
1152 ("DNT", "1"),
1153 ("Sec-GPC", "1"),
1154 ("Priority", "u=0, i"),
1155 ],
1156 },
1157 TestCase {
1158 description: "realistic browser example over tls",
1159 base_http_headers: vec![
1160 ("Host", "www.google.com"),
1161 (
1162 "User-Agent",
1163 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1164 ),
1165 (
1166 "Accept",
1167 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1168 ),
1169 ("Accept-Language", "en-US,en;q=0.9"),
1170 ("Accept-Encoding", "gzip, deflate, br"),
1171 ("Connection", "keep-alive"),
1172 ("Referer", "https://www.google.com/"),
1173 ("Upgrade-Insecure-Requests", "1"),
1174 ("x-rama-custom-header-marker", "1"),
1175 ("Cookie", "rama-ua-test=1"),
1176 ("Sec-Fetch-Dest", "document"),
1177 ("Sec-Fetch-Mode", "navigate"),
1178 ("Sec-Fetch-Site", "cross-site"),
1179 ("Sec-Fetch-User", "?1"),
1180 ("DNT", "1"),
1181 ("Sec-GPC", "1"),
1182 ("Priority", "u=0, i"),
1183 ],
1184 original_http_header_order: Some(vec![
1185 "x-show-price",
1186 "x-show-price-currency",
1187 "accept-language",
1188 "cookie",
1189 ]),
1190 original_headers: vec![
1191 ("x-show-price", "true"),
1192 ("x-show-price-currency", "USD"),
1193 ("accept-language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1194 ("cookie", "session=on; foo=bar"),
1195 ("x-requested-with", "XMLHttpRequest"),
1196 ],
1197 preserve_ua_header: false,
1198 request_authority: HostWithOptPort::new_with_port(
1199 Host::Name(Domain::from_static("www.google.com")),
1200 Protocol::HTTPS_DEFAULT_PORT,
1201 ),
1202 protocol: Protocol::HTTPS,
1203 requested_client_hints: None,
1204 expected: vec![
1205 (
1206 "User-Agent",
1207 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1208 ),
1209 (
1210 "Accept",
1211 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1212 ),
1213 ("Accept-Language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1214 ("Accept-Encoding", "gzip, deflate, br"),
1215 ("Connection", "keep-alive"),
1216 ("Upgrade-Insecure-Requests", "1"),
1217 ("x-show-price", "true"),
1218 ("x-show-price-currency", "USD"),
1219 ("x-requested-with", "XMLHttpRequest"),
1220 ("Cookie", "session=on; foo=bar"),
1221 ("Sec-Fetch-Dest", "document"),
1222 ("Sec-Fetch-Mode", "navigate"),
1223 ("Sec-Fetch-Site", "none"),
1224 ("Sec-Fetch-User", "?1"),
1225 ("DNT", "1"),
1226 ("Sec-GPC", "1"),
1227 ("Priority", "u=0, i"),
1228 ],
1229 },
1230 TestCase {
1231 description: "secure example request with referer",
1232 base_http_headers: vec![
1233 ("Host", "www.google.com"),
1234 (
1235 "User-Agent",
1236 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1237 ),
1238 (
1239 "Accept",
1240 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1241 ),
1242 ("Referer", "https://www.google.com/"),
1243 ("Sec-Fetch-Site", "cross-site"),
1244 ],
1245 original_http_header_order: None,
1246 original_headers: vec![("referer", "https://maps.google.com/")],
1247 preserve_ua_header: false,
1248 request_authority: HostWithOptPort::new_with_port(
1249 Host::Name(Domain::from_static("www.google.com")),
1250 Protocol::HTTPS_DEFAULT_PORT,
1251 ),
1252 protocol: Protocol::HTTPS,
1253 requested_client_hints: None,
1254 expected: vec![
1255 (
1256 "User-Agent",
1257 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1258 ),
1259 (
1260 "Accept",
1261 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1262 ),
1263 ("Referer", "https://maps.google.com/"),
1264 ("Sec-Fetch-Site", "same-site"),
1265 ],
1266 },
1267 TestCase {
1268 description: "realistic browser example over tls with requested client hints",
1269 base_http_headers: vec![
1270 ("Host", "www.google.com"),
1271 (
1272 "User-Agent",
1273 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1274 ),
1275 (
1276 "Accept",
1277 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1278 ),
1279 ("Accept-Language", "en-US,en;q=0.9"),
1280 ("Accept-Encoding", "gzip, deflate, br"),
1281 ("Connection", "keep-alive"),
1282 ("Referer", "https://www.google.com/"),
1283 ("Upgrade-Insecure-Requests", "1"),
1284 ("x-rama-custom-header-marker", "1"),
1285 ("Cookie", "rama-ua-test=1"),
1286 ("Sec-Fetch-Dest", "document"),
1287 ("Sec-Fetch-Mode", "navigate"),
1288 ("Sec-Fetch-Site", "cross-site"),
1289 ("Sec-Fetch-User", "?1"),
1290 ("Sec-CH-Downlink", "100"),
1291 ("Sec-CH-Ect", "4g"),
1292 ("Sec-CH-RTT", "100"),
1293 ("Sec-CH-UA-Arch", "arm"),
1294 ("Sec-CH-UA-Bitness", "64"),
1295 ("Sec-CH-UA-Full-Version", "120.0.0.0"),
1296 ("Sec-CH-UA-Full-Version-List", "Chrome 120.0.0.0"),
1297 ("Sec-CH-UA-Mobile", "?0"),
1298 ("Sec-CH-UA-Platform", "macOS"),
1299 ("Sec-CH-UA-Platform-Version", "10.15.7"),
1300 ("Sec-CH-UA-Model", "LimitedEdition"),
1301 ("DNT", "1"),
1302 ("Sec-GPC", "1"),
1303 ("Priority", "u=0, i"),
1304 ],
1305 original_http_header_order: Some(vec![
1306 "x-show-price",
1307 "x-show-price-currency",
1308 "accept-language",
1309 "cookie",
1310 ]),
1311 original_headers: vec![
1312 ("x-show-price", "true"),
1313 ("x-show-price-currency", "USD"),
1314 ("accept-language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1315 ("cookie", "session=on; foo=bar"),
1316 ("x-requested-with", "XMLHttpRequest"),
1317 ("sec-ch-ua-model", "Macintosh"),
1318 ("sec-ch-prefers-contrast", "xx"),
1319 ],
1320 preserve_ua_header: false,
1321 request_authority: HostWithOptPort::new_with_port(
1322 Host::Name(Domain::from_static("www.google.com")),
1323 Protocol::HTTPS_DEFAULT_PORT,
1324 ),
1325 protocol: Protocol::HTTPS,
1326 requested_client_hints: Some(vec![
1327 "Downlink",
1328 "Ect",
1329 "RTT",
1330 "Sec-CH-UA-Arch",
1331 "Sec-CH-UA-Bitness",
1332 "Sec-CH-Prefers-Reduced-Transparency",
1334 ]),
1335 expected: vec![
1336 (
1337 "User-Agent",
1338 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1339 ),
1340 (
1341 "Accept",
1342 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
1343 ),
1344 ("Accept-Language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"),
1345 ("Accept-Encoding", "gzip, deflate, br"),
1346 ("Connection", "keep-alive"),
1347 ("Upgrade-Insecure-Requests", "1"),
1348 ("x-show-price", "true"),
1349 ("x-show-price-currency", "USD"),
1350 ("x-requested-with", "XMLHttpRequest"),
1351 ("Cookie", "session=on; foo=bar"),
1352 ("Sec-Fetch-Dest", "document"),
1353 ("Sec-Fetch-Mode", "navigate"),
1354 ("Sec-Fetch-Site", "none"),
1355 ("Sec-Fetch-User", "?1"),
1356 ("Sec-CH-Downlink", "100"),
1357 ("Sec-CH-Ect", "4g"),
1358 ("Sec-CH-RTT", "100"),
1359 ("Sec-CH-UA-Arch", "arm"),
1360 ("Sec-CH-UA-Bitness", "64"),
1361 ("Sec-CH-UA-Mobile", "?0"), ("Sec-CH-UA-Platform", "macOS"), ("Sec-CH-UA-Model", "LimitedEdition"),
1364 ("DNT", "1"),
1366 ("Sec-GPC", "1"),
1367 ("Priority", "u=0, i"),
1368 ],
1369 },
1370 ];
1371
1372 for test_case in test_cases {
1373 let base_http_headers =
1374 HeaderMap::from_iter(test_case.base_http_headers.into_iter().map(
1375 |(name, value)| {
1376 (
1377 HeaderName::from_str(name).unwrap(),
1378 HeaderValue::from_static(value),
1379 )
1380 },
1381 ));
1382 let original_http_header_order = test_case.original_http_header_order.map(|headers| {
1383 InputHeaderOrder(
1384 headers
1385 .into_iter()
1386 .map(|header| HeaderName::from_str(header).unwrap())
1387 .collect(),
1388 )
1389 });
1390 let original_headers = HeaderMap::from_iter(
1391 test_case.original_headers.into_iter().map(|(name, value)| {
1392 (
1393 HeaderName::from_static(name),
1394 HeaderValue::from_static(value),
1395 )
1396 }),
1397 );
1398 let preserve_ua_header = test_case.preserve_ua_header;
1399 let requested_client_hints = test_case.requested_client_hints.map(|hints| {
1400 hints
1401 .into_iter()
1402 .map(|hint| ClientHint::from_str(hint).unwrap())
1403 .collect::<Vec<_>>()
1404 });
1405
1406 let output_headers = merge_http_headers(
1407 &base_http_headers,
1408 original_http_header_order,
1409 original_headers,
1410 preserve_ua_header,
1411 Some(Cow::Borrowed(&test_case.request_authority)),
1412 Some(Cow::Borrowed(&test_case.protocol)),
1413 None,
1414 requested_client_hints.as_deref(),
1415 );
1416
1417 let output_str = output_headers
1418 .into_ordered_iter()
1419 .map(|(name, value)| format!("{}: {}\r\n", name, value.to_str().unwrap()))
1420 .join("");
1421
1422 let expected_str = test_case
1423 .expected
1424 .iter()
1425 .map(|(name, value)| format!("{name}: {value}\r\n"))
1426 .join("");
1427
1428 assert_eq!(
1429 output_str, expected_str,
1430 "test case '{}' failed",
1431 test_case.description
1432 );
1433 }
1434 }
1435
1436 #[tokio::test]
1437 async fn test_get_base_h2_headers() {
1438 let ua = UserAgent::new(
1439 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1440 );
1441
1442 let ua_profile = UserAgentProfile {
1443 ua_kind: ua.ua_kind().unwrap(),
1444 ua_version: ua.ua_version(),
1445 platform: ua.platform(),
1446 http: Arc::new(HttpProfile {
1447 h1: Http1Profile {
1448 headers: HttpHeadersProfile {
1449 navigate: HeaderMap::default(),
1450 fetch: None,
1451 xhr: None,
1452 form: None,
1453 ws: None,
1454 },
1455 settings: Http1Settings::default(),
1456 },
1457 h2: Http2Profile {
1458 headers: HttpHeadersProfile {
1459 navigate: HeaderMap::from_iter([(
1460 ETAG,
1461 HeaderValue::from_static("navigate"),
1462 )]),
1463 fetch: None,
1464 xhr: None,
1465 form: None,
1466 ws: None,
1467 },
1468 settings: Http2Settings::default(),
1469 },
1470 }),
1471 #[cfg(feature = "tls")]
1472 tls: Arc::new(crate::profile::TlsProfile {
1473 client_hello: rama_tls::client::ClientHello::new(
1474 rama_tls::ProtocolVersion::TLSv1_3,
1475 Vec::new(),
1476 Vec::new(),
1477 Vec::new(),
1478 ),
1479 ws_client_config_overwrites: None,
1480 }),
1481 runtime: None,
1482 };
1483
1484 let ua_service = (
1485 UserAgentEmulateLayer::new(ua_profile),
1486 UserAgentEmulateHttpRequestModifierLayer::default(),
1487 )
1488 .into_layer(service_fn(async |req: Request| {
1489 Ok::<_, Infallible>(
1490 req.headers()
1491 .get(ETAG)
1492 .map(|header| header.to_str().unwrap().to_owned())
1493 .unwrap_or_default(),
1494 )
1495 }));
1496
1497 let req = Request::builder()
1498 .method(Method::GET)
1499 .body(Body::empty())
1500 .unwrap();
1501 let res = ua_service.serve(req).await.unwrap();
1502 assert_eq!(res, "");
1503
1504 let req = Request::builder()
1505 .method(Method::GET)
1506 .version(Version::HTTP_2)
1507 .body(Body::empty())
1508 .unwrap();
1509 let res = ua_service.serve(req).await.unwrap();
1510 assert_eq!(res, "navigate");
1511 }
1512
1513 #[tokio::test]
1514 async fn test_get_base_http_headers_profile_with_only_navigate_headers() {
1515 let ua = UserAgent::new(
1516 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1517 );
1518 let ua_profile = UserAgentProfile {
1519 ua_kind: ua.ua_kind().unwrap(),
1520 ua_version: ua.ua_version(),
1521 platform: ua.platform(),
1522 http: Arc::new(HttpProfile {
1523 h1: Http1Profile {
1524 headers: HttpHeadersProfile {
1525 navigate: HeaderMap::from_iter([(
1526 ETAG,
1527 HeaderValue::from_static("navigate"),
1528 )]),
1529 xhr: None,
1530 fetch: None,
1531 form: None,
1532 ws: None,
1533 },
1534 settings: Http1Settings::default(),
1535 },
1536 h2: Http2Profile {
1537 headers: HttpHeadersProfile {
1538 navigate: HeaderMap::default(),
1539 fetch: None,
1540 xhr: None,
1541 form: None,
1542 ws: None,
1543 },
1544 settings: Http2Settings::default(),
1545 },
1546 }),
1547 #[cfg(feature = "tls")]
1548 tls: Arc::new(crate::profile::TlsProfile {
1549 client_hello: rama_tls::client::ClientHello::new(
1550 rama_tls::ProtocolVersion::TLSv1_3,
1551 Vec::new(),
1552 Vec::new(),
1553 Vec::new(),
1554 ),
1555 ws_client_config_overwrites: None,
1556 }),
1557 runtime: None,
1558 };
1559
1560 let ua_service = (
1561 UserAgentEmulateLayer::new(ua_profile),
1562 UserAgentEmulateHttpRequestModifierLayer::default(),
1563 )
1564 .into_layer(service_fn(async |req: Request| {
1565 Ok::<_, Infallible>(
1566 req.headers()
1567 .get(ETAG)
1568 .map(|header| header.to_str().unwrap().to_owned())
1569 .unwrap_or_default(),
1570 )
1571 }));
1572
1573 let req = Request::builder()
1574 .method(Method::DELETE)
1575 .body(Body::empty())
1576 .unwrap();
1577 let res = ua_service.serve(req).await.unwrap();
1578 assert_eq!(res, "navigate");
1579 }
1580
1581 #[tokio::test]
1582 async fn test_get_base_http_headers_profile_without_fetch_headers() {
1583 let ua = UserAgent::new(
1584 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1585 );
1586 let ua_profile = UserAgentProfile {
1587 ua_kind: ua.ua_kind().unwrap(),
1588 ua_version: ua.ua_version(),
1589 platform: ua.platform(),
1590 http: Arc::new(HttpProfile {
1591 h1: Http1Profile {
1592 headers: HttpHeadersProfile {
1593 navigate: HeaderMap::from_iter([(
1594 ETAG,
1595 HeaderValue::from_static("navigate"),
1596 )]),
1597 xhr: Some(HeaderMap::from_iter([(
1598 ETAG,
1599 HeaderValue::from_static("xhr"),
1600 )])),
1601 fetch: None,
1602 form: Some(HeaderMap::from_iter([(
1603 ETAG,
1604 HeaderValue::from_static("form"),
1605 )])),
1606 ws: None,
1607 },
1608 settings: Http1Settings::default(),
1609 },
1610 h2: Http2Profile {
1611 headers: HttpHeadersProfile {
1612 navigate: HeaderMap::default(),
1613 fetch: None,
1614 xhr: None,
1615 form: None,
1616 ws: None,
1617 },
1618 settings: Http2Settings::default(),
1619 },
1620 }),
1621 #[cfg(feature = "tls")]
1622 tls: Arc::new(crate::profile::TlsProfile {
1623 client_hello: rama_tls::client::ClientHello::new(
1624 rama_tls::ProtocolVersion::TLSv1_3,
1625 Vec::new(),
1626 Vec::new(),
1627 Vec::new(),
1628 ),
1629 ws_client_config_overwrites: None,
1630 }),
1631 runtime: None,
1632 };
1633
1634 let ua_service = (
1635 UserAgentEmulateLayer::new(ua_profile),
1636 UserAgentEmulateHttpRequestModifierLayer::default(),
1637 )
1638 .into_layer(service_fn(async |req: Request| {
1639 Ok::<_, Infallible>(
1640 req.headers()
1641 .get(ETAG)
1642 .map(|header| header.to_str().unwrap().to_owned())
1643 .unwrap_or_default(),
1644 )
1645 }));
1646
1647 let req = Request::builder()
1648 .method(Method::DELETE)
1649 .body(Body::empty())
1650 .unwrap();
1651 let res = ua_service.serve(req).await.unwrap();
1652 assert_eq!(res, "xhr");
1653 }
1654
1655 #[tokio::test]
1656 async fn test_get_base_http_headers_profile_without_xhr_headers() {
1657 let ua = UserAgent::new(
1658 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1659 );
1660 let ua_profile = UserAgentProfile {
1661 ua_kind: ua.ua_kind().unwrap(),
1662 ua_version: ua.ua_version(),
1663 platform: ua.platform(),
1664 http: Arc::new(HttpProfile {
1665 h1: Http1Profile {
1666 headers: HttpHeadersProfile {
1667 navigate: HeaderMap::from_iter([(
1668 ETAG,
1669 HeaderValue::from_static("navigate"),
1670 )]),
1671 fetch: Some(HeaderMap::from_iter([(
1672 ETAG,
1673 HeaderValue::from_static("fetch"),
1674 )])),
1675 xhr: None,
1676 form: Some(HeaderMap::from_iter([(
1677 ETAG,
1678 HeaderValue::from_static("form"),
1679 )])),
1680 ws: None,
1681 },
1682 settings: Http1Settings::default(),
1683 },
1684 h2: Http2Profile {
1685 headers: HttpHeadersProfile {
1686 navigate: HeaderMap::default(),
1687 fetch: None,
1688 xhr: None,
1689 form: None,
1690 ws: None,
1691 },
1692 settings: Http2Settings::default(),
1693 },
1694 }),
1695 #[cfg(feature = "tls")]
1696 tls: Arc::new(crate::profile::TlsProfile {
1697 client_hello: rama_tls::client::ClientHello::new(
1698 rama_tls::ProtocolVersion::TLSv1_3,
1699 Vec::new(),
1700 Vec::new(),
1701 Vec::new(),
1702 ),
1703 ws_client_config_overwrites: None,
1704 }),
1705 runtime: None,
1706 };
1707
1708 let ua_service = (
1709 UserAgentEmulateLayer::new(ua_profile),
1710 UserAgentEmulateHttpRequestModifierLayer::default(),
1711 )
1712 .into_layer(service_fn(async |req: Request| {
1713 Ok::<_, Infallible>(
1714 req.headers()
1715 .get(ETAG)
1716 .map(|header| header.to_str().unwrap().to_owned())
1717 .unwrap_or_default(),
1718 )
1719 }));
1720
1721 let req = Request::builder()
1722 .method(Method::DELETE)
1723 .header(
1724 HeaderName::from_static("x-requested-with"),
1725 "XmlHttpRequest",
1726 )
1727 .body(Body::empty())
1728 .unwrap();
1729 let res = ua_service.serve(req).await.unwrap();
1730 assert_eq!(res, "fetch");
1731 }
1732
1733 #[tokio::test]
1734 async fn test_get_base_http_headers() {
1735 let ua = UserAgent::new(
1736 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
1737 );
1738 let ua_profile = UserAgentProfile {
1739 ua_kind: ua.ua_kind().unwrap(),
1740 ua_version: ua.ua_version(),
1741 platform: ua.platform(),
1742 http: Arc::new(HttpProfile {
1743 h1: Http1Profile {
1744 headers: HttpHeadersProfile {
1745 navigate: HeaderMap::from_iter([(
1746 ETAG,
1747 HeaderValue::from_static("navigate"),
1748 )]),
1749 fetch: Some(HeaderMap::from_iter([(
1750 ETAG,
1751 HeaderValue::from_static("fetch"),
1752 )])),
1753 xhr: Some(HeaderMap::from_iter([(
1754 ETAG,
1755 HeaderValue::from_static("xhr"),
1756 )])),
1757 form: Some(HeaderMap::from_iter([(
1758 ETAG,
1759 HeaderValue::from_static("form"),
1760 )])),
1761 ws: Some(HeaderMap::from_iter([(
1762 ETAG,
1763 HeaderValue::from_static("ws"),
1764 )])),
1765 },
1766 settings: Http1Settings::default(),
1767 },
1768 h2: Http2Profile {
1769 headers: HttpHeadersProfile {
1770 navigate: HeaderMap::from_iter([(
1771 ETAG,
1772 HeaderValue::from_static("navigate2"),
1773 )]),
1774 fetch: Some(HeaderMap::from_iter([(
1775 ETAG,
1776 HeaderValue::from_static("fetch2"),
1777 )])),
1778 xhr: Some(HeaderMap::from_iter([(
1779 ETAG,
1780 HeaderValue::from_static("xhr2"),
1781 )])),
1782 form: Some(HeaderMap::from_iter([(
1783 ETAG,
1784 HeaderValue::from_static("form2"),
1785 )])),
1786 ws: Some(HeaderMap::from_iter([(
1787 ETAG,
1788 HeaderValue::from_static("ws2"),
1789 )])),
1790 },
1791 settings: Http2Settings::default(),
1792 },
1793 }),
1794 #[cfg(feature = "tls")]
1795 tls: Arc::new(crate::profile::TlsProfile {
1796 client_hello: rama_tls::client::ClientHello::new(
1797 rama_tls::ProtocolVersion::TLSv1_3,
1798 Vec::new(),
1799 Vec::new(),
1800 Vec::new(),
1801 ),
1802 ws_client_config_overwrites: None,
1803 }),
1804 runtime: None,
1805 };
1806
1807 let ua_service = (
1808 UserAgentEmulateLayer::new(ua_profile),
1809 UserAgentEmulateHttpRequestModifierLayer::default(),
1810 )
1811 .into_layer(service_fn(async |req: Request| {
1812 Ok::<_, Infallible>(
1813 req.headers()
1814 .get(ETAG)
1815 .map(|header| header.to_str().unwrap().to_owned())
1816 .unwrap_or_default(),
1817 )
1818 }));
1819
1820 struct TestCase {
1821 description: &'static str,
1822 version: Option<Version>,
1823 method: Option<Method>,
1824 headers: Option<HeaderMap>,
1825 extensions: Option<Extensions>,
1826 expected: &'static str,
1827 }
1828
1829 let test_cases = [
1830 TestCase {
1831 description: "GET request",
1832 version: None,
1833 method: None,
1834 headers: None,
1835 extensions: None,
1836 expected: "navigate",
1837 },
1838 TestCase {
1839 description: "GET request with XRW header",
1840 version: None,
1841 method: None,
1842 headers: Some(
1843 [(
1844 HeaderName::from_static("x-requested-with"),
1845 HeaderValue::from_static("XmlHttpRequest"),
1846 )]
1847 .into_iter()
1848 .collect(),
1849 ),
1850 extensions: None,
1851 expected: "xhr",
1852 },
1853 TestCase {
1854 description: "GET request with RequestInitiator hint Navigate",
1855 version: None,
1856 method: None,
1857 headers: None,
1858 extensions: Some({
1859 let extensions = Extensions::default();
1860 extensions.insert(RequestInitiator::Navigate);
1861 extensions
1862 }),
1863 expected: "navigate",
1864 },
1865 TestCase {
1866 description: "GET request with RequestInitiator hint Form",
1867 version: None,
1868 method: None,
1869 headers: None,
1870 extensions: Some({
1871 let extensions = Extensions::default();
1872 extensions.insert(RequestInitiator::Form);
1873 extensions
1874 }),
1875 expected: "form",
1876 },
1877 TestCase {
1878 description: "explicit GET request",
1879 version: None,
1880 method: Some(Method::GET),
1881 headers: None,
1882 extensions: None,
1883 expected: "navigate",
1884 },
1885 TestCase {
1886 description: "HTTP/1.1 WebSocket upgrade request",
1887 version: None,
1888 method: Some(Method::GET),
1889 headers: Some(
1890 [(SEC_WEBSOCKET_VERSION, HeaderValue::from_static("13"))]
1891 .into_iter()
1892 .collect(),
1893 ),
1894 extensions: None,
1895 expected: "ws",
1896 },
1897 TestCase {
1898 description: "H2 WebSocket upgrade request",
1899 version: Some(Version::HTTP_2),
1900 method: Some(Method::CONNECT),
1901 headers: Some(
1902 [(SEC_WEBSOCKET_VERSION, HeaderValue::from_static("13"))]
1903 .into_iter()
1904 .collect(),
1905 ),
1906 extensions: None,
1907 expected: "ws2",
1908 },
1909 TestCase {
1910 description: "explicit POST request",
1911 version: None,
1912 method: Some(Method::POST),
1913 headers: None,
1914 extensions: None,
1915 expected: "fetch",
1916 },
1917 TestCase {
1918 description: "explicit POST request with XRW header",
1919 version: None,
1920 method: Some(Method::POST),
1921 headers: Some(
1922 [(
1923 HeaderName::from_static("x-requested-with"),
1924 HeaderValue::from_static("XmlHttpRequest"),
1925 )]
1926 .into_iter()
1927 .collect(),
1928 ),
1929 extensions: None,
1930 expected: "xhr",
1931 },
1932 TestCase {
1933 description: "explicit POST request with multipart/form-data and XRW header",
1934 version: None,
1935 method: Some(Method::POST),
1936 headers: Some(
1937 [
1938 (
1939 CONTENT_TYPE,
1940 HeaderValue::from_static(
1941 "multipart/form-data; boundary=ExampleBoundaryString",
1942 ),
1943 ),
1944 (
1945 HeaderName::from_static("x-requested-with"),
1946 HeaderValue::from_static("XmlHttpRequest"),
1947 ),
1948 ]
1949 .into_iter()
1950 .collect(),
1951 ),
1952 extensions: None,
1953 expected: "xhr",
1954 },
1955 TestCase {
1956 description: "explicit POST request with application/x-www-form-urlencoded and XRW header",
1957 version: None,
1958 method: Some(Method::POST),
1959 headers: Some(
1960 [
1961 (
1962 CONTENT_TYPE,
1963 HeaderValue::from_static("application/x-www-form-urlencoded"),
1964 ),
1965 (
1966 HeaderName::from_static("x-requested-with"),
1967 HeaderValue::from_static("XmlHttpRequest"),
1968 ),
1969 ]
1970 .into_iter()
1971 .collect(),
1972 ),
1973 extensions: None,
1974 expected: "xhr",
1975 },
1976 TestCase {
1977 description: "explicit POST request with multipart/form-data",
1978 version: None,
1979 method: Some(Method::POST),
1980 headers: Some(
1981 [(
1982 CONTENT_TYPE,
1983 HeaderValue::from_static(
1984 "multipart/form-data; boundary=ExampleBoundaryString",
1985 ),
1986 )]
1987 .into_iter()
1988 .collect(),
1989 ),
1990 extensions: None,
1991 expected: "form",
1992 },
1993 TestCase {
1994 description: "explicit POST request with application/x-www-form-urlencoded",
1995 version: None,
1996 method: Some(Method::POST),
1997 headers: Some(
1998 [(
1999 CONTENT_TYPE,
2000 HeaderValue::from_static("application/x-www-form-urlencoded"),
2001 )]
2002 .into_iter()
2003 .collect(),
2004 ),
2005 extensions: None,
2006 expected: "form",
2007 },
2008 TestCase {
2009 description: "explicit DELETE request with XRW header",
2010 version: None,
2011 method: Some(Method::DELETE),
2012 headers: Some(
2013 [(
2014 HeaderName::from_static("x-requested-with"),
2015 HeaderValue::from_static("XmlHttpRequest"),
2016 )]
2017 .into_iter()
2018 .collect(),
2019 ),
2020 extensions: None,
2021 expected: "xhr",
2022 },
2023 TestCase {
2024 description: "explicit DELETE request",
2025 version: None,
2026 method: Some(Method::DELETE),
2027 headers: None,
2028 extensions: None,
2029 expected: "fetch",
2030 },
2031 TestCase {
2032 description: "explicit DELETE request with RequestInitiator hint",
2033 version: None,
2034 method: Some(Method::DELETE),
2035 headers: None,
2036 extensions: Some({
2037 let extensions = Extensions::default();
2038 extensions.insert(RequestInitiator::Xhr);
2039 extensions
2040 }),
2041 expected: "xhr",
2042 },
2043 ];
2044
2045 for test_case in test_cases {
2046 let mut req = Request::builder()
2047 .version(test_case.version.unwrap_or(Version::HTTP_11))
2048 .method(test_case.method.unwrap_or(Method::GET))
2049 .body(Body::empty())
2050 .unwrap();
2051 if let Some(headers) = test_case.headers {
2052 req.headers_mut().extend(headers);
2053 }
2054 if let Some(extensions) = test_case.extensions {
2055 req.extensions().extend(&extensions);
2056 }
2057
2058 let res = ua_service.serve(req).await.unwrap();
2059 assert_eq!(res, test_case.expected, "{}", test_case.description);
2060 }
2061 }
2062
2063 #[test]
2064 fn test_compute_sec_fetch_site_value() {
2065 #[derive(Debug)]
2066 struct TestCase {
2067 referer: Option<&'static str>,
2068 method: Option<Method>,
2069 protocol: Protocol,
2070 request_authority: HostWithOptPort,
2071 expected_value: &'static str,
2072 }
2073
2074 let test_cases = [
2075 TestCase {
2076 referer: None,
2077 method: None,
2078 protocol: Protocol::HTTP,
2079 request_authority: HostWithOptPort::example_domain_http(),
2080 expected_value: "none",
2081 },
2082 TestCase {
2083 referer: Some("http://example.com/foo?q=1"),
2084 method: None,
2085 protocol: Protocol::HTTP,
2086 request_authority: HostWithOptPort::example_domain_http(),
2087 expected_value: "same-origin",
2088 },
2089 TestCase {
2090 referer: Some("http://example.com:8080/foo?q=1"),
2091 method: None,
2092 protocol: Protocol::HTTP,
2093 request_authority: HostWithOptPort::example_domain_http(),
2094 expected_value: "same-site",
2095 },
2096 TestCase {
2097 referer: Some("https://example.com/foo?q=1"),
2098 method: None,
2099 protocol: Protocol::HTTP,
2100 request_authority: HostWithOptPort::example_domain_http(),
2101 expected_value: "cross-site",
2102 },
2103 TestCase {
2104 referer: Some("http://example.com/foo?q=1"),
2105 method: None,
2106 protocol: Protocol::HTTPS,
2107 request_authority: HostWithOptPort::example_domain_http(),
2108 expected_value: "cross-site",
2109 },
2110 TestCase {
2111 referer: Some("http://example.be/foo?q=1"),
2112 method: None,
2113 protocol: Protocol::HTTP,
2114 request_authority: HostWithOptPort::example_domain_http(),
2115 expected_value: "cross-site",
2116 },
2117 TestCase {
2118 referer: Some("http://sub.example.com/foo?q=1"),
2119 method: None,
2120 protocol: Protocol::HTTP,
2121 request_authority: HostWithOptPort::example_domain_http(),
2122 expected_value: "same-site",
2123 },
2124 TestCase {
2125 referer: Some("http://example.com/foo?q=1"),
2126 method: None,
2127 protocol: Protocol::HTTP,
2128 request_authority: HostWithOptPort::new_with_port(
2129 Host::Name(Domain::from_static("sub.example.com")),
2130 Protocol::HTTP_DEFAULT_PORT,
2131 ),
2132 expected_value: "same-site",
2133 },
2134 TestCase {
2135 referer: Some("http://a.example.com/foo?q=1"),
2136 method: None,
2137 protocol: Protocol::HTTP,
2138 request_authority: HostWithOptPort::new_with_port(
2139 Host::Name(Domain::from_static("b.example.com")),
2140 Protocol::HTTP_DEFAULT_PORT,
2141 ),
2142 expected_value: "same-site",
2143 },
2144 TestCase {
2145 referer: Some("......."),
2146 method: None,
2147 protocol: Protocol::HTTP,
2148 request_authority: HostWithOptPort::new_with_port(
2149 Host::Name(Domain::from_static("b.example.com")),
2150 Protocol::HTTP_DEFAULT_PORT,
2151 ),
2152 expected_value: "cross-site",
2153 },
2154 ];
2155
2156 for test_case in test_cases {
2157 let original_header_referer_value = test_case.referer.map(HeaderValue::from_static);
2158 let computed_value = compute_sec_fetch_site_value(
2159 original_header_referer_value.as_ref(),
2160 test_case.method.as_ref(),
2161 Some(&test_case.protocol),
2162 Some(&test_case.request_authority),
2163 );
2164 assert_eq!(
2165 computed_value.to_str().unwrap(),
2166 test_case.expected_value,
2167 "test_case: {test_case:?}",
2168 );
2169 }
2170 }
2171}