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 futures::channel::mpsc::{channel, unbounded, Sender};
11use futures::channel::oneshot::channel as oneshot_channel;
12use futures::select;
13use futures::SinkExt;
14
15use crate::async_process::{self, Child, ExitStatus, Stdio};
16use crate::cmd::{to_command_response, CommandMessage};
17use crate::conn::Connection;
18use crate::detection::{self, DetectionOptions};
19use crate::error::{BrowserStderr, CdpError, Result};
20use crate::handler::browser::BrowserContext;
21use crate::handler::viewport::Viewport;
22use crate::handler::{Handler, HandlerConfig, HandlerMessage, REQUEST_TIMEOUT};
23use crate::listeners::{EventListenerRequest, EventStream};
24use crate::page::Page;
25use crate::utils;
26use chromiumoxide_cdp::cdp::browser_protocol::browser::{
27 BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
28};
29use chromiumoxide_cdp::cdp::browser_protocol::browser::{
30 PermissionDescriptor, PermissionSetting, SetPermissionParams,
31};
32use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
33use chromiumoxide_cdp::cdp::browser_protocol::storage::{
34 ClearCookiesParams, GetCookiesParams, SetCookiesParams,
35};
36use chromiumoxide_cdp::cdp::browser_protocol::target::{
37 CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
38 GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
39};
40
41use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
42use chromiumoxide_types::*;
43use spider_network_blocker::intercept_manager::NetworkInterceptManager;
44
45pub const LAUNCH_TIMEOUT: u64 = 20_000;
47
48lazy_static::lazy_static! {
49 static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
51 .timeout(Duration::from_secs(60))
52 .default_headers({
53 let mut m = HeaderMap::new();
54
55 m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
56
57 m
58 })
59 .tcp_keepalive(Some(Duration::from_secs(5)))
60 .pool_idle_timeout(Some(Duration::from_secs(60)))
61 .pool_max_idle_per_host(10)
62 .build()
63 .expect("client to build");
64}
65
66#[derive(Debug)]
68pub struct Browser {
69 pub(crate) sender: Sender<HandlerMessage>,
72 config: Option<BrowserConfig>,
74 child: Option<Child>,
76 debug_ws_url: String,
78 pub browser_context: BrowserContext,
80}
81
82#[derive(serde::Deserialize, Debug, Default)]
84pub struct BrowserConnection {
85 #[serde(rename = "Browser")]
86 pub browser: String,
88 #[serde(rename = "Protocol-Version")]
89 pub protocol_version: String,
91 #[serde(rename = "User-Agent")]
92 pub user_agent: String,
94 #[serde(rename = "V8-Version")]
95 pub v8_version: String,
97 #[serde(rename = "WebKit-Version")]
98 pub webkit_version: String,
100 #[serde(rename = "webSocketDebuggerUrl")]
101 pub web_socket_debugger_url: String,
103}
104
105impl Browser {
106 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
110 Self::connect_with_config(url, HandlerConfig::default()).await
111 }
112
113 pub async fn connect_with_config(
117 url: impl Into<String>,
118 config: HandlerConfig,
119 ) -> Result<(Self, Handler)> {
120 let mut debug_ws_url = url.into();
121
122 if debug_ws_url.starts_with("http") {
123 match REQUEST_CLIENT
124 .get(
125 if debug_ws_url.ends_with("/json/version")
126 || debug_ws_url.ends_with("/json/version/")
127 {
128 debug_ws_url.to_owned()
129 } else {
130 format!(
131 "{}{}json/version",
132 &debug_ws_url,
133 if debug_ws_url.ends_with('/') { "" } else { "/" }
134 )
135 },
136 )
137 .send()
138 .await
139 {
140 Ok(req) => {
141 if let Ok(b) = req.bytes().await {
142 if let Ok(connection) =
143 crate::serde_json::from_slice::<Box<BrowserConnection>>(&b)
144 {
145 if !connection.web_socket_debugger_url.is_empty() {
146 debug_ws_url = connection.web_socket_debugger_url;
147 }
148 }
149 }
150 }
151 Err(_) => return Err(CdpError::NoResponse),
152 }
153 }
154
155 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
156
157 let (tx, rx) = channel(1000);
158
159 let handler_config = BrowserConfig {
160 ignore_https_errors: config.ignore_https_errors,
161 viewport: config.viewport.clone(),
162 request_timeout: config.request_timeout,
163 request_intercept: config.request_intercept,
164 cache_enabled: config.cache_enabled,
165 ignore_visuals: config.ignore_visuals,
166 ignore_stylesheets: config.ignore_stylesheets,
167 ignore_javascript: config.ignore_javascript,
168 ignore_analytics: config.ignore_analytics,
169 ignore_ads: config.ignore_ads,
170 extra_headers: config.extra_headers.clone(),
171 only_html: config.only_html,
172 service_worker_enabled: config.service_worker_enabled,
173 intercept_manager: config.intercept_manager,
174 max_bytes_allowed: config.max_bytes_allowed,
175 whitelist_patterns: config.whitelist_patterns.clone(),
176 blacklist_patterns: config.blacklist_patterns.clone(),
177 ..Default::default()
178 };
179
180 let fut = Handler::new(conn, rx, config);
181 let browser_context = fut.default_browser_context().clone();
182
183 let browser = Self {
184 sender: tx,
185 config: Some(handler_config),
186 child: None,
187 debug_ws_url,
188 browser_context,
189 };
190
191 Ok((browser, fut))
192 }
193
194 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
203 config.executable = utils::canonicalize_except_snap(config.executable).await?;
205
206 let mut child = config.launch()?;
208
209 async fn with_child(
214 config: &BrowserConfig,
215 child: &mut Child,
216 ) -> Result<(String, Connection<CdpEventMessage>)> {
217 let dur = config.launch_timeout;
218 let timeout_fut = Box::pin(tokio::time::sleep(dur));
219
220 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
222 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
223 Ok((debug_ws_url, conn))
224 }
225
226 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
227 Ok(conn) => conn,
228 Err(e) => {
229 if let Ok(Some(_)) = child.try_wait() {
231 } else {
233 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
235 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
236 }
237 return Err(e);
238 }
239 };
240
241 let (tx, rx) = channel(1000);
245
246 let handler_config = HandlerConfig {
247 ignore_https_errors: config.ignore_https_errors,
248 viewport: config.viewport.clone(),
249 context_ids: Vec::new(),
250 request_timeout: config.request_timeout,
251 request_intercept: config.request_intercept,
252 cache_enabled: config.cache_enabled,
253 ignore_visuals: config.ignore_visuals,
254 ignore_stylesheets: config.ignore_stylesheets,
255 ignore_javascript: config.ignore_javascript,
256 ignore_analytics: config.ignore_analytics,
257 ignore_ads: config.ignore_ads,
258 extra_headers: config.extra_headers.clone(),
259 only_html: config.only_html,
260 service_worker_enabled: config.service_worker_enabled,
261 created_first_target: false,
262 intercept_manager: config.intercept_manager,
263 max_bytes_allowed: config.max_bytes_allowed,
264 whitelist_patterns: config.whitelist_patterns.clone(),
265 blacklist_patterns: config.blacklist_patterns.clone(),
266 };
267
268 let fut = Handler::new(conn, rx, handler_config);
269 let browser_context = fut.default_browser_context().clone();
270
271 let browser = Self {
272 sender: tx,
273 config: Some(config),
274 child: Some(child),
275 debug_ws_url,
276 browser_context,
277 };
278
279 Ok((browser, fut))
280 }
281
282 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
292 let (tx, rx) = oneshot_channel();
293
294 self.sender
295 .clone()
296 .send(HandlerMessage::FetchTargets(tx))
297 .await?;
298
299 rx.await?
300 }
301
302 pub async fn close(&self) -> Result<CloseReturns> {
309 let (tx, rx) = oneshot_channel();
310
311 self.sender
312 .clone()
313 .send(HandlerMessage::CloseBrowser(tx))
314 .await?;
315
316 rx.await?
317 }
318
319 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
328 if let Some(child) = self.child.as_mut() {
329 Ok(Some(child.wait().await?))
330 } else {
331 Ok(None)
332 }
333 }
334
335 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
344 if let Some(child) = self.child.as_mut() {
345 child.try_wait()
346 } else {
347 Ok(None)
348 }
349 }
350
351 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
362 self.child.as_mut()
363 }
364
365 pub fn has_child(&self) -> bool {
367 self.child.is_some()
368 }
369
370 pub async fn kill(&mut self) -> Option<io::Result<()>> {
381 match self.child.as_mut() {
382 Some(child) => Some(child.kill().await),
383 None => None,
384 }
385 }
386
387 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
393 if !self.is_incognito_configured() {
394 let browser_context_id = self
395 .create_browser_context(CreateBrowserContextParams::default())
396 .await?;
397 self.browser_context = BrowserContext::from(browser_context_id);
398 self.sender
399 .clone()
400 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
401 .await?;
402 }
403
404 Ok(self)
405 }
406
407 pub async fn quit_incognito_context_base(
413 &self,
414 browser_context_id: BrowserContextId,
415 ) -> Result<&Self> {
416 self.dispose_browser_context(browser_context_id.clone())
417 .await?;
418 self.sender
419 .clone()
420 .send(HandlerMessage::DisposeContext(BrowserContext::from(
421 browser_context_id,
422 )))
423 .await?;
424 Ok(self)
425 }
426
427 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
433 if let Some(id) = self.browser_context.take() {
434 let _ = self.quit_incognito_context_base(id).await;
435 }
436 Ok(self)
437 }
438
439 fn is_incognito_configured(&self) -> bool {
441 self.config
442 .as_ref()
443 .map(|c| c.incognito)
444 .unwrap_or_default()
445 }
446
447 pub fn websocket_address(&self) -> &String {
449 &self.debug_ws_url
450 }
451
452 pub fn is_incognito(&self) -> bool {
454 self.is_incognito_configured() || self.browser_context.is_incognito()
455 }
456
457 pub fn config(&self) -> Option<&BrowserConfig> {
459 self.config.as_ref()
460 }
461
462 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
464 let (tx, rx) = oneshot_channel();
465 let mut params = params.into();
466
467 if let Some(id) = self.browser_context.id() {
468 if params.browser_context_id.is_none() {
469 params.browser_context_id = Some(id.clone());
470 }
471 }
472
473 let _ = self
474 .sender
475 .clone()
476 .send(HandlerMessage::CreatePage(params, tx))
477 .await;
478
479 rx.await?
480 }
481
482 pub async fn version(&self) -> Result<GetVersionReturns> {
484 Ok(self.execute(GetVersionParams::default()).await?.result)
485 }
486
487 pub async fn user_agent(&self) -> Result<String> {
489 Ok(self.version().await?.user_agent)
490 }
491
492 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
494 let (tx, rx) = oneshot_channel();
495 let method = cmd.identifier();
496 let msg = CommandMessage::new(cmd, tx)?;
497
498 self.sender
499 .clone()
500 .send(HandlerMessage::Command(msg))
501 .await?;
502 let resp = rx.await??;
503 to_command_response::<T>(resp, method)
504 }
505
506 pub async fn set_permission(
510 &self,
511 permission: PermissionDescriptor,
512 setting: PermissionSetting,
513 origin: Option<impl Into<String>>,
514 embedded_origin: Option<impl Into<String>>,
515 browser_context_id: Option<BrowserContextId>,
516 ) -> Result<&Self> {
517 self.execute(SetPermissionParams {
518 permission,
519 setting,
520 origin: origin.map(Into::into),
521 embedded_origin: embedded_origin.map(Into::into),
522 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
523 })
524 .await?;
525 Ok(self)
526 }
527
528 pub async fn set_permission_for_origin(
530 &self,
531 origin: impl Into<String>,
532 embedded_origin: Option<impl Into<String>>,
533 permission: PermissionDescriptor,
534 setting: PermissionSetting,
535 ) -> Result<&Self> {
536 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
537 .await
538 }
539
540 pub async fn reset_permission_for_origin(
542 &self,
543 origin: impl Into<String>,
544 embedded_origin: Option<impl Into<String>>,
545 permission: PermissionDescriptor,
546 ) -> Result<&Self> {
547 self.set_permission_for_origin(
548 origin,
549 embedded_origin,
550 permission,
551 PermissionSetting::Prompt,
552 )
553 .await
554 }
555
556 pub async fn grant_all_permission_for_origin(
558 &self,
559 origin: impl Into<String>,
560 embedded_origin: Option<impl Into<String>>,
561 permission: PermissionDescriptor,
562 ) -> Result<&Self> {
563 self.set_permission_for_origin(
564 origin,
565 embedded_origin,
566 permission,
567 PermissionSetting::Granted,
568 )
569 .await
570 }
571
572 pub async fn deny_all_permission_for_origin(
574 &self,
575 origin: impl Into<String>,
576 embedded_origin: Option<impl Into<String>>,
577 permission: PermissionDescriptor,
578 ) -> Result<&Self> {
579 self.set_permission_for_origin(
580 origin,
581 embedded_origin,
582 permission,
583 PermissionSetting::Denied,
584 )
585 .await
586 }
587
588 pub async fn pages(&self) -> Result<Vec<Page>> {
590 let (tx, rx) = oneshot_channel();
591 self.sender
592 .clone()
593 .send(HandlerMessage::GetPages(tx))
594 .await?;
595 Ok(rx.await?)
596 }
597
598 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
600 let (tx, rx) = oneshot_channel();
601 self.sender
602 .clone()
603 .send(HandlerMessage::GetPage(target_id, tx))
604 .await?;
605 rx.await?.ok_or(CdpError::NotFound)
606 }
607
608 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
610 let (tx, rx) = unbounded();
611 self.sender
612 .clone()
613 .send(HandlerMessage::AddEventListener(
614 EventListenerRequest::new::<T>(tx),
615 ))
616 .await?;
617
618 Ok(EventStream::new(rx))
619 }
620
621 pub async fn create_browser_context(
623 &mut self,
624 params: CreateBrowserContextParams,
625 ) -> Result<BrowserContextId> {
626 let response = self.execute(params).await?;
627
628 Ok(response.result.browser_context_id)
629 }
630
631 pub async fn get_browser_contexts(
633 &mut self,
634 params: GetBrowserContextsParams,
635 ) -> Result<GetBrowserContextsReturns> {
636 let response = self.execute(params).await?;
637 Ok(response.result)
638 }
639
640 pub async fn send_new_context(
642 &mut self,
643 browser_context_id: BrowserContextId,
644 ) -> Result<&Self> {
645 self.browser_context = BrowserContext::from(browser_context_id);
646 self.sender
647 .clone()
648 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
649 .await?;
650 Ok(self)
651 }
652
653 pub async fn dispose_browser_context(
655 &self,
656 browser_context_id: impl Into<BrowserContextId>,
657 ) -> Result<&Self> {
658 self.execute(DisposeBrowserContextParams::new(browser_context_id))
659 .await?;
660
661 Ok(self)
662 }
663
664 pub async fn clear_cookies(&self) -> Result<&Self> {
666 self.execute(ClearCookiesParams::default()).await?;
667 Ok(self)
668 }
669
670 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
672 let mut cmd = GetCookiesParams::default();
673
674 cmd.browser_context_id = self.browser_context.id.clone();
675
676 Ok(self.execute(cmd).await?.result.cookies)
677 }
678
679 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
681 for cookie in &mut cookies {
682 if let Some(url) = cookie.url.as_ref() {
683 crate::page::validate_cookie_url(url)?;
684 }
685 }
686
687 let mut cookies_param = SetCookiesParams::new(cookies);
688
689 cookies_param.browser_context_id = self.browser_context.id.clone();
690
691 self.execute(cookies_param).await?;
692 Ok(self)
693 }
694}
695
696impl Drop for Browser {
697 fn drop(&mut self) {
698 if let Some(child) = self.child.as_mut() {
699 if let Ok(Some(_)) = child.try_wait() {
700 } else {
702 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
710 }
711 }
712 }
713}
714
715async fn ws_url_from_output(
725 child_process: &mut Child,
726 timeout_fut: impl Future<Output = ()> + Unpin,
727) -> Result<String> {
728 use futures::{AsyncBufReadExt, FutureExt};
729 let mut timeout_fut = timeout_fut.fuse();
730 let stderr = child_process.stderr.take().expect("no stderror");
731 let mut stderr_bytes = Vec::<u8>::new();
732 let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
733 let mut buf = futures::io::BufReader::new(stderr);
734 loop {
735 select! {
736 _ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
737 exit_status = exit_status_fut => {
738 return Err(match exit_status {
739 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
740 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
741 })
742 },
743 read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
744 match read_res {
745 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
746 Ok(byte_count) => {
747 if byte_count == 0 {
748 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
749 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
750 }
751 let start_offset = stderr_bytes.len() - byte_count;
752 let new_bytes = &stderr_bytes[start_offset..];
753 match std::str::from_utf8(new_bytes) {
754 Err(_) => {
755 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
756 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
757 }
758 Ok(line) => {
759 if let Some((_, ws)) = line.rsplit_once("listening on ") {
760 if ws.starts_with("ws") && ws.contains("devtools/browser") {
761 return Ok(ws.trim().to_string());
762 }
763 }
764 }
765 }
766 }
767 }
768 }
769 }
770 }
771}
772
773#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
774pub enum HeadlessMode {
775 False,
777 #[default]
779 True,
780 New,
782}
783
784#[derive(Debug, Clone, Default)]
785pub struct BrowserConfig {
786 headless: HeadlessMode,
789 sandbox: bool,
791 window_size: Option<(u32, u32)>,
793 port: u16,
795 executable: std::path::PathBuf,
800
801 extensions: Vec<String>,
809
810 pub process_envs: Option<HashMap<String, String>>,
813
814 pub user_data_dir: Option<PathBuf>,
816
817 incognito: bool,
819
820 launch_timeout: Duration,
822
823 ignore_https_errors: bool,
825 pub viewport: Option<Viewport>,
826 request_timeout: Duration,
828
829 args: Vec<String>,
831
832 disable_default_args: bool,
834
835 pub request_intercept: bool,
837
838 pub cache_enabled: bool,
840 pub service_worker_enabled: bool,
843 pub ignore_visuals: bool,
846 pub ignore_stylesheets: bool,
849 pub ignore_javascript: bool,
852 pub ignore_analytics: bool,
854 pub ignore_ads: bool,
856 pub extra_headers: Option<std::collections::HashMap<String, String>>,
858 pub only_html: bool,
860 pub intercept_manager: NetworkInterceptManager,
862 pub max_bytes_allowed: Option<u64>,
864 pub whitelist_patterns: Option<Vec<String>>,
866 pub blacklist_patterns: Option<Vec<String>>,
868}
869
870#[derive(Debug, Clone)]
871pub struct BrowserConfigBuilder {
872 headless: HeadlessMode,
874 sandbox: bool,
876 window_size: Option<(u32, u32)>,
878 port: u16,
880 executable: Option<PathBuf>,
883 executation_detection: DetectionOptions,
885 extensions: Vec<String>,
887 process_envs: Option<HashMap<String, String>>,
889 user_data_dir: Option<PathBuf>,
891 incognito: bool,
893 launch_timeout: Duration,
895 ignore_https_errors: bool,
897 viewport: Option<Viewport>,
899 request_timeout: Duration,
901 args: Vec<String>,
903 disable_default_args: bool,
905 request_intercept: bool,
907 cache_enabled: bool,
909 service_worker_enabled: bool,
911 ignore_visuals: bool,
913 ignore_ads: bool,
915 ignore_javascript: bool,
917 ignore_stylesheets: bool,
919 ignore_analytics: bool,
921 only_html: bool,
923 extra_headers: Option<std::collections::HashMap<String, String>>,
925 intercept_manager: NetworkInterceptManager,
927 max_bytes_allowed: Option<u64>,
929 whitelist_patterns: Option<Vec<String>>,
931 blacklist_patterns: Option<Vec<String>>,
933}
934
935impl BrowserConfig {
936 pub fn builder() -> BrowserConfigBuilder {
938 BrowserConfigBuilder::default()
939 }
940
941 pub fn with_executable(path: impl AsRef<Path>) -> Self {
943 Self::builder()
944 .chrome_executable(path)
945 .build()
946 .expect("path to executable exist")
947 }
948}
949
950impl Default for BrowserConfigBuilder {
951 fn default() -> Self {
952 Self {
953 headless: HeadlessMode::True,
954 sandbox: true,
955 window_size: None,
956 port: 0,
957 executable: None,
958 executation_detection: DetectionOptions::default(),
959 extensions: Vec::new(),
960 process_envs: None,
961 user_data_dir: None,
962 incognito: false,
963 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
964 ignore_https_errors: true,
965 viewport: Some(Default::default()),
966 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
967 args: Vec::new(),
968 disable_default_args: false,
969 request_intercept: false,
970 cache_enabled: true,
971 ignore_visuals: false,
972 ignore_ads: false,
973 ignore_javascript: false,
974 ignore_analytics: false,
975 ignore_stylesheets: false,
976 only_html: false,
977 extra_headers: Default::default(),
978 service_worker_enabled: true,
979 intercept_manager: NetworkInterceptManager::Unknown,
980 max_bytes_allowed: None,
981 whitelist_patterns: None,
982 blacklist_patterns: None,
983 }
984 }
985}
986
987impl BrowserConfigBuilder {
988 pub fn window_size(mut self, width: u32, height: u32) -> Self {
990 self.window_size = Some((width, height));
991 self
992 }
993 pub fn no_sandbox(mut self) -> Self {
995 self.sandbox = false;
996 self
997 }
998 pub fn with_head(mut self) -> Self {
1000 self.headless = HeadlessMode::False;
1001 self
1002 }
1003 pub fn new_headless_mode(mut self) -> Self {
1005 self.headless = HeadlessMode::New;
1006 self
1007 }
1008 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1010 self.headless = mode;
1011 self
1012 }
1013 pub fn incognito(mut self) -> Self {
1015 self.incognito = true;
1016 self
1017 }
1018
1019 pub fn respect_https_errors(mut self) -> Self {
1020 self.ignore_https_errors = false;
1021 self
1022 }
1023
1024 pub fn port(mut self, port: u16) -> Self {
1025 self.port = port;
1026 self
1027 }
1028
1029 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1030 self.max_bytes_allowed = max_bytes_allowed;
1031 self
1032 }
1033
1034 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1035 self.launch_timeout = timeout;
1036 self
1037 }
1038
1039 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1040 self.request_timeout = timeout;
1041 self
1042 }
1043
1044 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1050 self.viewport = viewport.into();
1051 self
1052 }
1053
1054 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1055 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1056 self
1057 }
1058
1059 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1060 self.executable = Some(path.as_ref().to_path_buf());
1061 self
1062 }
1063
1064 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1065 self.executation_detection = options;
1066 self
1067 }
1068
1069 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1070 self.extensions.push(extension.into());
1071 self
1072 }
1073
1074 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1075 where
1076 I: IntoIterator<Item = S>,
1077 S: Into<String>,
1078 {
1079 for ext in extensions {
1080 self.extensions.push(ext.into());
1081 }
1082 self
1083 }
1084
1085 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1086 self.process_envs
1087 .get_or_insert(HashMap::new())
1088 .insert(key.into(), val.into());
1089 self
1090 }
1091
1092 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1093 where
1094 I: IntoIterator<Item = (K, V)>,
1095 K: Into<String>,
1096 V: Into<String>,
1097 {
1098 self.process_envs
1099 .get_or_insert(HashMap::new())
1100 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1101 self
1102 }
1103
1104 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1105 self.args.push(arg.into());
1106 self
1107 }
1108
1109 pub fn args<I, S>(mut self, args: I) -> Self
1110 where
1111 I: IntoIterator<Item = S>,
1112 S: Into<String>,
1113 {
1114 for arg in args {
1115 self.args.push(arg.into());
1116 }
1117 self
1118 }
1119
1120 pub fn disable_default_args(mut self) -> Self {
1121 self.disable_default_args = true;
1122 self
1123 }
1124
1125 pub fn enable_request_intercept(mut self) -> Self {
1126 self.request_intercept = true;
1127 self
1128 }
1129
1130 pub fn disable_request_intercept(mut self) -> Self {
1131 self.request_intercept = false;
1132 self
1133 }
1134
1135 pub fn enable_cache(mut self) -> Self {
1136 self.cache_enabled = true;
1137 self
1138 }
1139
1140 pub fn disable_cache(mut self) -> Self {
1141 self.cache_enabled = false;
1142 self
1143 }
1144
1145 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1147 self.service_worker_enabled = bypass;
1148 self
1149 }
1150
1151 pub fn set_extra_headers(
1153 mut self,
1154 headers: Option<std::collections::HashMap<String, String>>,
1155 ) -> Self {
1156 self.extra_headers = headers;
1157 self
1158 }
1159
1160 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1162 self.whitelist_patterns = whitelist_patterns;
1163 self
1164 }
1165
1166 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1168 self.blacklist_patterns = blacklist_patterns;
1169 self
1170 }
1171
1172 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1174 let executable = if let Some(e) = self.executable {
1175 e
1176 } else {
1177 detection::default_executable(self.executation_detection)?
1178 };
1179
1180 Ok(BrowserConfig {
1181 headless: self.headless,
1182 sandbox: self.sandbox,
1183 window_size: self.window_size,
1184 port: self.port,
1185 executable,
1186 extensions: self.extensions,
1187 process_envs: self.process_envs,
1188 user_data_dir: self.user_data_dir,
1189 incognito: self.incognito,
1190 launch_timeout: self.launch_timeout,
1191 ignore_https_errors: self.ignore_https_errors,
1192 viewport: self.viewport,
1193 request_timeout: self.request_timeout,
1194 args: self.args,
1195 disable_default_args: self.disable_default_args,
1196 request_intercept: self.request_intercept,
1197 cache_enabled: self.cache_enabled,
1198 ignore_visuals: self.ignore_visuals,
1199 ignore_ads: self.ignore_ads,
1200 ignore_javascript: self.ignore_javascript,
1201 ignore_analytics: self.ignore_analytics,
1202 ignore_stylesheets: self.ignore_stylesheets,
1203 extra_headers: self.extra_headers,
1204 only_html: self.only_html,
1205 intercept_manager: self.intercept_manager,
1206 service_worker_enabled: self.service_worker_enabled,
1207 max_bytes_allowed: self.max_bytes_allowed,
1208 whitelist_patterns: self.whitelist_patterns,
1209 blacklist_patterns: self.blacklist_patterns,
1210 })
1211 }
1212}
1213
1214impl BrowserConfig {
1215 pub fn launch(&self) -> io::Result<Child> {
1216 let mut cmd = async_process::Command::new(&self.executable);
1217
1218 if self.disable_default_args {
1219 cmd.args(&self.args);
1220 } else {
1221 cmd.args(DEFAULT_ARGS).args(&self.args);
1222 }
1223
1224 if !self
1225 .args
1226 .iter()
1227 .any(|arg| arg.contains("--remote-debugging-port="))
1228 {
1229 cmd.arg(format!("--remote-debugging-port={}", self.port));
1230 }
1231
1232 cmd.args(
1233 self.extensions
1234 .iter()
1235 .map(|e| format!("--load-extension={e}")),
1236 );
1237
1238 if let Some(ref user_data) = self.user_data_dir {
1239 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1240 } else {
1241 cmd.arg(format!(
1245 "--user-data-dir={}",
1246 std::env::temp_dir().join("chromiumoxide-runner").display()
1247 ));
1248 }
1249
1250 if let Some((width, height)) = self.window_size {
1251 cmd.arg(format!("--window-size={width},{height}"));
1252 }
1253
1254 if !self.sandbox {
1255 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1256 }
1257
1258 match self.headless {
1259 HeadlessMode::False => (),
1260 HeadlessMode::True => {
1261 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1262 }
1263 HeadlessMode::New => {
1264 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1265 }
1266 }
1267
1268 if self.incognito {
1269 cmd.arg("--incognito");
1270 }
1271
1272 if let Some(ref envs) = self.process_envs {
1273 cmd.envs(envs);
1274 }
1275 cmd.stderr(Stdio::piped()).spawn()
1276 }
1277}
1278
1279#[deprecated(note = "Use detection::default_executable instead")]
1288pub fn default_executable() -> Result<std::path::PathBuf, String> {
1289 let options = DetectionOptions {
1290 msedge: false,
1291 unstable: false,
1292 };
1293 detection::default_executable(options)
1294}
1295
1296static DEFAULT_ARGS: [&str; 26] = [
1299 "--disable-background-networking",
1300 "--enable-features=NetworkService,NetworkServiceInProcess",
1301 "--disable-background-timer-throttling",
1302 "--disable-backgrounding-occluded-windows",
1303 "--disable-breakpad",
1304 "--disable-client-side-phishing-detection",
1305 "--disable-component-extensions-with-background-pages",
1306 "--disable-default-apps",
1307 "--disable-dev-shm-usage",
1308 "--disable-extensions",
1309 "--disable-features=TranslateUI",
1310 "--disable-hang-monitor",
1311 "--disable-ipc-flooding-protection",
1312 "--disable-popup-blocking",
1313 "--disable-prompt-on-repost",
1314 "--disable-renderer-backgrounding",
1315 "--disable-sync",
1316 "--force-color-profile=srgb",
1317 "--metrics-recording-only",
1318 "--no-first-run",
1319 "--enable-automation",
1320 "--password-store=basic",
1321 "--use-mock-keychain",
1322 "--enable-blink-features=IdleDetection",
1323 "--lang=en_US",
1324 "--disable-blink-features=AutomationControlled",
1325];