Skip to main content

leyline/core/session/
identity.rs

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}