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 let retries = config.connection_retries;
122
123 if debug_ws_url.starts_with("http") {
124 let version_url = if debug_ws_url.ends_with("/json/version")
125 || debug_ws_url.ends_with("/json/version/")
126 {
127 debug_ws_url.to_owned()
128 } else {
129 format!(
130 "{}{}json/version",
131 &debug_ws_url,
132 if debug_ws_url.ends_with('/') { "" } else { "/" }
133 )
134 };
135
136 let mut discovered = false;
137
138 for attempt in 0..=retries {
139 match REQUEST_CLIENT.get(&version_url).send().await {
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 discovered = true;
151 break;
152 }
153 Err(_) => {
154 if attempt < retries {
155 let backoff_ms = 50u64 * 3u64.saturating_pow(attempt);
156 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
157 }
158 }
159 }
160 }
161
162 if !discovered {
163 return Err(CdpError::NoResponse);
164 }
165 }
166
167 let conn =
168 Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
169
170 let (tx, rx) = channel(config.channel_capacity);
171
172 let handler_config = BrowserConfig {
173 ignore_https_errors: config.ignore_https_errors,
174 viewport: config.viewport.clone(),
175 request_timeout: config.request_timeout,
176 request_intercept: config.request_intercept,
177 cache_enabled: config.cache_enabled,
178 ignore_visuals: config.ignore_visuals,
179 ignore_stylesheets: config.ignore_stylesheets,
180 ignore_javascript: config.ignore_javascript,
181 ignore_analytics: config.ignore_analytics,
182 ignore_prefetch: config.ignore_prefetch,
183 ignore_ads: config.ignore_ads,
184 extra_headers: config.extra_headers.clone(),
185 only_html: config.only_html,
186 service_worker_enabled: config.service_worker_enabled,
187 intercept_manager: config.intercept_manager,
188 max_bytes_allowed: config.max_bytes_allowed,
189 whitelist_patterns: config.whitelist_patterns.clone(),
190 blacklist_patterns: config.blacklist_patterns.clone(),
191 ..Default::default()
192 };
193
194 let fut = Handler::new(conn, rx, config);
195 let browser_context = fut.default_browser_context().clone();
196
197 let browser = Self {
198 sender: tx,
199 config: Some(handler_config),
200 child: None,
201 debug_ws_url,
202 browser_context,
203 };
204
205 Ok((browser, fut))
206 }
207
208 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
217 config.executable = utils::canonicalize_except_snap(config.executable).await?;
219
220 let mut child = config.launch()?;
222
223 async fn with_child(
228 config: &BrowserConfig,
229 child: &mut Child,
230 ) -> Result<(String, Connection<CdpEventMessage>)> {
231 let dur = config.launch_timeout;
232 let timeout_fut = Box::pin(tokio::time::sleep(dur));
233
234 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
236 let conn = Connection::<CdpEventMessage>::connect_with_retries(
237 &debug_ws_url,
238 config.connection_retries,
239 )
240 .await?;
241 Ok((debug_ws_url, conn))
242 }
243
244 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
245 Ok(conn) => conn,
246 Err(e) => {
247 if let Ok(Some(_)) = child.try_wait() {
249 } else {
251 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
253 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
254 }
255 return Err(e);
256 }
257 };
258
259 let (tx, rx) = channel(config.channel_capacity);
263
264 let handler_config = HandlerConfig {
265 ignore_https_errors: config.ignore_https_errors,
266 viewport: config.viewport.clone(),
267 context_ids: Vec::new(),
268 request_timeout: config.request_timeout,
269 request_intercept: config.request_intercept,
270 cache_enabled: config.cache_enabled,
271 ignore_visuals: config.ignore_visuals,
272 ignore_stylesheets: config.ignore_stylesheets,
273 ignore_javascript: config.ignore_javascript,
274 ignore_analytics: config.ignore_analytics,
275 ignore_prefetch: config.ignore_prefetch,
276 ignore_ads: config.ignore_ads,
277 extra_headers: config.extra_headers.clone(),
278 only_html: config.only_html,
279 service_worker_enabled: config.service_worker_enabled,
280 created_first_target: false,
281 intercept_manager: config.intercept_manager,
282 max_bytes_allowed: config.max_bytes_allowed,
283 whitelist_patterns: config.whitelist_patterns.clone(),
284 blacklist_patterns: config.blacklist_patterns.clone(),
285 channel_capacity: config.channel_capacity,
286 connection_retries: config.connection_retries,
287 };
288
289 let fut = Handler::new(conn, rx, handler_config);
290 let browser_context = fut.default_browser_context().clone();
291
292 let browser = Self {
293 sender: tx,
294 config: Some(config),
295 child: Some(child),
296 debug_ws_url,
297 browser_context,
298 };
299
300 Ok((browser, fut))
301 }
302
303 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
313 let (tx, rx) = oneshot_channel();
314
315 self.sender
316 .clone()
317 .send(HandlerMessage::FetchTargets(tx))
318 .await?;
319
320 rx.await?
321 }
322
323 pub async fn close(&self) -> Result<CloseReturns> {
330 let (tx, rx) = oneshot_channel();
331
332 self.sender
333 .clone()
334 .send(HandlerMessage::CloseBrowser(tx))
335 .await?;
336
337 rx.await?
338 }
339
340 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
349 if let Some(child) = self.child.as_mut() {
350 Ok(Some(child.wait().await?))
351 } else {
352 Ok(None)
353 }
354 }
355
356 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
365 if let Some(child) = self.child.as_mut() {
366 child.try_wait()
367 } else {
368 Ok(None)
369 }
370 }
371
372 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
383 self.child.as_mut()
384 }
385
386 pub fn has_child(&self) -> bool {
388 self.child.is_some()
389 }
390
391 pub async fn kill(&mut self) -> Option<io::Result<()>> {
402 match self.child.as_mut() {
403 Some(child) => Some(child.kill().await),
404 None => None,
405 }
406 }
407
408 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
414 if !self.is_incognito_configured() {
415 let browser_context_id = self
416 .create_browser_context(CreateBrowserContextParams::default())
417 .await?;
418 self.browser_context = BrowserContext::from(browser_context_id);
419 self.sender
420 .clone()
421 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
422 .await?;
423 }
424
425 Ok(self)
426 }
427
428 pub async fn quit_incognito_context_base(
434 &self,
435 browser_context_id: BrowserContextId,
436 ) -> Result<&Self> {
437 self.dispose_browser_context(browser_context_id.clone())
438 .await?;
439 self.sender
440 .clone()
441 .send(HandlerMessage::DisposeContext(BrowserContext::from(
442 browser_context_id,
443 )))
444 .await?;
445 Ok(self)
446 }
447
448 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
454 if let Some(id) = self.browser_context.take() {
455 let _ = self.quit_incognito_context_base(id).await;
456 }
457 Ok(self)
458 }
459
460 fn is_incognito_configured(&self) -> bool {
462 self.config
463 .as_ref()
464 .map(|c| c.incognito)
465 .unwrap_or_default()
466 }
467
468 pub fn websocket_address(&self) -> &String {
470 &self.debug_ws_url
471 }
472
473 pub fn is_incognito(&self) -> bool {
475 self.is_incognito_configured() || self.browser_context.is_incognito()
476 }
477
478 pub fn config(&self) -> Option<&BrowserConfig> {
480 self.config.as_ref()
481 }
482
483 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
485 let (tx, rx) = oneshot_channel();
486 let mut params = params.into();
487
488 if let Some(id) = self.browser_context.id() {
489 if params.browser_context_id.is_none() {
490 params.browser_context_id = Some(id.clone());
491 }
492 }
493
494 let _ = self
495 .sender
496 .clone()
497 .send(HandlerMessage::CreatePage(params, tx))
498 .await;
499
500 rx.await?
501 }
502
503 pub async fn version(&self) -> Result<GetVersionReturns> {
505 Ok(self.execute(GetVersionParams::default()).await?.result)
506 }
507
508 pub async fn user_agent(&self) -> Result<String> {
510 Ok(self.version().await?.user_agent)
511 }
512
513 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
515 let (tx, rx) = oneshot_channel();
516 let method = cmd.identifier();
517 let msg = CommandMessage::new(cmd, tx)?;
518
519 self.sender
520 .clone()
521 .send(HandlerMessage::Command(msg))
522 .await?;
523 let resp = rx.await??;
524 to_command_response::<T>(resp, method)
525 }
526
527 pub async fn set_permission(
531 &self,
532 permission: PermissionDescriptor,
533 setting: PermissionSetting,
534 origin: Option<impl Into<String>>,
535 embedded_origin: Option<impl Into<String>>,
536 browser_context_id: Option<BrowserContextId>,
537 ) -> Result<&Self> {
538 self.execute(SetPermissionParams {
539 permission,
540 setting,
541 origin: origin.map(Into::into),
542 embedded_origin: embedded_origin.map(Into::into),
543 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
544 })
545 .await?;
546 Ok(self)
547 }
548
549 pub async fn set_permission_for_origin(
551 &self,
552 origin: impl Into<String>,
553 embedded_origin: Option<impl Into<String>>,
554 permission: PermissionDescriptor,
555 setting: PermissionSetting,
556 ) -> Result<&Self> {
557 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
558 .await
559 }
560
561 pub async fn reset_permission_for_origin(
563 &self,
564 origin: impl Into<String>,
565 embedded_origin: Option<impl Into<String>>,
566 permission: PermissionDescriptor,
567 ) -> Result<&Self> {
568 self.set_permission_for_origin(
569 origin,
570 embedded_origin,
571 permission,
572 PermissionSetting::Prompt,
573 )
574 .await
575 }
576
577 pub async fn grant_all_permission_for_origin(
579 &self,
580 origin: impl Into<String>,
581 embedded_origin: Option<impl Into<String>>,
582 permission: PermissionDescriptor,
583 ) -> Result<&Self> {
584 self.set_permission_for_origin(
585 origin,
586 embedded_origin,
587 permission,
588 PermissionSetting::Granted,
589 )
590 .await
591 }
592
593 pub async fn deny_all_permission_for_origin(
595 &self,
596 origin: impl Into<String>,
597 embedded_origin: Option<impl Into<String>>,
598 permission: PermissionDescriptor,
599 ) -> Result<&Self> {
600 self.set_permission_for_origin(
601 origin,
602 embedded_origin,
603 permission,
604 PermissionSetting::Denied,
605 )
606 .await
607 }
608
609 pub async fn pages(&self) -> Result<Vec<Page>> {
611 let (tx, rx) = oneshot_channel();
612 self.sender
613 .clone()
614 .send(HandlerMessage::GetPages(tx))
615 .await?;
616 Ok(rx.await?)
617 }
618
619 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
621 let (tx, rx) = oneshot_channel();
622 self.sender
623 .clone()
624 .send(HandlerMessage::GetPage(target_id, tx))
625 .await?;
626 rx.await?.ok_or(CdpError::NotFound)
627 }
628
629 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
631 let (tx, rx) = unbounded();
632 self.sender
633 .clone()
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 .clone()
669 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
670 .await?;
671 Ok(self)
672 }
673
674 pub async fn dispose_browser_context(
676 &self,
677 browser_context_id: impl Into<BrowserContextId>,
678 ) -> Result<&Self> {
679 self.execute(DisposeBrowserContextParams::new(browser_context_id))
680 .await?;
681
682 Ok(self)
683 }
684
685 pub async fn clear_cookies(&self) -> Result<&Self> {
687 self.execute(ClearCookiesParams::default()).await?;
688 Ok(self)
689 }
690
691 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
693 let cmd = GetCookiesParams {
694 browser_context_id: self.browser_context.id.clone(),
695 };
696
697 Ok(self.execute(cmd).await?.result.cookies)
698 }
699
700 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
702 for cookie in &mut cookies {
703 if let Some(url) = cookie.url.as_ref() {
704 crate::page::validate_cookie_url(url)?;
705 }
706 }
707
708 let mut cookies_param = SetCookiesParams::new(cookies);
709
710 cookies_param.browser_context_id = self.browser_context.id.clone();
711
712 self.execute(cookies_param).await?;
713 Ok(self)
714 }
715}
716
717impl Drop for Browser {
718 fn drop(&mut self) {
719 if let Some(child) = self.child.as_mut() {
720 if let Ok(Some(_)) = child.try_wait() {
721 } else {
723 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
731 }
732 }
733 }
734}
735
736async fn ws_url_from_output(
746 child_process: &mut Child,
747 timeout_fut: impl Future<Output = ()> + Unpin,
748) -> Result<String> {
749 use futures::{AsyncBufReadExt, FutureExt};
750 let mut timeout_fut = timeout_fut.fuse();
751 let stderr = child_process.stderr.take().expect("no stderror");
752 let mut stderr_bytes = Vec::<u8>::new();
753 let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
754 let mut buf = futures::io::BufReader::new(stderr);
755 loop {
756 select! {
757 _ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
758 exit_status = exit_status_fut => {
759 return Err(match exit_status {
760 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
761 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
762 })
763 },
764 read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
765 match read_res {
766 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
767 Ok(byte_count) => {
768 if byte_count == 0 {
769 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
770 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
771 }
772 let start_offset = stderr_bytes.len() - byte_count;
773 let new_bytes = &stderr_bytes[start_offset..];
774 match std::str::from_utf8(new_bytes) {
775 Err(_) => {
776 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
777 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
778 }
779 Ok(line) => {
780 if let Some((_, ws)) = line.rsplit_once("listening on ") {
781 if ws.starts_with("ws") && ws.contains("devtools/browser") {
782 return Ok(ws.trim().to_string());
783 }
784 }
785 }
786 }
787 }
788 }
789 }
790 }
791 }
792}
793
794#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
795pub enum HeadlessMode {
796 False,
798 #[default]
800 True,
801 New,
803}
804
805#[derive(Debug, Clone, Default)]
806pub struct BrowserConfig {
807 headless: HeadlessMode,
810 sandbox: bool,
812 window_size: Option<(u32, u32)>,
814 port: u16,
816 executable: std::path::PathBuf,
821
822 extensions: Vec<String>,
830
831 pub process_envs: Option<HashMap<String, String>>,
834
835 pub user_data_dir: Option<PathBuf>,
837
838 incognito: bool,
840
841 launch_timeout: Duration,
843
844 ignore_https_errors: bool,
846 pub viewport: Option<Viewport>,
847 request_timeout: Duration,
849
850 args: Vec<String>,
852
853 disable_default_args: bool,
855
856 pub request_intercept: bool,
858
859 pub cache_enabled: bool,
861 pub service_worker_enabled: bool,
864 pub ignore_visuals: bool,
867 pub ignore_stylesheets: bool,
870 pub ignore_javascript: bool,
873 pub ignore_analytics: bool,
875 pub ignore_prefetch: bool,
877 pub ignore_ads: bool,
879 pub extra_headers: Option<std::collections::HashMap<String, String>>,
881 pub only_html: bool,
883 pub intercept_manager: NetworkInterceptManager,
885 pub max_bytes_allowed: Option<u64>,
887 pub whitelist_patterns: Option<Vec<String>>,
889 pub blacklist_patterns: Option<Vec<String>>,
891 pub channel_capacity: usize,
894 pub connection_retries: u32,
897}
898
899#[derive(Debug, Clone)]
900pub struct BrowserConfigBuilder {
901 headless: HeadlessMode,
903 sandbox: bool,
905 window_size: Option<(u32, u32)>,
907 port: u16,
909 executable: Option<PathBuf>,
912 executation_detection: DetectionOptions,
914 extensions: Vec<String>,
916 process_envs: Option<HashMap<String, String>>,
918 user_data_dir: Option<PathBuf>,
920 incognito: bool,
922 launch_timeout: Duration,
924 ignore_https_errors: bool,
926 viewport: Option<Viewport>,
928 request_timeout: Duration,
930 args: Vec<String>,
932 disable_default_args: bool,
934 request_intercept: bool,
936 cache_enabled: bool,
938 service_worker_enabled: bool,
940 ignore_visuals: bool,
942 ignore_ads: bool,
944 ignore_javascript: bool,
946 ignore_stylesheets: bool,
948 ignore_prefetch: bool,
950 ignore_analytics: bool,
952 only_html: bool,
954 extra_headers: Option<std::collections::HashMap<String, String>>,
956 intercept_manager: NetworkInterceptManager,
958 max_bytes_allowed: Option<u64>,
960 whitelist_patterns: Option<Vec<String>>,
962 blacklist_patterns: Option<Vec<String>>,
964 channel_capacity: usize,
966 connection_retries: u32,
968}
969
970impl BrowserConfig {
971 pub fn builder() -> BrowserConfigBuilder {
973 BrowserConfigBuilder::default()
974 }
975
976 pub fn with_executable(path: impl AsRef<Path>) -> Self {
978 Self::builder()
979 .chrome_executable(path)
980 .build()
981 .expect("path to executable exist")
982 }
983}
984
985impl Default for BrowserConfigBuilder {
986 fn default() -> Self {
987 Self {
988 headless: HeadlessMode::True,
989 sandbox: true,
990 window_size: None,
991 port: 0,
992 executable: None,
993 executation_detection: DetectionOptions::default(),
994 extensions: Vec::new(),
995 process_envs: None,
996 user_data_dir: None,
997 incognito: false,
998 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
999 ignore_https_errors: true,
1000 viewport: Some(Default::default()),
1001 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1002 args: Vec::new(),
1003 disable_default_args: false,
1004 request_intercept: false,
1005 cache_enabled: true,
1006 ignore_visuals: false,
1007 ignore_ads: false,
1008 ignore_javascript: false,
1009 ignore_analytics: false,
1010 ignore_stylesheets: false,
1011 ignore_prefetch: true,
1012 only_html: false,
1013 extra_headers: Default::default(),
1014 service_worker_enabled: true,
1015 intercept_manager: NetworkInterceptManager::Unknown,
1016 max_bytes_allowed: None,
1017 whitelist_patterns: None,
1018 blacklist_patterns: None,
1019 channel_capacity: 1000,
1020 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1021 }
1022 }
1023}
1024
1025impl BrowserConfigBuilder {
1026 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1028 self.window_size = Some((width, height));
1029 self
1030 }
1031 pub fn no_sandbox(mut self) -> Self {
1033 self.sandbox = false;
1034 self
1035 }
1036 pub fn with_head(mut self) -> Self {
1038 self.headless = HeadlessMode::False;
1039 self
1040 }
1041 pub fn new_headless_mode(mut self) -> Self {
1043 self.headless = HeadlessMode::New;
1044 self
1045 }
1046 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1048 self.headless = mode;
1049 self
1050 }
1051 pub fn incognito(mut self) -> Self {
1053 self.incognito = true;
1054 self
1055 }
1056
1057 pub fn respect_https_errors(mut self) -> Self {
1058 self.ignore_https_errors = false;
1059 self
1060 }
1061
1062 pub fn port(mut self, port: u16) -> Self {
1063 self.port = port;
1064 self
1065 }
1066
1067 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1068 self.max_bytes_allowed = max_bytes_allowed;
1069 self
1070 }
1071
1072 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1073 self.launch_timeout = timeout;
1074 self
1075 }
1076
1077 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1078 self.request_timeout = timeout;
1079 self
1080 }
1081
1082 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1088 self.viewport = viewport.into();
1089 self
1090 }
1091
1092 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1093 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1094 self
1095 }
1096
1097 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1098 self.executable = Some(path.as_ref().to_path_buf());
1099 self
1100 }
1101
1102 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1103 self.executation_detection = options;
1104 self
1105 }
1106
1107 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1108 self.extensions.push(extension.into());
1109 self
1110 }
1111
1112 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1113 where
1114 I: IntoIterator<Item = S>,
1115 S: Into<String>,
1116 {
1117 for ext in extensions {
1118 self.extensions.push(ext.into());
1119 }
1120 self
1121 }
1122
1123 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1124 self.process_envs
1125 .get_or_insert(HashMap::new())
1126 .insert(key.into(), val.into());
1127 self
1128 }
1129
1130 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1131 where
1132 I: IntoIterator<Item = (K, V)>,
1133 K: Into<String>,
1134 V: Into<String>,
1135 {
1136 self.process_envs
1137 .get_or_insert(HashMap::new())
1138 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1139 self
1140 }
1141
1142 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1143 self.args.push(arg.into());
1144 self
1145 }
1146
1147 pub fn args<I, S>(mut self, args: I) -> Self
1148 where
1149 I: IntoIterator<Item = S>,
1150 S: Into<String>,
1151 {
1152 for arg in args {
1153 self.args.push(arg.into());
1154 }
1155 self
1156 }
1157
1158 pub fn disable_default_args(mut self) -> Self {
1159 self.disable_default_args = true;
1160 self
1161 }
1162
1163 pub fn enable_request_intercept(mut self) -> Self {
1164 self.request_intercept = true;
1165 self
1166 }
1167
1168 pub fn disable_request_intercept(mut self) -> Self {
1169 self.request_intercept = false;
1170 self
1171 }
1172
1173 pub fn enable_cache(mut self) -> Self {
1174 self.cache_enabled = true;
1175 self
1176 }
1177
1178 pub fn disable_cache(mut self) -> Self {
1179 self.cache_enabled = false;
1180 self
1181 }
1182
1183 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1185 self.service_worker_enabled = bypass;
1186 self
1187 }
1188
1189 pub fn set_extra_headers(
1191 mut self,
1192 headers: Option<std::collections::HashMap<String, String>>,
1193 ) -> Self {
1194 self.extra_headers = headers;
1195 self
1196 }
1197
1198 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1200 self.whitelist_patterns = whitelist_patterns;
1201 self
1202 }
1203
1204 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1206 self.blacklist_patterns = blacklist_patterns;
1207 self
1208 }
1209
1210 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1213 self.channel_capacity = capacity;
1214 self
1215 }
1216
1217 pub fn connection_retries(mut self, retries: u32) -> Self {
1220 self.connection_retries = retries;
1221 self
1222 }
1223
1224 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1226 let executable = if let Some(e) = self.executable {
1227 e
1228 } else {
1229 detection::default_executable(self.executation_detection)?
1230 };
1231
1232 Ok(BrowserConfig {
1233 headless: self.headless,
1234 sandbox: self.sandbox,
1235 window_size: self.window_size,
1236 port: self.port,
1237 executable,
1238 extensions: self.extensions,
1239 process_envs: self.process_envs,
1240 user_data_dir: self.user_data_dir,
1241 incognito: self.incognito,
1242 launch_timeout: self.launch_timeout,
1243 ignore_https_errors: self.ignore_https_errors,
1244 viewport: self.viewport,
1245 request_timeout: self.request_timeout,
1246 args: self.args,
1247 disable_default_args: self.disable_default_args,
1248 request_intercept: self.request_intercept,
1249 cache_enabled: self.cache_enabled,
1250 ignore_visuals: self.ignore_visuals,
1251 ignore_ads: self.ignore_ads,
1252 ignore_javascript: self.ignore_javascript,
1253 ignore_analytics: self.ignore_analytics,
1254 ignore_stylesheets: self.ignore_stylesheets,
1255 ignore_prefetch: self.ignore_prefetch,
1256 extra_headers: self.extra_headers,
1257 only_html: self.only_html,
1258 intercept_manager: self.intercept_manager,
1259 service_worker_enabled: self.service_worker_enabled,
1260 max_bytes_allowed: self.max_bytes_allowed,
1261 whitelist_patterns: self.whitelist_patterns,
1262 blacklist_patterns: self.blacklist_patterns,
1263 channel_capacity: self.channel_capacity,
1264 connection_retries: self.connection_retries,
1265 })
1266 }
1267}
1268
1269impl BrowserConfig {
1270 pub fn launch(&self) -> io::Result<Child> {
1271 let mut cmd = async_process::Command::new(&self.executable);
1272
1273 if self.disable_default_args {
1274 cmd.args(&self.args);
1275 } else {
1276 cmd.args(DEFAULT_ARGS).args(&self.args);
1277 }
1278
1279 if !self
1280 .args
1281 .iter()
1282 .any(|arg| arg.contains("--remote-debugging-port="))
1283 {
1284 cmd.arg(format!("--remote-debugging-port={}", self.port));
1285 }
1286
1287 cmd.args(
1288 self.extensions
1289 .iter()
1290 .map(|e| format!("--load-extension={e}")),
1291 );
1292
1293 if let Some(ref user_data) = self.user_data_dir {
1294 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1295 } else {
1296 cmd.arg(format!(
1300 "--user-data-dir={}",
1301 std::env::temp_dir().join("chromiumoxide-runner").display()
1302 ));
1303 }
1304
1305 if let Some((width, height)) = self.window_size {
1306 cmd.arg(format!("--window-size={width},{height}"));
1307 }
1308
1309 if !self.sandbox {
1310 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1311 }
1312
1313 match self.headless {
1314 HeadlessMode::False => (),
1315 HeadlessMode::True => {
1316 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1317 }
1318 HeadlessMode::New => {
1319 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1320 }
1321 }
1322
1323 if self.incognito {
1324 cmd.arg("--incognito");
1325 }
1326
1327 if let Some(ref envs) = self.process_envs {
1328 cmd.envs(envs);
1329 }
1330 cmd.stderr(Stdio::piped()).spawn()
1331 }
1332}
1333
1334#[deprecated(note = "Use detection::default_executable instead")]
1343pub fn default_executable() -> Result<std::path::PathBuf, String> {
1344 let options = DetectionOptions {
1345 msedge: false,
1346 unstable: false,
1347 };
1348 detection::default_executable(options)
1349}
1350
1351static DEFAULT_ARGS: [&str; 26] = [
1354 "--disable-background-networking",
1355 "--enable-features=NetworkService,NetworkServiceInProcess",
1356 "--disable-background-timer-throttling",
1357 "--disable-backgrounding-occluded-windows",
1358 "--disable-breakpad",
1359 "--disable-client-side-phishing-detection",
1360 "--disable-component-extensions-with-background-pages",
1361 "--disable-default-apps",
1362 "--disable-dev-shm-usage",
1363 "--disable-extensions",
1364 "--disable-features=TranslateUI",
1365 "--disable-hang-monitor",
1366 "--disable-ipc-flooding-protection",
1367 "--disable-popup-blocking",
1368 "--disable-prompt-on-repost",
1369 "--disable-renderer-backgrounding",
1370 "--disable-sync",
1371 "--force-color-profile=srgb",
1372 "--metrics-recording-only",
1373 "--no-first-run",
1374 "--enable-automation",
1375 "--password-store=basic",
1376 "--use-mock-keychain",
1377 "--enable-blink-features=IdleDetection",
1378 "--lang=en_US",
1379 "--disable-blink-features=AutomationControlled",
1380];