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
43pub const LAUNCH_TIMEOUT: u64 = 20_000;
45
46lazy_static::lazy_static! {
47 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
64pub fn request_client() -> &'static reqwest::Client {
67 &REQUEST_CLIENT
68}
69
70#[derive(Debug)]
72pub struct Browser {
73 pub(crate) sender: Sender<HandlerMessage>,
76 config: Option<BrowserConfig>,
78 child: Option<Child>,
80 debug_ws_url: String,
82 pub browser_context: BrowserContext,
84}
85
86#[derive(serde::Deserialize, Debug, Default)]
88pub struct BrowserConnection {
89 #[serde(rename = "Browser")]
90 pub browser: String,
92 #[serde(rename = "Protocol-Version")]
93 pub protocol_version: String,
95 #[serde(rename = "User-Agent")]
96 pub user_agent: String,
98 #[serde(rename = "V8-Version")]
99 pub v8_version: String,
101 #[serde(rename = "WebKit-Version")]
102 pub webkit_version: String,
104 #[serde(rename = "webSocketDebuggerUrl")]
105 pub web_socket_debugger_url: String,
107}
108
109impl Browser {
110 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
114 Self::connect_with_config(url, HandlerConfig::default()).await
115 }
116
117 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 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 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 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
243 crate::bg_cleanup::init_worker();
249
250 config.executable = utils::canonicalize_except_snap(config.executable).await?;
252
253 let mut child = config.launch()?;
255
256 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 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 if let Ok(Some(_)) = child.try_wait() {
282 } else {
284 let _ = child.kill().await;
286 let _ = child.wait().await;
287 }
288 return Err(e);
289 }
290 };
291
292 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 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 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 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 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 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 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
422 self.child.as_mut()
423 }
424
425 pub fn has_child(&self) -> bool {
427 self.child.is_some()
428 }
429
430 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 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 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 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 fn is_incognito_configured(&self) -> bool {
499 self.config
500 .as_ref()
501 .map(|c| c.incognito)
502 .unwrap_or_default()
503 }
504
505 pub fn websocket_address(&self) -> &String {
507 &self.debug_ws_url
508 }
509
510 pub fn is_incognito(&self) -> bool {
512 self.is_incognito_configured() || self.browser_context.is_incognito()
513 }
514
515 pub fn config(&self) -> Option<&BrowserConfig> {
517 self.config.as_ref()
518 }
519
520 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 pub async fn version(&self) -> Result<GetVersionReturns> {
541 Ok(self.execute(GetVersionParams::default()).await?.result)
542 }
543
544 pub async fn user_agent(&self) -> Result<String> {
546 Ok(self.version().await?.user_agent)
547 }
548
549 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn clear_cookies(&self) -> Result<&Self> {
714 self.execute(ClearCookiesParams::default()).await?;
715 Ok(self)
716 }
717
718 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 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 } else {
750 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
758 }
759 }
760 }
761}
762
763async 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 False,
832 #[default]
834 True,
835 New,
837}
838
839#[derive(Debug, Clone, Default)]
840pub struct BrowserConfig {
841 headless: HeadlessMode,
844 sandbox: bool,
846 window_size: Option<(u32, u32)>,
848 port: u16,
850 executable: std::path::PathBuf,
855
856 extensions: Vec<String>,
864
865 pub process_envs: Option<HashMap<String, String>>,
868
869 pub user_data_dir: Option<PathBuf>,
871
872 incognito: bool,
874
875 launch_timeout: Duration,
877
878 ignore_https_errors: bool,
880 pub viewport: Option<Viewport>,
881 request_timeout: Duration,
883
884 args: Vec<String>,
886
887 disable_default_args: bool,
889
890 pub request_intercept: bool,
892
893 pub cache_enabled: bool,
895 pub service_worker_enabled: bool,
898 pub ignore_visuals: bool,
901 pub ignore_stylesheets: bool,
904 pub ignore_javascript: bool,
907 pub allow_first_party_stylesheets: bool,
911 pub allow_first_party_javascript: bool,
914 pub allow_first_party_visuals: bool,
917 pub ignore_analytics: bool,
919 pub ignore_prefetch: bool,
921 pub ignore_ads: bool,
923 pub extra_headers: Option<std::collections::HashMap<String, String>>,
925 pub only_html: bool,
927 pub intercept_manager: NetworkInterceptManager,
929 pub max_bytes_allowed: Option<u64>,
931 pub max_redirects: Option<usize>,
934 pub max_main_frame_navigations: Option<u32>,
938 pub whitelist_patterns: Option<Vec<String>>,
940 pub blacklist_patterns: Option<Vec<String>>,
942 #[cfg(feature = "adblock")]
946 pub adblock_filter_rules: Option<Vec<String>>,
947 pub channel_capacity: usize,
950 pub page_channel_capacity: usize,
955 pub connection_retries: u32,
958}
959
960#[derive(Debug, Clone)]
961pub struct BrowserConfigBuilder {
962 headless: HeadlessMode,
964 sandbox: bool,
966 window_size: Option<(u32, u32)>,
968 port: u16,
970 executable: Option<PathBuf>,
973 executation_detection: DetectionOptions,
975 extensions: Vec<String>,
977 process_envs: Option<HashMap<String, String>>,
979 user_data_dir: Option<PathBuf>,
981 incognito: bool,
983 launch_timeout: Duration,
985 ignore_https_errors: bool,
987 viewport: Option<Viewport>,
989 request_timeout: Duration,
991 args: Vec<String>,
993 disable_default_args: bool,
995 request_intercept: bool,
997 cache_enabled: bool,
999 service_worker_enabled: bool,
1001 ignore_visuals: bool,
1003 ignore_ads: bool,
1005 ignore_javascript: bool,
1007 ignore_stylesheets: bool,
1009 allow_first_party_stylesheets: bool,
1012 allow_first_party_javascript: bool,
1015 allow_first_party_visuals: bool,
1018 ignore_prefetch: bool,
1020 ignore_analytics: bool,
1022 only_html: bool,
1024 extra_headers: Option<std::collections::HashMap<String, String>>,
1026 intercept_manager: NetworkInterceptManager,
1028 max_bytes_allowed: Option<u64>,
1030 max_redirects: Option<usize>,
1032 max_main_frame_navigations: Option<u32>,
1034 whitelist_patterns: Option<Vec<String>>,
1036 blacklist_patterns: Option<Vec<String>>,
1038 #[cfg(feature = "adblock")]
1040 adblock_filter_rules: Option<Vec<String>>,
1041 channel_capacity: usize,
1043 page_channel_capacity: usize,
1045 connection_retries: u32,
1047}
1048
1049impl BrowserConfig {
1050 pub fn builder() -> BrowserConfigBuilder {
1052 BrowserConfigBuilder::default()
1053 }
1054
1055 pub fn with_executable(path: impl AsRef<Path>) -> Self {
1057 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 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1114 self.window_size = Some((width, height));
1115 self
1116 }
1117 pub fn no_sandbox(mut self) -> Self {
1119 self.sandbox = false;
1120 self
1121 }
1122 pub fn with_head(mut self) -> Self {
1124 self.headless = HeadlessMode::False;
1125 self
1126 }
1127 pub fn new_headless_mode(mut self) -> Self {
1129 self.headless = HeadlessMode::New;
1130 self
1131 }
1132 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1134 self.headless = mode;
1135 self
1136 }
1137 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 pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
1164 self.max_redirects = max_redirects;
1165 self
1166 }
1167
1168 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 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 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 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1304 self.service_worker_enabled = bypass;
1305 self
1306 }
1307
1308 pub fn allow_first_party_stylesheets(mut self, allow: bool) -> Self {
1311 self.allow_first_party_stylesheets = allow;
1312 self
1313 }
1314
1315 pub fn allow_first_party_javascript(mut self, allow: bool) -> Self {
1318 self.allow_first_party_javascript = allow;
1319 self
1320 }
1321
1322 pub fn allow_first_party_visuals(mut self, allow: bool) -> Self {
1326 self.allow_first_party_visuals = allow;
1327 self
1328 }
1329
1330 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 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1341 self.whitelist_patterns = whitelist_patterns;
1342 self
1343 }
1344
1345 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1347 self.blacklist_patterns = blacklist_patterns;
1348 self
1349 }
1350
1351 #[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 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1362 self.channel_capacity = capacity;
1363 self
1364 }
1365
1366 pub fn page_channel_capacity(mut self, capacity: usize) -> Self {
1375 self.page_channel_capacity = capacity;
1376 self
1377 }
1378
1379 pub fn connection_retries(mut self, retries: u32) -> Self {
1382 self.connection_retries = retries;
1383 self
1384 }
1385
1386 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 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#[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
1522static 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];