1use crate::core::error::{Error, Kind, Result};
2use crate::profile::browser::digest;
3use crate::profile::{Browser, ChromiumBrand, Family, Platform};
4
5use super::builder::derive::{DerivedIdentity, IdentityInputs, IdentitySource, derive_identity};
6use super::{Session, SessionInner};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
9#[serde(try_from = "SavedIdentity", into = "SavedIdentity")]
10pub struct Identity {
11 http: Browser,
12 tls: Browser,
13 platform: Platform,
14 brand: Option<ChromiumBrand>,
15}
16
17#[derive(serde::Serialize, serde::Deserialize)]
18struct SavedIdentity {
19 http: Browser,
20 tls: Browser,
21 platform: Platform,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 brand: Option<ChromiumBrand>,
24}
25
26impl From<Identity> for SavedIdentity {
27 fn from(id: Identity) -> Self {
28 Self {
29 http: id.http,
30 tls: id.tls,
31 platform: id.platform,
32 brand: id.brand,
33 }
34 }
35}
36
37impl TryFrom<SavedIdentity> for Identity {
38 type Error = Error;
39
40 fn try_from(saved: SavedIdentity) -> Result<Self> {
41 if saved.http.family() != saved.tls.family() {
42 return Err(Error::new(Kind::Config).with_message(format!(
43 "saved identity tls {} is not the same family as {}",
44 saved.tls, saved.http
45 )));
46 }
47 Ok(Self {
48 http: saved.http,
49 tls: saved.tls,
50 platform: saved.platform,
51 brand: saved.brand,
52 })
53 }
54}
55
56impl Identity {
57 #[must_use]
58 pub fn locked(browser: Browser, platform: Platform) -> Self {
59 Self {
60 http: browser,
61 tls: browser.hello_rep(),
62 platform,
63 brand: None,
64 }
65 }
66
67 #[must_use]
68 pub fn with_brand(self, brand: ChromiumBrand) -> Self {
69 Self {
70 brand: Some(brand),
71 ..self
72 }
73 }
74
75 #[must_use]
76 pub fn brand(self) -> Option<ChromiumBrand> {
77 self.brand
78 }
79
80 pub fn rotate_tls(self, tls: Browser) -> Result<Self> {
81 if self.http.family() != tls.family() {
82 return Err(Error::new(Kind::Config).with_message(format!(
83 "tls rotate {tls} is not the same family as {}",
84 self.http
85 )));
86 }
87 Ok(Self {
88 tls: tls.hello_rep(),
89 ..self
90 })
91 }
92
93 pub fn rotate_hello(self) -> Result<Self> {
94 let mut hellos: Vec<Browser> = Vec::new();
95 for candidate in Browser::all() {
96 if candidate.family() != self.http.family() {
97 continue;
98 }
99 let rep = candidate.hello_rep();
100 if !hellos.contains(&rep) {
101 hellos.push(rep);
102 }
103 }
104 if hellos.len() < 2 {
105 return Err(Error::new(Kind::Config)
106 .with_message(format!("no other hello in family {}", self.http.family())));
107 }
108 let current = self.tls.hello_rep();
109 let index = hellos.iter().position(|&h| h == current).unwrap_or(0);
110 self.rotate_tls(hellos[(index + 1) % hellos.len()])
111 }
112
113 pub fn switch_family(self, dest: Browser) -> Result<Self> {
114 if dest.family() == self.http.family() {
115 return Err(Error::new(Kind::Config).with_message(format!(
116 "switch_family {dest} is the same family as {} — rotate_tls instead",
117 self.http
118 )));
119 }
120 let id = Self::locked(dest, self.platform);
121 drop(id.user_agent()?);
122 Ok(id)
123 }
124
125 #[must_use]
126 pub fn http(self) -> Browser {
127 self.http
128 }
129
130 #[must_use]
131 pub fn tls(self) -> Browser {
132 self.tls
133 }
134
135 #[must_use]
136 pub fn platform(self) -> Platform {
137 self.platform
138 }
139
140 pub(crate) fn user_agent(self) -> Result<String> {
141 self.http
142 .platform_profile(self.platform)
143 .resolve_presented(self.platform, ChromiumBrand::Chrome)
144 .map(|identity| identity.user_agent)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149#[non_exhaustive]
150pub struct SessionIdentity {
151 identity: Option<Identity>,
152 platform: Platform,
153 brand: Option<ChromiumBrand>,
154 user_agent: String,
155 saved: Option<Identity>,
156 profile_id: Option<String>,
157 profile_toml: Option<std::sync::Arc<str>>,
158 languages: Option<Vec<String>>,
159}
160
161impl SessionIdentity {
162 #[must_use]
163 pub fn to_identity(&self) -> Option<Identity> {
164 self.saved
165 }
166
167 #[must_use]
168 pub fn profile_id(&self) -> Option<&str> {
169 self.profile_id.as_deref()
170 }
171
172 #[must_use]
173 pub fn identity(&self) -> Option<Identity> {
174 self.identity
175 }
176
177 #[must_use]
178 pub fn export_profile(&self) -> Option<String> {
179 self.profile_toml.as_deref().map(str::to_owned)
180 }
181
182 #[must_use]
183 pub fn languages(&self) -> Option<&[String]> {
184 self.languages.as_deref()
185 }
186
187 #[must_use]
188 pub fn browser(&self) -> Option<Browser> {
189 self.identity.map(Identity::http)
190 }
191
192 #[must_use]
193 pub fn platform(&self) -> Platform {
194 self.platform
195 }
196
197 #[must_use]
198 pub fn brand(&self) -> Option<ChromiumBrand> {
199 self.brand
200 }
201
202 #[must_use]
203 pub fn user_agent(&self) -> &str {
204 &self.user_agent
205 }
206}
207
208impl Session {
209 #[must_use]
210 pub fn identity(&self) -> SessionIdentity {
211 let brand = match self.inner.brand {
212 ChromiumBrand::Chrome => self
213 .inner
214 .browser
215 .filter(|browser| browser.family() == Family::Chrome)
216 .map(|_| ChromiumBrand::Chrome),
217 other => Some(other),
218 };
219 let saved = self.saved_identity(brand);
220 let profile_toml = self
221 .inner
222 .impersonates
223 .then(|| self.inner.profile.source())
224 .flatten()
225 .map(std::sync::Arc::from);
226 let profile_id = match (saved, profile_toml.as_deref()) {
227 (Some(id), _) => Some(profile_id(id, self.inner.brand)),
228 (None, Some(source)) => Some(source_profile_id(
229 source,
230 self.inner.platform,
231 self.inner.brand,
232 )),
233 (None, None) => None,
234 };
235 SessionIdentity {
236 identity: self.inner.identity,
237 platform: self.inner.platform,
238 brand,
239 user_agent: self.inner.user_agent.clone(),
240 saved,
241 profile_id,
242 profile_toml,
243 languages: self.inner.languages.clone(),
244 }
245 }
246
247 fn saved_identity(&self, brand: Option<ChromiumBrand>) -> Option<Identity> {
248 let identity = self.inner.identity?;
249 Some(Identity {
250 tls: self.inner.browser?,
251 brand,
252 ..identity
253 })
254 }
255
256 pub fn with_identity(&self, identity: Identity) -> Result<Self> {
257 if self.inner.browser.is_none() {
258 return Err(Error::new(Kind::Config)
259 .with_message("with_identity requires an impersonating session"));
260 }
261 let mut session = self.clone();
262 let inner = std::sync::Arc::make_mut(&mut session.inner);
263 let brand = identity.brand().unwrap_or(inner.brand);
264 let derived = derive_identity(IdentityInputs {
265 source: IdentitySource::Browser {
266 tls: identity.tls(),
267 http: identity.http(),
268 },
269 platform: identity.platform(),
270 brand,
271 compression: inner.compression,
272 #[cfg(feature = "http3")]
273 h3_required: inner.protocol_policy.requires_h3(),
274 tcp: inner.connector.tcp_profile(),
275 audit: inner.audit_tls.is_some(),
276 default_headers: &inner.default_headers,
277 languages: inner.languages.as_deref(),
278 })?;
279 inner.connector = inner
280 .connector
281 .with_profile(&derived.profile)
282 .map_err(Error::from)?;
283 inner.pool = std::sync::Arc::new(inner.pool.shared_view(profile_hash(identity, brand)));
284 inner.adopt(derived);
285 inner.brand = brand;
286 Ok(session)
287 }
288}
289
290fn profile_id(identity: Identity, brand: ChromiumBrand) -> String {
291 let hash = profile_hash(identity, brand);
292 format!("{hash:016x}")
293}
294
295fn profile_hash(identity: Identity, brand: ChromiumBrand) -> u64 {
296 let http = identity.http.profile_digest().to_le_bytes();
297 let tls = identity.tls.profile_digest().to_le_bytes();
298 digest(&[
299 &http,
300 &tls,
301 identity.platform.id().as_bytes(),
302 brand.id().as_bytes(),
303 ])
304}
305
306fn source_profile_id(source: &str, platform: Platform, brand: ChromiumBrand) -> String {
307 match Browser::matching_source(source) {
308 Some(browser) => profile_id(
309 Identity {
310 http: browser,
311 tls: browser,
312 platform,
313 brand: None,
314 },
315 brand,
316 ),
317 None => format!(
318 "{:016x}",
319 digest(&[
320 source.as_bytes(),
321 platform.id().as_bytes(),
322 brand.id().as_bytes()
323 ])
324 ),
325 }
326}
327
328impl SessionInner {
329 fn adopt(&mut self, derived: DerivedIdentity) {
330 let DerivedIdentity {
331 browser,
332 identity,
333 platform,
334 profile,
335 header_style,
336 header_order,
337 user_agent,
338 sec_ch_ua,
339 accept_language,
340 h2_config,
341 #[cfg(feature = "http3")]
342 h3_config,
343 audit_tls,
344 } = derived;
345 self.browser = browser;
346 self.identity = identity;
347 self.platform = platform;
348 self.profile = profile;
349 self.header_style = header_style;
350 self.header_order = header_order;
351 self.user_agent = user_agent;
352 self.sec_ch_ua = sec_ch_ua;
353 self.accept_language = accept_language;
354 self.h2_config = h2_config;
355 #[cfg(feature = "http3")]
356 {
357 self.h3_config = h3_config;
358 }
359 self.audit_tls = audit_tls;
360 }
361}