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 * 3u64.saturating_pow(attempt);
146 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
147 }
148 };
149
150 match REQUEST_CLIENT.get(&version_url).send().await {
151 Ok(req) => match req.bytes().await {
152 Ok(b) => {
153 match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
154 Ok(connection)
155 if !connection.web_socket_debugger_url.is_empty() =>
156 {
157 debug_ws_url = connection.web_socket_debugger_url;
158 discovered = true;
159 break;
160 }
161 _ => {
162 retry().await;
164 }
165 }
166 }
167 Err(_) => {
168 retry().await;
169 }
170 },
171 Err(_) => {
172 retry().await;
173 }
174 }
175 }
176
177 if !discovered {
178 return Err(CdpError::NoResponse);
179 }
180 }
181
182 let conn =
183 Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
184
185 let (tx, rx) = channel(config.channel_capacity);
186
187 let handler_config = BrowserConfig {
188 ignore_https_errors: config.ignore_https_errors,
189 viewport: config.viewport.clone(),
190 request_timeout: config.request_timeout,
191 request_intercept: config.request_intercept,
192 cache_enabled: config.cache_enabled,
193 ignore_visuals: config.ignore_visuals,
194 ignore_stylesheets: config.ignore_stylesheets,
195 ignore_javascript: config.ignore_javascript,
196 ignore_analytics: config.ignore_analytics,
197 ignore_prefetch: config.ignore_prefetch,
198 ignore_ads: config.ignore_ads,
199 extra_headers: config.extra_headers.clone(),
200 only_html: config.only_html,
201 service_worker_enabled: config.service_worker_enabled,
202 intercept_manager: config.intercept_manager,
203 max_bytes_allowed: config.max_bytes_allowed,
204 max_redirects: config.max_redirects,
205 max_main_frame_navigations: config.max_main_frame_navigations,
206 whitelist_patterns: config.whitelist_patterns.clone(),
207 blacklist_patterns: config.blacklist_patterns.clone(),
208 ..Default::default()
209 };
210
211 let fut = Handler::new(conn, rx, config);
212 let browser_context = fut.default_browser_context().clone();
213
214 let browser = Self {
215 sender: tx,
216 config: Some(handler_config),
217 child: None,
218 debug_ws_url,
219 browser_context,
220 };
221
222 Ok((browser, fut))
223 }
224
225 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
234 config.executable = utils::canonicalize_except_snap(config.executable).await?;
236
237 let mut child = config.launch()?;
239
240 async fn with_child(
245 config: &BrowserConfig,
246 child: &mut Child,
247 ) -> Result<(String, Connection<CdpEventMessage>)> {
248 let dur = config.launch_timeout;
249 let timeout_fut = Box::pin(tokio::time::sleep(dur));
250
251 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
253 let conn = Connection::<CdpEventMessage>::connect_with_retries(
254 &debug_ws_url,
255 config.connection_retries,
256 )
257 .await?;
258 Ok((debug_ws_url, conn))
259 }
260
261 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
262 Ok(conn) => conn,
263 Err(e) => {
264 if let Ok(Some(_)) = child.try_wait() {
266 } else {
268 let _ = child.kill().await;
270 let _ = child.wait().await;
271 }
272 return Err(e);
273 }
274 };
275
276 let (tx, rx) = channel(config.channel_capacity);
280
281 let handler_config = HandlerConfig {
282 ignore_https_errors: config.ignore_https_errors,
283 viewport: config.viewport.clone(),
284 context_ids: Vec::new(),
285 request_timeout: config.request_timeout,
286 request_intercept: config.request_intercept,
287 cache_enabled: config.cache_enabled,
288 ignore_visuals: config.ignore_visuals,
289 ignore_stylesheets: config.ignore_stylesheets,
290 ignore_javascript: config.ignore_javascript,
291 ignore_analytics: config.ignore_analytics,
292 ignore_prefetch: config.ignore_prefetch,
293 ignore_ads: config.ignore_ads,
294 extra_headers: config.extra_headers.clone(),
295 only_html: config.only_html,
296 service_worker_enabled: config.service_worker_enabled,
297 created_first_target: false,
298 intercept_manager: config.intercept_manager,
299 max_bytes_allowed: config.max_bytes_allowed,
300 max_redirects: config.max_redirects,
301 max_main_frame_navigations: config.max_main_frame_navigations,
302 whitelist_patterns: config.whitelist_patterns.clone(),
303 blacklist_patterns: config.blacklist_patterns.clone(),
304 #[cfg(feature = "adblock")]
305 adblock_filter_rules: config.adblock_filter_rules.clone(),
306 channel_capacity: config.channel_capacity,
307 connection_retries: config.connection_retries,
308 };
309
310 let fut = Handler::new(conn, rx, handler_config);
311 let browser_context = fut.default_browser_context().clone();
312
313 let browser = Self {
314 sender: tx,
315 config: Some(config),
316 child: Some(child),
317 debug_ws_url,
318 browser_context,
319 };
320
321 Ok((browser, fut))
322 }
323
324 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
334 let (tx, rx) = oneshot_channel();
335
336 self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
337
338 rx.await?
339 }
340
341 pub async fn close(&self) -> Result<CloseReturns> {
348 let (tx, rx) = oneshot_channel();
349
350 self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
351
352 rx.await?
353 }
354
355 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
364 if let Some(child) = self.child.as_mut() {
365 Ok(Some(child.wait().await?))
366 } else {
367 Ok(None)
368 }
369 }
370
371 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
380 if let Some(child) = self.child.as_mut() {
381 child.try_wait()
382 } else {
383 Ok(None)
384 }
385 }
386
387 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
398 self.child.as_mut()
399 }
400
401 pub fn has_child(&self) -> bool {
403 self.child.is_some()
404 }
405
406 pub async fn kill(&mut self) -> Option<io::Result<()>> {
417 match self.child.as_mut() {
418 Some(child) => Some(child.kill().await),
419 None => None,
420 }
421 }
422
423 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
429 if !self.is_incognito_configured() {
430 let browser_context_id = self
431 .create_browser_context(CreateBrowserContextParams::default())
432 .await?;
433 self.browser_context = BrowserContext::from(browser_context_id);
434 self.sender
435 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
436 .await?;
437 }
438
439 Ok(self)
440 }
441
442 pub async fn quit_incognito_context_base(
448 &self,
449 browser_context_id: BrowserContextId,
450 ) -> Result<&Self> {
451 self.dispose_browser_context(browser_context_id.clone())
452 .await?;
453 self.sender
454 .send(HandlerMessage::DisposeContext(BrowserContext::from(
455 browser_context_id,
456 )))
457 .await?;
458 Ok(self)
459 }
460
461 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
467 if let Some(id) = self.browser_context.take() {
468 let _ = self.quit_incognito_context_base(id).await;
469 }
470 Ok(self)
471 }
472
473 fn is_incognito_configured(&self) -> bool {
475 self.config
476 .as_ref()
477 .map(|c| c.incognito)
478 .unwrap_or_default()
479 }
480
481 pub fn websocket_address(&self) -> &String {
483 &self.debug_ws_url
484 }
485
486 pub fn is_incognito(&self) -> bool {
488 self.is_incognito_configured() || self.browser_context.is_incognito()
489 }
490
491 pub fn config(&self) -> Option<&BrowserConfig> {
493 self.config.as_ref()
494 }
495
496 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
498 let (tx, rx) = oneshot_channel();
499 let mut params = params.into();
500
501 if let Some(id) = self.browser_context.id() {
502 if params.browser_context_id.is_none() {
503 params.browser_context_id = Some(id.clone());
504 }
505 }
506
507 let _ = self
508 .sender
509 .send(HandlerMessage::CreatePage(params, tx))
510 .await;
511
512 rx.await?
513 }
514
515 pub async fn version(&self) -> Result<GetVersionReturns> {
517 Ok(self.execute(GetVersionParams::default()).await?.result)
518 }
519
520 pub async fn user_agent(&self) -> Result<String> {
522 Ok(self.version().await?.user_agent)
523 }
524
525 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
527 let (tx, rx) = oneshot_channel();
528 let method = cmd.identifier();
529 let msg = CommandMessage::new(cmd, tx)?;
530
531 self.sender.send(HandlerMessage::Command(msg)).await?;
532 let resp = rx.await??;
533 to_command_response::<T>(resp, method)
534 }
535
536 pub async fn set_permission(
540 &self,
541 permission: PermissionDescriptor,
542 setting: PermissionSetting,
543 origin: Option<impl Into<String>>,
544 embedded_origin: Option<impl Into<String>>,
545 browser_context_id: Option<BrowserContextId>,
546 ) -> Result<&Self> {
547 self.execute(SetPermissionParams {
548 permission,
549 setting,
550 origin: origin.map(Into::into),
551 embedded_origin: embedded_origin.map(Into::into),
552 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
553 })
554 .await?;
555 Ok(self)
556 }
557
558 pub async fn set_permission_for_origin(
560 &self,
561 origin: impl Into<String>,
562 embedded_origin: Option<impl Into<String>>,
563 permission: PermissionDescriptor,
564 setting: PermissionSetting,
565 ) -> Result<&Self> {
566 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
567 .await
568 }
569
570 pub async fn reset_permission_for_origin(
572 &self,
573 origin: impl Into<String>,
574 embedded_origin: Option<impl Into<String>>,
575 permission: PermissionDescriptor,
576 ) -> Result<&Self> {
577 self.set_permission_for_origin(
578 origin,
579 embedded_origin,
580 permission,
581 PermissionSetting::Prompt,
582 )
583 .await
584 }
585
586 pub async fn grant_all_permission_for_origin(
588 &self,
589 origin: impl Into<String>,
590 embedded_origin: Option<impl Into<String>>,
591 permission: PermissionDescriptor,
592 ) -> Result<&Self> {
593 self.set_permission_for_origin(
594 origin,
595 embedded_origin,
596 permission,
597 PermissionSetting::Granted,
598 )
599 .await
600 }
601
602 pub async fn deny_all_permission_for_origin(
604 &self,
605 origin: impl Into<String>,
606 embedded_origin: Option<impl Into<String>>,
607 permission: PermissionDescriptor,
608 ) -> Result<&Self> {
609 self.set_permission_for_origin(
610 origin,
611 embedded_origin,
612 permission,
613 PermissionSetting::Denied,
614 )
615 .await
616 }
617
618 pub async fn pages(&self) -> Result<Vec<Page>> {
620 let (tx, rx) = oneshot_channel();
621 self.sender.send(HandlerMessage::GetPages(tx)).await?;
622 Ok(rx.await?)
623 }
624
625 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
627 let (tx, rx) = oneshot_channel();
628 self.sender
629 .send(HandlerMessage::GetPage(target_id, tx))
630 .await?;
631 rx.await?.ok_or(CdpError::NotFound)
632 }
633
634 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
636 let (tx, rx) = unbounded_channel();
637 self.sender
638 .send(HandlerMessage::AddEventListener(
639 EventListenerRequest::new::<T>(tx),
640 ))
641 .await?;
642
643 Ok(EventStream::new(rx))
644 }
645
646 pub async fn create_browser_context(
648 &mut self,
649 params: CreateBrowserContextParams,
650 ) -> Result<BrowserContextId> {
651 let response = self.execute(params).await?;
652
653 Ok(response.result.browser_context_id)
654 }
655
656 pub async fn get_browser_contexts(
658 &mut self,
659 params: GetBrowserContextsParams,
660 ) -> Result<GetBrowserContextsReturns> {
661 let response = self.execute(params).await?;
662 Ok(response.result)
663 }
664
665 pub async fn send_new_context(
667 &mut self,
668 browser_context_id: BrowserContextId,
669 ) -> Result<&Self> {
670 self.browser_context = BrowserContext::from(browser_context_id);
671 self.sender
672 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
673 .await?;
674 Ok(self)
675 }
676
677 pub async fn dispose_browser_context(
679 &self,
680 browser_context_id: impl Into<BrowserContextId>,
681 ) -> Result<&Self> {
682 self.execute(DisposeBrowserContextParams::new(browser_context_id))
683 .await?;
684
685 Ok(self)
686 }
687
688 pub async fn clear_cookies(&self) -> Result<&Self> {
690 self.execute(ClearCookiesParams::default()).await?;
691 Ok(self)
692 }
693
694 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
696 let cmd = GetCookiesParams {
697 browser_context_id: self.browser_context.id.clone(),
698 };
699
700 Ok(self.execute(cmd).await?.result.cookies)
701 }
702
703 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
705 for cookie in &mut cookies {
706 if let Some(url) = cookie.url.as_ref() {
707 crate::page::validate_cookie_url(url)?;
708 }
709 }
710
711 let mut cookies_param = SetCookiesParams::new(cookies);
712
713 cookies_param.browser_context_id = self.browser_context.id.clone();
714
715 self.execute(cookies_param).await?;
716 Ok(self)
717 }
718}
719
720impl Drop for Browser {
721 fn drop(&mut self) {
722 if let Some(child) = self.child.as_mut() {
723 if let Ok(Some(_)) = child.try_wait() {
724 } else {
726 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
734 }
735 }
736 }
737}
738
739async fn ws_url_from_output(
749 child_process: &mut Child,
750 timeout_fut: impl Future<Output = ()> + Unpin,
751) -> Result<String> {
752 use tokio::io::AsyncBufReadExt;
753 let stderr = match child_process.stderr.take() {
754 Some(stderr) => stderr,
755 None => {
756 return Err(CdpError::LaunchIo(
757 io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
758 BrowserStderr::new(Vec::new()),
759 ));
760 }
761 };
762 let mut stderr_bytes = Vec::<u8>::new();
763 let mut buf = tokio::io::BufReader::new(stderr);
764 let mut timeout_fut = timeout_fut;
765 loop {
766 tokio::select! {
767 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
768 exit_status = child_process.wait() => {
769 return Err(match exit_status {
770 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
771 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
772 })
773 },
774 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
775 match read_res {
776 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
777 Ok(byte_count) => {
778 if byte_count == 0 {
779 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
780 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
781 }
782 let start_offset = stderr_bytes.len() - byte_count;
783 let new_bytes = &stderr_bytes[start_offset..];
784 match std::str::from_utf8(new_bytes) {
785 Err(_) => {
786 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
787 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
788 }
789 Ok(line) => {
790 if let Some((_, ws)) = line.rsplit_once("listening on ") {
791 if ws.starts_with("ws") && ws.contains("devtools/browser") {
792 return Ok(ws.trim().to_string());
793 }
794 }
795 }
796 }
797 }
798 }
799 }
800 }
801 }
802}
803
804#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
805pub enum HeadlessMode {
806 False,
808 #[default]
810 True,
811 New,
813}
814
815#[derive(Debug, Clone, Default)]
816pub struct BrowserConfig {
817 headless: HeadlessMode,
820 sandbox: bool,
822 window_size: Option<(u32, u32)>,
824 port: u16,
826 executable: std::path::PathBuf,
831
832 extensions: Vec<String>,
840
841 pub process_envs: Option<HashMap<String, String>>,
844
845 pub user_data_dir: Option<PathBuf>,
847
848 incognito: bool,
850
851 launch_timeout: Duration,
853
854 ignore_https_errors: bool,
856 pub viewport: Option<Viewport>,
857 request_timeout: Duration,
859
860 args: Vec<String>,
862
863 disable_default_args: bool,
865
866 pub request_intercept: bool,
868
869 pub cache_enabled: bool,
871 pub service_worker_enabled: bool,
874 pub ignore_visuals: bool,
877 pub ignore_stylesheets: bool,
880 pub ignore_javascript: bool,
883 pub ignore_analytics: bool,
885 pub ignore_prefetch: bool,
887 pub ignore_ads: bool,
889 pub extra_headers: Option<std::collections::HashMap<String, String>>,
891 pub only_html: bool,
893 pub intercept_manager: NetworkInterceptManager,
895 pub max_bytes_allowed: Option<u64>,
897 pub max_redirects: Option<usize>,
900 pub max_main_frame_navigations: Option<u32>,
904 pub whitelist_patterns: Option<Vec<String>>,
906 pub blacklist_patterns: Option<Vec<String>>,
908 #[cfg(feature = "adblock")]
912 pub adblock_filter_rules: Option<Vec<String>>,
913 pub channel_capacity: usize,
916 pub connection_retries: u32,
919}
920
921#[derive(Debug, Clone)]
922pub struct BrowserConfigBuilder {
923 headless: HeadlessMode,
925 sandbox: bool,
927 window_size: Option<(u32, u32)>,
929 port: u16,
931 executable: Option<PathBuf>,
934 executation_detection: DetectionOptions,
936 extensions: Vec<String>,
938 process_envs: Option<HashMap<String, String>>,
940 user_data_dir: Option<PathBuf>,
942 incognito: bool,
944 launch_timeout: Duration,
946 ignore_https_errors: bool,
948 viewport: Option<Viewport>,
950 request_timeout: Duration,
952 args: Vec<String>,
954 disable_default_args: bool,
956 request_intercept: bool,
958 cache_enabled: bool,
960 service_worker_enabled: bool,
962 ignore_visuals: bool,
964 ignore_ads: bool,
966 ignore_javascript: bool,
968 ignore_stylesheets: bool,
970 ignore_prefetch: bool,
972 ignore_analytics: bool,
974 only_html: bool,
976 extra_headers: Option<std::collections::HashMap<String, String>>,
978 intercept_manager: NetworkInterceptManager,
980 max_bytes_allowed: Option<u64>,
982 max_redirects: Option<usize>,
984 max_main_frame_navigations: Option<u32>,
986 whitelist_patterns: Option<Vec<String>>,
988 blacklist_patterns: Option<Vec<String>>,
990 #[cfg(feature = "adblock")]
992 adblock_filter_rules: Option<Vec<String>>,
993 channel_capacity: usize,
995 connection_retries: u32,
997}
998
999impl BrowserConfig {
1000 pub fn builder() -> BrowserConfigBuilder {
1002 BrowserConfigBuilder::default()
1003 }
1004
1005 pub fn with_executable(path: impl AsRef<Path>) -> Self {
1007 Self::builder().chrome_executable(path).build().unwrap()
1010 }
1011}
1012
1013impl Default for BrowserConfigBuilder {
1014 fn default() -> Self {
1015 Self {
1016 headless: HeadlessMode::True,
1017 sandbox: true,
1018 window_size: None,
1019 port: 0,
1020 executable: None,
1021 executation_detection: DetectionOptions::default(),
1022 extensions: Vec::new(),
1023 process_envs: None,
1024 user_data_dir: None,
1025 incognito: false,
1026 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1027 ignore_https_errors: true,
1028 viewport: Some(Default::default()),
1029 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1030 args: Vec::new(),
1031 disable_default_args: false,
1032 request_intercept: false,
1033 cache_enabled: true,
1034 ignore_visuals: false,
1035 ignore_ads: false,
1036 ignore_javascript: false,
1037 ignore_analytics: false,
1038 ignore_stylesheets: false,
1039 ignore_prefetch: true,
1040 only_html: false,
1041 extra_headers: Default::default(),
1042 service_worker_enabled: true,
1043 intercept_manager: NetworkInterceptManager::Unknown,
1044 max_bytes_allowed: None,
1045 max_redirects: None,
1046 max_main_frame_navigations: None,
1047 whitelist_patterns: None,
1048 blacklist_patterns: None,
1049 #[cfg(feature = "adblock")]
1050 adblock_filter_rules: None,
1051 channel_capacity: 4096,
1052 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1053 }
1054 }
1055}
1056
1057impl BrowserConfigBuilder {
1058 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1060 self.window_size = Some((width, height));
1061 self
1062 }
1063 pub fn no_sandbox(mut self) -> Self {
1065 self.sandbox = false;
1066 self
1067 }
1068 pub fn with_head(mut self) -> Self {
1070 self.headless = HeadlessMode::False;
1071 self
1072 }
1073 pub fn new_headless_mode(mut self) -> Self {
1075 self.headless = HeadlessMode::New;
1076 self
1077 }
1078 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1080 self.headless = mode;
1081 self
1082 }
1083 pub fn incognito(mut self) -> Self {
1085 self.incognito = true;
1086 self
1087 }
1088
1089 pub fn respect_https_errors(mut self) -> Self {
1090 self.ignore_https_errors = false;
1091 self
1092 }
1093
1094 pub fn port(mut self, port: u16) -> Self {
1095 self.port = port;
1096 self
1097 }
1098
1099 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1100 self.max_bytes_allowed = max_bytes_allowed;
1101 self
1102 }
1103
1104 pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
1110 self.max_redirects = max_redirects;
1111 self
1112 }
1113
1114 pub fn with_max_main_frame_navigations(mut self, cap: Option<u32>) -> Self {
1122 self.max_main_frame_navigations = cap;
1123 self
1124 }
1125
1126 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1127 self.launch_timeout = timeout;
1128 self
1129 }
1130
1131 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1132 self.request_timeout = timeout;
1133 self
1134 }
1135
1136 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1142 self.viewport = viewport.into();
1143 self
1144 }
1145
1146 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1147 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1148 self
1149 }
1150
1151 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1152 self.executable = Some(path.as_ref().to_path_buf());
1153 self
1154 }
1155
1156 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1157 self.executation_detection = options;
1158 self
1159 }
1160
1161 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1162 self.extensions.push(extension.into());
1163 self
1164 }
1165
1166 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1167 where
1168 I: IntoIterator<Item = S>,
1169 S: Into<String>,
1170 {
1171 for ext in extensions {
1172 self.extensions.push(ext.into());
1173 }
1174 self
1175 }
1176
1177 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1178 self.process_envs
1179 .get_or_insert(HashMap::new())
1180 .insert(key.into(), val.into());
1181 self
1182 }
1183
1184 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1185 where
1186 I: IntoIterator<Item = (K, V)>,
1187 K: Into<String>,
1188 V: Into<String>,
1189 {
1190 self.process_envs
1191 .get_or_insert(HashMap::new())
1192 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1193 self
1194 }
1195
1196 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1197 self.args.push(arg.into());
1198 self
1199 }
1200
1201 pub fn args<I, S>(mut self, args: I) -> Self
1202 where
1203 I: IntoIterator<Item = S>,
1204 S: Into<String>,
1205 {
1206 for arg in args {
1207 self.args.push(arg.into());
1208 }
1209 self
1210 }
1211
1212 pub fn disable_default_args(mut self) -> Self {
1213 self.disable_default_args = true;
1214 self
1215 }
1216
1217 pub fn enable_request_intercept(mut self) -> Self {
1218 self.request_intercept = true;
1219 self
1220 }
1221
1222 pub fn disable_request_intercept(mut self) -> Self {
1223 self.request_intercept = false;
1224 self
1225 }
1226
1227 pub fn enable_cache(mut self) -> Self {
1228 self.cache_enabled = true;
1229 self
1230 }
1231
1232 pub fn disable_cache(mut self) -> Self {
1233 self.cache_enabled = false;
1234 self
1235 }
1236
1237 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1239 self.service_worker_enabled = bypass;
1240 self
1241 }
1242
1243 pub fn set_extra_headers(
1245 mut self,
1246 headers: Option<std::collections::HashMap<String, String>>,
1247 ) -> Self {
1248 self.extra_headers = headers;
1249 self
1250 }
1251
1252 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1254 self.whitelist_patterns = whitelist_patterns;
1255 self
1256 }
1257
1258 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1260 self.blacklist_patterns = blacklist_patterns;
1261 self
1262 }
1263
1264 #[cfg(feature = "adblock")]
1267 pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1268 self.adblock_filter_rules = Some(rules);
1269 self
1270 }
1271
1272 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1275 self.channel_capacity = capacity;
1276 self
1277 }
1278
1279 pub fn connection_retries(mut self, retries: u32) -> Self {
1282 self.connection_retries = retries;
1283 self
1284 }
1285
1286 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1288 let executable = if let Some(e) = self.executable {
1289 e
1290 } else {
1291 detection::default_executable(self.executation_detection)?
1292 };
1293
1294 Ok(BrowserConfig {
1295 headless: self.headless,
1296 sandbox: self.sandbox,
1297 window_size: self.window_size,
1298 port: self.port,
1299 executable,
1300 extensions: self.extensions,
1301 process_envs: self.process_envs,
1302 user_data_dir: self.user_data_dir,
1303 incognito: self.incognito,
1304 launch_timeout: self.launch_timeout,
1305 ignore_https_errors: self.ignore_https_errors,
1306 viewport: self.viewport,
1307 request_timeout: self.request_timeout,
1308 args: self.args,
1309 disable_default_args: self.disable_default_args,
1310 request_intercept: self.request_intercept,
1311 cache_enabled: self.cache_enabled,
1312 ignore_visuals: self.ignore_visuals,
1313 ignore_ads: self.ignore_ads,
1314 ignore_javascript: self.ignore_javascript,
1315 ignore_analytics: self.ignore_analytics,
1316 ignore_stylesheets: self.ignore_stylesheets,
1317 ignore_prefetch: self.ignore_prefetch,
1318 extra_headers: self.extra_headers,
1319 only_html: self.only_html,
1320 intercept_manager: self.intercept_manager,
1321 service_worker_enabled: self.service_worker_enabled,
1322 max_bytes_allowed: self.max_bytes_allowed,
1323 max_redirects: self.max_redirects,
1324 max_main_frame_navigations: self.max_main_frame_navigations,
1325 whitelist_patterns: self.whitelist_patterns,
1326 blacklist_patterns: self.blacklist_patterns,
1327 #[cfg(feature = "adblock")]
1328 adblock_filter_rules: self.adblock_filter_rules,
1329 channel_capacity: self.channel_capacity,
1330 connection_retries: self.connection_retries,
1331 })
1332 }
1333}
1334
1335impl BrowserConfig {
1336 pub fn launch(&self) -> io::Result<Child> {
1337 let mut cmd = async_process::Command::new(&self.executable);
1338
1339 if self.disable_default_args {
1340 cmd.args(&self.args);
1341 } else {
1342 cmd.args(DEFAULT_ARGS).args(&self.args);
1343 }
1344
1345 if !self
1346 .args
1347 .iter()
1348 .any(|arg| arg.contains("--remote-debugging-port="))
1349 {
1350 cmd.arg(format!("--remote-debugging-port={}", self.port));
1351 }
1352
1353 cmd.args(
1354 self.extensions
1355 .iter()
1356 .map(|e| format!("--load-extension={e}")),
1357 );
1358
1359 if let Some(ref user_data) = self.user_data_dir {
1360 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1361 } else {
1362 cmd.arg(format!(
1366 "--user-data-dir={}",
1367 std::env::temp_dir().join("chromiumoxide-runner").display()
1368 ));
1369 }
1370
1371 if let Some((width, height)) = self.window_size {
1372 cmd.arg(format!("--window-size={width},{height}"));
1373 }
1374
1375 if !self.sandbox {
1376 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1377 }
1378
1379 match self.headless {
1380 HeadlessMode::False => (),
1381 HeadlessMode::True => {
1382 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1383 }
1384 HeadlessMode::New => {
1385 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1386 }
1387 }
1388
1389 if self.incognito {
1390 cmd.arg("--incognito");
1391 }
1392
1393 if let Some(ref envs) = self.process_envs {
1394 cmd.envs(envs);
1395 }
1396 cmd.stderr(Stdio::piped()).spawn()
1397 }
1398}
1399
1400#[deprecated(note = "Use detection::default_executable instead")]
1409pub fn default_executable() -> Result<std::path::PathBuf, String> {
1410 let options = DetectionOptions {
1411 msedge: false,
1412 unstable: false,
1413 };
1414 detection::default_executable(options)
1415}
1416
1417static DEFAULT_ARGS: [&str; 26] = [
1420 "--disable-background-networking",
1421 "--enable-features=NetworkService,NetworkServiceInProcess",
1422 "--disable-background-timer-throttling",
1423 "--disable-backgrounding-occluded-windows",
1424 "--disable-breakpad",
1425 "--disable-client-side-phishing-detection",
1426 "--disable-component-extensions-with-background-pages",
1427 "--disable-default-apps",
1428 "--disable-dev-shm-usage",
1429 "--disable-extensions",
1430 "--disable-features=TranslateUI",
1431 "--disable-hang-monitor",
1432 "--disable-ipc-flooding-protection",
1433 "--disable-popup-blocking",
1434 "--disable-prompt-on-repost",
1435 "--disable-renderer-backgrounding",
1436 "--disable-sync",
1437 "--force-color-profile=srgb",
1438 "--metrics-recording-only",
1439 "--no-first-run",
1440 "--enable-automation",
1441 "--password-store=basic",
1442 "--use-mock-keychain",
1443 "--enable-blink-features=IdleDetection",
1444 "--lang=en_US",
1445 "--disable-blink-features=AutomationControlled",
1446];