cloudscraper_rs/modules/tls/
mod.rs

1//! TLS fingerprint rotation utilities.
2//!
3//! Supplies browser TLS profiles plus per-domain rotation to vary JA3
4//! fingerprints and cipher suites.
5
6use rand::Rng;
7use rand::seq::SliceRandom;
8use std::collections::HashMap;
9
10use super::spoofing::BrowserType;
11
12use crate::challenges::solvers::TlsProfileManager;
13
14#[derive(Debug, Clone)]
15pub struct BrowserProfile {
16    pub browser: BrowserType,
17    pub ja3: String,
18    pub cipher_suites: Vec<String>,
19    pub alpn_protocols: Vec<String>,
20    pub tls_extensions: Vec<u16>,
21}
22
23#[derive(Debug, Clone)]
24pub struct TLSConfig {
25    pub rotate_ja3: bool,
26    pub rotate_ciphers: bool,
27    pub preferred_browser: BrowserType,
28    pub rotation_interval: usize,
29}
30
31impl Default for TLSConfig {
32    fn default() -> Self {
33        Self {
34            rotate_ja3: true,
35            rotate_ciphers: true,
36            preferred_browser: BrowserType::Chrome,
37            rotation_interval: 5,
38        }
39    }
40}
41
42#[derive(Debug)]
43struct DomainTLSState {
44    profile_index: usize,
45    requests_since_rotation: usize,
46}
47
48impl DomainTLSState {
49    fn new(index: usize) -> Self {
50        Self {
51            profile_index: index,
52            requests_since_rotation: 0,
53        }
54    }
55}
56
57/// Default TLS manager mirroring smart JA3 rotation.
58#[derive(Debug)]
59pub struct DefaultTLSManager {
60    config: TLSConfig,
61    profiles: Vec<BrowserProfile>,
62    per_domain: HashMap<String, DomainTLSState>,
63    rng: rand::rngs::ThreadRng,
64}
65
66impl DefaultTLSManager {
67    pub fn new(config: TLSConfig) -> Self {
68        let mut manager = Self {
69            profiles: build_default_profiles(),
70            rng: rand::thread_rng(),
71            per_domain: HashMap::new(),
72            config,
73        };
74        // Ensure preferred browser is first in rotation order for quicker access.
75        manager.promote_preferred_profile();
76        manager
77    }
78
79    fn promote_preferred_profile(&mut self) {
80        if let Some(pos) = self
81            .profiles
82            .iter()
83            .position(|p| p.browser == self.config.preferred_browser)
84        {
85            self.profiles.swap(0, pos);
86        }
87    }
88
89    fn domain_state_mut(&mut self, domain: &str) -> &mut DomainTLSState {
90        let idx = self.rng.gen_range(0..self.profiles.len());
91        self.per_domain
92            .entry(domain.to_string())
93            .or_insert_with(|| DomainTLSState::new(idx))
94    }
95
96    pub fn current_profile(&mut self, domain: &str) -> BrowserProfile {
97        let should_rotate = {
98            let state = self.domain_state_mut(domain);
99            state.requests_since_rotation += 1;
100            state.requests_since_rotation >= self.config.rotation_interval
101        };
102
103        if should_rotate {
104            self.rotate_profile(domain);
105        }
106
107        let index = self.domain_state_mut(domain).profile_index;
108        self.profiles[index].clone()
109    }
110
111    pub fn rotate_profile(&mut self, domain: &str) {
112        let profiles_len = self.profiles.len();
113        let current_index = {
114            let state = self.domain_state_mut(domain);
115            state.requests_since_rotation = 0;
116            state.profile_index
117        };
118
119        if profiles_len <= 1 {
120            return;
121        }
122
123        let mut candidates: Vec<usize> = (0..profiles_len).collect();
124        candidates.retain(|idx| *idx != current_index);
125        if let Some(next_index) = candidates.choose(&mut self.rng).copied() {
126            let state = self.domain_state_mut(domain);
127            state.profile_index = next_index;
128        }
129    }
130
131    pub fn add_custom_profile(&mut self, profile: BrowserProfile) {
132        self.profiles.push(profile);
133    }
134}
135
136impl Default for DefaultTLSManager {
137    fn default() -> Self {
138        Self::new(TLSConfig::default())
139    }
140}
141
142impl TlsProfileManager for DefaultTLSManager {
143    fn rotate_profile(&mut self, domain: &str) {
144        DefaultTLSManager::rotate_profile(self, domain);
145    }
146}
147
148fn build_default_profiles() -> Vec<BrowserProfile> {
149    vec![
150        BrowserProfile {
151            browser: BrowserType::Chrome,
152            ja3: "771,4866-4865-4867-49196-49195-52393,0-11-10-35-13-45-16-43,29-23-24,0".into(),
153            cipher_suites: vec![
154                "TLS_AES_128_GCM_SHA256".into(),
155                "TLS_AES_256_GCM_SHA384".into(),
156                "TLS_CHACHA20_POLY1305_SHA256".into(),
157            ],
158            alpn_protocols: vec!["h2".into(), "http/1.1".into()],
159            tls_extensions: vec![0, 11, 10, 35, 13, 45, 16, 43],
160        },
161        BrowserProfile {
162            browser: BrowserType::Firefox,
163            ja3: "771,4866-4865-4867-49196-49200,0-11-10-35-13-27,23-24,0".into(),
164            cipher_suites: vec![
165                "TLS_AES_128_GCM_SHA256".into(),
166                "TLS_AES_256_GCM_SHA384".into(),
167                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".into(),
168            ],
169            alpn_protocols: vec!["h2".into(), "http/1.1".into()],
170            tls_extensions: vec![0, 11, 10, 35, 13, 27],
171        },
172        BrowserProfile {
173            browser: BrowserType::Safari,
174            ja3: "771,4865-4866-4867-49195-49196,0-11-10-35-13-16,29-23-24,0".into(),
175            cipher_suites: vec![
176                "TLS_AES_128_GCM_SHA256".into(),
177                "TLS_CHACHA20_POLY1305_SHA256".into(),
178                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".into(),
179            ],
180            alpn_protocols: vec!["h2".into(), "http/1.1".into()],
181            tls_extensions: vec![0, 11, 10, 35, 13, 16],
182        },
183        BrowserProfile {
184            browser: BrowserType::MobileChrome,
185            ja3: "771,4866-4865-4867-49196,0-11-10-35-13-45,29-23-24,0".into(),
186            cipher_suites: vec![
187                "TLS_AES_128_GCM_SHA256".into(),
188                "TLS_CHACHA20_POLY1305_SHA256".into(),
189            ],
190            alpn_protocols: vec!["h2".into(), "http/1.1".into()],
191            tls_extensions: vec![0, 11, 10, 35, 13, 45],
192        },
193        BrowserProfile {
194            browser: BrowserType::MobileSafari,
195            ja3: "771,4865-4866-4867-49195,0-11-10-35-16,29-23-24,0".into(),
196            cipher_suites: vec![
197                "TLS_AES_128_GCM_SHA256".into(),
198                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".into(),
199            ],
200            alpn_protocols: vec!["h2".into(), "http/1.1".into()],
201            tls_extensions: vec![0, 11, 10, 35, 16],
202        },
203    ]
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn rotates_profiles() {
212        let mut manager = DefaultTLSManager::default();
213        let profile1 = manager.current_profile("example.com");
214        manager.rotate_profile("example.com");
215        let profile2 = manager.current_profile("example.com");
216        assert!(profile1.ja3 != profile2.ja3 || profile1.browser != profile2.browser);
217    }
218}