Skip to main content

chromiumoxide/
browser.rs

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