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
64#[derive(Debug)]
66pub struct Browser {
67 pub(crate) sender: Sender<HandlerMessage>,
70 config: Option<BrowserConfig>,
72 child: Option<Child>,
74 debug_ws_url: String,
76 pub browser_context: BrowserContext,
78}
79
80#[derive(serde::Deserialize, Debug, Default)]
82pub struct BrowserConnection {
83 #[serde(rename = "Browser")]
84 pub browser: String,
86 #[serde(rename = "Protocol-Version")]
87 pub protocol_version: String,
89 #[serde(rename = "User-Agent")]
90 pub user_agent: String,
92 #[serde(rename = "V8-Version")]
93 pub v8_version: String,
95 #[serde(rename = "WebKit-Version")]
96 pub webkit_version: String,
98 #[serde(rename = "webSocketDebuggerUrl")]
99 pub web_socket_debugger_url: String,
101}
102
103impl Browser {
104 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
108 Self::connect_with_config(url, HandlerConfig::default()).await
109 }
110
111 pub async fn connect_with_config(
115 url: impl Into<String>,
116 config: HandlerConfig,
117 ) -> Result<(Self, Handler)> {
118 let mut debug_ws_url = url.into();
119 let retries = config.connection_retries;
120
121 if debug_ws_url.starts_with("http") {
122 let version_url = if debug_ws_url.ends_with("/json/version")
123 || debug_ws_url.ends_with("/json/version/")
124 {
125 debug_ws_url.to_owned()
126 } else {
127 format!(
128 "{}{}json/version",
129 &debug_ws_url,
130 if debug_ws_url.ends_with('/') { "" } else { "/" }
131 )
132 };
133
134 let mut discovered = false;
135
136 for attempt in 0..=retries {
137 let retry = || async {
138 if attempt < retries {
139 let backoff_ms = 50u64 * 3u64.saturating_pow(attempt);
140 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
141 }
142 };
143
144 match REQUEST_CLIENT.get(&version_url).send().await {
145 Ok(req) => match req.bytes().await {
146 Ok(b) => {
147 match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
148 Ok(connection)
149 if !connection.web_socket_debugger_url.is_empty() =>
150 {
151 debug_ws_url = connection.web_socket_debugger_url;
152 discovered = true;
153 break;
154 }
155 _ => {
156 retry().await;
158 }
159 }
160 }
161 Err(_) => {
162 retry().await;
163 }
164 },
165 Err(_) => {
166 retry().await;
167 }
168 }
169 }
170
171 if !discovered {
172 return Err(CdpError::NoResponse);
173 }
174 }
175
176 let conn =
177 Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
178
179 let (tx, rx) = channel(config.channel_capacity);
180
181 let handler_config = BrowserConfig {
182 ignore_https_errors: config.ignore_https_errors,
183 viewport: config.viewport.clone(),
184 request_timeout: config.request_timeout,
185 request_intercept: config.request_intercept,
186 cache_enabled: config.cache_enabled,
187 ignore_visuals: config.ignore_visuals,
188 ignore_stylesheets: config.ignore_stylesheets,
189 ignore_javascript: config.ignore_javascript,
190 ignore_analytics: config.ignore_analytics,
191 ignore_prefetch: config.ignore_prefetch,
192 ignore_ads: config.ignore_ads,
193 extra_headers: config.extra_headers.clone(),
194 only_html: config.only_html,
195 service_worker_enabled: config.service_worker_enabled,
196 intercept_manager: config.intercept_manager,
197 max_bytes_allowed: config.max_bytes_allowed,
198 whitelist_patterns: config.whitelist_patterns.clone(),
199 blacklist_patterns: config.blacklist_patterns.clone(),
200 ..Default::default()
201 };
202
203 let fut = Handler::new(conn, rx, config);
204 let browser_context = fut.default_browser_context().clone();
205
206 let browser = Self {
207 sender: tx,
208 config: Some(handler_config),
209 child: None,
210 debug_ws_url,
211 browser_context,
212 };
213
214 Ok((browser, fut))
215 }
216
217 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
226 config.executable = utils::canonicalize_except_snap(config.executable).await?;
228
229 let mut child = config.launch()?;
231
232 async fn with_child(
237 config: &BrowserConfig,
238 child: &mut Child,
239 ) -> Result<(String, Connection<CdpEventMessage>)> {
240 let dur = config.launch_timeout;
241 let timeout_fut = Box::pin(tokio::time::sleep(dur));
242
243 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
245 let conn = Connection::<CdpEventMessage>::connect_with_retries(
246 &debug_ws_url,
247 config.connection_retries,
248 )
249 .await?;
250 Ok((debug_ws_url, conn))
251 }
252
253 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
254 Ok(conn) => conn,
255 Err(e) => {
256 if let Ok(Some(_)) = child.try_wait() {
258 } else {
260 let _ = child.kill().await;
262 let _ = child.wait().await;
263 }
264 return Err(e);
265 }
266 };
267
268 let (tx, rx) = channel(config.channel_capacity);
272
273 let handler_config = HandlerConfig {
274 ignore_https_errors: config.ignore_https_errors,
275 viewport: config.viewport.clone(),
276 context_ids: Vec::new(),
277 request_timeout: config.request_timeout,
278 request_intercept: config.request_intercept,
279 cache_enabled: config.cache_enabled,
280 ignore_visuals: config.ignore_visuals,
281 ignore_stylesheets: config.ignore_stylesheets,
282 ignore_javascript: config.ignore_javascript,
283 ignore_analytics: config.ignore_analytics,
284 ignore_prefetch: config.ignore_prefetch,
285 ignore_ads: config.ignore_ads,
286 extra_headers: config.extra_headers.clone(),
287 only_html: config.only_html,
288 service_worker_enabled: config.service_worker_enabled,
289 created_first_target: false,
290 intercept_manager: config.intercept_manager,
291 max_bytes_allowed: config.max_bytes_allowed,
292 whitelist_patterns: config.whitelist_patterns.clone(),
293 blacklist_patterns: config.blacklist_patterns.clone(),
294 #[cfg(feature = "adblock")]
295 adblock_filter_rules: config.adblock_filter_rules.clone(),
296 channel_capacity: config.channel_capacity,
297 connection_retries: config.connection_retries,
298 };
299
300 let fut = Handler::new(conn, rx, handler_config);
301 let browser_context = fut.default_browser_context().clone();
302
303 let browser = Self {
304 sender: tx,
305 config: Some(config),
306 child: Some(child),
307 debug_ws_url,
308 browser_context,
309 };
310
311 Ok((browser, fut))
312 }
313
314 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
324 let (tx, rx) = oneshot_channel();
325
326 self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
327
328 rx.await?
329 }
330
331 pub async fn close(&self) -> Result<CloseReturns> {
338 let (tx, rx) = oneshot_channel();
339
340 self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
341
342 rx.await?
343 }
344
345 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
354 if let Some(child) = self.child.as_mut() {
355 Ok(Some(child.wait().await?))
356 } else {
357 Ok(None)
358 }
359 }
360
361 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
370 if let Some(child) = self.child.as_mut() {
371 child.try_wait()
372 } else {
373 Ok(None)
374 }
375 }
376
377 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
388 self.child.as_mut()
389 }
390
391 pub fn has_child(&self) -> bool {
393 self.child.is_some()
394 }
395
396 pub async fn kill(&mut self) -> Option<io::Result<()>> {
407 match self.child.as_mut() {
408 Some(child) => Some(child.kill().await),
409 None => None,
410 }
411 }
412
413 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
419 if !self.is_incognito_configured() {
420 let browser_context_id = self
421 .create_browser_context(CreateBrowserContextParams::default())
422 .await?;
423 self.browser_context = BrowserContext::from(browser_context_id);
424 self.sender
425 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
426 .await?;
427 }
428
429 Ok(self)
430 }
431
432 pub async fn quit_incognito_context_base(
438 &self,
439 browser_context_id: BrowserContextId,
440 ) -> Result<&Self> {
441 self.dispose_browser_context(browser_context_id.clone())
442 .await?;
443 self.sender
444 .send(HandlerMessage::DisposeContext(BrowserContext::from(
445 browser_context_id,
446 )))
447 .await?;
448 Ok(self)
449 }
450
451 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
457 if let Some(id) = self.browser_context.take() {
458 let _ = self.quit_incognito_context_base(id).await;
459 }
460 Ok(self)
461 }
462
463 fn is_incognito_configured(&self) -> bool {
465 self.config
466 .as_ref()
467 .map(|c| c.incognito)
468 .unwrap_or_default()
469 }
470
471 pub fn websocket_address(&self) -> &String {
473 &self.debug_ws_url
474 }
475
476 pub fn is_incognito(&self) -> bool {
478 self.is_incognito_configured() || self.browser_context.is_incognito()
479 }
480
481 pub fn config(&self) -> Option<&BrowserConfig> {
483 self.config.as_ref()
484 }
485
486 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
488 let (tx, rx) = oneshot_channel();
489 let mut params = params.into();
490
491 if let Some(id) = self.browser_context.id() {
492 if params.browser_context_id.is_none() {
493 params.browser_context_id = Some(id.clone());
494 }
495 }
496
497 let _ = self
498 .sender
499 .send(HandlerMessage::CreatePage(params, tx))
500 .await;
501
502 rx.await?
503 }
504
505 pub async fn version(&self) -> Result<GetVersionReturns> {
507 Ok(self.execute(GetVersionParams::default()).await?.result)
508 }
509
510 pub async fn user_agent(&self) -> Result<String> {
512 Ok(self.version().await?.user_agent)
513 }
514
515 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
517 let (tx, rx) = oneshot_channel();
518 let method = cmd.identifier();
519 let msg = CommandMessage::new(cmd, tx)?;
520
521 self.sender.send(HandlerMessage::Command(msg)).await?;
522 let resp = rx.await??;
523 to_command_response::<T>(resp, method)
524 }
525
526 pub async fn set_permission(
530 &self,
531 permission: PermissionDescriptor,
532 setting: PermissionSetting,
533 origin: Option<impl Into<String>>,
534 embedded_origin: Option<impl Into<String>>,
535 browser_context_id: Option<BrowserContextId>,
536 ) -> Result<&Self> {
537 self.execute(SetPermissionParams {
538 permission,
539 setting,
540 origin: origin.map(Into::into),
541 embedded_origin: embedded_origin.map(Into::into),
542 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
543 })
544 .await?;
545 Ok(self)
546 }
547
548 pub async fn set_permission_for_origin(
550 &self,
551 origin: impl Into<String>,
552 embedded_origin: Option<impl Into<String>>,
553 permission: PermissionDescriptor,
554 setting: PermissionSetting,
555 ) -> Result<&Self> {
556 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
557 .await
558 }
559
560 pub async fn reset_permission_for_origin(
562 &self,
563 origin: impl Into<String>,
564 embedded_origin: Option<impl Into<String>>,
565 permission: PermissionDescriptor,
566 ) -> Result<&Self> {
567 self.set_permission_for_origin(
568 origin,
569 embedded_origin,
570 permission,
571 PermissionSetting::Prompt,
572 )
573 .await
574 }
575
576 pub async fn grant_all_permission_for_origin(
578 &self,
579 origin: impl Into<String>,
580 embedded_origin: Option<impl Into<String>>,
581 permission: PermissionDescriptor,
582 ) -> Result<&Self> {
583 self.set_permission_for_origin(
584 origin,
585 embedded_origin,
586 permission,
587 PermissionSetting::Granted,
588 )
589 .await
590 }
591
592 pub async fn deny_all_permission_for_origin(
594 &self,
595 origin: impl Into<String>,
596 embedded_origin: Option<impl Into<String>>,
597 permission: PermissionDescriptor,
598 ) -> Result<&Self> {
599 self.set_permission_for_origin(
600 origin,
601 embedded_origin,
602 permission,
603 PermissionSetting::Denied,
604 )
605 .await
606 }
607
608 pub async fn pages(&self) -> Result<Vec<Page>> {
610 let (tx, rx) = oneshot_channel();
611 self.sender.send(HandlerMessage::GetPages(tx)).await?;
612 Ok(rx.await?)
613 }
614
615 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
617 let (tx, rx) = oneshot_channel();
618 self.sender
619 .send(HandlerMessage::GetPage(target_id, tx))
620 .await?;
621 rx.await?.ok_or(CdpError::NotFound)
622 }
623
624 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
626 let (tx, rx) = unbounded_channel();
627 self.sender
628 .send(HandlerMessage::AddEventListener(
629 EventListenerRequest::new::<T>(tx),
630 ))
631 .await?;
632
633 Ok(EventStream::new(rx))
634 }
635
636 pub async fn create_browser_context(
638 &mut self,
639 params: CreateBrowserContextParams,
640 ) -> Result<BrowserContextId> {
641 let response = self.execute(params).await?;
642
643 Ok(response.result.browser_context_id)
644 }
645
646 pub async fn get_browser_contexts(
648 &mut self,
649 params: GetBrowserContextsParams,
650 ) -> Result<GetBrowserContextsReturns> {
651 let response = self.execute(params).await?;
652 Ok(response.result)
653 }
654
655 pub async fn send_new_context(
657 &mut self,
658 browser_context_id: BrowserContextId,
659 ) -> Result<&Self> {
660 self.browser_context = BrowserContext::from(browser_context_id);
661 self.sender
662 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
663 .await?;
664 Ok(self)
665 }
666
667 pub async fn dispose_browser_context(
669 &self,
670 browser_context_id: impl Into<BrowserContextId>,
671 ) -> Result<&Self> {
672 self.execute(DisposeBrowserContextParams::new(browser_context_id))
673 .await?;
674
675 Ok(self)
676 }
677
678 pub async fn clear_cookies(&self) -> Result<&Self> {
680 self.execute(ClearCookiesParams::default()).await?;
681 Ok(self)
682 }
683
684 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
686 let cmd = GetCookiesParams {
687 browser_context_id: self.browser_context.id.clone(),
688 };
689
690 Ok(self.execute(cmd).await?.result.cookies)
691 }
692
693 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
695 for cookie in &mut cookies {
696 if let Some(url) = cookie.url.as_ref() {
697 crate::page::validate_cookie_url(url)?;
698 }
699 }
700
701 let mut cookies_param = SetCookiesParams::new(cookies);
702
703 cookies_param.browser_context_id = self.browser_context.id.clone();
704
705 self.execute(cookies_param).await?;
706 Ok(self)
707 }
708}
709
710impl Drop for Browser {
711 fn drop(&mut self) {
712 if let Some(child) = self.child.as_mut() {
713 if let Ok(Some(_)) = child.try_wait() {
714 } else {
716 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
724 }
725 }
726 }
727}
728
729async fn ws_url_from_output(
739 child_process: &mut Child,
740 timeout_fut: impl Future<Output = ()> + Unpin,
741) -> Result<String> {
742 use tokio::io::AsyncBufReadExt;
743 let stderr = match child_process.stderr.take() {
744 Some(stderr) => stderr,
745 None => {
746 return Err(CdpError::LaunchIo(
747 io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
748 BrowserStderr::new(Vec::new()),
749 ));
750 }
751 };
752 let mut stderr_bytes = Vec::<u8>::new();
753 let mut buf = tokio::io::BufReader::new(stderr);
754 let mut timeout_fut = timeout_fut;
755 loop {
756 tokio::select! {
757 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
758 exit_status = child_process.wait() => {
759 return Err(match exit_status {
760 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
761 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
762 })
763 },
764 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
765 match read_res {
766 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
767 Ok(byte_count) => {
768 if byte_count == 0 {
769 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
770 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
771 }
772 let start_offset = stderr_bytes.len() - byte_count;
773 let new_bytes = &stderr_bytes[start_offset..];
774 match std::str::from_utf8(new_bytes) {
775 Err(_) => {
776 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
777 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
778 }
779 Ok(line) => {
780 if let Some((_, ws)) = line.rsplit_once("listening on ") {
781 if ws.starts_with("ws") && ws.contains("devtools/browser") {
782 return Ok(ws.trim().to_string());
783 }
784 }
785 }
786 }
787 }
788 }
789 }
790 }
791 }
792}
793
794#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
795pub enum HeadlessMode {
796 False,
798 #[default]
800 True,
801 New,
803}
804
805#[derive(Debug, Clone, Default)]
806pub struct BrowserConfig {
807 headless: HeadlessMode,
810 sandbox: bool,
812 window_size: Option<(u32, u32)>,
814 port: u16,
816 executable: std::path::PathBuf,
821
822 extensions: Vec<String>,
830
831 pub process_envs: Option<HashMap<String, String>>,
834
835 pub user_data_dir: Option<PathBuf>,
837
838 incognito: bool,
840
841 launch_timeout: Duration,
843
844 ignore_https_errors: bool,
846 pub viewport: Option<Viewport>,
847 request_timeout: Duration,
849
850 args: Vec<String>,
852
853 disable_default_args: bool,
855
856 pub request_intercept: bool,
858
859 pub cache_enabled: bool,
861 pub service_worker_enabled: bool,
864 pub ignore_visuals: bool,
867 pub ignore_stylesheets: bool,
870 pub ignore_javascript: bool,
873 pub ignore_analytics: bool,
875 pub ignore_prefetch: bool,
877 pub ignore_ads: bool,
879 pub extra_headers: Option<std::collections::HashMap<String, String>>,
881 pub only_html: bool,
883 pub intercept_manager: NetworkInterceptManager,
885 pub max_bytes_allowed: Option<u64>,
887 pub whitelist_patterns: Option<Vec<String>>,
889 pub blacklist_patterns: Option<Vec<String>>,
891 #[cfg(feature = "adblock")]
895 pub adblock_filter_rules: Option<Vec<String>>,
896 pub channel_capacity: usize,
899 pub connection_retries: u32,
902}
903
904#[derive(Debug, Clone)]
905pub struct BrowserConfigBuilder {
906 headless: HeadlessMode,
908 sandbox: bool,
910 window_size: Option<(u32, u32)>,
912 port: u16,
914 executable: Option<PathBuf>,
917 executation_detection: DetectionOptions,
919 extensions: Vec<String>,
921 process_envs: Option<HashMap<String, String>>,
923 user_data_dir: Option<PathBuf>,
925 incognito: bool,
927 launch_timeout: Duration,
929 ignore_https_errors: bool,
931 viewport: Option<Viewport>,
933 request_timeout: Duration,
935 args: Vec<String>,
937 disable_default_args: bool,
939 request_intercept: bool,
941 cache_enabled: bool,
943 service_worker_enabled: bool,
945 ignore_visuals: bool,
947 ignore_ads: bool,
949 ignore_javascript: bool,
951 ignore_stylesheets: bool,
953 ignore_prefetch: bool,
955 ignore_analytics: bool,
957 only_html: bool,
959 extra_headers: Option<std::collections::HashMap<String, String>>,
961 intercept_manager: NetworkInterceptManager,
963 max_bytes_allowed: Option<u64>,
965 whitelist_patterns: Option<Vec<String>>,
967 blacklist_patterns: Option<Vec<String>>,
969 #[cfg(feature = "adblock")]
971 adblock_filter_rules: Option<Vec<String>>,
972 channel_capacity: usize,
974 connection_retries: u32,
976}
977
978impl BrowserConfig {
979 pub fn builder() -> BrowserConfigBuilder {
981 BrowserConfigBuilder::default()
982 }
983
984 pub fn with_executable(path: impl AsRef<Path>) -> Self {
986 Self::builder().chrome_executable(path).build().unwrap()
989 }
990}
991
992impl Default for BrowserConfigBuilder {
993 fn default() -> Self {
994 Self {
995 headless: HeadlessMode::True,
996 sandbox: true,
997 window_size: None,
998 port: 0,
999 executable: None,
1000 executation_detection: DetectionOptions::default(),
1001 extensions: Vec::new(),
1002 process_envs: None,
1003 user_data_dir: None,
1004 incognito: false,
1005 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1006 ignore_https_errors: true,
1007 viewport: Some(Default::default()),
1008 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1009 args: Vec::new(),
1010 disable_default_args: false,
1011 request_intercept: false,
1012 cache_enabled: true,
1013 ignore_visuals: false,
1014 ignore_ads: false,
1015 ignore_javascript: false,
1016 ignore_analytics: false,
1017 ignore_stylesheets: false,
1018 ignore_prefetch: true,
1019 only_html: false,
1020 extra_headers: Default::default(),
1021 service_worker_enabled: true,
1022 intercept_manager: NetworkInterceptManager::Unknown,
1023 max_bytes_allowed: None,
1024 whitelist_patterns: None,
1025 blacklist_patterns: None,
1026 #[cfg(feature = "adblock")]
1027 adblock_filter_rules: None,
1028 channel_capacity: 1000,
1029 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1030 }
1031 }
1032}
1033
1034impl BrowserConfigBuilder {
1035 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1037 self.window_size = Some((width, height));
1038 self
1039 }
1040 pub fn no_sandbox(mut self) -> Self {
1042 self.sandbox = false;
1043 self
1044 }
1045 pub fn with_head(mut self) -> Self {
1047 self.headless = HeadlessMode::False;
1048 self
1049 }
1050 pub fn new_headless_mode(mut self) -> Self {
1052 self.headless = HeadlessMode::New;
1053 self
1054 }
1055 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1057 self.headless = mode;
1058 self
1059 }
1060 pub fn incognito(mut self) -> Self {
1062 self.incognito = true;
1063 self
1064 }
1065
1066 pub fn respect_https_errors(mut self) -> Self {
1067 self.ignore_https_errors = false;
1068 self
1069 }
1070
1071 pub fn port(mut self, port: u16) -> Self {
1072 self.port = port;
1073 self
1074 }
1075
1076 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1077 self.max_bytes_allowed = max_bytes_allowed;
1078 self
1079 }
1080
1081 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1082 self.launch_timeout = timeout;
1083 self
1084 }
1085
1086 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1087 self.request_timeout = timeout;
1088 self
1089 }
1090
1091 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1097 self.viewport = viewport.into();
1098 self
1099 }
1100
1101 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1102 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1103 self
1104 }
1105
1106 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1107 self.executable = Some(path.as_ref().to_path_buf());
1108 self
1109 }
1110
1111 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1112 self.executation_detection = options;
1113 self
1114 }
1115
1116 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1117 self.extensions.push(extension.into());
1118 self
1119 }
1120
1121 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1122 where
1123 I: IntoIterator<Item = S>,
1124 S: Into<String>,
1125 {
1126 for ext in extensions {
1127 self.extensions.push(ext.into());
1128 }
1129 self
1130 }
1131
1132 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1133 self.process_envs
1134 .get_or_insert(HashMap::new())
1135 .insert(key.into(), val.into());
1136 self
1137 }
1138
1139 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1140 where
1141 I: IntoIterator<Item = (K, V)>,
1142 K: Into<String>,
1143 V: Into<String>,
1144 {
1145 self.process_envs
1146 .get_or_insert(HashMap::new())
1147 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1148 self
1149 }
1150
1151 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1152 self.args.push(arg.into());
1153 self
1154 }
1155
1156 pub fn args<I, S>(mut self, args: I) -> Self
1157 where
1158 I: IntoIterator<Item = S>,
1159 S: Into<String>,
1160 {
1161 for arg in args {
1162 self.args.push(arg.into());
1163 }
1164 self
1165 }
1166
1167 pub fn disable_default_args(mut self) -> Self {
1168 self.disable_default_args = true;
1169 self
1170 }
1171
1172 pub fn enable_request_intercept(mut self) -> Self {
1173 self.request_intercept = true;
1174 self
1175 }
1176
1177 pub fn disable_request_intercept(mut self) -> Self {
1178 self.request_intercept = false;
1179 self
1180 }
1181
1182 pub fn enable_cache(mut self) -> Self {
1183 self.cache_enabled = true;
1184 self
1185 }
1186
1187 pub fn disable_cache(mut self) -> Self {
1188 self.cache_enabled = false;
1189 self
1190 }
1191
1192 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1194 self.service_worker_enabled = bypass;
1195 self
1196 }
1197
1198 pub fn set_extra_headers(
1200 mut self,
1201 headers: Option<std::collections::HashMap<String, String>>,
1202 ) -> Self {
1203 self.extra_headers = headers;
1204 self
1205 }
1206
1207 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1209 self.whitelist_patterns = whitelist_patterns;
1210 self
1211 }
1212
1213 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1215 self.blacklist_patterns = blacklist_patterns;
1216 self
1217 }
1218
1219 #[cfg(feature = "adblock")]
1222 pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1223 self.adblock_filter_rules = Some(rules);
1224 self
1225 }
1226
1227 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1230 self.channel_capacity = capacity;
1231 self
1232 }
1233
1234 pub fn connection_retries(mut self, retries: u32) -> Self {
1237 self.connection_retries = retries;
1238 self
1239 }
1240
1241 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1243 let executable = if let Some(e) = self.executable {
1244 e
1245 } else {
1246 detection::default_executable(self.executation_detection)?
1247 };
1248
1249 Ok(BrowserConfig {
1250 headless: self.headless,
1251 sandbox: self.sandbox,
1252 window_size: self.window_size,
1253 port: self.port,
1254 executable,
1255 extensions: self.extensions,
1256 process_envs: self.process_envs,
1257 user_data_dir: self.user_data_dir,
1258 incognito: self.incognito,
1259 launch_timeout: self.launch_timeout,
1260 ignore_https_errors: self.ignore_https_errors,
1261 viewport: self.viewport,
1262 request_timeout: self.request_timeout,
1263 args: self.args,
1264 disable_default_args: self.disable_default_args,
1265 request_intercept: self.request_intercept,
1266 cache_enabled: self.cache_enabled,
1267 ignore_visuals: self.ignore_visuals,
1268 ignore_ads: self.ignore_ads,
1269 ignore_javascript: self.ignore_javascript,
1270 ignore_analytics: self.ignore_analytics,
1271 ignore_stylesheets: self.ignore_stylesheets,
1272 ignore_prefetch: self.ignore_prefetch,
1273 extra_headers: self.extra_headers,
1274 only_html: self.only_html,
1275 intercept_manager: self.intercept_manager,
1276 service_worker_enabled: self.service_worker_enabled,
1277 max_bytes_allowed: self.max_bytes_allowed,
1278 whitelist_patterns: self.whitelist_patterns,
1279 blacklist_patterns: self.blacklist_patterns,
1280 #[cfg(feature = "adblock")]
1281 adblock_filter_rules: self.adblock_filter_rules,
1282 channel_capacity: self.channel_capacity,
1283 connection_retries: self.connection_retries,
1284 })
1285 }
1286}
1287
1288impl BrowserConfig {
1289 pub fn launch(&self) -> io::Result<Child> {
1290 let mut cmd = async_process::Command::new(&self.executable);
1291
1292 if self.disable_default_args {
1293 cmd.args(&self.args);
1294 } else {
1295 cmd.args(DEFAULT_ARGS).args(&self.args);
1296 }
1297
1298 if !self
1299 .args
1300 .iter()
1301 .any(|arg| arg.contains("--remote-debugging-port="))
1302 {
1303 cmd.arg(format!("--remote-debugging-port={}", self.port));
1304 }
1305
1306 cmd.args(
1307 self.extensions
1308 .iter()
1309 .map(|e| format!("--load-extension={e}")),
1310 );
1311
1312 if let Some(ref user_data) = self.user_data_dir {
1313 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1314 } else {
1315 cmd.arg(format!(
1319 "--user-data-dir={}",
1320 std::env::temp_dir().join("chromiumoxide-runner").display()
1321 ));
1322 }
1323
1324 if let Some((width, height)) = self.window_size {
1325 cmd.arg(format!("--window-size={width},{height}"));
1326 }
1327
1328 if !self.sandbox {
1329 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1330 }
1331
1332 match self.headless {
1333 HeadlessMode::False => (),
1334 HeadlessMode::True => {
1335 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1336 }
1337 HeadlessMode::New => {
1338 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1339 }
1340 }
1341
1342 if self.incognito {
1343 cmd.arg("--incognito");
1344 }
1345
1346 if let Some(ref envs) = self.process_envs {
1347 cmd.envs(envs);
1348 }
1349 cmd.stderr(Stdio::piped()).spawn()
1350 }
1351}
1352
1353#[deprecated(note = "Use detection::default_executable instead")]
1362pub fn default_executable() -> Result<std::path::PathBuf, String> {
1363 let options = DetectionOptions {
1364 msedge: false,
1365 unstable: false,
1366 };
1367 detection::default_executable(options)
1368}
1369
1370static DEFAULT_ARGS: [&str; 26] = [
1373 "--disable-background-networking",
1374 "--enable-features=NetworkService,NetworkServiceInProcess",
1375 "--disable-background-timer-throttling",
1376 "--disable-backgrounding-occluded-windows",
1377 "--disable-breakpad",
1378 "--disable-client-side-phishing-detection",
1379 "--disable-component-extensions-with-background-pages",
1380 "--disable-default-apps",
1381 "--disable-dev-shm-usage",
1382 "--disable-extensions",
1383 "--disable-features=TranslateUI",
1384 "--disable-hang-monitor",
1385 "--disable-ipc-flooding-protection",
1386 "--disable-popup-blocking",
1387 "--disable-prompt-on-repost",
1388 "--disable-renderer-backgrounding",
1389 "--disable-sync",
1390 "--force-color-profile=srgb",
1391 "--metrics-recording-only",
1392 "--no-first-run",
1393 "--enable-automation",
1394 "--password-store=basic",
1395 "--use-mock-keychain",
1396 "--enable-blink-features=IdleDetection",
1397 "--lang=en_US",
1398 "--disable-blink-features=AutomationControlled",
1399];