Skip to main content

leyline/core/session/
builder.rs

1use std::sync::Arc;
2
3use crate::cookie::Jar;
4use crate::core::config::HostLimits;
5use crate::core::proxy_pool::ProxyPool;
6use crate::core::{
7    CompressionConfig, DnsConfig, IntoParamPair, IntoUrl, PoolConfig, ProxyConfig, RedirectPolicy,
8    SocketConfig, TimeoutConfig, WebSocketConfig,
9};
10use crate::profile::{Browser, BrowserProfile, ChromiumBrand, Platform};
11use crate::tcp::TcpProfile;
12use crate::tls::TlsTrustConfig;
13use crate::trace::Trace;
14
15pub(crate) use self::bearer::BearerToken;
16use self::derive::{IdentityInputs, IdentitySource, derive_identity};
17use super::proxy::InvalidEnvProxy;
18use super::{Identity, ProtocolPolicy, Session};
19use crate::core::error::Result;
20
21const AUTHORIZATION: &str = "authorization";
22const USER_AGENT: &str = "user-agent";
23
24mod assemble;
25mod bearer;
26pub(super) mod connect;
27mod debug;
28pub(super) mod derive;
29mod validate;
30
31#[must_use = "builders are lazy: nothing happens until `.send()` / `.build()`"]
32pub struct SessionBuilder {
33    browser: Option<Browser>,
34    profile: Option<Arc<BrowserProfile>>,
35    platform: Platform,
36    platform_explicit: bool,
37    brand: ChromiumBrand,
38    proxy_config: ProxyConfig,
39    dns_config: DnsConfig,
40    timeouts: TimeoutConfig,
41    pool_config: PoolConfig,
42    socket_config: SocketConfig,
43    redirect_policy: RedirectPolicy,
44    compression: CompressionConfig,
45    expected_profile_id: Option<String>,
46    websocket_config: WebSocketConfig,
47    https_only: bool,
48    audit: bool,
49    cookie_jar: Option<Jar>,
50    tcp_profile: Option<TcpProfile>,
51    protocol_policy: ProtocolPolicy,
52    #[cfg(feature = "http3")]
53    protocol_explicit: bool,
54    config_error: Option<String>,
55    default_headers: Vec<(String, String)>,
56    bearer: Option<BearerToken>,
57    http_identity: Option<Browser>,
58    tls_trust: TlsTrustConfig,
59    default_retry: crate::core::retry::RetryPolicy,
60    trace: Option<Arc<dyn Trace>>,
61    base_url: Option<url::Url>,
62    languages: Option<Vec<String>>,
63    host_limits: HostLimits,
64    proxy_pool: Option<ProxyPool>,
65}
66
67impl SessionBuilder {
68    pub(super) fn new() -> Self {
69        Self {
70            browser: None,
71            profile: None,
72            platform: Platform::default(),
73            platform_explicit: false,
74            brand: ChromiumBrand::default(),
75            proxy_config: ProxyConfig::default(),
76            dns_config: DnsConfig::default(),
77            timeouts: TimeoutConfig::default(),
78            pool_config: PoolConfig::default(),
79            socket_config: SocketConfig::default(),
80            redirect_policy: RedirectPolicy::default(),
81            compression: CompressionConfig::default(),
82            expected_profile_id: None,
83            websocket_config: WebSocketConfig::default(),
84            https_only: false,
85            audit: false,
86            cookie_jar: None,
87            tcp_profile: None,
88            protocol_policy: ProtocolPolicy::Auto,
89            #[cfg(feature = "http3")]
90            protocol_explicit: false,
91            config_error: None,
92            default_headers: Vec::new(),
93            bearer: None,
94            http_identity: None,
95            tls_trust: TlsTrustConfig::default(),
96            default_retry: crate::core::retry::RetryPolicy::none(),
97            trace: None,
98            base_url: None,
99            languages: None,
100            host_limits: HostLimits::default(),
101            proxy_pool: None,
102        }
103    }
104
105    pub fn proxy(mut self, config: impl Into<ProxyConfig>) -> Self {
106        self.proxy_config = config.into();
107        self
108    }
109
110    pub fn timeout(mut self, config: impl Into<TimeoutConfig>) -> Self {
111        self.timeouts = config.into();
112        self
113    }
114
115    pub fn retry(mut self, policy: crate::core::retry::RetryPolicy) -> Self {
116        self.default_retry = policy;
117        self
118    }
119
120    pub fn pool(mut self, config: PoolConfig) -> Self {
121        self.pool_config = config;
122        self
123    }
124
125    pub fn browser(mut self, browser: Browser) -> Self {
126        self.profile = None;
127        self.http_identity = None;
128        self.browser = Some(browser);
129        self
130    }
131
132    pub fn profile(mut self, profile: BrowserProfile) -> Self {
133        self.browser = None;
134        self.http_identity = None;
135        self.profile = Some(Arc::new(profile));
136        self
137    }
138
139    fn impersonates(&self) -> bool {
140        self.browser.is_some() || self.profile.is_some()
141    }
142
143    fn identity_source(&self) -> IdentitySource {
144        match (&self.profile, self.browser) {
145            (Some(profile), _) => IdentitySource::Profile(Arc::clone(profile)),
146            (None, Some(tls)) => IdentitySource::Browser {
147                tls,
148                http: self.http_identity.unwrap_or(tls),
149            },
150            (None, None) => IdentitySource::Bare,
151        }
152    }
153
154    fn identity_inputs<'a>(&'a self, tcp: &'a TcpProfile) -> IdentityInputs<'a> {
155        IdentityInputs {
156            source: self.identity_source(),
157            platform: self.platform,
158            brand: self.brand,
159            compression: self.compression,
160            #[cfg(feature = "http3")]
161            h3_required: self.protocol_policy.requires_h3(),
162            tcp,
163            audit: self.audit,
164            default_headers: &self.default_headers,
165            languages: self.languages.as_deref(),
166        }
167    }
168
169    pub fn brand(mut self, brand: ChromiumBrand) -> Self {
170        self.brand = brand;
171        self
172    }
173
174    pub fn platform(mut self, platform: Platform) -> Self {
175        self.platform = platform;
176        self.platform_explicit = true;
177        self
178    }
179
180    pub fn redirect(mut self, policy: RedirectPolicy) -> Self {
181        self.redirect_policy = policy;
182        self
183    }
184
185    pub fn cookie_jar(mut self, jar: Jar) -> Self {
186        self.cookie_jar = Some(jar);
187        self
188    }
189
190    pub fn tcp_profile(mut self, profile: TcpProfile) -> Self {
191        self.tcp_profile = Some(profile);
192        self
193    }
194
195    pub fn dns(mut self, config: impl Into<DnsConfig>) -> Self {
196        self.dns_config = config.into();
197        self
198    }
199
200    pub fn tls_trust(mut self, trust: TlsTrustConfig) -> Self {
201        self.tls_trust = trust;
202        self
203    }
204
205    pub fn socket(mut self, config: SocketConfig) -> Self {
206        self.socket_config = config;
207        self
208    }
209
210    pub fn compression(mut self, config: CompressionConfig) -> Self {
211        self.compression = config;
212        self
213    }
214
215    pub fn expect_profile_id(mut self, id: &str) -> Self {
216        self.expected_profile_id = Some(id.to_owned());
217        self
218    }
219
220    pub fn websocket_config(mut self, config: WebSocketConfig) -> Self {
221        self.websocket_config = config;
222        self
223    }
224
225    pub fn https_only(mut self, enabled: bool) -> Self {
226        self.https_only = enabled;
227        self
228    }
229
230    pub fn trace(mut self, hook: impl Trace) -> Self {
231        self.trace = Some(Arc::new(hook));
232        self
233    }
234
235    pub fn audit(mut self, enabled: bool) -> Self {
236        self.audit = enabled;
237        self
238    }
239
240    pub fn protocol(mut self, policy: ProtocolPolicy) -> Self {
241        self.protocol_policy = policy;
242        #[cfg(feature = "http3")]
243        {
244            self.protocol_explicit = true;
245        }
246        self
247    }
248
249    pub fn identity(mut self, id: Identity) -> Self {
250        self = self.browser(id.tls()).platform(id.platform());
251        self.http_identity = (id.http() != id.tls()).then(|| id.http());
252        if let Some(brand) = id.brand() {
253            self.brand = brand;
254        }
255        self
256    }
257
258    pub fn user_agent(mut self, value: &str) -> Self {
259        self.check_header(USER_AGENT, value);
260        self.remove_default(USER_AGENT);
261        self.default_headers
262            .push((USER_AGENT.to_owned(), value.to_owned()));
263        self
264    }
265
266    pub fn languages<I, S>(mut self, langs: I) -> Self
267    where
268        I: IntoIterator<Item = S>,
269        S: AsRef<str>,
270    {
271        let tags: Vec<String> = langs
272            .into_iter()
273            .map(|tag| tag.as_ref().trim().to_owned())
274            .collect();
275        let checked = if tags.is_empty() {
276            Err("languages needs at least one language tag".to_owned())
277        } else {
278            tags.iter()
279                .try_for_each(|tag| crate::profile::languages::validate_language(tag))
280        };
281        if let Err(error) = checked {
282            self.config_error.get_or_insert(error);
283        }
284        self.languages = Some(tags);
285        self
286    }
287
288    pub fn host_limits(mut self, limits: HostLimits) -> Self {
289        if let Some(error) = limits.config_error() {
290            self.config_error.get_or_insert(error);
291        }
292        self.host_limits = limits;
293        self
294    }
295
296    pub fn proxy_pool(mut self, pool: ProxyPool) -> Self {
297        self.proxy_pool = Some(pool);
298        self
299    }
300
301    pub fn base_url(mut self, url: impl IntoUrl) -> Self {
302        match super::helpers::base_url(url) {
303            Ok(url) => self.base_url = Some(url),
304            Err(error) => {
305                self.base_url = None;
306                self.config_error.get_or_insert(error.to_string());
307            }
308        }
309        self
310    }
311
312    pub fn headers<I, P>(mut self, headers: I) -> Self
313    where
314        I: IntoIterator<Item = P>,
315        P: IntoParamPair,
316    {
317        for pair in headers {
318            let (name, value) = pair.into_param_pair();
319            self.check_header(&name, &value);
320            self.push_default(name, value);
321        }
322        self
323    }
324
325    fn check_header(&mut self, name: &str, value: &str) {
326        if self.config_error.is_none()
327            && (crate::core::headers::name(name).is_err()
328                || crate::core::headers::value(value).is_err())
329        {
330            self.config_error = Some(format!("invalid header `{}`", name.escape_debug()));
331        }
332    }
333
334    pub(super) fn into_builtin(self) -> Session {
335        self.build_with(InvalidEnvProxy::FailRequests)
336            .expect("bundled profile data is statically valid; default trust loading only warns")
337    }
338
339    pub fn build(self) -> Result<Session> {
340        self.build_with(InvalidEnvProxy::FailBuild)
341    }
342
343    fn build_with(mut self, on_invalid: InvalidEnvProxy) -> Result<Session> {
344        self.validate(on_invalid)?;
345        let tcp_profile = self.resolve_tcp_profile();
346        let derived = derive_identity(self.identity_inputs(&tcp_profile))?;
347        let connector = self.build_connector(&derived.profile, &tcp_profile)?;
348        let expected = self.expected_profile_id.take();
349        let session = self.into_session(derived, connector);
350        Self::check_profile_id(expected.as_deref(), session.identity().profile_id())?;
351        Ok(session)
352    }
353}