Skip to main content

rama_ua/layer/emulate/
service.rs

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/// Service to select a [`UserAgentProfile`] and inject its info into the input [`Extensions`].
35///
36/// Note that actual http emulation is done by also ensuring a service
37/// such as [`UserAgentEmulateHttpRequestModifier`] and [`UserAgentEmulateHttpConnectModifier`] is in use within your connector stack.
38/// Tls emulation is facilitated by a tls client connector which respects
39/// the injected (tls) client profile.
40///
41/// See the implementation of[`EasyHttpWebClient`] for the reference implementation of how
42/// one can make use of this profile to emulate a user agent on the tls layer.
43///
44/// [`UserAgentProfile`]: crate::profile::UserAgentProfile
45/// [`Extensions`]: rama_core::extensions::Extensions
46/// [`EasyHttpWebClient`]: https://ramaproxy.org/docs/rama/http/client/struct.EasyHttpWebClient.html
47#[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    /// Create a new [`UserAgentEmulateService`] with the given inner service and provider.
59    ///
60    /// Provider is implicitly expected to be a [`UserAgentProvider`].
61    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        /// When no user agent profile was found it will
74        /// fail the request unless optional is true. In case of
75        /// the latter the service will do nothing.
76        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        /// If true, the service will try to auto-detect the user agent from the request,
84        /// but only in case that info is not yet found in the context.
85        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        /// Define a header that if present is to contain a CSV header name list,
93        /// that allows you to define the desired header order for the (extra) headers
94        /// found in the input (http) request.
95        ///
96        /// Extra meaning any headers not considered a base header and already defined
97        /// by the (selected) User Agent Profile.
98        ///
99        /// This can be useful because your http client might not respect the header casing
100        /// and/or order of the headers taken together. Using this metadata allows you to
101        /// communicate this data through anyway. If however your http client does respect
102        /// casing and order, or you don't care about some of it, you might not need it.
103        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        /// Choose what to do in case no profile could be selected
111        /// using the regular pre-conditions as specified by the provider.
112        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        // inject the selected user agent profile into the context
240        req.extensions()
241            .insert(SelectedUserAgentProfile::from(profile));
242
243        // serve emulated http(s) request via inner service
244        self.inner.serve(req).await.into_box_error()
245    }
246}
247
248#[derive(Debug, Clone, Default)]
249// A connector which is to be used in combination
250// with the [`UserAgentEmulateService`] to facilitate the
251// http emulation based on the injected http profile.
252pub struct UserAgentEmulateHttpConnectModifier<S> {
253    inner: S,
254}
255
256impl<S> UserAgentEmulateHttpConnectModifier<S> {
257    #[inline]
258    /// Create a new [`UserAgentEmulateHttpConnectModifier`].
259    #[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        // defensive forward-compat arm; native Version is currently exhaustive
340        #[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]
353// A http service which is to be used in combination
354// with the [`UserAgentEmulateService`] to facilitate the
355// http emulation based on the injected http profile.
356pub struct UserAgentEmulateHttpRequestModifier<S> {
357    inner: S,
358}
359
360impl<S> UserAgentEmulateHttpRequestModifier<S> {
361    #[inline]
362    #[must_use]
363    /// Create a new [`UserAgentEmulateHttpRequestModifier`].
364    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]
447// A http layer which is to be used in combination
448// with the [`UserAgentEmulateService`] to facilitate the
449// http emulation based on the injected http profile.
450pub 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        // NOTE: the primitive checks below are pretty bad,
483        // feel free to help improve. Just need to make sure it has good enough fallbacks,
484        // and that they are cheap enough to check.
485        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    // to support clients that pass in client hints as well. We'll ignore their
614    // header values, but can still take the hint none the less
615    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    // put all "base" headers in correct order, and with proper name casing
643    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                        // assumption: is_header_allowed ensures that this only
684                        // is reached in a secure context :)
685                        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    // respect original header order of original headers where possible
701    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) // add all remaining original headers in any order within the right loc
721            .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                    // Referer is usually a full URL; fall back to authority-form
738                    // so a bare/degenerate host is still treated as an origin.
739                    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                    // requested but not available
1333                    "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"), // not requested, but low entropy
1362                    ("Sec-CH-UA-Platform", "macOS"), // not requested, but low entropy
1363                    ("Sec-CH-UA-Model", "LimitedEdition"),
1364                    // sec-ch-prefers-contrast was requested, but UA profile doesn't contain it :)
1365                    ("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}