Skip to main content

hpx_emulation/
emulation.rs

1pub(crate) mod device;
2#[cfg(feature = "emulation-rand")]
3mod rand;
4
5use device::{chrome::*, firefox::*, okhttp::*, opera::*, safari::*};
6#[cfg(feature = "emulation-serde")]
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "emulation-rand")]
9use strum_macros::VariantArray;
10use typed_builder::TypedBuilder;
11
12macro_rules! define_enum {
13    (
14        $(#[$meta:meta])*
15        with_dispatch,
16        $name:ident, $default_variant:ident,
17        $(
18            $variant:ident => ($rename:expr, $emulation_fn:path)
19        ),* $(,)?
20    ) => {
21        $(#[$meta])*
22        #[non_exhaustive]
23        #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
24        #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
25        #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
26        pub enum $name {
27            $(
28                #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
29                $variant,
30            )*
31        }
32
33        impl Default for $name {
34            fn default() -> Self {
35                $name::$default_variant
36            }
37        }
38
39        impl $name {
40            pub fn into_emulation(self, opt: EmulationOption) -> hpx::Emulation {
41                match self {
42                    $(
43                        $name::$variant => $emulation_fn(opt),
44                    )*
45                }
46            }
47        }
48    };
49
50    (
51        $(#[$meta:meta])*
52        plain,
53        $name:ident, $default_variant:ident,
54        $(
55            $variant:ident => $rename:expr
56        ),* $(,)?
57    ) => {
58        $(#[$meta])*
59        #[non_exhaustive]
60        #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
61        #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
62        #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
63        pub enum $name {
64            $(
65                #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
66                $variant,
67            )*
68        }
69
70        impl Default for $name {
71            fn default() -> Self {
72                $name::$default_variant
73            }
74        }
75    };
76}
77
78define_enum!(
79    /// Represents different browser versions for emulation.
80    ///
81    /// The `Emulation` enum provides variants for different browser versions that can be used
82    /// to emulate HTTP requests. Each variant corresponds to a specific browser version.
83    ///
84    /// # Naming Convention
85    ///
86    /// The naming convention for the variants follows the pattern `browser_version`, where
87    /// `browser` is the name of the browser (e.g., `chrome`, `firefox`, `safari`) and `version`
88    /// is the version number. For example, `Chrome100` represents Chrome version 100.
89    ///
90    /// The serialized names of the variants use underscores to separate the browser name and
91    /// version number, following the pattern `browser_version`. For example, `Chrome100` is
92    /// serialized as `"chrome_100"`.
93    with_dispatch,
94    Emulation, Chrome143,
95
96    // Chrome versions
97    Chrome100 => ("chrome_100", v100::emulation),
98    Chrome101 => ("chrome_101", v101::emulation),
99    Chrome104 => ("chrome_104", v104::emulation),
100    Chrome105 => ("chrome_105", v105::emulation),
101    Chrome106 => ("chrome_106", v106::emulation),
102    Chrome107 => ("chrome_107", v107::emulation),
103    Chrome108 => ("chrome_108", v108::emulation),
104    Chrome109 => ("chrome_109", v109::emulation),
105    Chrome110 => ("chrome_110", v110::emulation),
106    Chrome114 => ("chrome_114", v114::emulation),
107    Chrome116 => ("chrome_116", v116::emulation),
108    Chrome117 => ("chrome_117", v117::emulation),
109    Chrome118 => ("chrome_118", v118::emulation),
110    Chrome119 => ("chrome_119", v119::emulation),
111    Chrome120 => ("chrome_120", v120::emulation),
112    Chrome123 => ("chrome_123", v123::emulation),
113    Chrome124 => ("chrome_124", v124::emulation),
114    Chrome126 => ("chrome_126", v126::emulation),
115    Chrome127 => ("chrome_127", v127::emulation),
116    Chrome128 => ("chrome_128", v128::emulation),
117    Chrome129 => ("chrome_129", v129::emulation),
118    Chrome130 => ("chrome_130", v130::emulation),
119    Chrome131 => ("chrome_131", v131::emulation),
120    Chrome132 => ("chrome_132", v132::emulation),
121    Chrome133 => ("chrome_133", v133::emulation),
122    Chrome134 => ("chrome_134", v134::emulation),
123    Chrome135 => ("chrome_135", v135::emulation),
124    Chrome136 => ("chrome_136", v136::emulation),
125    Chrome137 => ("chrome_137", v137::emulation),
126    Chrome138 => ("chrome_138", v138::emulation),
127    Chrome139 => ("chrome_139", v139::emulation),
128    Chrome140 => ("chrome_140", v140::emulation),
129    Chrome141 => ("chrome_141", v141::emulation),
130    Chrome142 => ("chrome_142", v142::emulation),
131    Chrome143 => ("chrome_143", v143::emulation),
132    Chrome144 => ("chrome_144", v144::emulation),
133    Chrome145 => ("chrome_145", v145::emulation),
134    Chrome146 => ("chrome_146", v146::emulation),
135    Chrome147 => ("chrome_147", v147::emulation),
136    Chrome148 => ("chrome_148", v148::emulation),
137    Chrome149 => ("chrome_149", v149::emulation),
138
139    // Edge versions
140    Edge101 => ("edge_101", edge101::emulation),
141    Edge122 => ("edge_122", edge122::emulation),
142    Edge127 => ("edge_127", edge127::emulation),
143    Edge131 => ("edge_131", edge131::emulation),
144    Edge134 => ("edge_134", edge134::emulation),
145    Edge135 => ("edge_135", edge135::emulation),
146    Edge136 => ("edge_136", edge136::emulation),
147    Edge137 => ("edge_137", edge137::emulation),
148    Edge138 => ("edge_138", edge138::emulation),
149    Edge139 => ("edge_139", edge139::emulation),
150    Edge140 => ("edge_140", edge140::emulation),
151    Edge141 => ("edge_141", edge141::emulation),
152    Edge142 => ("edge_142", edge142::emulation),
153    Edge143 => ("edge_143", edge143::emulation),
154    Edge144 => ("edge_144", edge144::emulation),
155    Edge145 => ("edge_145", edge145::emulation),
156    Edge146 => ("edge_146", edge146::emulation),
157    Edge147 => ("edge_147", edge147::emulation),
158    Edge148 => ("edge_148", edge148::emulation),
159
160    // Opera versions
161    Opera116 => ("opera_116", opera116::emulation),
162    Opera117 => ("opera_117", opera117::emulation),
163    Opera118 => ("opera_118", opera118::emulation),
164    Opera119 => ("opera_119", opera119::emulation),
165    Opera120 => ("opera_120", opera120::emulation),
166    Opera121 => ("opera_121", opera121::emulation),
167    Opera122 => ("opera_122", opera122::emulation),
168    Opera123 => ("opera_123", opera123::emulation),
169    Opera124 => ("opera_124", opera124::emulation),
170    Opera125 => ("opera_125", opera125::emulation),
171    Opera126 => ("opera_126", opera126::emulation),
172    Opera127 => ("opera_127", opera127::emulation),
173    Opera128 => ("opera_128", opera128::emulation),
174    Opera129 => ("opera_129", opera129::emulation),
175    Opera130 => ("opera_130", opera130::emulation),
176    Opera131 => ("opera_131", opera131::emulation),
177
178    // Safari versions
179    SafariIos17_2 => ("safari_ios_17.2", safari_ios_17_2::emulation),
180    SafariIos17_4_1 => ("safari_ios_17.4.1", safari_ios_17_4_1::emulation),
181    SafariIos16_5 => ("safari_ios_16.5", safari_ios_16_5::emulation),
182    Safari15_3 => ("safari_15.3", safari15_3::emulation),
183    Safari15_5 => ("safari_15.5", safari15_5::emulation),
184    Safari15_6_1 => ("safari_15.6.1", safari15_6_1::emulation),
185    Safari16 => ("safari_16", safari16::emulation),
186    Safari16_5 => ("safari_16.5", safari16_5::emulation),
187    Safari17_0 => ("safari_17.0", safari17_0::emulation),
188    Safari17_2_1 => ("safari_17.2.1", safari17_2_1::emulation),
189    Safari17_4_1 => ("safari_17.4.1", safari17_4_1::emulation),
190    Safari17_5 => ("safari_17.5", safari17_5::emulation),
191    Safari17_6 => ("safari_17.6", safari17_6::emulation),
192    Safari18 => ("safari_18", safari18::emulation),
193    SafariIPad18 => ("safari_ipad_18", safari_ipad_18::emulation),
194    Safari18_2 => ("safari_18.2", safari18_2::emulation),
195    SafariIos18_1_1 => ("safari_ios_18.1.1", safari_ios_18_1_1::emulation),
196    Safari18_3 => ("safari_18.3", safari18_3::emulation),
197    Safari18_3_1 => ("safari_18.3.1", safari18_3_1::emulation),
198    Safari18_5 => ("safari_18.5", safari18_5::emulation),
199    Safari26 => ("safari_26", safari26::emulation),
200    Safari26_1 => ("safari_26.1", safari26_1::emulation),
201    Safari26_2 => ("safari_26.2", safari26_2::emulation),
202    SafariIPad26 => ("safari_ipad_26", safari_ipad_26::emulation),
203    SafariIPad26_2 => ("safari_ipad_26.2", safari_ipad_26_2::emulation),
204    SafariIos26 => ("safari_ios_26", safari_ios_26::emulation),
205    SafariIos26_2 => ("safari_ios_26.2", safari_ios_26_2::emulation),
206    Safari19 => ("safari_19", safari19::emulation),
207    SafariIos19 => ("safari_ios_19", safari_ios_19::emulation),
208    SafariIPad19 => ("safari_ipad_19", safari_ipad_19::emulation),
209    Safari20 => ("safari_20", safari20::emulation),
210    SafariIos20 => ("safari_ios_20", safari_ios_20::emulation),
211    SafariIPad20 => ("safari_ipad_20", safari_ipad_20::emulation),
212    Safari21 => ("safari_21", safari21::emulation),
213    SafariIos21 => ("safari_ios_21", safari_ios_21::emulation),
214    SafariIPad21 => ("safari_ipad_21", safari_ipad_21::emulation),
215    Safari22 => ("safari_22", safari22::emulation),
216    SafariIos22 => ("safari_ios_22", safari_ios_22::emulation),
217    SafariIPad22 => ("safari_ipad_22", safari_ipad_22::emulation),
218    Safari23 => ("safari_23", safari23::emulation),
219    SafariIos23 => ("safari_ios_23", safari_ios_23::emulation),
220    SafariIPad23 => ("safari_ipad_23", safari_ipad_23::emulation),
221    Safari24 => ("safari_24", safari24::emulation),
222    SafariIos24 => ("safari_ios_24", safari_ios_24::emulation),
223    SafariIPad24 => ("safari_ipad_24", safari_ipad_24::emulation),
224    Safari25 => ("safari_25", safari25::emulation),
225    SafariIos25 => ("safari_ios_25", safari_ios_25::emulation),
226    SafariIPad25 => ("safari_ipad_25", safari_ipad_25::emulation),
227    Safari26_3 => ("safari_26.3", safari26_3::emulation),
228    SafariIos26_3 => ("safari_ios_26.3", safari_ios_26_3::emulation),
229    SafariIPad26_3 => ("safari_ipad_26.3", safari_ipad_26_3::emulation),
230    Safari26_4 => ("safari_26.4", safari26_4::emulation),
231    SafariIos26_4 => ("safari_ios_26.4", safari_ios_26_4::emulation),
232    SafariIPad26_4 => ("safari_ipad_26.4", safari_ipad_26_4::emulation),
233
234    // Firefox versions
235    Firefox109 => ("firefox_109", ff109::emulation),
236    Firefox117 => ("firefox_117", ff117::emulation),
237    Firefox128 => ("firefox_128", ff128::emulation),
238    Firefox133 => ("firefox_133", ff133::emulation),
239    Firefox135 => ("firefox_135", ff135::emulation),
240    FirefoxPrivate135 => ("firefox_private_135", ff_private_135::emulation),
241    FirefoxAndroid135 => ("firefox_android_135", ff_android_135::emulation),
242    Firefox136 => ("firefox_136", ff136::emulation),
243    FirefoxPrivate136 => ("firefox_private_136", ff_private_136::emulation),
244    Firefox137 => ("firefox_137", ff137::emulation),
245    Firefox138 => ("firefox_138", ff138::emulation),
246    Firefox139 => ("firefox_139", ff139::emulation),
247    Firefox140 => ("firefox_140", ff140::emulation),
248    Firefox141 => ("firefox_141", ff141::emulation),
249    Firefox142 => ("firefox_142", ff142::emulation),
250    Firefox143 => ("firefox_143", ff143::emulation),
251    Firefox144 => ("firefox_144", ff144::emulation),
252    Firefox145 => ("firefox_145", ff145::emulation),
253    Firefox146 => ("firefox_146", ff146::emulation),
254    Firefox147 => ("firefox_147", ff147::emulation),
255    Firefox148 => ("firefox_148", ff148::emulation),
256    Firefox149 => ("firefox_149", ff149::emulation),
257    Firefox150 => ("firefox_150", ff150::emulation),
258    Firefox151 => ("firefox_151", ff151::emulation),
259
260    // OkHttp versions
261    OkHttp3_9 => ("okhttp_3.9", okhttp3_9::emulation),
262    OkHttp3_11 => ("okhttp_3.11", okhttp3_11::emulation),
263    OkHttp3_13 => ("okhttp_3.13", okhttp3_13::emulation),
264    OkHttp3_14 => ("okhttp_3.14", okhttp3_14::emulation),
265    OkHttp4_9 => ("okhttp_4.9", okhttp4_9::emulation),
266    OkHttp4_10 => ("okhttp_4.10", okhttp4_10::emulation),
267    OkHttp4_12 => ("okhttp_4.12", okhttp4_12::emulation),
268    OkHttp5 => ("okhttp_5", okhttp5::emulation)
269
270);
271
272/// ======== Emulation impls ========
273impl hpx::EmulationFactory for Emulation {
274    #[inline]
275    fn emulation(self) -> hpx::Emulation {
276        EmulationOption::builder()
277            .emulation(self)
278            .build()
279            .emulation()
280    }
281}
282
283define_enum!(
284    /// Represents different operating systems for emulation.
285    ///
286    /// The `EmulationOS` enum provides variants for different operating systems that can be used
287    /// to emulate HTTP requests. Each variant corresponds to a specific operating system.
288    ///
289    /// # Naming Convention
290    ///
291    /// The naming convention for the variants follows the pattern `os_name`, where
292    /// `os_name` is the name of the operating system (e.g., `windows`, `macos`, `linux`, `android`, `ios`).
293    ///
294    /// The serialized names of the variants use lowercase letters to represent the operating system names.
295    /// For example, `Windows` is serialized as `"windows"`.
296    plain,
297    EmulationOS, MacOS,
298    Windows => "windows",
299    MacOS => "macos",
300    Linux => "linux",
301    Android => "android",
302    IOS => "ios"
303);
304
305define_enum!(
306    /// Represents the platform for emulation.
307    ///
308    /// Maps to [`EmulationOS`] via [`From<Platform> for EmulationOS`].
309    /// Default is `Windows`.
310    plain,
311    Platform, Windows,
312    Windows => "windows",
313    MacOS => "macos",
314    Linux => "linux",
315    Android => "android",
316    IOS => "ios"
317);
318
319impl Platform {
320    /// Returns `true` for mobile platforms (`Android` and `IOS`).
321    #[inline]
322    pub const fn is_mobile(&self) -> bool {
323        matches!(self, Platform::Android | Platform::IOS)
324    }
325
326    /// Returns the `sec-ch-ua-platform` header value for this platform.
327    #[inline]
328    pub const fn sec_ch_ua_platform(self) -> &'static str {
329        match self {
330            Self::Windows => "\"Windows\"",
331            Self::MacOS => "\"macOS\"",
332            Self::Linux => "\"Linux\"",
333            Self::Android => "\"Android\"",
334            Self::IOS => "\"iOS\"",
335        }
336    }
337}
338
339impl From<Platform> for EmulationOS {
340    #[inline]
341    fn from(platform: Platform) -> Self {
342        match platform {
343            Platform::Windows => Self::Windows,
344            Platform::MacOS => Self::MacOS,
345            Platform::Linux => Self::Linux,
346            Platform::Android => Self::Android,
347            Platform::IOS => Self::IOS,
348        }
349    }
350}
351
352/// ======== EmulationOS impls ========
353impl EmulationOS {
354    #[inline]
355    const fn platform(&self) -> &'static str {
356        match self {
357            EmulationOS::MacOS => "\"macOS\"",
358            EmulationOS::Linux => "\"Linux\"",
359            EmulationOS::Windows => "\"Windows\"",
360            EmulationOS::Android => "\"Android\"",
361            EmulationOS::IOS => "\"iOS\"",
362        }
363    }
364
365    #[inline]
366    const fn is_mobile(&self) -> bool {
367        matches!(self, EmulationOS::Android | EmulationOS::IOS)
368    }
369}
370
371/// Represents the configuration options for emulating a browser and operating system.
372///
373/// The `EmulationOption` struct allows you to configure various aspects of browser and OS
374/// emulation, including the browser version, operating system, and whether to skip certain features
375/// like HTTP/2 or headers.
376///
377/// This struct is typically used to build an `EmulationProvider` that can be applied to HTTP
378/// clients for making requests that mimic specific browser and OS configurations.
379///
380/// # Fields
381///
382/// - `emulation`: The browser version to emulate. Defaults to `Emulation::default()`.
383/// - `emulation_os`: The operating system to emulate. Defaults to `EmulationOS::default()`.
384/// - `skip_http2`: Whether to skip HTTP/2 support. Defaults to `false`.
385/// - `skip_headers`: Whether to skip adding default headers. Defaults to `false`.
386#[derive(Default, Clone, Debug, TypedBuilder)]
387pub struct EmulationOption {
388    /// The browser version to emulate.
389    #[builder(default)]
390    emulation: Emulation,
391
392    /// The operating system.
393    #[builder(default)]
394    emulation_os: EmulationOS,
395
396    /// The platform.
397    #[builder(default)]
398    platform: Platform,
399
400    /// Whether to skip HTTP/2.
401    #[builder(default = false)]
402    skip_http2: bool,
403
404    /// Whether to skip headers.
405    #[builder(default = false)]
406    skip_headers: bool,
407}
408
409/// ======== EmulationOption impls ========
410impl hpx::EmulationFactory for EmulationOption {
411    #[inline]
412    fn emulation(mut self) -> hpx::Emulation {
413        // ponytail: platform takes precedence over emulation_os when platform
414        // is not the default (Windows). This avoids the ambiguity of MacOS being
415        // both the default and a valid explicit choice.
416        if self.platform != Platform::Windows {
417            self.emulation_os = EmulationOS::from(self.platform);
418        }
419        self.emulation.into_emulation(self)
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use hpx::EmulationFactory;
426
427    use super::*;
428
429    #[test]
430    fn platform_is_mobile() {
431        assert!(!Platform::Windows.is_mobile());
432        assert!(!Platform::MacOS.is_mobile());
433        assert!(!Platform::Linux.is_mobile());
434        assert!(Platform::Android.is_mobile());
435        assert!(Platform::IOS.is_mobile());
436    }
437
438    #[test]
439    fn platform_to_emulation_os() {
440        assert!(matches!(
441            EmulationOS::from(Platform::Windows),
442            EmulationOS::Windows
443        ));
444        assert!(matches!(
445            EmulationOS::from(Platform::MacOS),
446            EmulationOS::MacOS
447        ));
448        assert!(matches!(
449            EmulationOS::from(Platform::Linux),
450            EmulationOS::Linux
451        ));
452        assert!(matches!(
453            EmulationOS::from(Platform::Android),
454            EmulationOS::Android
455        ));
456        assert!(matches!(EmulationOS::from(Platform::IOS), EmulationOS::IOS));
457    }
458
459    #[test]
460    fn emulation_option_builder_with_platform() {
461        let option = EmulationOption::builder().platform(Platform::Linux).build();
462        assert!(matches!(option.platform, Platform::Linux));
463    }
464
465    #[test]
466    fn platform_affects_emulation_output() {
467        let mut emu = EmulationOption::builder()
468            .emulation(Emulation::Chrome147)
469            .platform(Platform::Linux)
470            .build()
471            .emulation();
472        let ua = emu
473            .headers_mut()
474            .get(http::header::USER_AGENT)
475            .unwrap()
476            .to_str()
477            .unwrap();
478        assert!(
479            ua.contains("Linux"),
480            "expected Linux in User-Agent, got: {ua}"
481        );
482        assert!(
483            !ua.contains("Macintosh"),
484            "did not expect Macintosh in User-Agent, got: {ua}"
485        );
486    }
487
488    #[test]
489    #[cfg(feature = "emulation-rand")]
490    fn variant_count_at_least_120() {
491        use strum::VariantArray;
492        assert!(
493            Emulation::VARIANTS.len() >= 120,
494            "Expected at least 120 Emulation variants, found {}",
495            Emulation::VARIANTS.len()
496        );
497    }
498
499    #[test]
500    fn platform_sec_ch_ua_platform() {
501        assert_eq!(Platform::Linux.sec_ch_ua_platform(), "\"Linux\"");
502        assert_eq!(Platform::Windows.sec_ch_ua_platform(), "\"Windows\"");
503        assert_eq!(Platform::MacOS.sec_ch_ua_platform(), "\"macOS\"");
504        assert_eq!(Platform::Android.sec_ch_ua_platform(), "\"Android\"");
505        assert_eq!(Platform::IOS.sec_ch_ua_platform(), "\"iOS\"");
506    }
507
508    #[test]
509    fn explicit_emulation_os_preserved_when_platform_default() {
510        // When platform is default (Windows) but emulation_os is explicitly set,
511        // emulation_os should be preserved.
512        let mut em = EmulationOption::builder()
513            .emulation(Emulation::Chrome147)
514            .emulation_os(EmulationOS::Linux)
515            .build()
516            .emulation();
517        let ua = em
518            .headers_mut()
519            .get(http::header::USER_AGENT)
520            .unwrap()
521            .to_str()
522            .unwrap();
523        assert!(
524            ua.contains("Linux"),
525            "expected Linux in User-Agent when emulation_os=Linux, got: {ua}"
526        );
527    }
528}