1use 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#[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}