cloudscraper_rs/modules/spoofing/
mod.rs

1//! Browser fingerprint spoofing utilities.
2//!
3//! Generates browser fingerprints with configurable consistency so solvers can
4//! present stable client identities when required.
5
6use chrono::{DateTime, Utc};
7use rand::{Rng, seq::SliceRandom};
8use std::collections::HashMap;
9
10use crate::challenges::solvers::FingerprintManager;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum BrowserType {
14    Chrome,
15    Firefox,
16    Safari,
17    Edge,
18    MobileChrome,
19    MobileSafari,
20}
21
22#[derive(Debug, Clone)]
23pub struct BrowserFingerprint {
24    pub user_agent: String,
25    pub accept_language: String,
26    pub platform: String,
27    pub screen_resolution: (u16, u16),
28    pub timezone: String,
29    pub webgl_vendor: String,
30    pub webgl_renderer: String,
31    pub canvas_fingerprint: String,
32    pub audio_fingerprint: String,
33    pub created_at: DateTime<Utc>,
34}
35
36#[derive(Debug, Clone, Copy)]
37pub enum ConsistencyLevel {
38    None,
39    Domain,
40    Global,
41}
42
43/// Generates realistic fingerprints for spoofing Canvas/WebGL APIs.
44#[derive(Debug)]
45pub struct FingerprintGenerator {
46    browser: BrowserType,
47    consistency: ConsistencyLevel,
48    cache: HashMap<String, BrowserFingerprint>,
49    global: Option<BrowserFingerprint>,
50}
51
52impl FingerprintGenerator {
53    pub fn new(browser: BrowserType) -> Self {
54        Self {
55            browser,
56            consistency: ConsistencyLevel::Domain,
57            cache: HashMap::new(),
58            global: None,
59        }
60    }
61
62    pub fn with_consistency(mut self, level: ConsistencyLevel) -> Self {
63        self.consistency = level;
64        self
65    }
66
67    pub fn set_browser(&mut self, browser: BrowserType) {
68        if self.browser != browser {
69            self.cache.clear();
70            self.global = None;
71            self.browser = browser;
72        }
73    }
74
75    pub fn generate_for(&mut self, domain: &str) -> BrowserFingerprint {
76        match self.consistency {
77            ConsistencyLevel::None => Self::random_fingerprint(self.browser),
78            ConsistencyLevel::Global => {
79                if self.global.is_none() {
80                    self.global = Some(Self::random_fingerprint(self.browser));
81                }
82                self.global.clone().unwrap()
83            }
84            ConsistencyLevel::Domain => {
85                let browser = self.browser;
86                self.cache
87                    .entry(domain.to_string())
88                    .or_insert_with(|| Self::random_fingerprint(browser))
89                    .clone()
90            }
91        }
92    }
93
94    pub fn invalidate(&mut self, domain: &str) {
95        self.cache.remove(domain);
96    }
97
98    fn random_fingerprint(browser: BrowserType) -> BrowserFingerprint {
99        let templates = templates_for_browser(browser);
100        let mut rng = rand::thread_rng();
101        let template = templates.choose(&mut rng).unwrap_or(&templates[0]);
102
103        let screen_resolution = template
104            .screen_resolutions
105            .choose(&mut rng)
106            .copied()
107            .unwrap_or((1920, 1080));
108
109        let timezone = template
110            .timezones
111            .choose(&mut rng)
112            .cloned()
113            .unwrap_or_else(|| "UTC".to_string());
114
115        let webgl_vendor = template
116            .webgl_vendors
117            .choose(&mut rng)
118            .cloned()
119            .unwrap_or_else(|| "Google Inc.".into());
120        let webgl_renderer = template
121            .webgl_renderers
122            .choose(&mut rng)
123            .cloned()
124            .unwrap_or_else(|| "ANGLE (NVIDIA GeForce GTX 1660)".into());
125
126        let canvas_seed: u64 = rng.r#gen();
127        let audio_seed: u64 = rng.r#gen();
128
129        BrowserFingerprint {
130            user_agent: template.user_agent.clone(),
131            accept_language: template
132                .accept_languages
133                .choose(&mut rng)
134                .cloned()
135                .unwrap_or_else(|| "en-US,en;q=0.9".into()),
136            platform: template.platform.clone(),
137            screen_resolution,
138            timezone,
139            webgl_vendor,
140            webgl_renderer,
141            canvas_fingerprint: format!("canvas-{canvas_seed:016x}"),
142            audio_fingerprint: format!("audio-{audio_seed:016x}"),
143            created_at: Utc::now(),
144        }
145    }
146}
147
148impl Default for FingerprintGenerator {
149    fn default() -> Self {
150        Self::new(BrowserType::Chrome)
151    }
152}
153
154impl FingerprintManager for FingerprintGenerator {
155    fn invalidate(&mut self, domain: &str) {
156        FingerprintGenerator::invalidate(self, domain);
157    }
158}
159
160#[derive(Clone)]
161struct FingerprintTemplate {
162    user_agent: String,
163    platform: String,
164    accept_languages: Vec<String>,
165    screen_resolutions: Vec<(u16, u16)>,
166    timezones: Vec<String>,
167    webgl_vendors: Vec<String>,
168    webgl_renderers: Vec<String>,
169}
170
171fn templates_for_browser(browser: BrowserType) -> Vec<FingerprintTemplate> {
172    match browser {
173        BrowserType::Chrome | BrowserType::Edge => vec![FingerprintTemplate {
174            user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36".into(),
175            platform: "Win32".into(),
176            accept_languages: vec!["en-US,en;q=0.9".into(), "en-GB,en;q=0.8".into()],
177            screen_resolutions: vec![(1920, 1080), (2560, 1440), (1366, 768)],
178            timezones: vec!["America/New_York".into(), "Europe/Berlin".into(), "Asia/Tokyo".into()],
179            webgl_vendors: vec!["Google Inc.".into(), "Microsoft".into()],
180            webgl_renderers: vec![
181                "ANGLE (NVIDIA GeForce RTX 3080)".into(),
182                "ANGLE (AMD Radeon RX 6800)".into(),
183            ],
184        }],
185        BrowserType::Firefox => vec![FingerprintTemplate {
186            user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0".into(),
187            platform: "Win64".into(),
188            accept_languages: vec!["en-US,en;q=0.8".into(), "fr-FR,fr;q=0.7".into()],
189            screen_resolutions: vec![(1920, 1080), (1680, 1050)],
190            timezones: vec!["America/Los_Angeles".into(), "Europe/London".into()],
191            webgl_vendors: vec!["Mozilla".into(), "Google Inc.".into()],
192            webgl_renderers: vec![
193                "ANGLE (NVIDIA GeForce GTX 1050 Ti)".into(),
194                "ANGLE (Intel(R) UHD Graphics 630)".into(),
195            ],
196        }],
197        BrowserType::Safari => vec![FingerprintTemplate {
198            user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15".into(),
199            platform: "MacIntel".into(),
200            accept_languages: vec!["en-US,en;q=0.9".into(), "en-AU,en;q=0.8".into()],
201            screen_resolutions: vec![(2560, 1600), (2880, 1800)],
202            timezones: vec!["America/Los_Angeles".into(), "Australia/Sydney".into()],
203            webgl_vendors: vec!["Apple".into()],
204            webgl_renderers: vec!["Apple GPU".into(), "Metal Renderer".into()],
205        }],
206        BrowserType::MobileChrome => vec![FingerprintTemplate {
207            user_agent: "Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36".into(),
208            platform: "Linux armv8l".into(),
209            accept_languages: vec!["en-US,en;q=0.8".into(), "es-ES,es;q=0.7".into()],
210            screen_resolutions: vec![(1080, 2400), (1170, 2532)],
211            timezones: vec!["America/New_York".into(), "Europe/Madrid".into()],
212            webgl_vendors: vec!["Qualcomm".into(), "ARM".into()],
213            webgl_renderers: vec!["Adreno (TM) 730".into(), "Mali-G710".into()],
214        }],
215        BrowserType::MobileSafari => vec![FingerprintTemplate {
216            user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1".into(),
217            platform: "iPhone".into(),
218            accept_languages: vec!["en-US,en;q=0.9".into(), "ja-JP,ja;q=0.8".into()],
219            screen_resolutions: vec![(1170, 2532), (1125, 2436)],
220            timezones: vec!["America/Chicago".into(), "Asia/Tokyo".into()],
221            webgl_vendors: vec!["Apple".into()],
222            webgl_renderers: vec!["Apple A16 GPU".into(), "Apple A15 GPU".into()],
223        }],
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn generates_consistent_domain_fingerprints() {
233        let mut generator = FingerprintGenerator::default();
234        let fp1 = generator.generate_for("example.com");
235        let fp2 = generator.generate_for("example.com");
236        let fp3 = generator.generate_for("example.org");
237        assert_eq!(fp1.user_agent, fp2.user_agent);
238        assert_ne!(fp1.canvas_fingerprint, fp3.canvas_fingerprint);
239    }
240}