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