Skip to main content

chromiumoxide/
browser.rs

1use hashbrown::HashMap;
2use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE};
3use std::future::Future;
4use std::time::Duration;
5use std::{
6    io,
7    path::{Path, PathBuf},
8};
9
10use tokio::sync::mpsc::{channel, unbounded_channel, Sender};
11use tokio::sync::oneshot::channel as oneshot_channel;
12
13use crate::async_process::{self, Child, ExitStatus, Stdio};
14use crate::cmd::{to_command_response, CommandMessage};
15use crate::conn::{ConnectHeaderError, ConnectHeaders, Connection};
16use crate::detection::{self, DetectionOptions};
17use crate::error::{BrowserStderr, CdpError, Result};
18use crate::handler::browser::BrowserContext;
19use crate::handler::viewport::Viewport;
20use crate::handler::{Handler, HandlerConfig, HandlerMessage, REQUEST_TIMEOUT};
21use crate::listeners::{EventListenerRequest, EventStream};
22use crate::page::Page;
23use crate::utils;
24use chromiumoxide_cdp::cdp::browser_protocol::browser::{
25    BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
26};
27use chromiumoxide_cdp::cdp::browser_protocol::browser::{
28    PermissionDescriptor, PermissionSetting, SetPermissionParams,
29};
30use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
31use chromiumoxide_cdp::cdp::browser_protocol::storage::{
32    ClearCookiesParams, GetCookiesParams, SetCookiesParams,
33};
34use chromiumoxide_cdp::cdp::browser_protocol::target::{
35    CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
36    GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
37};
38
39use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
40use chromiumoxide_types::*;
41use spider_network_blocker::intercept_manager::NetworkInterceptManager;
42
43/// Default `Browser::launch` timeout in MS
44pub const LAUNCH_TIMEOUT: u64 = 20_000;
45
46lazy_static::lazy_static! {
47    /// The request client to get the web socket url.
48    static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
49        .timeout(Duration::from_secs(60))
50        .default_headers({
51            let mut m = HeaderMap::new();
52
53            m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
54
55            m
56        })
57        .tcp_keepalive(Some(Duration::from_secs(5)))
58        .pool_idle_timeout(Some(Duration::from_secs(60)))
59        .pool_max_idle_per_host(10)
60        .build()
61        .expect("client to build");
62}
63
64/// Returns chromey's global `reqwest::Client` for reuse by other subsystems
65/// (e.g. remote cache uploads via `spider_remote_cache`).
66pub fn request_client() -> &'static reqwest::Client {
67    &REQUEST_CLIENT
68}
69
70/// A [`Browser`] is created when chromiumoxide connects to a Chromium instance.
71#[derive(Debug)]
72pub struct Browser {
73    /// The `Sender` to send messages to the connection handler that drives the
74    /// websocket
75    pub(crate) sender: Sender<HandlerMessage>,
76    /// How the spawned chromium instance was configured, if any
77    config: Option<BrowserConfig>,
78    /// The spawned chromium instance
79    child: Option<Child>,
80    /// The debug web socket url of the chromium instance
81    debug_ws_url: String,
82    /// The context of the browser
83    pub browser_context: BrowserContext,
84}
85
86/// Browser connection information.
87#[derive(serde::Deserialize, Debug, Default)]
88pub struct BrowserConnection {
89    #[serde(rename = "Browser")]
90    /// The browser name
91    pub browser: String,
92    #[serde(rename = "Protocol-Version")]
93    /// Browser version
94    pub protocol_version: String,
95    #[serde(rename = "User-Agent")]
96    /// User Agent used by default.
97    pub user_agent: String,
98    #[serde(rename = "V8-Version")]
99    /// The v8 engine version
100    pub v8_version: String,
101    #[serde(rename = "WebKit-Version")]
102    /// Webkit version
103    pub webkit_version: String,
104    #[serde(rename = "webSocketDebuggerUrl")]
105    /// Remote debugging address
106    pub web_socket_debugger_url: String,
107}
108
109impl Browser {
110    /// Connect to an already running chromium instance via the given URL.
111    ///
112    /// If the URL is a http(s) URL, it will first attempt to retrieve the Websocket URL from the `json/version` endpoint.
113    pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
114        Self::connect_with_config(url, HandlerConfig::default()).await
115    }
116
117    // Connect to an already running chromium instance with a given `HandlerConfig`.
118    ///
119    /// If the URL is a http URL, it will first attempt to retrieve the Websocket URL from the `json/version` endpoint.
120    pub async fn connect_with_config(
121        url: impl Into<String>,
122        config: HandlerConfig,
123    ) -> Result<(Self, Handler)> {
124        let mut debug_ws_url = url.into();
125        let retries = config.connection_retries;
126
127        if debug_ws_url.starts_with("http") {
128            let version_url = if debug_ws_url.ends_with("/json/version")
129                || debug_ws_url.ends_with("/json/version/")
130            {
131                debug_ws_url.to_owned()
132            } else {
133                format!(
134                    "{}{}json/version",
135                    &debug_ws_url,
136                    if debug_ws_url.ends_with('/') { "" } else { "/" }
137                )
138            };
139
140            let mut discovered = false;
141
142            for attempt in 0..=retries {
143                let retry = || async {
144                    if attempt < retries {
145                        // Cap at conn.rs MAX_BACKOFF_MS so a large
146                        // `connection_retries` can't synthesise a multi-day
147                        // sleep — `50 * 3^attempt` blows past u64 around
148                        // attempt=40 and overshoots a sane bound far earlier.
149                        let backoff_ms = 50u64
150                            .saturating_mul(3u64.saturating_pow(attempt))
151                            .min(crate::conn::MAX_BACKOFF_MS);
152                        tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
153                    }
154                };
155
156                match REQUEST_CLIENT.get(&version_url).send().await {
157                    Ok(req) => match req.bytes().await {
158                        Ok(b) => {
159                            match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
160                                Ok(connection)
161                                    if !connection.web_socket_debugger_url.is_empty() =>
162                                {
163                                    debug_ws_url = connection.web_socket_debugger_url;
164                                    discovered = true;
165                                    break;
166                                }
167                                _ => {
168                                    // JSON parse failed or webSocketDebuggerUrl was empty — retry
169                                    retry().await;
170                                }
171                            }
172                        }
173                        Err(_) => {
174                            retry().await;
175                        }
176                    },
177                    Err(_) => {
178                        retry().await;
179                    }
180                }
181            }
182
183            if !discovered {
184                return Err(CdpError::NoResponse);
185            }
186        }
187
188        let conn = Connection::<CdpEventMessage>::connect_with_retries_and_headers(
189            &debug_ws_url,
190            retries,
191            &config.connect_headers,
192        )
193        .await?;
194
195        let (tx, rx) = channel(config.channel_capacity);
196
197        let handler_config = BrowserConfig {
198            ignore_https_errors: config.ignore_https_errors,
199            viewport: config.viewport.clone(),
200            request_timeout: config.request_timeout,
201            request_intercept: config.request_intercept,
202            cache_enabled: config.cache_enabled,
203            ignore_visuals: config.ignore_visuals,
204            ignore_stylesheets: config.ignore_stylesheets,
205            ignore_javascript: config.ignore_javascript,
206            ignore_analytics: config.ignore_analytics,
207            ignore_prefetch: config.ignore_prefetch,
208            ignore_ads: config.ignore_ads,
209            allow_first_party_stylesheets: config.allow_first_party_stylesheets,
210            allow_first_party_javascript: config.allow_first_party_javascript,
211            allow_first_party_visuals: config.allow_first_party_visuals,
212            extra_headers: config.extra_headers.clone(),
213            only_html: config.only_html,
214            service_worker_enabled: config.service_worker_enabled,
215            intercept_manager: config.intercept_manager,
216            max_bytes_allowed: config.max_bytes_allowed,
217            max_redirects: config.max_redirects,
218            max_main_frame_navigations: config.max_main_frame_navigations,
219            whitelist_patterns: config.whitelist_patterns.clone(),
220            blacklist_patterns: config.blacklist_patterns.clone(),
221            connect_headers: config.connect_headers.clone(),
222            ..Default::default()
223        };
224
225        let fut = Handler::new(conn, rx, config);
226        let browser_context = fut.default_browser_context().clone();
227
228        let browser = Self {
229            sender: tx,
230            config: Some(handler_config),
231            child: None,
232            debug_ws_url,
233            browser_context,
234        };
235
236        Ok((browser, fut))
237    }
238
239    /// Launches a new instance of `chromium` in the background and attaches to
240    /// its debug web socket.
241    ///
242    /// This fails when no chromium executable could be detected.
243    ///
244    /// This fails if no web socket url could be detected from the child
245    /// processes stderr for more than the configured `launch_timeout`
246    /// (20 seconds by default).
247    pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
248        // Eagerly initialize the background cleanup worker in this
249        // runtime so that later `Drop` calls on CDP streams / temp
250        // files (from `bg_cleanup::submit`) land on a live receiver.
251        // This is a single atomic load after the first call — safe
252        // and cheap to invoke on every `launch`.
253        crate::bg_cleanup::init_worker();
254
255        // Canonalize paths to reduce issues with sandboxing
256        config.executable = utils::canonicalize_except_snap(config.executable).await?;
257
258        // Launch a new chromium instance
259        let mut child = config.launch()?;
260
261        /// Faillible initialization to run once the child process is created.
262        ///
263        /// All faillible calls must be executed inside this function. This ensures that all
264        /// errors are caught and that the child process is properly cleaned-up.
265        async fn with_child(
266            config: &BrowserConfig,
267            child: &mut Child,
268        ) -> Result<(String, Connection<CdpEventMessage>)> {
269            let dur = config.launch_timeout;
270            let timeout_fut = Box::pin(tokio::time::sleep(dur));
271
272            // extract the ws:
273            let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
274            let conn = Connection::<CdpEventMessage>::connect_with_retries_and_headers(
275                &debug_ws_url,
276                config.connection_retries,
277                &config.connect_headers,
278            )
279            .await?;
280            Ok((debug_ws_url, conn))
281        }
282
283        let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
284            Ok(conn) => conn,
285            Err(e) => {
286                // An initialization error occurred, clean up the process
287                if let Ok(Some(_)) = child.try_wait() {
288                    // already exited, do nothing, may happen if the browser crashed
289                } else {
290                    // the process is still alive, kill it and wait for exit (avoid zombie processes)
291                    let _ = child.kill().await;
292                    let _ = child.wait().await;
293                }
294                return Err(e);
295            }
296        };
297
298        // Only infaillible calls are allowed after this point to avoid clean-up issues with the
299        // child process.
300
301        let (tx, rx) = channel(config.channel_capacity);
302
303        let handler_config = HandlerConfig {
304            ignore_https_errors: config.ignore_https_errors,
305            viewport: config.viewport.clone(),
306            context_ids: Vec::new(),
307            request_timeout: config.request_timeout,
308            request_intercept: config.request_intercept,
309            cache_enabled: config.cache_enabled,
310            ignore_visuals: config.ignore_visuals,
311            ignore_stylesheets: config.ignore_stylesheets,
312            ignore_javascript: config.ignore_javascript,
313            ignore_analytics: config.ignore_analytics,
314            ignore_prefetch: config.ignore_prefetch,
315            ignore_ads: config.ignore_ads,
316            allow_first_party_stylesheets: config.allow_first_party_stylesheets,
317            allow_first_party_javascript: config.allow_first_party_javascript,
318            allow_first_party_visuals: config.allow_first_party_visuals,
319            extra_headers: config.extra_headers.clone(),
320            only_html: config.only_html,
321            service_worker_enabled: config.service_worker_enabled,
322            created_first_target: false,
323            intercept_manager: config.intercept_manager,
324            max_bytes_allowed: config.max_bytes_allowed,
325            max_redirects: config.max_redirects,
326            max_main_frame_navigations: config.max_main_frame_navigations,
327            whitelist_patterns: config.whitelist_patterns.clone(),
328            blacklist_patterns: config.blacklist_patterns.clone(),
329            // The local-launch path drives a real Chrome, which ignores the
330            // vendor `Interception.setPolicy` method — so leave it off here.
331            // Remote engines opt in via `HandlerConfig`/`connect_with_config`.
332            remote_local_policy: false,
333            #[cfg(feature = "adblock")]
334            adblock_filter_rules: config.adblock_filter_rules.clone(),
335            channel_capacity: config.channel_capacity,
336            page_channel_capacity: config.page_channel_capacity,
337            connection_retries: config.connection_retries,
338            connect_headers: config.connect_headers.clone(),
339        };
340
341        let fut = Handler::new(conn, rx, handler_config);
342        let browser_context = fut.default_browser_context().clone();
343
344        let browser = Self {
345            sender: tx,
346            config: Some(config),
347            child: Some(child),
348            debug_ws_url,
349            browser_context,
350        };
351
352        Ok((browser, fut))
353    }
354
355    /// Request to fetch all existing browser targets.
356    ///
357    /// By default, only targets launched after the browser connection are tracked
358    /// when connecting to a existing browser instance with the devtools websocket url
359    /// This function fetches existing targets on the browser and adds them as pages internally
360    ///
361    /// The pages are not guaranteed to be ready as soon as the function returns
362    /// You should wait a few millis if you need to use a page
363    /// Returns [TargetInfo]
364    pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
365        let (tx, rx) = oneshot_channel();
366
367        self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
368
369        rx.await?
370    }
371
372    /// Request for the browser to close completely.
373    ///
374    /// If the browser was spawned by [`Browser::launch`], it is recommended to wait for the
375    /// spawned instance exit, to avoid "zombie" processes ([`Browser::wait`],
376    /// [`Browser::wait_sync`], [`Browser::try_wait`]).
377    /// [`Browser::drop`] waits automatically if needed.
378    pub async fn close(&self) -> Result<CloseReturns> {
379        let (tx, rx) = oneshot_channel();
380
381        self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
382
383        rx.await?
384    }
385
386    /// Asynchronously wait for the spawned chromium instance to exit completely.
387    ///
388    /// The instance is spawned by [`Browser::launch`]. `wait` is usually called after
389    /// [`Browser::close`]. You can call this explicitly to collect the process and avoid
390    /// "zombie" processes.
391    ///
392    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
393    /// connected to an existing browser through [`Browser::connect`])
394    pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
395        if let Some(child) = self.child.as_mut() {
396            Ok(Some(child.wait().await?))
397        } else {
398            Ok(None)
399        }
400    }
401
402    /// If the spawned chromium instance has completely exited, wait for it.
403    ///
404    /// The instance is spawned by [`Browser::launch`]. `try_wait` is usually called after
405    /// [`Browser::close`]. You can call this explicitly to collect the process and avoid
406    /// "zombie" processes.
407    ///
408    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
409    /// connected to an existing browser through [`Browser::connect`])
410    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
411        if let Some(child) = self.child.as_mut() {
412            child.try_wait()
413        } else {
414            Ok(None)
415        }
416    }
417
418    /// Get the spawned chromium instance
419    ///
420    /// The instance is spawned by [`Browser::launch`]. The result is a [`async_process::Child`]
421    /// value. It acts as a compat wrapper for an `async-std` or `tokio` child process.
422    ///
423    /// You may use [`async_process::Child::as_mut_inner`] to retrieve the concrete implementation
424    /// for the selected runtime.
425    ///
426    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
427    /// connected to an existing browser through [`Browser::connect`])
428    pub fn get_mut_child(&mut self) -> Option<&mut Child> {
429        self.child.as_mut()
430    }
431
432    /// Has a browser instance launched on system.
433    pub fn has_child(&self) -> bool {
434        self.child.is_some()
435    }
436
437    /// Forcibly kill the spawned chromium instance
438    ///
439    /// The instance is spawned by [`Browser::launch`]. `kill` will automatically wait for the child
440    /// process to exit to avoid "zombie" processes.
441    ///
442    /// This method is provided to help if the browser does not close by itself. You should prefer
443    /// to use [`Browser::close`].
444    ///
445    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
446    /// connected to an existing browser through [`Browser::connect`])
447    pub async fn kill(&mut self) -> Option<io::Result<()>> {
448        match self.child.as_mut() {
449            Some(child) => Some(child.kill().await),
450            None => None,
451        }
452    }
453
454    /// If not launched as incognito this creates a new incognito browser
455    /// context. After that this browser exists within the incognito session.
456    /// New pages created while being in incognito mode will also run in the
457    /// incognito context. Incognito contexts won't share cookies/cache with
458    /// other browser contexts.
459    pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
460        if !self.is_incognito_configured() {
461            let browser_context_id = self
462                .create_browser_context(CreateBrowserContextParams::default())
463                .await?;
464            self.browser_context = BrowserContext::from(browser_context_id);
465            self.sender
466                .send(HandlerMessage::InsertContext(self.browser_context.clone()))
467                .await?;
468        }
469
470        Ok(self)
471    }
472
473    /// If a incognito session was created with
474    /// `Browser::start_incognito_context` this disposes this context.
475    ///
476    /// # Note This will also dispose all pages that were running within the
477    /// incognito context.
478    pub async fn quit_incognito_context_base(
479        &self,
480        browser_context_id: BrowserContextId,
481    ) -> Result<&Self> {
482        self.dispose_browser_context(browser_context_id.clone())
483            .await?;
484        self.sender
485            .send(HandlerMessage::DisposeContext(BrowserContext::from(
486                browser_context_id,
487            )))
488            .await?;
489        Ok(self)
490    }
491
492    /// If a incognito session was created with
493    /// `Browser::start_incognito_context` this disposes this context.
494    ///
495    /// # Note This will also dispose all pages that were running within the
496    /// incognito context.
497    pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
498        if let Some(id) = self.browser_context.take() {
499            let _ = self.quit_incognito_context_base(id).await;
500        }
501        Ok(self)
502    }
503
504    /// Whether incognito mode was configured from the start
505    fn is_incognito_configured(&self) -> bool {
506        self.config
507            .as_ref()
508            .map(|c| c.incognito)
509            .unwrap_or_default()
510    }
511
512    /// Returns the address of the websocket this browser is attached to
513    pub fn websocket_address(&self) -> &String {
514        &self.debug_ws_url
515    }
516
517    /// Whether the BrowserContext is incognito.
518    pub fn is_incognito(&self) -> bool {
519        self.is_incognito_configured() || self.browser_context.is_incognito()
520    }
521
522    /// The config of the spawned chromium instance if any.
523    pub fn config(&self) -> Option<&BrowserConfig> {
524        self.config.as_ref()
525    }
526
527    /// Create a new browser page
528    pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
529        let (tx, rx) = oneshot_channel();
530        let mut params = params.into();
531
532        if let Some(id) = self.browser_context.id() {
533            if params.browser_context_id.is_none() {
534                params.browser_context_id = Some(id.clone());
535            }
536        }
537
538        let _ = self
539            .sender
540            .send(HandlerMessage::CreatePage(params, tx))
541            .await;
542
543        rx.await?
544    }
545
546    /// Version information about the browser
547    pub async fn version(&self) -> Result<GetVersionReturns> {
548        Ok(self.execute(GetVersionParams::default()).await?.result)
549    }
550
551    /// Returns the user agent of the browser
552    pub async fn user_agent(&self) -> Result<String> {
553        Ok(self.version().await?.user_agent)
554    }
555
556    /// Call a browser method.
557    pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
558        let (tx, rx) = oneshot_channel();
559        let method = cmd.identifier();
560        let msg = CommandMessage::new(cmd, tx)?;
561
562        self.sender.send(HandlerMessage::Command(msg)).await?;
563        let resp = rx.await??;
564        to_command_response::<T>(resp, method)
565    }
566
567    /// Set permission settings for given embedding and embedded origins.
568    /// [PermissionDescriptor](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionDescriptor)
569    /// [PermissionSetting](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionSetting)
570    pub async fn set_permission(
571        &self,
572        permission: PermissionDescriptor,
573        setting: PermissionSetting,
574        origin: Option<impl Into<String>>,
575        embedded_origin: Option<impl Into<String>>,
576        browser_context_id: Option<BrowserContextId>,
577    ) -> Result<&Self> {
578        self.execute(SetPermissionParams {
579            permission,
580            setting,
581            origin: origin.map(Into::into),
582            embedded_origin: embedded_origin.map(Into::into),
583            browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
584        })
585        .await?;
586        Ok(self)
587    }
588
589    /// Convenience: set a permission for a single origin using the current browser context.
590    pub async fn set_permission_for_origin(
591        &self,
592        origin: impl Into<String>,
593        embedded_origin: Option<impl Into<String>>,
594        permission: PermissionDescriptor,
595        setting: PermissionSetting,
596    ) -> Result<&Self> {
597        self.set_permission(permission, setting, Some(origin), embedded_origin, None)
598            .await
599    }
600
601    /// "Reset" a permission override by setting it back to Prompt.
602    pub async fn reset_permission_for_origin(
603        &self,
604        origin: impl Into<String>,
605        embedded_origin: Option<impl Into<String>>,
606        permission: PermissionDescriptor,
607    ) -> Result<&Self> {
608        self.set_permission_for_origin(
609            origin,
610            embedded_origin,
611            permission,
612            PermissionSetting::Prompt,
613        )
614        .await
615    }
616
617    /// "Grant" all permissions.
618    pub async fn grant_all_permission_for_origin(
619        &self,
620        origin: impl Into<String>,
621        embedded_origin: Option<impl Into<String>>,
622        permission: PermissionDescriptor,
623    ) -> Result<&Self> {
624        self.set_permission_for_origin(
625            origin,
626            embedded_origin,
627            permission,
628            PermissionSetting::Granted,
629        )
630        .await
631    }
632
633    /// "Deny" all permissions.
634    pub async fn deny_all_permission_for_origin(
635        &self,
636        origin: impl Into<String>,
637        embedded_origin: Option<impl Into<String>>,
638        permission: PermissionDescriptor,
639    ) -> Result<&Self> {
640        self.set_permission_for_origin(
641            origin,
642            embedded_origin,
643            permission,
644            PermissionSetting::Denied,
645        )
646        .await
647    }
648
649    /// Return all of the pages of the browser
650    pub async fn pages(&self) -> Result<Vec<Page>> {
651        let (tx, rx) = oneshot_channel();
652        self.sender.send(HandlerMessage::GetPages(tx)).await?;
653        Ok(rx.await?)
654    }
655
656    /// Return page of given target_id
657    pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
658        let (tx, rx) = oneshot_channel();
659        self.sender
660            .send(HandlerMessage::GetPage(target_id, tx))
661            .await?;
662        rx.await?.ok_or(CdpError::NotFound)
663    }
664
665    /// Set listener for browser event
666    pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
667        let (tx, rx) = unbounded_channel();
668        self.sender
669            .send(HandlerMessage::AddEventListener(
670                EventListenerRequest::new::<T>(tx),
671            ))
672            .await?;
673
674        Ok(EventStream::new(rx))
675    }
676
677    /// Creates a new empty browser context.
678    pub async fn create_browser_context(
679        &mut self,
680        params: CreateBrowserContextParams,
681    ) -> Result<BrowserContextId> {
682        let response = self.execute(params).await?;
683
684        Ok(response.result.browser_context_id)
685    }
686
687    /// Returns all browser contexts created with Target.createBrowserContext method.
688    pub async fn get_browser_contexts(
689        &mut self,
690        params: GetBrowserContextsParams,
691    ) -> Result<GetBrowserContextsReturns> {
692        let response = self.execute(params).await?;
693        Ok(response.result)
694    }
695
696    /// Send a new empty browser context.
697    pub async fn send_new_context(
698        &mut self,
699        browser_context_id: BrowserContextId,
700    ) -> Result<&Self> {
701        self.browser_context = BrowserContext::from(browser_context_id);
702        self.sender
703            .send(HandlerMessage::InsertContext(self.browser_context.clone()))
704            .await?;
705        Ok(self)
706    }
707
708    /// Deletes a browser context.
709    pub async fn dispose_browser_context(
710        &self,
711        browser_context_id: impl Into<BrowserContextId>,
712    ) -> Result<&Self> {
713        self.execute(DisposeBrowserContextParams::new(browser_context_id))
714            .await?;
715
716        Ok(self)
717    }
718
719    /// Clears cookies.
720    pub async fn clear_cookies(&self) -> Result<&Self> {
721        self.execute(ClearCookiesParams::default()).await?;
722        Ok(self)
723    }
724
725    /// Returns all browser cookies.
726    pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
727        let cmd = GetCookiesParams {
728            browser_context_id: self.browser_context.id.clone(),
729        };
730
731        Ok(self.execute(cmd).await?.result.cookies)
732    }
733
734    /// Sets given cookies.
735    pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
736        for cookie in &mut cookies {
737            if let Some(url) = cookie.url.as_ref() {
738                crate::page::validate_cookie_url(url)?;
739            }
740        }
741
742        let mut cookies_param = SetCookiesParams::new(cookies);
743
744        cookies_param.browser_context_id = self.browser_context.id.clone();
745
746        self.execute(cookies_param).await?;
747        Ok(self)
748    }
749}
750
751impl Drop for Browser {
752    fn drop(&mut self) {
753        if let Some(child) = self.child.as_mut() {
754            if let Ok(Some(_)) = child.try_wait() {
755                // Already exited, do nothing. Usually occurs after using the method close or kill.
756            } else {
757                // We set the `kill_on_drop` property for the child process, so no need to explicitely
758                // kill it here. It can't really be done anyway since the method is async.
759                //
760                // On Unix, the process will be reaped in the background by the runtime automatically
761                // so it won't leave any resources locked. It is, however, a better practice for the user to
762                // do it himself since the runtime doesn't provide garantees as to when the reap occurs, so we
763                // warn him here.
764                tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
765            }
766        }
767    }
768}
769
770/// Resolve devtools WebSocket URL from the provided browser process
771///
772/// If an error occurs, it returns the browser's stderr output.
773///
774/// The URL resolution fails if:
775/// - [`CdpError::LaunchTimeout`]: `timeout_fut` completes, this corresponds to a timeout
776/// - [`CdpError::LaunchExit`]: the browser process exits (or is killed)
777/// - [`CdpError::LaunchIo`]: an input/output error occurs when await the process exit or reading
778///   the browser's stderr: end of stream, invalid UTF-8, other
779async fn ws_url_from_output(
780    child_process: &mut Child,
781    timeout_fut: impl Future<Output = ()> + Unpin,
782) -> Result<String> {
783    use tokio::io::AsyncBufReadExt;
784    let stderr = match child_process.stderr.take() {
785        Some(stderr) => stderr,
786        None => {
787            return Err(CdpError::LaunchIo(
788                io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
789                BrowserStderr::new(Vec::new()),
790            ));
791        }
792    };
793    let mut stderr_bytes = Vec::<u8>::new();
794    let mut buf = tokio::io::BufReader::new(stderr);
795    let mut timeout_fut = timeout_fut;
796    loop {
797        tokio::select! {
798            _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
799            exit_status = child_process.wait() => {
800                return Err(match exit_status {
801                    Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
802                    Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
803                })
804            },
805            read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
806                match read_res {
807                    Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
808                    Ok(byte_count) => {
809                        if byte_count == 0 {
810                            let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
811                            return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
812                        }
813                        let start_offset = stderr_bytes.len() - byte_count;
814                        let new_bytes = &stderr_bytes[start_offset..];
815                        match std::str::from_utf8(new_bytes) {
816                            Err(_) => {
817                                let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
818                                return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
819                            }
820                            Ok(line) => {
821                                if let Some((_, ws)) = line.rsplit_once("listening on ") {
822                                    if ws.starts_with("ws") && ws.contains("devtools/browser") {
823                                        return Ok(ws.trim().to_string());
824                                    }
825                                }
826                            }
827                        }
828                    }
829                }
830            }
831        }
832    }
833}
834
835#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
836pub enum HeadlessMode {
837    /// The "headful" mode.
838    False,
839    /// The old headless mode.
840    #[default]
841    True,
842    /// The new headless mode. See also: https://developer.chrome.com/docs/chromium/new-headless
843    New,
844}
845
846#[derive(Debug, Clone, Default)]
847pub struct BrowserConfig {
848    /// Determines whether to run headless version of the browser. Defaults to
849    /// true.
850    headless: HeadlessMode,
851    /// Determines whether to run the browser with a sandbox.
852    sandbox: bool,
853    /// Launch the browser with a specific window width and height.
854    window_size: Option<(u32, u32)>,
855    /// Launch the browser with a specific debugging port.
856    port: u16,
857    /// Path for Chrome or Chromium.
858    ///
859    /// If unspecified, the create will try to automatically detect a suitable
860    /// binary.
861    executable: std::path::PathBuf,
862
863    /// A list of Chrome extensions to load.
864    ///
865    /// An extension should be a path to a folder containing the extension code.
866    /// CRX files cannot be used directly and must be first extracted.
867    ///
868    /// Note that Chrome does not support loading extensions in headless-mode.
869    /// See https://bugs.chromium.org/p/chromium/issues/detail?id=706008#c5
870    extensions: Vec<String>,
871
872    /// Environment variables to set for the Chromium process.
873    /// Passes value through to std::process::Command::envs.
874    pub process_envs: Option<HashMap<String, String>>,
875
876    /// Data dir for user data
877    pub user_data_dir: Option<PathBuf>,
878
879    /// Whether to launch the `Browser` in incognito mode.
880    incognito: bool,
881
882    /// Timeout duration for `Browser::launch`.
883    launch_timeout: Duration,
884
885    /// Ignore https errors, default is true.
886    ignore_https_errors: bool,
887    pub viewport: Option<Viewport>,
888    /// The duration after a request with no response should time out.
889    request_timeout: Duration,
890
891    /// Additional command line arguments to pass to the browser instance.
892    args: Vec<String>,
893
894    /// Whether to disable DEFAULT_ARGS or not, default is false.
895    disable_default_args: bool,
896
897    /// Whether to enable request interception.
898    pub request_intercept: bool,
899
900    /// Whether to enable cache.
901    pub cache_enabled: bool,
902    /// Whether to enable or disable Service Workers.
903    /// Disabling may reduce background network activity and caching effects.
904    pub service_worker_enabled: bool,
905    /// Whether to ignore image/visual requests during interception.
906    /// Can reduce bandwidth and speed up crawling when visuals are unnecessary.
907    pub ignore_visuals: bool,
908    /// Whether to ignore stylesheet (CSS) requests during interception.
909    /// Useful for content-only crawls.
910    pub ignore_stylesheets: bool,
911    /// Whether to ignore JavaScript requests during interception.
912    /// This still allows critical framework bundles to pass when applicable.
913    pub ignore_javascript: bool,
914    /// When `ignore_stylesheets` would skip a stylesheet, allow it through if
915    /// the request URL is first-party (registrable domain matches the page's
916    /// primary frame). Default `true`. Set `false` for strict block-all.
917    pub allow_first_party_stylesheets: bool,
918    /// When a downstream blocker would skip a script, allow it through if
919    /// first-party. Default `true`.
920    pub allow_first_party_javascript: bool,
921    /// When `ignore_visuals` would skip an image/media/font, allow it through
922    /// if first-party. Default `true`.
923    pub allow_first_party_visuals: bool,
924    /// Whether to ignore analytics/telemetry requests during interception.
925    pub ignore_analytics: bool,
926    /// Ignore prefetch request.
927    pub ignore_prefetch: bool,
928    /// Whether to ignore ad network requests during interception.
929    pub ignore_ads: bool,
930    /// Extra headers.
931    pub extra_headers: Option<std::collections::HashMap<String, String>>,
932    /// Only html
933    pub only_html: bool,
934    /// The interception intercept manager.
935    pub intercept_manager: NetworkInterceptManager,
936    /// The max bytes to receive.
937    pub max_bytes_allowed: Option<u64>,
938    /// Cap on Document-type redirect hops before the navigation is aborted.
939    /// `None` disables enforcement; `Some(n)` mirrors `reqwest::redirect::Policy::limited(n)`.
940    pub max_redirects: Option<usize>,
941    /// Cap on main-frame cross-document navigations per `goto`. Defends against
942    /// JS / meta-refresh loops that bypass the HTTP redirect guard. `None`
943    /// disables the guard.
944    pub max_main_frame_navigations: Option<u32>,
945    /// Whitelist patterns to allow through the network.
946    pub whitelist_patterns: Option<Vec<String>>,
947    /// Blacklist patterns to block through the network.
948    pub blacklist_patterns: Option<Vec<String>>,
949    /// Extra ABP/uBO filter rules to load into the adblock engine (requires `adblock` feature).
950    /// These are merged with the built-in `ADBLOCK_PATTERNS` for richer blocking
951    /// (e.g. EasyList / EasyPrivacy content).
952    #[cfg(feature = "adblock")]
953    pub adblock_filter_rules: Option<Vec<String>>,
954    /// Capacity of the channel between browser handle and handler.
955    /// Defaults to 1000.
956    pub channel_capacity: usize,
957    /// Capacity of the per-page mpsc channel carrying `TargetMessage`s
958    /// from each `Page` to the handler. Defaults to 2048; override via
959    /// `page_channel_capacity(N)` on the builder. Values of `0` are
960    /// clamped to `1` at channel creation.
961    pub page_channel_capacity: usize,
962    /// Number of WebSocket connection retry attempts with exponential backoff.
963    /// Defaults to 4.
964    pub connection_retries: u32,
965    /// Headers added to the CDP WebSocket upgrade request. See
966    /// [`BrowserConfigBuilder::connect_header`].
967    pub connect_headers: ConnectHeaders,
968}
969
970#[derive(Debug, Clone)]
971pub struct BrowserConfigBuilder {
972    /// Headless mode configuration for the browser.
973    headless: HeadlessMode,
974    /// Whether to run the browser with a sandbox.
975    sandbox: bool,
976    /// Optional initial browser window size `(width, height)`.
977    window_size: Option<(u32, u32)>,
978    /// DevTools debugging port to bind to.
979    port: u16,
980    /// Optional explicit path to the Chrome/Chromium executable.
981    /// If `None`, auto-detection may be attempted based on `executation_detection`.
982    executable: Option<PathBuf>,
983    /// Controls auto-detection behavior for finding a Chrome/Chromium binary.
984    executation_detection: DetectionOptions,
985    /// List of unpacked extensions (directories) to load at startup.
986    extensions: Vec<String>,
987    /// Environment variables to set on the spawned Chromium process.
988    process_envs: Option<HashMap<String, String>>,
989    /// User data directory to persist browser state, or `None` for ephemeral.
990    user_data_dir: Option<PathBuf>,
991    /// Whether to start the browser in incognito (off-the-record) mode.
992    incognito: bool,
993    /// Maximum time to wait for the browser to launch and become ready.
994    launch_timeout: Duration,
995    /// Whether to ignore HTTPS/TLS errors during navigation and requests.
996    ignore_https_errors: bool,
997    /// Default page viewport configuration applied on startup.
998    viewport: Option<Viewport>,
999    /// Timeout for individual network requests without response progress.
1000    request_timeout: Duration,
1001    /// Additional command-line flags passed directly to the browser process.
1002    args: Vec<String>,
1003    /// Disable the default argument set and use only the provided `args`.
1004    disable_default_args: bool,
1005    /// Enable Network.requestInterception for request filtering/handling.
1006    request_intercept: bool,
1007    /// Enable the browser cache for navigations and subresources.
1008    cache_enabled: bool,
1009    /// Enable/disable Service Workers.
1010    service_worker_enabled: bool,
1011    /// Drop image/visual requests when interception is enabled.
1012    ignore_visuals: bool,
1013    /// Drop ad network requests when interception is enabled.
1014    ignore_ads: bool,
1015    /// Drop JavaScript requests when interception is enabled.
1016    ignore_javascript: bool,
1017    /// Drop stylesheet (CSS) requests when interception is enabled.
1018    ignore_stylesheets: bool,
1019    /// Allow first-party stylesheets through when `ignore_stylesheets` is on.
1020    /// Default `true`. Set `false` to strictly block all CSS.
1021    allow_first_party_stylesheets: bool,
1022    /// Allow first-party JS through when downstream blockers would skip it.
1023    /// Default `true`.
1024    allow_first_party_javascript: bool,
1025    /// Allow first-party visuals through when `ignore_visuals` is on.
1026    /// Default `true`.
1027    allow_first_party_visuals: bool,
1028    /// Ignore prefetch domains.
1029    ignore_prefetch: bool,
1030    /// Drop analytics/telemetry requests when interception is enabled.
1031    ignore_analytics: bool,
1032    /// If `true`, limit fetching to HTML documents.
1033    only_html: bool,
1034    /// Extra HTTP headers to include with every request.
1035    extra_headers: Option<std::collections::HashMap<String, String>>,
1036    /// Network interception manager used to configure filtering behavior.
1037    intercept_manager: NetworkInterceptManager,
1038    /// Optional upper bound on bytes that may be received (per session/run).
1039    max_bytes_allowed: Option<u64>,
1040    /// Optional cap on Document redirect hops per navigation (`None` = disabled).
1041    max_redirects: Option<usize>,
1042    /// Optional cap on main-frame cross-document navigations per goto.
1043    max_main_frame_navigations: Option<u32>,
1044    /// Whitelist patterns to allow through the network.
1045    whitelist_patterns: Option<Vec<String>>,
1046    /// Blacklist patterns to block through the network.
1047    blacklist_patterns: Option<Vec<String>>,
1048    /// Extra ABP/uBO filter rules for the adblock engine.
1049    #[cfg(feature = "adblock")]
1050    adblock_filter_rules: Option<Vec<String>>,
1051    /// Capacity of the channel between browser handle and handler.
1052    channel_capacity: usize,
1053    /// Capacity of the per-page mpsc `TargetMessage` channel.
1054    page_channel_capacity: usize,
1055    /// Number of WebSocket connection retry attempts.
1056    connection_retries: u32,
1057    /// Headers added to the CDP WebSocket upgrade request.
1058    connect_headers: ConnectHeaders,
1059}
1060
1061impl BrowserConfig {
1062    /// Browser builder default config.
1063    pub fn builder() -> BrowserConfigBuilder {
1064        BrowserConfigBuilder::default()
1065    }
1066
1067    /// Launch with the executable path.
1068    pub fn with_executable(path: impl AsRef<Path>) -> Self {
1069        // SAFETY: build() only fails when no executable is provided,
1070        // but we always provide one via chrome_executable().
1071        Self::builder().chrome_executable(path).build().unwrap()
1072    }
1073}
1074
1075impl Default for BrowserConfigBuilder {
1076    fn default() -> Self {
1077        Self {
1078            headless: HeadlessMode::True,
1079            sandbox: true,
1080            window_size: None,
1081            port: 0,
1082            executable: None,
1083            executation_detection: DetectionOptions::default(),
1084            extensions: Vec::new(),
1085            process_envs: None,
1086            user_data_dir: None,
1087            incognito: false,
1088            launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1089            ignore_https_errors: true,
1090            viewport: Some(Default::default()),
1091            request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1092            args: Vec::new(),
1093            disable_default_args: false,
1094            request_intercept: false,
1095            cache_enabled: true,
1096            ignore_visuals: false,
1097            ignore_ads: false,
1098            ignore_javascript: false,
1099            ignore_analytics: false,
1100            ignore_stylesheets: false,
1101            allow_first_party_stylesheets: true,
1102            allow_first_party_javascript: true,
1103            allow_first_party_visuals: true,
1104            ignore_prefetch: true,
1105            only_html: false,
1106            extra_headers: Default::default(),
1107            service_worker_enabled: true,
1108            intercept_manager: NetworkInterceptManager::Unknown,
1109            max_bytes_allowed: None,
1110            max_redirects: None,
1111            max_main_frame_navigations: None,
1112            whitelist_patterns: None,
1113            blacklist_patterns: None,
1114            #[cfg(feature = "adblock")]
1115            adblock_filter_rules: None,
1116            channel_capacity: 4096,
1117            page_channel_capacity: crate::handler::page::DEFAULT_PAGE_CHANNEL_CAPACITY,
1118            connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1119            connect_headers: ConnectHeaders::new(),
1120        }
1121    }
1122}
1123
1124impl BrowserConfigBuilder {
1125    /// Configure window size.
1126    pub fn window_size(mut self, width: u32, height: u32) -> Self {
1127        self.window_size = Some((width, height));
1128        self
1129    }
1130    /// Configure sandboxing.
1131    pub fn no_sandbox(mut self) -> Self {
1132        self.sandbox = false;
1133        self
1134    }
1135    /// Configure the launch to start non headless.
1136    pub fn with_head(mut self) -> Self {
1137        self.headless = HeadlessMode::False;
1138        self
1139    }
1140    /// Configure the launch with the new headless mode.
1141    pub fn new_headless_mode(mut self) -> Self {
1142        self.headless = HeadlessMode::New;
1143        self
1144    }
1145    /// Configure the launch with headless.
1146    pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1147        self.headless = mode;
1148        self
1149    }
1150    /// Configure the launch in incognito.
1151    pub fn incognito(mut self) -> Self {
1152        self.incognito = true;
1153        self
1154    }
1155
1156    pub fn respect_https_errors(mut self) -> Self {
1157        self.ignore_https_errors = false;
1158        self
1159    }
1160
1161    pub fn port(mut self, port: u16) -> Self {
1162        self.port = port;
1163        self
1164    }
1165
1166    pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1167        self.max_bytes_allowed = max_bytes_allowed;
1168        self
1169    }
1170
1171    /// Cap the number of Document-type redirect hops per navigation.
1172    ///
1173    /// `None` disables enforcement (default, preserves Chromium's own ~20-hop cap).
1174    /// `Some(n)` aborts once a navigation chain exceeds `n` by emitting
1175    /// `net::ERR_TOO_MANY_REDIRECTS` and calling `Page.stopLoading`.
1176    pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
1177        self.max_redirects = max_redirects;
1178        self
1179    }
1180
1181    /// Cap the number of main-frame cross-document navigations allowed per
1182    /// `goto` call.
1183    ///
1184    /// Defends against JS `location.href` / meta-refresh loops that bypass
1185    /// HTTP-level redirect detection — each hop looks like a fresh document
1186    /// to Chromium, so `with_max_redirects` alone cannot catch them. `None`
1187    /// disables the guard (default).
1188    pub fn with_max_main_frame_navigations(mut self, cap: Option<u32>) -> Self {
1189        self.max_main_frame_navigations = cap;
1190        self
1191    }
1192
1193    pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1194        self.launch_timeout = timeout;
1195        self
1196    }
1197
1198    pub fn request_timeout(mut self, timeout: Duration) -> Self {
1199        self.request_timeout = timeout;
1200        self
1201    }
1202
1203    /// Configures the viewport of the browser, which defaults to `800x600`.
1204    /// `None` disables viewport emulation (i.e., it uses the browsers default
1205    /// configuration, which fills the available space. This is similar to what
1206    /// Playwright does when you provide `null` as the value of its `viewport`
1207    /// option).
1208    pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1209        self.viewport = viewport.into();
1210        self
1211    }
1212
1213    pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1214        self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1215        self
1216    }
1217
1218    pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1219        self.executable = Some(path.as_ref().to_path_buf());
1220        self
1221    }
1222
1223    pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1224        self.executation_detection = options;
1225        self
1226    }
1227
1228    /// Prefer Microsoft Edge over Chrome/Chromium during auto-detection.
1229    ///
1230    /// Convenience over [`Self::chrome_detection`] that enables both
1231    /// [`DetectionOptions::msedge`] and [`DetectionOptions::prefer_msedge`] so
1232    /// Edge binaries are resolved first when both browsers are installed.
1233    pub fn prefer_edge(mut self) -> Self {
1234        self.executation_detection.msedge = true;
1235        self.executation_detection.prefer_msedge = true;
1236        self
1237    }
1238
1239    pub fn extension(mut self, extension: impl Into<String>) -> Self {
1240        self.extensions.push(extension.into());
1241        self
1242    }
1243
1244    pub fn extensions<I, S>(mut self, extensions: I) -> Self
1245    where
1246        I: IntoIterator<Item = S>,
1247        S: Into<String>,
1248    {
1249        for ext in extensions {
1250            self.extensions.push(ext.into());
1251        }
1252        self
1253    }
1254
1255    pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1256        self.process_envs
1257            .get_or_insert(HashMap::new())
1258            .insert(key.into(), val.into());
1259        self
1260    }
1261
1262    pub fn envs<I, K, V>(mut self, envs: I) -> Self
1263    where
1264        I: IntoIterator<Item = (K, V)>,
1265        K: Into<String>,
1266        V: Into<String>,
1267    {
1268        self.process_envs
1269            .get_or_insert(HashMap::new())
1270            .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1271        self
1272    }
1273
1274    pub fn arg(mut self, arg: impl Into<String>) -> Self {
1275        self.args.push(arg.into());
1276        self
1277    }
1278
1279    pub fn args<I, S>(mut self, args: I) -> Self
1280    where
1281        I: IntoIterator<Item = S>,
1282        S: Into<String>,
1283    {
1284        for arg in args {
1285            self.args.push(arg.into());
1286        }
1287        self
1288    }
1289
1290    pub fn disable_default_args(mut self) -> Self {
1291        self.disable_default_args = true;
1292        self
1293    }
1294
1295    pub fn enable_request_intercept(mut self) -> Self {
1296        self.request_intercept = true;
1297        self
1298    }
1299
1300    pub fn disable_request_intercept(mut self) -> Self {
1301        self.request_intercept = false;
1302        self
1303    }
1304
1305    pub fn enable_cache(mut self) -> Self {
1306        self.cache_enabled = true;
1307        self
1308    }
1309
1310    pub fn disable_cache(mut self) -> Self {
1311        self.cache_enabled = false;
1312        self
1313    }
1314
1315    /// Set service worker enabled.
1316    pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1317        self.service_worker_enabled = bypass;
1318        self
1319    }
1320
1321    /// When `ignore_stylesheets` is on, allow first-party CSS through.
1322    /// Default `true`. Set `false` for strict block-all-CSS semantics.
1323    pub fn allow_first_party_stylesheets(mut self, allow: bool) -> Self {
1324        self.allow_first_party_stylesheets = allow;
1325        self
1326    }
1327
1328    /// Allow first-party scripts through downstream blockers (intercept
1329    /// manager / adblock / blocklists). Default `true`.
1330    pub fn allow_first_party_javascript(mut self, allow: bool) -> Self {
1331        self.allow_first_party_javascript = allow;
1332        self
1333    }
1334
1335    /// When `ignore_visuals` is on, allow first-party images/media/fonts
1336    /// through. Default `true`. Set `false` for strictly bandwidth-minimal
1337    /// crawls that drop ALL visuals.
1338    pub fn allow_first_party_visuals(mut self, allow: bool) -> Self {
1339        self.allow_first_party_visuals = allow;
1340        self
1341    }
1342
1343    /// Set extra request headers.
1344    pub fn set_extra_headers(
1345        mut self,
1346        headers: Option<std::collections::HashMap<String, String>>,
1347    ) -> Self {
1348        self.extra_headers = headers;
1349        self
1350    }
1351
1352    /// Set whitelist patterns to allow through network interception allowing.
1353    pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1354        self.whitelist_patterns = whitelist_patterns;
1355        self
1356    }
1357
1358    /// Set blacklist patterns to block through network interception.
1359    pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1360        self.blacklist_patterns = blacklist_patterns;
1361        self
1362    }
1363
1364    /// Set extra ABP/uBO filter rules for the adblock engine.
1365    /// Pass EasyList/EasyPrivacy content lines for richer blocking coverage.
1366    #[cfg(feature = "adblock")]
1367    pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1368        self.adblock_filter_rules = Some(rules);
1369        self
1370    }
1371
1372    /// Set the capacity of the channel between browser handle and handler.
1373    /// Defaults to 1000.
1374    pub fn channel_capacity(mut self, capacity: usize) -> Self {
1375        self.channel_capacity = capacity;
1376        self
1377    }
1378
1379    /// Set the capacity of the per-page mpsc channel carrying
1380    /// `TargetMessage`s from each `Page` to the handler.
1381    ///
1382    /// Defaults to 2048 (the previous hard-coded value). Tune upward to
1383    /// absorb bursts of commands without pushing them onto the
1384    /// `CommandFuture` async-send fallback path; tune downward to apply
1385    /// back-pressure sooner. Values of `0` are clamped to `1` at channel
1386    /// creation time (tokio panics on a zero-capacity mpsc).
1387    pub fn page_channel_capacity(mut self, capacity: usize) -> Self {
1388        self.page_channel_capacity = capacity;
1389        self
1390    }
1391
1392    /// Set the number of WebSocket connection retry attempts with exponential backoff.
1393    /// Defaults to 4. Set to 0 for a single attempt with no retries.
1394    pub fn connection_retries(mut self, retries: u32) -> Self {
1395        self.connection_retries = retries;
1396        self
1397    }
1398
1399    /// Add one header to the CDP WebSocket upgrade request.
1400    ///
1401    /// This is the HTTP `GET` that opens the socket, so a gateway in front of
1402    /// the browser sees the header on the handshake. It is separate from
1403    /// [`Self::set_extra_headers`], which sets headers on the page's own
1404    /// network requests. Every connect attempt, including retries, carries
1405    /// the header. Setting a name twice keeps the last value.
1406    ///
1407    /// Fails, without panicking, on a name that is not a valid header name,
1408    /// a value that is not visible ASCII, a name the handshake owns (`host`,
1409    /// `connection`, `upgrade`, `sec-websocket-*`), or a set that would pass
1410    /// [`ConnectHeaders::MAX_TOTAL_BYTES`] (8 KiB) on the wire.
1411    pub fn connect_header<N, V>(
1412        mut self,
1413        name: N,
1414        value: V,
1415    ) -> std::result::Result<Self, ConnectHeaderError>
1416    where
1417        N: TryInto<HeaderName>,
1418        N::Error: std::fmt::Display,
1419        V: TryInto<HeaderValue>,
1420        V::Error: std::fmt::Display,
1421    {
1422        self.connect_headers.insert(name, value)?;
1423        Ok(self)
1424    }
1425
1426    /// Replace the CDP WebSocket upgrade headers with `headers`.
1427    ///
1428    /// Accepts a [`ConnectHeaders`] or a `HeaderMap`. Validation is the same
1429    /// as [`Self::connect_header`]; a map with a repeated name keeps its last
1430    /// value.
1431    pub fn connect_headers<H>(mut self, headers: H) -> std::result::Result<Self, ConnectHeaderError>
1432    where
1433        H: TryInto<ConnectHeaders, Error = ConnectHeaderError>,
1434    {
1435        self.connect_headers = headers.try_into()?;
1436        Ok(self)
1437    }
1438
1439    /// Build the browser.
1440    pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1441        let executable = if let Some(e) = self.executable {
1442            e
1443        } else {
1444            detection::default_executable(self.executation_detection)?
1445        };
1446
1447        Ok(BrowserConfig {
1448            headless: self.headless,
1449            sandbox: self.sandbox,
1450            window_size: self.window_size,
1451            port: self.port,
1452            executable,
1453            extensions: self.extensions,
1454            process_envs: self.process_envs,
1455            user_data_dir: self.user_data_dir,
1456            incognito: self.incognito,
1457            launch_timeout: self.launch_timeout,
1458            ignore_https_errors: self.ignore_https_errors,
1459            viewport: self.viewport,
1460            request_timeout: self.request_timeout,
1461            args: self.args,
1462            disable_default_args: self.disable_default_args,
1463            request_intercept: self.request_intercept,
1464            cache_enabled: self.cache_enabled,
1465            ignore_visuals: self.ignore_visuals,
1466            ignore_ads: self.ignore_ads,
1467            ignore_javascript: self.ignore_javascript,
1468            ignore_analytics: self.ignore_analytics,
1469            ignore_stylesheets: self.ignore_stylesheets,
1470            allow_first_party_stylesheets: self.allow_first_party_stylesheets,
1471            allow_first_party_javascript: self.allow_first_party_javascript,
1472            allow_first_party_visuals: self.allow_first_party_visuals,
1473            ignore_prefetch: self.ignore_prefetch,
1474            extra_headers: self.extra_headers,
1475            only_html: self.only_html,
1476            intercept_manager: self.intercept_manager,
1477            service_worker_enabled: self.service_worker_enabled,
1478            max_bytes_allowed: self.max_bytes_allowed,
1479            max_redirects: self.max_redirects,
1480            max_main_frame_navigations: self.max_main_frame_navigations,
1481            whitelist_patterns: self.whitelist_patterns,
1482            blacklist_patterns: self.blacklist_patterns,
1483            #[cfg(feature = "adblock")]
1484            adblock_filter_rules: self.adblock_filter_rules,
1485            channel_capacity: self.channel_capacity,
1486            page_channel_capacity: self.page_channel_capacity,
1487            connection_retries: self.connection_retries,
1488            connect_headers: self.connect_headers,
1489        })
1490    }
1491}
1492
1493impl BrowserConfig {
1494    pub fn launch(&self) -> io::Result<Child> {
1495        let mut cmd = async_process::Command::new(&self.executable);
1496
1497        if self.disable_default_args {
1498            cmd.args(&self.args);
1499        } else {
1500            cmd.args(DEFAULT_ARGS).args(&self.args);
1501        }
1502
1503        if !self
1504            .args
1505            .iter()
1506            .any(|arg| arg.contains("--remote-debugging-port="))
1507        {
1508            cmd.arg(format!("--remote-debugging-port={}", self.port));
1509        }
1510
1511        cmd.args(
1512            self.extensions
1513                .iter()
1514                .map(|e| format!("--load-extension={e}")),
1515        );
1516
1517        if let Some(ref user_data) = self.user_data_dir {
1518            cmd.arg(format!("--user-data-dir={}", user_data.display()));
1519        } else {
1520            // If the user did not specify a data directory, this would default to the systems default
1521            // data directory. In most cases, we would rather have a fresh instance of Chromium. Specify
1522            // a temp dir just for chromiumoxide instead.
1523            cmd.arg(format!(
1524                "--user-data-dir={}",
1525                std::env::temp_dir().join("chromiumoxide-runner").display()
1526            ));
1527        }
1528
1529        if let Some((width, height)) = self.window_size {
1530            cmd.arg(format!("--window-size={width},{height}"));
1531        }
1532
1533        if !self.sandbox {
1534            cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1535        }
1536
1537        match self.headless {
1538            HeadlessMode::False => (),
1539            HeadlessMode::True => {
1540                cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1541            }
1542            HeadlessMode::New => {
1543                cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1544            }
1545        }
1546
1547        if self.incognito {
1548            cmd.arg("--incognito");
1549        }
1550
1551        if let Some(ref envs) = self.process_envs {
1552            cmd.envs(envs);
1553        }
1554        cmd.stderr(Stdio::piped()).spawn()
1555    }
1556}
1557
1558/// Returns the path to Chrome's executable.
1559///
1560/// If the `CHROME` environment variable is set, `default_executable` will
1561/// use it as the default path. Otherwise, the filenames `google-chrome-stable`
1562/// `chromium`, `chromium-browser`, `chrome` and `chrome-browser` are
1563/// searched for in standard places. If that fails,
1564/// `/Applications/Google Chrome.app/...` (on MacOS) or the registry (on
1565/// Windows) is consulted. If all of the above fail, an error is returned.
1566#[deprecated(note = "Use detection::default_executable instead")]
1567pub fn default_executable() -> Result<std::path::PathBuf, String> {
1568    let options = DetectionOptions {
1569        msedge: false,
1570        unstable: false,
1571        prefer_msedge: false,
1572    };
1573    detection::default_executable(options)
1574}
1575
1576/// These are passed to the Chrome binary by default.
1577/// Via https://github.com/puppeteer/puppeteer/blob/4846b8723cf20d3551c0d755df394cc5e0c82a94/src/node/Launcher.ts#L157
1578static DEFAULT_ARGS: [&str; 26] = [
1579    "--disable-background-networking",
1580    "--enable-features=NetworkService,NetworkServiceInProcess",
1581    "--disable-background-timer-throttling",
1582    "--disable-backgrounding-occluded-windows",
1583    "--disable-breakpad",
1584    "--disable-client-side-phishing-detection",
1585    "--disable-component-extensions-with-background-pages",
1586    "--disable-default-apps",
1587    "--disable-dev-shm-usage",
1588    "--disable-extensions",
1589    "--disable-features=TranslateUI",
1590    "--disable-hang-monitor",
1591    "--disable-ipc-flooding-protection",
1592    "--disable-popup-blocking",
1593    "--disable-prompt-on-repost",
1594    "--disable-renderer-backgrounding",
1595    "--disable-sync",
1596    "--force-color-profile=srgb",
1597    "--metrics-recording-only",
1598    "--no-first-run",
1599    "--enable-automation",
1600    "--password-store=basic",
1601    "--use-mock-keychain",
1602    "--enable-blink-features=IdleDetection",
1603    "--lang=en_US",
1604    "--disable-blink-features=AutomationControlled",
1605];