Skip to main content

leyline/core/session/
identity.rs

1use crate::core::error::{Error, Kind, Result};
2use crate::profile::{Browser, ChromiumBrand, Family, Platform};
3
4use super::builder::derive::{
5    DerivedIdentity, IdentityInputs, IdentitySource, derive_identity, resolve_presented,
6};
7use super::{Session, SessionInner};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct Identity {
11    http: Browser,
12    tls: Browser,
13    platform: Platform,
14}
15
16impl Identity {
17    #[must_use]
18    pub fn locked(browser: Browser, platform: Platform) -> Self {
19        Self {
20            http: browser,
21            tls: browser.hello_rep(),
22            platform,
23        }
24    }
25
26    pub fn rotate_tls(self, tls: Browser) -> Result<Self> {
27        if self.http.family() != tls.family() {
28            return Err(Error::new(Kind::Config).with_message(format!(
29                "tls rotate {tls} is not the same family as {}",
30                self.http
31            )));
32        }
33        Ok(Self {
34            tls: tls.hello_rep(),
35            ..self
36        })
37    }
38
39    pub fn rotate_hello(self) -> Result<Self> {
40        let mut hellos: Vec<Browser> = Vec::new();
41        for candidate in Browser::all() {
42            if candidate.family() != self.http.family() {
43                continue;
44            }
45            let rep = candidate.hello_rep();
46            if !hellos.contains(&rep) {
47                hellos.push(rep);
48            }
49        }
50        if hellos.len() < 2 {
51            return Err(Error::new(Kind::Config)
52                .with_message(format!("no other hello in family {}", self.http.family())));
53        }
54        let current = self.tls.hello_rep();
55        let index = hellos.iter().position(|&h| h == current).unwrap_or(0);
56        self.rotate_tls(hellos[(index + 1) % hellos.len()])
57    }
58
59    pub fn switch_family(self, dest: Browser) -> Result<Self> {
60        if dest.family() == self.http.family() {
61            return Err(Error::new(Kind::Config).with_message(format!(
62                "switch_family {dest} is the same family as {} — rotate_tls instead",
63                self.http
64            )));
65        }
66        let id = Self::locked(dest, self.platform);
67        drop(id.user_agent()?);
68        Ok(id)
69    }
70
71    #[must_use]
72    pub fn http(self) -> Browser {
73        self.http
74    }
75
76    #[must_use]
77    pub fn tls(self) -> Browser {
78        self.tls
79    }
80
81    #[must_use]
82    pub fn platform(self) -> Platform {
83        self.platform
84    }
85
86    pub(crate) fn user_agent(self) -> Result<String> {
87        resolve_presented(
88            self.http.platform_profile(self.platform),
89            self.platform,
90            ChromiumBrand::Chrome,
91        )
92        .map(|identity| identity.user_agent)
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[non_exhaustive]
98pub struct SessionIdentity {
99    identity: Option<Identity>,
100    platform: Platform,
101    brand: Option<ChromiumBrand>,
102    user_agent: String,
103}
104
105impl SessionIdentity {
106    #[must_use]
107    pub fn identity(&self) -> Option<Identity> {
108        self.identity
109    }
110
111    #[must_use]
112    pub fn browser(&self) -> Option<Browser> {
113        self.identity.map(Identity::http)
114    }
115
116    #[must_use]
117    pub fn platform(&self) -> Platform {
118        self.platform
119    }
120
121    #[must_use]
122    pub fn brand(&self) -> Option<ChromiumBrand> {
123        self.brand
124    }
125
126    #[must_use]
127    pub fn user_agent(&self) -> &str {
128        &self.user_agent
129    }
130}
131
132impl Session {
133    #[must_use]
134    pub fn identity(&self) -> SessionIdentity {
135        let brand = match self.inner.brand {
136            ChromiumBrand::Chrome => self
137                .inner
138                .browser
139                .filter(|browser| browser.family() == Family::Chrome)
140                .map(|_| ChromiumBrand::Chrome),
141            other => Some(other),
142        };
143        SessionIdentity {
144            identity: self.inner.identity,
145            platform: self.inner.platform,
146            brand,
147            user_agent: self.inner.user_agent.clone(),
148        }
149    }
150
151    pub fn with_identity(&self, identity: Identity) -> Result<Self> {
152        if self.inner.browser.is_none() {
153            return Err(Error::new(Kind::Config)
154                .with_message("with_identity requires an impersonating session"));
155        }
156        let mut session = self.clone();
157        let inner = std::sync::Arc::make_mut(&mut session.inner);
158        let derived = derive_identity(IdentityInputs {
159            source: IdentitySource::Browser {
160                tls: identity.tls(),
161                http: identity.http(),
162            },
163            platform: identity.platform(),
164            brand: inner.brand,
165            compression: inner.compression,
166            #[cfg(feature = "http3")]
167            h3_required: inner.protocol_policy.requires_h3(),
168            tcp: inner.connector.tcp_profile(),
169            audit: inner.audit_tls.is_some(),
170            default_headers: &inner.default_headers,
171        })?;
172        inner.connector = inner
173            .connector
174            .with_profile(&derived.profile)
175            .map_err(Error::from)?;
176        inner.pool = std::sync::Arc::new(inner.pool.fresh());
177        inner.adopt(derived);
178        Ok(session)
179    }
180}
181
182impl SessionInner {
183    fn adopt(&mut self, derived: DerivedIdentity) {
184        let DerivedIdentity {
185            browser,
186            identity,
187            platform,
188            profile,
189            header_style,
190            header_order,
191            user_agent,
192            sec_ch_ua,
193            accept_language,
194            h2_config,
195            #[cfg(feature = "http3")]
196            h3_config,
197            audit_tls,
198        } = derived;
199        self.browser = browser;
200        self.identity = identity;
201        self.platform = platform;
202        self.profile = profile;
203        self.header_style = header_style;
204        self.header_order = header_order;
205        self.user_agent = user_agent;
206        self.sec_ch_ua = sec_ch_ua;
207        self.accept_language = accept_language;
208        self.h2_config = h2_config;
209        #[cfg(feature = "http3")]
210        {
211            self.h3_config = h3_config;
212        }
213        self.audit_tls = audit_tls;
214    }
215}