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 whitelist_patterns: config.whitelist_patterns.clone(),
205 blacklist_patterns: config.blacklist_patterns.clone(),
206 ..Default::default()
207 };
208
209 let fut = Handler::new(conn, rx, config);
210 let browser_context = fut.default_browser_context().clone();
211
212 let browser = Self {
213 sender: tx,
214 config: Some(handler_config),
215 child: None,
216 debug_ws_url,
217 browser_context,
218 };
219
220 Ok((browser, fut))
221 }
222
223 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
232 config.executable = utils::canonicalize_except_snap(config.executable).await?;
234
235 let mut child = config.launch()?;
237
238 async fn with_child(
243 config: &BrowserConfig,
244 child: &mut Child,
245 ) -> Result<(String, Connection<CdpEventMessage>)> {
246 let dur = config.launch_timeout;
247 let timeout_fut = Box::pin(tokio::time::sleep(dur));
248
249 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
251 let conn = Connection::<CdpEventMessage>::connect_with_retries(
252 &debug_ws_url,
253 config.connection_retries,
254 )
255 .await?;
256 Ok((debug_ws_url, conn))
257 }
258
259 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
260 Ok(conn) => conn,
261 Err(e) => {
262 if let Ok(Some(_)) = child.try_wait() {
264 } else {
266 let _ = child.kill().await;
268 let _ = child.wait().await;
269 }
270 return Err(e);
271 }
272 };
273
274 let (tx, rx) = channel(config.channel_capacity);
278
279 let handler_config = HandlerConfig {
280 ignore_https_errors: config.ignore_https_errors,
281 viewport: config.viewport.clone(),
282 context_ids: Vec::new(),
283 request_timeout: config.request_timeout,
284 request_intercept: config.request_intercept,
285 cache_enabled: config.cache_enabled,
286 ignore_visuals: config.ignore_visuals,
287 ignore_stylesheets: config.ignore_stylesheets,
288 ignore_javascript: config.ignore_javascript,
289 ignore_analytics: config.ignore_analytics,
290 ignore_prefetch: config.ignore_prefetch,
291 ignore_ads: config.ignore_ads,
292 extra_headers: config.extra_headers.clone(),
293 only_html: config.only_html,
294 service_worker_enabled: config.service_worker_enabled,
295 created_first_target: false,
296 intercept_manager: config.intercept_manager,
297 max_bytes_allowed: config.max_bytes_allowed,
298 whitelist_patterns: config.whitelist_patterns.clone(),
299 blacklist_patterns: config.blacklist_patterns.clone(),
300 #[cfg(feature = "adblock")]
301 adblock_filter_rules: config.adblock_filter_rules.clone(),
302 channel_capacity: config.channel_capacity,
303 connection_retries: config.connection_retries,
304 };
305
306 let fut = Handler::new(conn, rx, handler_config);
307 let browser_context = fut.default_browser_context().clone();
308
309 let browser = Self {
310 sender: tx,
311 config: Some(config),
312 child: Some(child),
313 debug_ws_url,
314 browser_context,
315 };
316
317 Ok((browser, fut))
318 }
319
320 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
330 let (tx, rx) = oneshot_channel();
331
332 self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
333
334 rx.await?
335 }
336
337 pub async fn close(&self) -> Result<CloseReturns> {
344 let (tx, rx) = oneshot_channel();
345
346 self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
347
348 rx.await?
349 }
350
351 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
360 if let Some(child) = self.child.as_mut() {
361 Ok(Some(child.wait().await?))
362 } else {
363 Ok(None)
364 }
365 }
366
367 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
376 if let Some(child) = self.child.as_mut() {
377 child.try_wait()
378 } else {
379 Ok(None)
380 }
381 }
382
383 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
394 self.child.as_mut()
395 }
396
397 pub fn has_child(&self) -> bool {
399 self.child.is_some()
400 }
401
402 pub async fn kill(&mut self) -> Option<io::Result<()>> {
413 match self.child.as_mut() {
414 Some(child) => Some(child.kill().await),
415 None => None,
416 }
417 }
418
419 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
425 if !self.is_incognito_configured() {
426 let browser_context_id = self
427 .create_browser_context(CreateBrowserContextParams::default())
428 .await?;
429 self.browser_context = BrowserContext::from(browser_context_id);
430 self.sender
431 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
432 .await?;
433 }
434
435 Ok(self)
436 }
437
438 pub async fn quit_incognito_context_base(
444 &self,
445 browser_context_id: BrowserContextId,
446 ) -> Result<&Self> {
447 self.dispose_browser_context(browser_context_id.clone())
448 .await?;
449 self.sender
450 .send(HandlerMessage::DisposeContext(BrowserContext::from(
451 browser_context_id,
452 )))
453 .await?;
454 Ok(self)
455 }
456
457 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
463 if let Some(id) = self.browser_context.take() {
464 let _ = self.quit_incognito_context_base(id).await;
465 }
466 Ok(self)
467 }
468
469 fn is_incognito_configured(&self) -> bool {
471 self.config
472 .as_ref()
473 .map(|c| c.incognito)
474 .unwrap_or_default()
475 }
476
477 pub fn websocket_address(&self) -> &String {
479 &self.debug_ws_url
480 }
481
482 pub fn is_incognito(&self) -> bool {
484 self.is_incognito_configured() || self.browser_context.is_incognito()
485 }
486
487 pub fn config(&self) -> Option<&BrowserConfig> {
489 self.config.as_ref()
490 }
491
492 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
494 let (tx, rx) = oneshot_channel();
495 let mut params = params.into();
496
497 if let Some(id) = self.browser_context.id() {
498 if params.browser_context_id.is_none() {
499 params.browser_context_id = Some(id.clone());
500 }
501 }
502
503 let _ = self
504 .sender
505 .send(HandlerMessage::CreatePage(params, tx))
506 .await;
507
508 rx.await?
509 }
510
511 pub async fn version(&self) -> Result<GetVersionReturns> {
513 Ok(self.execute(GetVersionParams::default()).await?.result)
514 }
515
516 pub async fn user_agent(&self) -> Result<String> {
518 Ok(self.version().await?.user_agent)
519 }
520
521 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
523 let (tx, rx) = oneshot_channel();
524 let method = cmd.identifier();
525 let msg = CommandMessage::new(cmd, tx)?;
526
527 self.sender.send(HandlerMessage::Command(msg)).await?;
528 let resp = rx.await??;
529 to_command_response::<T>(resp, method)
530 }
531
532 pub async fn set_permission(
536 &self,
537 permission: PermissionDescriptor,
538 setting: PermissionSetting,
539 origin: Option<impl Into<String>>,
540 embedded_origin: Option<impl Into<String>>,
541 browser_context_id: Option<BrowserContextId>,
542 ) -> Result<&Self> {
543 self.execute(SetPermissionParams {
544 permission,
545 setting,
546 origin: origin.map(Into::into),
547 embedded_origin: embedded_origin.map(Into::into),
548 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
549 })
550 .await?;
551 Ok(self)
552 }
553
554 pub async fn set_permission_for_origin(
556 &self,
557 origin: impl Into<String>,
558 embedded_origin: Option<impl Into<String>>,
559 permission: PermissionDescriptor,
560 setting: PermissionSetting,
561 ) -> Result<&Self> {
562 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
563 .await
564 }
565
566 pub async fn reset_permission_for_origin(
568 &self,
569 origin: impl Into<String>,
570 embedded_origin: Option<impl Into<String>>,
571 permission: PermissionDescriptor,
572 ) -> Result<&Self> {
573 self.set_permission_for_origin(
574 origin,
575 embedded_origin,
576 permission,
577 PermissionSetting::Prompt,
578 )
579 .await
580 }
581
582 pub async fn grant_all_permission_for_origin(
584 &self,
585 origin: impl Into<String>,
586 embedded_origin: Option<impl Into<String>>,
587 permission: PermissionDescriptor,
588 ) -> Result<&Self> {
589 self.set_permission_for_origin(
590 origin,
591 embedded_origin,
592 permission,
593 PermissionSetting::Granted,
594 )
595 .await
596 }
597
598 pub async fn deny_all_permission_for_origin(
600 &self,
601 origin: impl Into<String>,
602 embedded_origin: Option<impl Into<String>>,
603 permission: PermissionDescriptor,
604 ) -> Result<&Self> {
605 self.set_permission_for_origin(
606 origin,
607 embedded_origin,
608 permission,
609 PermissionSetting::Denied,
610 )
611 .await
612 }
613
614 pub async fn pages(&self) -> Result<Vec<Page>> {
616 let (tx, rx) = oneshot_channel();
617 self.sender.send(HandlerMessage::GetPages(tx)).await?;
618 Ok(rx.await?)
619 }
620
621 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
623 let (tx, rx) = oneshot_channel();
624 self.sender
625 .send(HandlerMessage::GetPage(target_id, tx))
626 .await?;
627 rx.await?.ok_or(CdpError::NotFound)
628 }
629
630 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
632 let (tx, rx) = unbounded_channel();
633 self.sender
634 .send(HandlerMessage::AddEventListener(
635 EventListenerRequest::new::<T>(tx),
636 ))
637 .await?;
638
639 Ok(EventStream::new(rx))
640 }
641
642 pub async fn create_browser_context(
644 &mut self,
645 params: CreateBrowserContextParams,
646 ) -> Result<BrowserContextId> {
647 let response = self.execute(params).await?;
648
649 Ok(response.result.browser_context_id)
650 }
651
652 pub async fn get_browser_contexts(
654 &mut self,
655 params: GetBrowserContextsParams,
656 ) -> Result<GetBrowserContextsReturns> {
657 let response = self.execute(params).await?;
658 Ok(response.result)
659 }
660
661 pub async fn send_new_context(
663 &mut self,
664 browser_context_id: BrowserContextId,
665 ) -> Result<&Self> {
666 self.browser_context = BrowserContext::from(browser_context_id);
667 self.sender
668 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
669 .await?;
670 Ok(self)
671 }
672
673 pub async fn dispose_browser_context(
675 &self,
676 browser_context_id: impl Into<BrowserContextId>,
677 ) -> Result<&Self> {
678 self.execute(DisposeBrowserContextParams::new(browser_context_id))
679 .await?;
680
681 Ok(self)
682 }
683
684 pub async fn clear_cookies(&self) -> Result<&Self> {
686 self.execute(ClearCookiesParams::default()).await?;
687 Ok(self)
688 }
689
690 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
692 let cmd = GetCookiesParams {
693 browser_context_id: self.browser_context.id.clone(),
694 };
695
696 Ok(self.execute(cmd).await?.result.cookies)
697 }
698
699 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
701 for cookie in &mut cookies {
702 if let Some(url) = cookie.url.as_ref() {
703 crate::page::validate_cookie_url(url)?;
704 }
705 }
706
707 let mut cookies_param = SetCookiesParams::new(cookies);
708
709 cookies_param.browser_context_id = self.browser_context.id.clone();
710
711 self.execute(cookies_param).await?;
712 Ok(self)
713 }
714}
715
716impl Drop for Browser {
717 fn drop(&mut self) {
718 if let Some(child) = self.child.as_mut() {
719 if let Ok(Some(_)) = child.try_wait() {
720 } else {
722 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
730 }
731 }
732 }
733}
734
735async fn ws_url_from_output(
745 child_process: &mut Child,
746 timeout_fut: impl Future<Output = ()> + Unpin,
747) -> Result<String> {
748 use tokio::io::AsyncBufReadExt;
749 let stderr = match child_process.stderr.take() {
750 Some(stderr) => stderr,
751 None => {
752 return Err(CdpError::LaunchIo(
753 io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
754 BrowserStderr::new(Vec::new()),
755 ));
756 }
757 };
758 let mut stderr_bytes = Vec::<u8>::new();
759 let mut buf = tokio::io::BufReader::new(stderr);
760 let mut timeout_fut = timeout_fut;
761 loop {
762 tokio::select! {
763 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
764 exit_status = child_process.wait() => {
765 return Err(match exit_status {
766 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
767 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
768 })
769 },
770 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
771 match read_res {
772 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
773 Ok(byte_count) => {
774 if byte_count == 0 {
775 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
776 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
777 }
778 let start_offset = stderr_bytes.len() - byte_count;
779 let new_bytes = &stderr_bytes[start_offset..];
780 match std::str::from_utf8(new_bytes) {
781 Err(_) => {
782 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
783 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
784 }
785 Ok(line) => {
786 if let Some((_, ws)) = line.rsplit_once("listening on ") {
787 if ws.starts_with("ws") && ws.contains("devtools/browser") {
788 return Ok(ws.trim().to_string());
789 }
790 }
791 }
792 }
793 }
794 }
795 }
796 }
797 }
798}
799
800#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
801pub enum HeadlessMode {
802 False,
804 #[default]
806 True,
807 New,
809}
810
811#[derive(Debug, Clone, Default)]
812pub struct BrowserConfig {
813 headless: HeadlessMode,
816 sandbox: bool,
818 window_size: Option<(u32, u32)>,
820 port: u16,
822 executable: std::path::PathBuf,
827
828 extensions: Vec<String>,
836
837 pub process_envs: Option<HashMap<String, String>>,
840
841 pub user_data_dir: Option<PathBuf>,
843
844 incognito: bool,
846
847 launch_timeout: Duration,
849
850 ignore_https_errors: bool,
852 pub viewport: Option<Viewport>,
853 request_timeout: Duration,
855
856 args: Vec<String>,
858
859 disable_default_args: bool,
861
862 pub request_intercept: bool,
864
865 pub cache_enabled: bool,
867 pub service_worker_enabled: bool,
870 pub ignore_visuals: bool,
873 pub ignore_stylesheets: bool,
876 pub ignore_javascript: bool,
879 pub ignore_analytics: bool,
881 pub ignore_prefetch: bool,
883 pub ignore_ads: bool,
885 pub extra_headers: Option<std::collections::HashMap<String, String>>,
887 pub only_html: bool,
889 pub intercept_manager: NetworkInterceptManager,
891 pub max_bytes_allowed: Option<u64>,
893 pub whitelist_patterns: Option<Vec<String>>,
895 pub blacklist_patterns: Option<Vec<String>>,
897 #[cfg(feature = "adblock")]
901 pub adblock_filter_rules: Option<Vec<String>>,
902 pub channel_capacity: usize,
905 pub connection_retries: u32,
908}
909
910#[derive(Debug, Clone)]
911pub struct BrowserConfigBuilder {
912 headless: HeadlessMode,
914 sandbox: bool,
916 window_size: Option<(u32, u32)>,
918 port: u16,
920 executable: Option<PathBuf>,
923 executation_detection: DetectionOptions,
925 extensions: Vec<String>,
927 process_envs: Option<HashMap<String, String>>,
929 user_data_dir: Option<PathBuf>,
931 incognito: bool,
933 launch_timeout: Duration,
935 ignore_https_errors: bool,
937 viewport: Option<Viewport>,
939 request_timeout: Duration,
941 args: Vec<String>,
943 disable_default_args: bool,
945 request_intercept: bool,
947 cache_enabled: bool,
949 service_worker_enabled: bool,
951 ignore_visuals: bool,
953 ignore_ads: bool,
955 ignore_javascript: bool,
957 ignore_stylesheets: bool,
959 ignore_prefetch: bool,
961 ignore_analytics: bool,
963 only_html: bool,
965 extra_headers: Option<std::collections::HashMap<String, String>>,
967 intercept_manager: NetworkInterceptManager,
969 max_bytes_allowed: Option<u64>,
971 whitelist_patterns: Option<Vec<String>>,
973 blacklist_patterns: Option<Vec<String>>,
975 #[cfg(feature = "adblock")]
977 adblock_filter_rules: Option<Vec<String>>,
978 channel_capacity: usize,
980 connection_retries: u32,
982}
983
984impl BrowserConfig {
985 pub fn builder() -> BrowserConfigBuilder {
987 BrowserConfigBuilder::default()
988 }
989
990 pub fn with_executable(path: impl AsRef<Path>) -> Self {
992 Self::builder().chrome_executable(path).build().unwrap()
995 }
996}
997
998impl Default for BrowserConfigBuilder {
999 fn default() -> Self {
1000 Self {
1001 headless: HeadlessMode::True,
1002 sandbox: true,
1003 window_size: None,
1004 port: 0,
1005 executable: None,
1006 executation_detection: DetectionOptions::default(),
1007 extensions: Vec::new(),
1008 process_envs: None,
1009 user_data_dir: None,
1010 incognito: false,
1011 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1012 ignore_https_errors: true,
1013 viewport: Some(Default::default()),
1014 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1015 args: Vec::new(),
1016 disable_default_args: false,
1017 request_intercept: false,
1018 cache_enabled: true,
1019 ignore_visuals: false,
1020 ignore_ads: false,
1021 ignore_javascript: false,
1022 ignore_analytics: false,
1023 ignore_stylesheets: false,
1024 ignore_prefetch: true,
1025 only_html: false,
1026 extra_headers: Default::default(),
1027 service_worker_enabled: true,
1028 intercept_manager: NetworkInterceptManager::Unknown,
1029 max_bytes_allowed: None,
1030 whitelist_patterns: None,
1031 blacklist_patterns: None,
1032 #[cfg(feature = "adblock")]
1033 adblock_filter_rules: None,
1034 channel_capacity: 4096,
1035 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1036 }
1037 }
1038}
1039
1040impl BrowserConfigBuilder {
1041 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1043 self.window_size = Some((width, height));
1044 self
1045 }
1046 pub fn no_sandbox(mut self) -> Self {
1048 self.sandbox = false;
1049 self
1050 }
1051 pub fn with_head(mut self) -> Self {
1053 self.headless = HeadlessMode::False;
1054 self
1055 }
1056 pub fn new_headless_mode(mut self) -> Self {
1058 self.headless = HeadlessMode::New;
1059 self
1060 }
1061 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1063 self.headless = mode;
1064 self
1065 }
1066 pub fn incognito(mut self) -> Self {
1068 self.incognito = true;
1069 self
1070 }
1071
1072 pub fn respect_https_errors(mut self) -> Self {
1073 self.ignore_https_errors = false;
1074 self
1075 }
1076
1077 pub fn port(mut self, port: u16) -> Self {
1078 self.port = port;
1079 self
1080 }
1081
1082 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1083 self.max_bytes_allowed = max_bytes_allowed;
1084 self
1085 }
1086
1087 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1088 self.launch_timeout = timeout;
1089 self
1090 }
1091
1092 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1093 self.request_timeout = timeout;
1094 self
1095 }
1096
1097 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1103 self.viewport = viewport.into();
1104 self
1105 }
1106
1107 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1108 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1109 self
1110 }
1111
1112 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1113 self.executable = Some(path.as_ref().to_path_buf());
1114 self
1115 }
1116
1117 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1118 self.executation_detection = options;
1119 self
1120 }
1121
1122 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1123 self.extensions.push(extension.into());
1124 self
1125 }
1126
1127 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1128 where
1129 I: IntoIterator<Item = S>,
1130 S: Into<String>,
1131 {
1132 for ext in extensions {
1133 self.extensions.push(ext.into());
1134 }
1135 self
1136 }
1137
1138 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1139 self.process_envs
1140 .get_or_insert(HashMap::new())
1141 .insert(key.into(), val.into());
1142 self
1143 }
1144
1145 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1146 where
1147 I: IntoIterator<Item = (K, V)>,
1148 K: Into<String>,
1149 V: Into<String>,
1150 {
1151 self.process_envs
1152 .get_or_insert(HashMap::new())
1153 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1154 self
1155 }
1156
1157 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1158 self.args.push(arg.into());
1159 self
1160 }
1161
1162 pub fn args<I, S>(mut self, args: I) -> Self
1163 where
1164 I: IntoIterator<Item = S>,
1165 S: Into<String>,
1166 {
1167 for arg in args {
1168 self.args.push(arg.into());
1169 }
1170 self
1171 }
1172
1173 pub fn disable_default_args(mut self) -> Self {
1174 self.disable_default_args = true;
1175 self
1176 }
1177
1178 pub fn enable_request_intercept(mut self) -> Self {
1179 self.request_intercept = true;
1180 self
1181 }
1182
1183 pub fn disable_request_intercept(mut self) -> Self {
1184 self.request_intercept = false;
1185 self
1186 }
1187
1188 pub fn enable_cache(mut self) -> Self {
1189 self.cache_enabled = true;
1190 self
1191 }
1192
1193 pub fn disable_cache(mut self) -> Self {
1194 self.cache_enabled = false;
1195 self
1196 }
1197
1198 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1200 self.service_worker_enabled = bypass;
1201 self
1202 }
1203
1204 pub fn set_extra_headers(
1206 mut self,
1207 headers: Option<std::collections::HashMap<String, String>>,
1208 ) -> Self {
1209 self.extra_headers = headers;
1210 self
1211 }
1212
1213 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1215 self.whitelist_patterns = whitelist_patterns;
1216 self
1217 }
1218
1219 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1221 self.blacklist_patterns = blacklist_patterns;
1222 self
1223 }
1224
1225 #[cfg(feature = "adblock")]
1228 pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1229 self.adblock_filter_rules = Some(rules);
1230 self
1231 }
1232
1233 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1236 self.channel_capacity = capacity;
1237 self
1238 }
1239
1240 pub fn connection_retries(mut self, retries: u32) -> Self {
1243 self.connection_retries = retries;
1244 self
1245 }
1246
1247 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1249 let executable = if let Some(e) = self.executable {
1250 e
1251 } else {
1252 detection::default_executable(self.executation_detection)?
1253 };
1254
1255 Ok(BrowserConfig {
1256 headless: self.headless,
1257 sandbox: self.sandbox,
1258 window_size: self.window_size,
1259 port: self.port,
1260 executable,
1261 extensions: self.extensions,
1262 process_envs: self.process_envs,
1263 user_data_dir: self.user_data_dir,
1264 incognito: self.incognito,
1265 launch_timeout: self.launch_timeout,
1266 ignore_https_errors: self.ignore_https_errors,
1267 viewport: self.viewport,
1268 request_timeout: self.request_timeout,
1269 args: self.args,
1270 disable_default_args: self.disable_default_args,
1271 request_intercept: self.request_intercept,
1272 cache_enabled: self.cache_enabled,
1273 ignore_visuals: self.ignore_visuals,
1274 ignore_ads: self.ignore_ads,
1275 ignore_javascript: self.ignore_javascript,
1276 ignore_analytics: self.ignore_analytics,
1277 ignore_stylesheets: self.ignore_stylesheets,
1278 ignore_prefetch: self.ignore_prefetch,
1279 extra_headers: self.extra_headers,
1280 only_html: self.only_html,
1281 intercept_manager: self.intercept_manager,
1282 service_worker_enabled: self.service_worker_enabled,
1283 max_bytes_allowed: self.max_bytes_allowed,
1284 whitelist_patterns: self.whitelist_patterns,
1285 blacklist_patterns: self.blacklist_patterns,
1286 #[cfg(feature = "adblock")]
1287 adblock_filter_rules: self.adblock_filter_rules,
1288 channel_capacity: self.channel_capacity,
1289 connection_retries: self.connection_retries,
1290 })
1291 }
1292}
1293
1294impl BrowserConfig {
1295 pub fn launch(&self) -> io::Result<Child> {
1296 let mut cmd = async_process::Command::new(&self.executable);
1297
1298 if self.disable_default_args {
1299 cmd.args(&self.args);
1300 } else {
1301 cmd.args(DEFAULT_ARGS).args(&self.args);
1302 }
1303
1304 if !self
1305 .args
1306 .iter()
1307 .any(|arg| arg.contains("--remote-debugging-port="))
1308 {
1309 cmd.arg(format!("--remote-debugging-port={}", self.port));
1310 }
1311
1312 cmd.args(
1313 self.extensions
1314 .iter()
1315 .map(|e| format!("--load-extension={e}")),
1316 );
1317
1318 if let Some(ref user_data) = self.user_data_dir {
1319 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1320 } else {
1321 cmd.arg(format!(
1325 "--user-data-dir={}",
1326 std::env::temp_dir().join("chromiumoxide-runner").display()
1327 ));
1328 }
1329
1330 if let Some((width, height)) = self.window_size {
1331 cmd.arg(format!("--window-size={width},{height}"));
1332 }
1333
1334 if !self.sandbox {
1335 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1336 }
1337
1338 match self.headless {
1339 HeadlessMode::False => (),
1340 HeadlessMode::True => {
1341 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1342 }
1343 HeadlessMode::New => {
1344 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1345 }
1346 }
1347
1348 if self.incognito {
1349 cmd.arg("--incognito");
1350 }
1351
1352 if let Some(ref envs) = self.process_envs {
1353 cmd.envs(envs);
1354 }
1355 cmd.stderr(Stdio::piped()).spawn()
1356 }
1357}
1358
1359#[deprecated(note = "Use detection::default_executable instead")]
1368pub fn default_executable() -> Result<std::path::PathBuf, String> {
1369 let options = DetectionOptions {
1370 msedge: false,
1371 unstable: false,
1372 };
1373 detection::default_executable(options)
1374}
1375
1376static DEFAULT_ARGS: [&str; 26] = [
1379 "--disable-background-networking",
1380 "--enable-features=NetworkService,NetworkServiceInProcess",
1381 "--disable-background-timer-throttling",
1382 "--disable-backgrounding-occluded-windows",
1383 "--disable-breakpad",
1384 "--disable-client-side-phishing-detection",
1385 "--disable-component-extensions-with-background-pages",
1386 "--disable-default-apps",
1387 "--disable-dev-shm-usage",
1388 "--disable-extensions",
1389 "--disable-features=TranslateUI",
1390 "--disable-hang-monitor",
1391 "--disable-ipc-flooding-protection",
1392 "--disable-popup-blocking",
1393 "--disable-prompt-on-repost",
1394 "--disable-renderer-backgrounding",
1395 "--disable-sync",
1396 "--force-color-profile=srgb",
1397 "--metrics-recording-only",
1398 "--no-first-run",
1399 "--enable-automation",
1400 "--password-store=basic",
1401 "--use-mock-keychain",
1402 "--enable-blink-features=IdleDetection",
1403 "--lang=en_US",
1404 "--disable-blink-features=AutomationControlled",
1405];