Skip to main content

hpx/client/
emulation.rs

1use http::{HeaderMap, HeaderValue};
2
3#[cfg(feature = "http1")]
4use super::core::http1::Http1Options;
5#[cfg(feature = "http2")]
6use super::core::http2::Http2Options;
7use super::layer::config::TransportOptions;
8use crate::{
9    header::{self, OrigHeaderMap},
10    tls::TlsOptions,
11};
12
13/// Factory trait for creating emulation configurations.
14///
15/// This trait allows different types (enums, structs, etc.) to provide
16/// their own emulation configurations. It's particularly useful for:
17/// - Predefined browser profiles
18/// - Dynamic configuration based on runtime conditions
19/// - User-defined custom emulation strategies
20pub trait EmulationFactory {
21    /// Creates an [`Emulation`] instance from this factory.
22    fn emulation(self) -> Emulation;
23}
24
25/// Built-in browser-style emulation profiles provided by `hpx`.
26///
27/// These profiles provide convenient header-level defaults for common client
28/// personas while keeping transport options configurable through
29/// [`EmulationBuilder`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31#[non_exhaustive]
32pub enum BrowserProfile {
33    /// Chrome-like request headers.
34    #[default]
35    Chrome,
36    /// Firefox-like request headers.
37    Firefox,
38    /// Safari-like request headers.
39    Safari,
40    /// Edge-like request headers.
41    Edge,
42    /// OkHttp-like request headers.
43    OkHttp,
44}
45
46impl BrowserProfile {
47    #[inline]
48    const fn user_agent(self) -> &'static str {
49        match self {
50            Self::Chrome => {
51                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
52            }
53            Self::Firefox => {
54                "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.0; rv:146.0) Gecko/20100101 Firefox/146.0"
55            }
56            Self::Safari => {
57                "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15"
58            }
59            Self::Edge => {
60                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"
61            }
62            Self::OkHttp => "okhttp/5.0.0",
63        }
64    }
65
66    #[inline]
67    const fn accept(self) -> &'static str {
68        match self {
69            Self::Firefox => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
70            Self::OkHttp => "*/*",
71            _ => {
72                "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
73            }
74        }
75    }
76}
77
78/// Builder for creating an [`Emulation`] configuration.
79#[derive(Debug)]
80#[must_use]
81pub struct EmulationBuilder {
82    emulation: Emulation,
83}
84
85/// HTTP emulation configuration for mimicking different HTTP clients.
86///
87/// This struct combines transport-layer options (HTTP/1, HTTP/2, TLS) with
88/// request-level settings (headers, header case preservation) to provide
89/// a complete emulation profile for web browsers, mobile applications,
90/// API clients, and other HTTP implementations.
91#[derive(Debug, Clone, Default)]
92#[non_exhaustive]
93pub struct Emulation {
94    headers: HeaderMap,
95    orig_headers: OrigHeaderMap,
96    transport: TransportOptions,
97}
98
99// ==== impl EmulationBuilder ====
100
101impl EmulationBuilder {
102    /// Sets the  HTTP/1 options configuration.
103    #[cfg(feature = "http1")]
104    #[inline]
105    pub fn http1_options(mut self, opts: Http1Options) -> Self {
106        *self.emulation.http1_options_mut() = Some(opts);
107        self
108    }
109
110    /// Sets the HTTP/2 options configuration.
111    #[cfg(feature = "http2")]
112    #[inline]
113    pub fn http2_options(mut self, opts: Http2Options) -> Self {
114        *self.emulation.http2_options_mut() = Some(opts);
115        self
116    }
117
118    /// Sets the  TLS options configuration.
119    #[inline]
120    pub fn tls_options(mut self, opts: TlsOptions) -> Self {
121        *self.emulation.tls_options_mut() = Some(opts);
122        self
123    }
124
125    /// Sets the default headers.
126    #[inline]
127    pub fn headers(mut self, src: HeaderMap) -> Self {
128        crate::util::replace_headers(&mut self.emulation.headers, src);
129        self
130    }
131
132    /// Sets the original headers.
133    #[inline]
134    pub fn orig_headers(mut self, src: OrigHeaderMap) -> Self {
135        self.emulation.orig_headers.extend(src);
136        self
137    }
138
139    /// Builds the [`Emulation`] instance.
140    #[inline]
141    pub fn build(self) -> Emulation {
142        self.emulation
143    }
144}
145
146// ==== impl Emulation ====
147
148impl Emulation {
149    /// Creates a new [`EmulationBuilder`].
150    #[inline]
151    pub fn builder() -> EmulationBuilder {
152        EmulationBuilder {
153            emulation: Emulation::default(),
154        }
155    }
156
157    /// Returns a mutable reference to the TLS options, if set.
158    #[inline]
159    pub fn tls_options_mut(&mut self) -> &mut Option<TlsOptions> {
160        self.transport.tls_options_mut()
161    }
162
163    /// Returns a mutable reference to the HTTP/1 options, if set.
164    #[cfg(feature = "http1")]
165    #[inline]
166    pub fn http1_options_mut(&mut self) -> &mut Option<Http1Options> {
167        self.transport.http1_options_mut()
168    }
169
170    /// Returns a mutable reference to the HTTP/2 options, if set.
171    #[cfg(feature = "http2")]
172    #[inline]
173    pub fn http2_options_mut(&mut self) -> &mut Option<Http2Options> {
174        self.transport.http2_options_mut()
175    }
176
177    /// Returns a mutable reference to the emulation headers, if set.
178    #[inline]
179    pub fn headers_mut(&mut self) -> &mut HeaderMap {
180        &mut self.headers
181    }
182
183    /// Returns a mutable reference to the original headers, if set.
184    #[inline]
185    pub fn orig_headers_mut(&mut self) -> &mut OrigHeaderMap {
186        &mut self.orig_headers
187    }
188
189    /// Decomposes the [`Emulation`] into its components.
190    #[inline]
191    pub(crate) fn into_parts(self) -> (TransportOptions, HeaderMap, OrigHeaderMap) {
192        (self.transport, self.headers, self.orig_headers)
193    }
194}
195
196impl EmulationFactory for Emulation {
197    #[inline]
198    fn emulation(self) -> Emulation {
199        self
200    }
201}
202
203impl EmulationFactory for BrowserProfile {
204    #[inline]
205    fn emulation(self) -> Emulation {
206        let mut headers = HeaderMap::new();
207        headers.insert(
208            header::USER_AGENT,
209            HeaderValue::from_static(self.user_agent()),
210        );
211        headers.insert(header::ACCEPT, HeaderValue::from_static(self.accept()));
212        headers.insert(
213            header::ACCEPT_LANGUAGE,
214            HeaderValue::from_static("en-US,en;q=0.9"),
215        );
216
217        match self {
218            BrowserProfile::Chrome | BrowserProfile::Edge | BrowserProfile::Safari => {
219                headers.insert("sec-fetch-dest", HeaderValue::from_static("document"));
220                headers.insert("sec-fetch-mode", HeaderValue::from_static("navigate"));
221                headers.insert("sec-fetch-site", HeaderValue::from_static("none"));
222            }
223            BrowserProfile::Firefox | BrowserProfile::OkHttp => {}
224        }
225
226        Emulation::builder().headers(headers).build()
227    }
228}
229
230#[cfg(feature = "http1")]
231impl EmulationFactory for Http1Options {
232    #[inline]
233    fn emulation(self) -> Emulation {
234        Emulation::builder().http1_options(self).build()
235    }
236}
237
238#[cfg(feature = "http2")]
239impl EmulationFactory for Http2Options {
240    #[inline]
241    fn emulation(self) -> Emulation {
242        Emulation::builder().http2_options(self).build()
243    }
244}
245
246impl EmulationFactory for TlsOptions {
247    #[inline]
248    fn emulation(self) -> Emulation {
249        Emulation::builder().tls_options(self).build()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn browser_profile_sets_user_agent_header() {
259        let emulation = BrowserProfile::Firefox.emulation();
260        let headers = emulation.headers;
261        assert!(headers.contains_key(header::USER_AGENT));
262    }
263}