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::network::{Cookie, CookieParam};
30use chromiumoxide_cdp::cdp::browser_protocol::storage::{
31 ClearCookiesParams, GetCookiesParams, SetCookiesParams,
32};
33use chromiumoxide_cdp::cdp::browser_protocol::target::{
34 CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
35 GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
36};
37use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
38use chromiumoxide_types::*;
39use spider_network_blocker::intercept_manager::NetworkInterceptManager;
40
41pub const LAUNCH_TIMEOUT: u64 = 20_000;
43
44lazy_static::lazy_static! {
45 static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
47 .timeout(Duration::from_secs(60))
48 .default_headers({
49 let mut m = HeaderMap::new();
50
51 m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
52
53 m
54 })
55 .tcp_keepalive(Some(Duration::from_secs(5)))
56 .pool_idle_timeout(Some(Duration::from_secs(60)))
57 .pool_max_idle_per_host(10)
58 .build()
59 .expect("client to build");
60}
61
62#[derive(Debug)]
64pub struct Browser {
65 pub(crate) sender: Sender<HandlerMessage>,
68 config: Option<BrowserConfig>,
70 child: Option<Child>,
72 debug_ws_url: String,
74 pub browser_context: BrowserContext,
76}
77
78#[derive(serde::Deserialize, Debug, Default)]
80pub struct BrowserConnection {
81 #[serde(rename = "Browser")]
82 pub browser: String,
84 #[serde(rename = "Protocol-Version")]
85 pub protocol_version: String,
87 #[serde(rename = "User-Agent")]
88 pub user_agent: String,
90 #[serde(rename = "V8-Version")]
91 pub v8_version: String,
93 #[serde(rename = "WebKit-Version")]
94 pub webkit_version: String,
96 #[serde(rename = "webSocketDebuggerUrl")]
97 pub web_socket_debugger_url: String,
99}
100
101impl Browser {
102 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
106 Self::connect_with_config(url, HandlerConfig::default()).await
107 }
108
109 pub async fn connect_with_config(
113 url: impl Into<String>,
114 config: HandlerConfig,
115 ) -> Result<(Self, Handler)> {
116 let mut debug_ws_url = url.into();
117
118 if debug_ws_url.starts_with("http") {
119 match REQUEST_CLIENT
120 .get(
121 if debug_ws_url.ends_with("/json/version")
122 || debug_ws_url.ends_with("/json/version/")
123 {
124 debug_ws_url.to_owned()
125 } else {
126 format!(
127 "{}{}json/version",
128 &debug_ws_url,
129 if debug_ws_url.ends_with('/') { "" } else { "/" }
130 )
131 },
132 )
133 .send()
134 .await
135 {
136 Ok(req) => {
137 let connection: BrowserConnection =
138 crate::serde_json::from_slice(&req.bytes().await.unwrap_or_default())
139 .unwrap_or_default();
140 if !connection.web_socket_debugger_url.is_empty() {
141 debug_ws_url = connection.web_socket_debugger_url;
142 }
143 }
144 Err(_) => return Err(CdpError::NoResponse),
145 }
146 }
147
148 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
149
150 let (tx, rx) = channel(1000);
151
152 let handler_config = BrowserConfig {
153 ignore_https_errors: config.ignore_https_errors,
154 viewport: config.viewport.clone(),
155 request_timeout: config.request_timeout,
156 request_intercept: config.request_intercept,
157 cache_enabled: config.cache_enabled,
158 ignore_visuals: config.ignore_visuals,
159 ignore_stylesheets: config.ignore_stylesheets,
160 ignore_javascript: config.ignore_javascript,
161 ignore_analytics: config.ignore_analytics,
162 ignore_ads: config.ignore_ads,
163 extra_headers: config.extra_headers.clone(),
164 only_html: config.only_html,
165 service_worker_enabled: config.service_worker_enabled,
166 intercept_manager: config.intercept_manager,
167 max_bytes_allowed: config.max_bytes_allowed,
168 ..Default::default()
169 };
170
171 let fut = Handler::new(conn, rx, config);
172 let browser_context = fut.default_browser_context().clone();
173
174 let browser = Self {
175 sender: tx,
176 config: Some(handler_config),
177 child: None,
178 debug_ws_url,
179 browser_context,
180 };
181
182 Ok((browser, fut))
183 }
184
185 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
194 config.executable = utils::canonicalize_except_snap(config.executable).await?;
196
197 let mut child = config.launch()?;
199
200 async fn with_child(
205 config: &BrowserConfig,
206 child: &mut Child,
207 ) -> Result<(String, Connection<CdpEventMessage>)> {
208 let dur = config.launch_timeout;
209 let timeout_fut = Box::pin(tokio::time::sleep(dur));
210
211 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
213 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
214 Ok((debug_ws_url, conn))
215 }
216
217 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
218 Ok(conn) => conn,
219 Err(e) => {
220 if let Ok(Some(_)) = child.try_wait() {
222 } else {
224 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
226 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
227 }
228 return Err(e);
229 }
230 };
231
232 let (tx, rx) = channel(1000);
236
237 let handler_config = HandlerConfig {
238 ignore_https_errors: config.ignore_https_errors,
239 viewport: config.viewport.clone(),
240 context_ids: Vec::new(),
241 request_timeout: config.request_timeout,
242 request_intercept: config.request_intercept,
243 cache_enabled: config.cache_enabled,
244 ignore_visuals: config.ignore_visuals,
245 ignore_stylesheets: config.ignore_stylesheets,
246 ignore_javascript: config.ignore_javascript,
247 ignore_analytics: config.ignore_analytics,
248 ignore_ads: config.ignore_ads,
249 extra_headers: config.extra_headers.clone(),
250 only_html: config.only_html,
251 service_worker_enabled: config.service_worker_enabled,
252 created_first_target: false,
253 intercept_manager: config.intercept_manager,
254 max_bytes_allowed: config.max_bytes_allowed,
255 };
256
257 let fut = Handler::new(conn, rx, handler_config);
258 let browser_context = fut.default_browser_context().clone();
259
260 let browser = Self {
261 sender: tx,
262 config: Some(config),
263 child: Some(child),
264 debug_ws_url,
265 browser_context,
266 };
267
268 Ok((browser, fut))
269 }
270
271 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
281 let (tx, rx) = oneshot_channel();
282
283 self.sender
284 .clone()
285 .send(HandlerMessage::FetchTargets(tx))
286 .await?;
287
288 rx.await?
289 }
290
291 pub async fn close(&self) -> Result<CloseReturns> {
298 let (tx, rx) = oneshot_channel();
299
300 self.sender
301 .clone()
302 .send(HandlerMessage::CloseBrowser(tx))
303 .await?;
304
305 rx.await?
306 }
307
308 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
317 if let Some(child) = self.child.as_mut() {
318 Ok(Some(child.wait().await?))
319 } else {
320 Ok(None)
321 }
322 }
323
324 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
333 if let Some(child) = self.child.as_mut() {
334 child.try_wait()
335 } else {
336 Ok(None)
337 }
338 }
339
340 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
351 self.child.as_mut()
352 }
353
354 pub fn has_child(&self) -> bool {
356 self.child.is_some()
357 }
358
359 pub async fn kill(&mut self) -> Option<io::Result<()>> {
370 match self.child.as_mut() {
371 Some(child) => Some(child.kill().await),
372 None => None,
373 }
374 }
375
376 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
382 if !self.is_incognito_configured() {
383 let browser_context_id = self
384 .create_browser_context(CreateBrowserContextParams::default())
385 .await?;
386 self.browser_context = BrowserContext::from(browser_context_id);
387 self.sender
388 .clone()
389 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
390 .await?;
391 }
392
393 Ok(self)
394 }
395
396 pub async fn quit_incognito_context_base(
402 &self,
403 browser_context_id: BrowserContextId,
404 ) -> Result<&Self> {
405 self.dispose_browser_context(browser_context_id.clone())
406 .await?;
407 self.sender
408 .clone()
409 .send(HandlerMessage::DisposeContext(BrowserContext::from(
410 browser_context_id,
411 )))
412 .await?;
413 Ok(self)
414 }
415
416 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
422 if let Some(id) = self.browser_context.take() {
423 let _ = self.quit_incognito_context_base(id).await;
424 }
425 Ok(self)
426 }
427
428 fn is_incognito_configured(&self) -> bool {
430 self.config
431 .as_ref()
432 .map(|c| c.incognito)
433 .unwrap_or_default()
434 }
435
436 pub fn websocket_address(&self) -> &String {
438 &self.debug_ws_url
439 }
440
441 pub fn is_incognito(&self) -> bool {
443 self.is_incognito_configured() || self.browser_context.is_incognito()
444 }
445
446 pub fn config(&self) -> Option<&BrowserConfig> {
448 self.config.as_ref()
449 }
450
451 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
453 let (tx, rx) = oneshot_channel();
454 let mut params = params.into();
455
456 if let Some(id) = self.browser_context.id() {
457 if params.browser_context_id.is_none() {
458 params.browser_context_id = Some(id.clone());
459 }
460 }
461
462 let _ = self
463 .sender
464 .clone()
465 .send(HandlerMessage::CreatePage(params, tx))
466 .await;
467
468 rx.await?
469 }
470
471 pub async fn version(&self) -> Result<GetVersionReturns> {
473 Ok(self.execute(GetVersionParams::default()).await?.result)
474 }
475
476 pub async fn user_agent(&self) -> Result<String> {
478 Ok(self.version().await?.user_agent)
479 }
480
481 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
483 let (tx, rx) = oneshot_channel();
484 let method = cmd.identifier();
485 let msg = CommandMessage::new(cmd, tx)?;
486
487 self.sender
488 .clone()
489 .send(HandlerMessage::Command(msg))
490 .await?;
491 let resp = rx.await??;
492 to_command_response::<T>(resp, method)
493 }
494
495 pub async fn pages(&self) -> Result<Vec<Page>> {
497 let (tx, rx) = oneshot_channel();
498 self.sender
499 .clone()
500 .send(HandlerMessage::GetPages(tx))
501 .await?;
502 Ok(rx.await?)
503 }
504
505 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
507 let (tx, rx) = oneshot_channel();
508 self.sender
509 .clone()
510 .send(HandlerMessage::GetPage(target_id, tx))
511 .await?;
512 rx.await?.ok_or(CdpError::NotFound)
513 }
514
515 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
517 let (tx, rx) = unbounded();
518 self.sender
519 .clone()
520 .send(HandlerMessage::AddEventListener(
521 EventListenerRequest::new::<T>(tx),
522 ))
523 .await?;
524
525 Ok(EventStream::new(rx))
526 }
527
528 pub async fn create_browser_context(
530 &mut self,
531 params: CreateBrowserContextParams,
532 ) -> Result<BrowserContextId> {
533 let response = self.execute(params).await?;
534 Ok(response.result.browser_context_id)
535 }
536
537 pub async fn get_browser_contexts(
539 &mut self,
540 params: GetBrowserContextsParams,
541 ) -> Result<GetBrowserContextsReturns> {
542 let response = self.execute(params).await?;
543 Ok(response.result)
544 }
545
546 pub async fn send_new_context(
548 &mut self,
549 browser_context_id: BrowserContextId,
550 ) -> Result<&Self> {
551 self.browser_context = BrowserContext::from(browser_context_id);
552 self.sender
553 .clone()
554 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
555 .await?;
556 Ok(self)
557 }
558
559 pub async fn dispose_browser_context(
561 &self,
562 browser_context_id: impl Into<BrowserContextId>,
563 ) -> Result<&Self> {
564 self.execute(DisposeBrowserContextParams::new(browser_context_id))
565 .await?;
566
567 Ok(self)
568 }
569
570 pub async fn clear_cookies(&self) -> Result<&Self> {
572 self.execute(ClearCookiesParams::default()).await?;
573 Ok(self)
574 }
575
576 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
578 let mut cmd = GetCookiesParams::default();
579
580 cmd.browser_context_id = self.browser_context.id.clone();
581
582 Ok(self.execute(cmd).await?.result.cookies)
583 }
584
585 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
587 for cookie in &mut cookies {
588 if let Some(url) = cookie.url.as_ref() {
589 crate::page::validate_cookie_url(url)?;
590 }
591 }
592
593 let mut cookies_param = SetCookiesParams::new(cookies);
594
595 cookies_param.browser_context_id = self.browser_context.id.clone();
596
597 self.execute(cookies_param).await?;
598 Ok(self)
599 }
600}
601
602impl Drop for Browser {
603 fn drop(&mut self) {
604 if let Some(child) = self.child.as_mut() {
605 if let Ok(Some(_)) = child.try_wait() {
606 } else {
608 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
616 }
617 }
618 }
619}
620
621async fn ws_url_from_output(
631 child_process: &mut Child,
632 timeout_fut: impl Future<Output = ()> + Unpin,
633) -> Result<String> {
634 use futures::{AsyncBufReadExt, FutureExt};
635 let mut timeout_fut = timeout_fut.fuse();
636 let stderr = child_process.stderr.take().expect("no stderror");
637 let mut stderr_bytes = Vec::<u8>::new();
638 let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
639 let mut buf = futures::io::BufReader::new(stderr);
640 loop {
641 select! {
642 _ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
643 exit_status = exit_status_fut => {
644 return Err(match exit_status {
645 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
646 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
647 })
648 },
649 read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
650 match read_res {
651 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
652 Ok(byte_count) => {
653 if byte_count == 0 {
654 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
655 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
656 }
657 let start_offset = stderr_bytes.len() - byte_count;
658 let new_bytes = &stderr_bytes[start_offset..];
659 match std::str::from_utf8(new_bytes) {
660 Err(_) => {
661 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
662 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
663 }
664 Ok(line) => {
665 if let Some((_, ws)) = line.rsplit_once("listening on ") {
666 if ws.starts_with("ws") && ws.contains("devtools/browser") {
667 return Ok(ws.trim().to_string());
668 }
669 }
670 }
671 }
672 }
673 }
674 }
675 }
676 }
677}
678
679#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
680pub enum HeadlessMode {
681 False,
683 #[default]
685 True,
686 New,
688}
689
690#[derive(Debug, Clone, Default)]
691pub struct BrowserConfig {
692 headless: HeadlessMode,
695 sandbox: bool,
697 window_size: Option<(u32, u32)>,
699 port: u16,
701 executable: std::path::PathBuf,
706
707 extensions: Vec<String>,
715
716 pub process_envs: Option<HashMap<String, String>>,
719
720 pub user_data_dir: Option<PathBuf>,
722
723 incognito: bool,
725
726 launch_timeout: Duration,
728
729 ignore_https_errors: bool,
731 pub viewport: Option<Viewport>,
732 request_timeout: Duration,
734
735 args: Vec<String>,
737
738 disable_default_args: bool,
740
741 pub request_intercept: bool,
743
744 pub cache_enabled: bool,
746 pub service_worker_enabled: bool,
749 pub ignore_visuals: bool,
752 pub ignore_stylesheets: bool,
755 pub ignore_javascript: bool,
758 pub ignore_analytics: bool,
760 pub ignore_ads: bool,
762 pub extra_headers: Option<std::collections::HashMap<String, String>>,
764 pub only_html: bool,
766 pub intercept_manager: NetworkInterceptManager,
768 pub max_bytes_allowed: Option<u64>,
770}
771
772#[derive(Debug, Clone)]
773pub struct BrowserConfigBuilder {
774 headless: HeadlessMode,
776 sandbox: bool,
778 window_size: Option<(u32, u32)>,
780 port: u16,
782 executable: Option<PathBuf>,
785 executation_detection: DetectionOptions,
787 extensions: Vec<String>,
789 process_envs: Option<HashMap<String, String>>,
791 user_data_dir: Option<PathBuf>,
793 incognito: bool,
795 launch_timeout: Duration,
797 ignore_https_errors: bool,
799 viewport: Option<Viewport>,
801 request_timeout: Duration,
803 args: Vec<String>,
805 disable_default_args: bool,
807 request_intercept: bool,
809 cache_enabled: bool,
811 service_worker_enabled: bool,
813 ignore_visuals: bool,
815 ignore_ads: bool,
817 ignore_javascript: bool,
819 ignore_stylesheets: bool,
821 ignore_analytics: bool,
823 only_html: bool,
825 extra_headers: Option<std::collections::HashMap<String, String>>,
827 intercept_manager: NetworkInterceptManager,
829 max_bytes_allowed: Option<u64>,
831}
832
833impl BrowserConfig {
834 pub fn builder() -> BrowserConfigBuilder {
836 BrowserConfigBuilder::default()
837 }
838
839 pub fn with_executable(path: impl AsRef<Path>) -> Self {
841 Self::builder()
842 .chrome_executable(path)
843 .build()
844 .expect("path to executable exist")
845 }
846}
847
848impl Default for BrowserConfigBuilder {
849 fn default() -> Self {
850 Self {
851 headless: HeadlessMode::True,
852 sandbox: true,
853 window_size: None,
854 port: 0,
855 executable: None,
856 executation_detection: DetectionOptions::default(),
857 extensions: Vec::new(),
858 process_envs: None,
859 user_data_dir: None,
860 incognito: false,
861 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
862 ignore_https_errors: true,
863 viewport: Some(Default::default()),
864 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
865 args: Vec::new(),
866 disable_default_args: false,
867 request_intercept: false,
868 cache_enabled: true,
869 ignore_visuals: false,
870 ignore_ads: false,
871 ignore_javascript: false,
872 ignore_analytics: false,
873 ignore_stylesheets: false,
874 only_html: false,
875 extra_headers: Default::default(),
876 service_worker_enabled: true,
877 intercept_manager: NetworkInterceptManager::Unknown,
878 max_bytes_allowed: None,
879 }
880 }
881}
882
883impl BrowserConfigBuilder {
884 pub fn window_size(mut self, width: u32, height: u32) -> Self {
886 self.window_size = Some((width, height));
887 self
888 }
889 pub fn no_sandbox(mut self) -> Self {
891 self.sandbox = false;
892 self
893 }
894 pub fn with_head(mut self) -> Self {
896 self.headless = HeadlessMode::False;
897 self
898 }
899 pub fn new_headless_mode(mut self) -> Self {
901 self.headless = HeadlessMode::New;
902 self
903 }
904 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
906 self.headless = mode;
907 self
908 }
909 pub fn incognito(mut self) -> Self {
911 self.incognito = true;
912 self
913 }
914
915 pub fn respect_https_errors(mut self) -> Self {
916 self.ignore_https_errors = false;
917 self
918 }
919
920 pub fn port(mut self, port: u16) -> Self {
921 self.port = port;
922 self
923 }
924
925 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
926 self.max_bytes_allowed = max_bytes_allowed;
927 self
928 }
929
930 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
931 self.launch_timeout = timeout;
932 self
933 }
934
935 pub fn request_timeout(mut self, timeout: Duration) -> Self {
936 self.request_timeout = timeout;
937 self
938 }
939
940 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
946 self.viewport = viewport.into();
947 self
948 }
949
950 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
951 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
952 self
953 }
954
955 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
956 self.executable = Some(path.as_ref().to_path_buf());
957 self
958 }
959
960 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
961 self.executation_detection = options;
962 self
963 }
964
965 pub fn extension(mut self, extension: impl Into<String>) -> Self {
966 self.extensions.push(extension.into());
967 self
968 }
969
970 pub fn extensions<I, S>(mut self, extensions: I) -> Self
971 where
972 I: IntoIterator<Item = S>,
973 S: Into<String>,
974 {
975 for ext in extensions {
976 self.extensions.push(ext.into());
977 }
978 self
979 }
980
981 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
982 self.process_envs
983 .get_or_insert(HashMap::new())
984 .insert(key.into(), val.into());
985 self
986 }
987
988 pub fn envs<I, K, V>(mut self, envs: I) -> Self
989 where
990 I: IntoIterator<Item = (K, V)>,
991 K: Into<String>,
992 V: Into<String>,
993 {
994 self.process_envs
995 .get_or_insert(HashMap::new())
996 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
997 self
998 }
999
1000 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1001 self.args.push(arg.into());
1002 self
1003 }
1004
1005 pub fn args<I, S>(mut self, args: I) -> Self
1006 where
1007 I: IntoIterator<Item = S>,
1008 S: Into<String>,
1009 {
1010 for arg in args {
1011 self.args.push(arg.into());
1012 }
1013 self
1014 }
1015
1016 pub fn disable_default_args(mut self) -> Self {
1017 self.disable_default_args = true;
1018 self
1019 }
1020
1021 pub fn enable_request_intercept(mut self) -> Self {
1022 self.request_intercept = true;
1023 self
1024 }
1025
1026 pub fn disable_request_intercept(mut self) -> Self {
1027 self.request_intercept = false;
1028 self
1029 }
1030
1031 pub fn enable_cache(mut self) -> Self {
1032 self.cache_enabled = true;
1033 self
1034 }
1035
1036 pub fn disable_cache(mut self) -> Self {
1037 self.cache_enabled = false;
1038 self
1039 }
1040
1041 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1042 self.service_worker_enabled = bypass;
1043 self
1044 }
1045
1046 pub fn set_extra_headers(
1047 mut self,
1048 headers: Option<std::collections::HashMap<String, String>>,
1049 ) -> Self {
1050 self.extra_headers = headers;
1051 self
1052 }
1053
1054 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1055 let executable = if let Some(e) = self.executable {
1056 e
1057 } else {
1058 detection::default_executable(self.executation_detection)?
1059 };
1060
1061 Ok(BrowserConfig {
1062 headless: self.headless,
1063 sandbox: self.sandbox,
1064 window_size: self.window_size,
1065 port: self.port,
1066 executable,
1067 extensions: self.extensions,
1068 process_envs: self.process_envs,
1069 user_data_dir: self.user_data_dir,
1070 incognito: self.incognito,
1071 launch_timeout: self.launch_timeout,
1072 ignore_https_errors: self.ignore_https_errors,
1073 viewport: self.viewport,
1074 request_timeout: self.request_timeout,
1075 args: self.args,
1076 disable_default_args: self.disable_default_args,
1077 request_intercept: self.request_intercept,
1078 cache_enabled: self.cache_enabled,
1079 ignore_visuals: self.ignore_visuals,
1080 ignore_ads: self.ignore_ads,
1081 ignore_javascript: self.ignore_javascript,
1082 ignore_analytics: self.ignore_analytics,
1083 ignore_stylesheets: self.ignore_stylesheets,
1084 extra_headers: self.extra_headers,
1085 only_html: self.only_html,
1086 intercept_manager: self.intercept_manager,
1087 service_worker_enabled: self.service_worker_enabled,
1088 max_bytes_allowed: self.max_bytes_allowed,
1089 })
1090 }
1091}
1092
1093impl BrowserConfig {
1094 pub fn launch(&self) -> io::Result<Child> {
1095 let mut cmd = async_process::Command::new(&self.executable);
1096
1097 if self.disable_default_args {
1098 cmd.args(&self.args);
1099 } else {
1100 cmd.args(DEFAULT_ARGS).args(&self.args);
1101 }
1102
1103 if !self
1104 .args
1105 .iter()
1106 .any(|arg| arg.contains("--remote-debugging-port="))
1107 {
1108 cmd.arg(format!("--remote-debugging-port={}", self.port));
1109 }
1110
1111 cmd.args(
1112 self.extensions
1113 .iter()
1114 .map(|e| format!("--load-extension={e}")),
1115 );
1116
1117 if let Some(ref user_data) = self.user_data_dir {
1118 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1119 } else {
1120 cmd.arg(format!(
1124 "--user-data-dir={}",
1125 std::env::temp_dir().join("chromiumoxide-runner").display()
1126 ));
1127 }
1128
1129 if let Some((width, height)) = self.window_size {
1130 cmd.arg(format!("--window-size={width},{height}"));
1131 }
1132
1133 if !self.sandbox {
1134 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1135 }
1136
1137 match self.headless {
1138 HeadlessMode::False => (),
1139 HeadlessMode::True => {
1140 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1141 }
1142 HeadlessMode::New => {
1143 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1144 }
1145 }
1146
1147 if self.incognito {
1148 cmd.arg("--incognito");
1149 }
1150
1151 if let Some(ref envs) = self.process_envs {
1152 cmd.envs(envs);
1153 }
1154 cmd.stderr(Stdio::piped()).spawn()
1155 }
1156}
1157
1158#[deprecated(note = "Use detection::default_executable instead")]
1167pub fn default_executable() -> Result<std::path::PathBuf, String> {
1168 let options = DetectionOptions {
1169 msedge: false,
1170 unstable: false,
1171 };
1172 detection::default_executable(options)
1173}
1174
1175static DEFAULT_ARGS: [&str; 26] = [
1178 "--disable-background-networking",
1179 "--enable-features=NetworkService,NetworkServiceInProcess",
1180 "--disable-background-timer-throttling",
1181 "--disable-backgrounding-occluded-windows",
1182 "--disable-breakpad",
1183 "--disable-client-side-phishing-detection",
1184 "--disable-component-extensions-with-background-pages",
1185 "--disable-default-apps",
1186 "--disable-dev-shm-usage",
1187 "--disable-extensions",
1188 "--disable-features=TranslateUI",
1189 "--disable-hang-monitor",
1190 "--disable-ipc-flooding-protection",
1191 "--disable-popup-blocking",
1192 "--disable-prompt-on-repost",
1193 "--disable-renderer-backgrounding",
1194 "--disable-sync",
1195 "--force-color-profile=srgb",
1196 "--metrics-recording-only",
1197 "--no-first-run",
1198 "--enable-automation",
1199 "--password-store=basic",
1200 "--use-mock-keychain",
1201 "--enable-blink-features=IdleDetection",
1202 "--lang=en_US",
1203 "--disable-blink-features=AutomationControlled",
1204];