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_prefetch: config.ignore_prefetch,
170 ignore_ads: config.ignore_ads,
171 extra_headers: config.extra_headers.clone(),
172 only_html: config.only_html,
173 service_worker_enabled: config.service_worker_enabled,
174 intercept_manager: config.intercept_manager,
175 max_bytes_allowed: config.max_bytes_allowed,
176 whitelist_patterns: config.whitelist_patterns.clone(),
177 blacklist_patterns: config.blacklist_patterns.clone(),
178 ..Default::default()
179 };
180
181 let fut = Handler::new(conn, rx, config);
182 let browser_context = fut.default_browser_context().clone();
183
184 let browser = Self {
185 sender: tx,
186 config: Some(handler_config),
187 child: None,
188 debug_ws_url,
189 browser_context,
190 };
191
192 Ok((browser, fut))
193 }
194
195 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
204 config.executable = utils::canonicalize_except_snap(config.executable).await?;
206
207 let mut child = config.launch()?;
209
210 async fn with_child(
215 config: &BrowserConfig,
216 child: &mut Child,
217 ) -> Result<(String, Connection<CdpEventMessage>)> {
218 let dur = config.launch_timeout;
219 let timeout_fut = Box::pin(tokio::time::sleep(dur));
220
221 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
223 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
224 Ok((debug_ws_url, conn))
225 }
226
227 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
228 Ok(conn) => conn,
229 Err(e) => {
230 if let Ok(Some(_)) = child.try_wait() {
232 } else {
234 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
236 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
237 }
238 return Err(e);
239 }
240 };
241
242 let (tx, rx) = channel(1000);
246
247 let handler_config = HandlerConfig {
248 ignore_https_errors: config.ignore_https_errors,
249 viewport: config.viewport.clone(),
250 context_ids: Vec::new(),
251 request_timeout: config.request_timeout,
252 request_intercept: config.request_intercept,
253 cache_enabled: config.cache_enabled,
254 ignore_visuals: config.ignore_visuals,
255 ignore_stylesheets: config.ignore_stylesheets,
256 ignore_javascript: config.ignore_javascript,
257 ignore_analytics: config.ignore_analytics,
258 ignore_prefetch: config.ignore_prefetch,
259 ignore_ads: config.ignore_ads,
260 extra_headers: config.extra_headers.clone(),
261 only_html: config.only_html,
262 service_worker_enabled: config.service_worker_enabled,
263 created_first_target: false,
264 intercept_manager: config.intercept_manager,
265 max_bytes_allowed: config.max_bytes_allowed,
266 whitelist_patterns: config.whitelist_patterns.clone(),
267 blacklist_patterns: config.blacklist_patterns.clone(),
268 };
269
270 let fut = Handler::new(conn, rx, handler_config);
271 let browser_context = fut.default_browser_context().clone();
272
273 let browser = Self {
274 sender: tx,
275 config: Some(config),
276 child: Some(child),
277 debug_ws_url,
278 browser_context,
279 };
280
281 Ok((browser, fut))
282 }
283
284 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
294 let (tx, rx) = oneshot_channel();
295
296 self.sender
297 .clone()
298 .send(HandlerMessage::FetchTargets(tx))
299 .await?;
300
301 rx.await?
302 }
303
304 pub async fn close(&self) -> Result<CloseReturns> {
311 let (tx, rx) = oneshot_channel();
312
313 self.sender
314 .clone()
315 .send(HandlerMessage::CloseBrowser(tx))
316 .await?;
317
318 rx.await?
319 }
320
321 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
330 if let Some(child) = self.child.as_mut() {
331 Ok(Some(child.wait().await?))
332 } else {
333 Ok(None)
334 }
335 }
336
337 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
346 if let Some(child) = self.child.as_mut() {
347 child.try_wait()
348 } else {
349 Ok(None)
350 }
351 }
352
353 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
364 self.child.as_mut()
365 }
366
367 pub fn has_child(&self) -> bool {
369 self.child.is_some()
370 }
371
372 pub async fn kill(&mut self) -> Option<io::Result<()>> {
383 match self.child.as_mut() {
384 Some(child) => Some(child.kill().await),
385 None => None,
386 }
387 }
388
389 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
395 if !self.is_incognito_configured() {
396 let browser_context_id = self
397 .create_browser_context(CreateBrowserContextParams::default())
398 .await?;
399 self.browser_context = BrowserContext::from(browser_context_id);
400 self.sender
401 .clone()
402 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
403 .await?;
404 }
405
406 Ok(self)
407 }
408
409 pub async fn quit_incognito_context_base(
415 &self,
416 browser_context_id: BrowserContextId,
417 ) -> Result<&Self> {
418 self.dispose_browser_context(browser_context_id.clone())
419 .await?;
420 self.sender
421 .clone()
422 .send(HandlerMessage::DisposeContext(BrowserContext::from(
423 browser_context_id,
424 )))
425 .await?;
426 Ok(self)
427 }
428
429 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
435 if let Some(id) = self.browser_context.take() {
436 let _ = self.quit_incognito_context_base(id).await;
437 }
438 Ok(self)
439 }
440
441 fn is_incognito_configured(&self) -> bool {
443 self.config
444 .as_ref()
445 .map(|c| c.incognito)
446 .unwrap_or_default()
447 }
448
449 pub fn websocket_address(&self) -> &String {
451 &self.debug_ws_url
452 }
453
454 pub fn is_incognito(&self) -> bool {
456 self.is_incognito_configured() || self.browser_context.is_incognito()
457 }
458
459 pub fn config(&self) -> Option<&BrowserConfig> {
461 self.config.as_ref()
462 }
463
464 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
466 let (tx, rx) = oneshot_channel();
467 let mut params = params.into();
468
469 if let Some(id) = self.browser_context.id() {
470 if params.browser_context_id.is_none() {
471 params.browser_context_id = Some(id.clone());
472 }
473 }
474
475 let _ = self
476 .sender
477 .clone()
478 .send(HandlerMessage::CreatePage(params, tx))
479 .await;
480
481 rx.await?
482 }
483
484 pub async fn version(&self) -> Result<GetVersionReturns> {
486 Ok(self.execute(GetVersionParams::default()).await?.result)
487 }
488
489 pub async fn user_agent(&self) -> Result<String> {
491 Ok(self.version().await?.user_agent)
492 }
493
494 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
496 let (tx, rx) = oneshot_channel();
497 let method = cmd.identifier();
498 let msg = CommandMessage::new(cmd, tx)?;
499
500 self.sender
501 .clone()
502 .send(HandlerMessage::Command(msg))
503 .await?;
504 let resp = rx.await??;
505 to_command_response::<T>(resp, method)
506 }
507
508 pub async fn set_permission(
512 &self,
513 permission: PermissionDescriptor,
514 setting: PermissionSetting,
515 origin: Option<impl Into<String>>,
516 embedded_origin: Option<impl Into<String>>,
517 browser_context_id: Option<BrowserContextId>,
518 ) -> Result<&Self> {
519 self.execute(SetPermissionParams {
520 permission,
521 setting,
522 origin: origin.map(Into::into),
523 embedded_origin: embedded_origin.map(Into::into),
524 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
525 })
526 .await?;
527 Ok(self)
528 }
529
530 pub async fn set_permission_for_origin(
532 &self,
533 origin: impl Into<String>,
534 embedded_origin: Option<impl Into<String>>,
535 permission: PermissionDescriptor,
536 setting: PermissionSetting,
537 ) -> Result<&Self> {
538 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
539 .await
540 }
541
542 pub async fn reset_permission_for_origin(
544 &self,
545 origin: impl Into<String>,
546 embedded_origin: Option<impl Into<String>>,
547 permission: PermissionDescriptor,
548 ) -> Result<&Self> {
549 self.set_permission_for_origin(
550 origin,
551 embedded_origin,
552 permission,
553 PermissionSetting::Prompt,
554 )
555 .await
556 }
557
558 pub async fn grant_all_permission_for_origin(
560 &self,
561 origin: impl Into<String>,
562 embedded_origin: Option<impl Into<String>>,
563 permission: PermissionDescriptor,
564 ) -> Result<&Self> {
565 self.set_permission_for_origin(
566 origin,
567 embedded_origin,
568 permission,
569 PermissionSetting::Granted,
570 )
571 .await
572 }
573
574 pub async fn deny_all_permission_for_origin(
576 &self,
577 origin: impl Into<String>,
578 embedded_origin: Option<impl Into<String>>,
579 permission: PermissionDescriptor,
580 ) -> Result<&Self> {
581 self.set_permission_for_origin(
582 origin,
583 embedded_origin,
584 permission,
585 PermissionSetting::Denied,
586 )
587 .await
588 }
589
590 pub async fn pages(&self) -> Result<Vec<Page>> {
592 let (tx, rx) = oneshot_channel();
593 self.sender
594 .clone()
595 .send(HandlerMessage::GetPages(tx))
596 .await?;
597 Ok(rx.await?)
598 }
599
600 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
602 let (tx, rx) = oneshot_channel();
603 self.sender
604 .clone()
605 .send(HandlerMessage::GetPage(target_id, tx))
606 .await?;
607 rx.await?.ok_or(CdpError::NotFound)
608 }
609
610 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
612 let (tx, rx) = unbounded();
613 self.sender
614 .clone()
615 .send(HandlerMessage::AddEventListener(
616 EventListenerRequest::new::<T>(tx),
617 ))
618 .await?;
619
620 Ok(EventStream::new(rx))
621 }
622
623 pub async fn create_browser_context(
625 &mut self,
626 params: CreateBrowserContextParams,
627 ) -> Result<BrowserContextId> {
628 let response = self.execute(params).await?;
629
630 Ok(response.result.browser_context_id)
631 }
632
633 pub async fn get_browser_contexts(
635 &mut self,
636 params: GetBrowserContextsParams,
637 ) -> Result<GetBrowserContextsReturns> {
638 let response = self.execute(params).await?;
639 Ok(response.result)
640 }
641
642 pub async fn send_new_context(
644 &mut self,
645 browser_context_id: BrowserContextId,
646 ) -> Result<&Self> {
647 self.browser_context = BrowserContext::from(browser_context_id);
648 self.sender
649 .clone()
650 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
651 .await?;
652 Ok(self)
653 }
654
655 pub async fn dispose_browser_context(
657 &self,
658 browser_context_id: impl Into<BrowserContextId>,
659 ) -> Result<&Self> {
660 self.execute(DisposeBrowserContextParams::new(browser_context_id))
661 .await?;
662
663 Ok(self)
664 }
665
666 pub async fn clear_cookies(&self) -> Result<&Self> {
668 self.execute(ClearCookiesParams::default()).await?;
669 Ok(self)
670 }
671
672 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
674 let mut cmd = GetCookiesParams::default();
675
676 cmd.browser_context_id = self.browser_context.id.clone();
677
678 Ok(self.execute(cmd).await?.result.cookies)
679 }
680
681 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
683 for cookie in &mut cookies {
684 if let Some(url) = cookie.url.as_ref() {
685 crate::page::validate_cookie_url(url)?;
686 }
687 }
688
689 let mut cookies_param = SetCookiesParams::new(cookies);
690
691 cookies_param.browser_context_id = self.browser_context.id.clone();
692
693 self.execute(cookies_param).await?;
694 Ok(self)
695 }
696}
697
698impl Drop for Browser {
699 fn drop(&mut self) {
700 if let Some(child) = self.child.as_mut() {
701 if let Ok(Some(_)) = child.try_wait() {
702 } else {
704 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
712 }
713 }
714 }
715}
716
717async fn ws_url_from_output(
727 child_process: &mut Child,
728 timeout_fut: impl Future<Output = ()> + Unpin,
729) -> Result<String> {
730 use futures::{AsyncBufReadExt, FutureExt};
731 let mut timeout_fut = timeout_fut.fuse();
732 let stderr = child_process.stderr.take().expect("no stderror");
733 let mut stderr_bytes = Vec::<u8>::new();
734 let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
735 let mut buf = futures::io::BufReader::new(stderr);
736 loop {
737 select! {
738 _ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
739 exit_status = exit_status_fut => {
740 return Err(match exit_status {
741 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
742 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
743 })
744 },
745 read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
746 match read_res {
747 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
748 Ok(byte_count) => {
749 if byte_count == 0 {
750 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
751 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
752 }
753 let start_offset = stderr_bytes.len() - byte_count;
754 let new_bytes = &stderr_bytes[start_offset..];
755 match std::str::from_utf8(new_bytes) {
756 Err(_) => {
757 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
758 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
759 }
760 Ok(line) => {
761 if let Some((_, ws)) = line.rsplit_once("listening on ") {
762 if ws.starts_with("ws") && ws.contains("devtools/browser") {
763 return Ok(ws.trim().to_string());
764 }
765 }
766 }
767 }
768 }
769 }
770 }
771 }
772 }
773}
774
775#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
776pub enum HeadlessMode {
777 False,
779 #[default]
781 True,
782 New,
784}
785
786#[derive(Debug, Clone, Default)]
787pub struct BrowserConfig {
788 headless: HeadlessMode,
791 sandbox: bool,
793 window_size: Option<(u32, u32)>,
795 port: u16,
797 executable: std::path::PathBuf,
802
803 extensions: Vec<String>,
811
812 pub process_envs: Option<HashMap<String, String>>,
815
816 pub user_data_dir: Option<PathBuf>,
818
819 incognito: bool,
821
822 launch_timeout: Duration,
824
825 ignore_https_errors: bool,
827 pub viewport: Option<Viewport>,
828 request_timeout: Duration,
830
831 args: Vec<String>,
833
834 disable_default_args: bool,
836
837 pub request_intercept: bool,
839
840 pub cache_enabled: bool,
842 pub service_worker_enabled: bool,
845 pub ignore_visuals: bool,
848 pub ignore_stylesheets: bool,
851 pub ignore_javascript: bool,
854 pub ignore_analytics: bool,
856 pub ignore_prefetch: bool,
858 pub ignore_ads: bool,
860 pub extra_headers: Option<std::collections::HashMap<String, String>>,
862 pub only_html: bool,
864 pub intercept_manager: NetworkInterceptManager,
866 pub max_bytes_allowed: Option<u64>,
868 pub whitelist_patterns: Option<Vec<String>>,
870 pub blacklist_patterns: Option<Vec<String>>,
872}
873
874#[derive(Debug, Clone)]
875pub struct BrowserConfigBuilder {
876 headless: HeadlessMode,
878 sandbox: bool,
880 window_size: Option<(u32, u32)>,
882 port: u16,
884 executable: Option<PathBuf>,
887 executation_detection: DetectionOptions,
889 extensions: Vec<String>,
891 process_envs: Option<HashMap<String, String>>,
893 user_data_dir: Option<PathBuf>,
895 incognito: bool,
897 launch_timeout: Duration,
899 ignore_https_errors: bool,
901 viewport: Option<Viewport>,
903 request_timeout: Duration,
905 args: Vec<String>,
907 disable_default_args: bool,
909 request_intercept: bool,
911 cache_enabled: bool,
913 service_worker_enabled: bool,
915 ignore_visuals: bool,
917 ignore_ads: bool,
919 ignore_javascript: bool,
921 ignore_stylesheets: bool,
923 ignore_prefetch: bool,
925 ignore_analytics: bool,
927 only_html: bool,
929 extra_headers: Option<std::collections::HashMap<String, String>>,
931 intercept_manager: NetworkInterceptManager,
933 max_bytes_allowed: Option<u64>,
935 whitelist_patterns: Option<Vec<String>>,
937 blacklist_patterns: Option<Vec<String>>,
939}
940
941impl BrowserConfig {
942 pub fn builder() -> BrowserConfigBuilder {
944 BrowserConfigBuilder::default()
945 }
946
947 pub fn with_executable(path: impl AsRef<Path>) -> Self {
949 Self::builder()
950 .chrome_executable(path)
951 .build()
952 .expect("path to executable exist")
953 }
954}
955
956impl Default for BrowserConfigBuilder {
957 fn default() -> Self {
958 Self {
959 headless: HeadlessMode::True,
960 sandbox: true,
961 window_size: None,
962 port: 0,
963 executable: None,
964 executation_detection: DetectionOptions::default(),
965 extensions: Vec::new(),
966 process_envs: None,
967 user_data_dir: None,
968 incognito: false,
969 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
970 ignore_https_errors: true,
971 viewport: Some(Default::default()),
972 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
973 args: Vec::new(),
974 disable_default_args: false,
975 request_intercept: false,
976 cache_enabled: true,
977 ignore_visuals: false,
978 ignore_ads: false,
979 ignore_javascript: false,
980 ignore_analytics: false,
981 ignore_stylesheets: false,
982 ignore_prefetch: true,
983 only_html: false,
984 extra_headers: Default::default(),
985 service_worker_enabled: true,
986 intercept_manager: NetworkInterceptManager::Unknown,
987 max_bytes_allowed: None,
988 whitelist_patterns: None,
989 blacklist_patterns: None,
990 }
991 }
992}
993
994impl BrowserConfigBuilder {
995 pub fn window_size(mut self, width: u32, height: u32) -> Self {
997 self.window_size = Some((width, height));
998 self
999 }
1000 pub fn no_sandbox(mut self) -> Self {
1002 self.sandbox = false;
1003 self
1004 }
1005 pub fn with_head(mut self) -> Self {
1007 self.headless = HeadlessMode::False;
1008 self
1009 }
1010 pub fn new_headless_mode(mut self) -> Self {
1012 self.headless = HeadlessMode::New;
1013 self
1014 }
1015 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1017 self.headless = mode;
1018 self
1019 }
1020 pub fn incognito(mut self) -> Self {
1022 self.incognito = true;
1023 self
1024 }
1025
1026 pub fn respect_https_errors(mut self) -> Self {
1027 self.ignore_https_errors = false;
1028 self
1029 }
1030
1031 pub fn port(mut self, port: u16) -> Self {
1032 self.port = port;
1033 self
1034 }
1035
1036 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1037 self.max_bytes_allowed = max_bytes_allowed;
1038 self
1039 }
1040
1041 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1042 self.launch_timeout = timeout;
1043 self
1044 }
1045
1046 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1047 self.request_timeout = timeout;
1048 self
1049 }
1050
1051 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1057 self.viewport = viewport.into();
1058 self
1059 }
1060
1061 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1062 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1063 self
1064 }
1065
1066 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1067 self.executable = Some(path.as_ref().to_path_buf());
1068 self
1069 }
1070
1071 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1072 self.executation_detection = options;
1073 self
1074 }
1075
1076 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1077 self.extensions.push(extension.into());
1078 self
1079 }
1080
1081 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1082 where
1083 I: IntoIterator<Item = S>,
1084 S: Into<String>,
1085 {
1086 for ext in extensions {
1087 self.extensions.push(ext.into());
1088 }
1089 self
1090 }
1091
1092 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1093 self.process_envs
1094 .get_or_insert(HashMap::new())
1095 .insert(key.into(), val.into());
1096 self
1097 }
1098
1099 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1100 where
1101 I: IntoIterator<Item = (K, V)>,
1102 K: Into<String>,
1103 V: Into<String>,
1104 {
1105 self.process_envs
1106 .get_or_insert(HashMap::new())
1107 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1108 self
1109 }
1110
1111 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1112 self.args.push(arg.into());
1113 self
1114 }
1115
1116 pub fn args<I, S>(mut self, args: I) -> Self
1117 where
1118 I: IntoIterator<Item = S>,
1119 S: Into<String>,
1120 {
1121 for arg in args {
1122 self.args.push(arg.into());
1123 }
1124 self
1125 }
1126
1127 pub fn disable_default_args(mut self) -> Self {
1128 self.disable_default_args = true;
1129 self
1130 }
1131
1132 pub fn enable_request_intercept(mut self) -> Self {
1133 self.request_intercept = true;
1134 self
1135 }
1136
1137 pub fn disable_request_intercept(mut self) -> Self {
1138 self.request_intercept = false;
1139 self
1140 }
1141
1142 pub fn enable_cache(mut self) -> Self {
1143 self.cache_enabled = true;
1144 self
1145 }
1146
1147 pub fn disable_cache(mut self) -> Self {
1148 self.cache_enabled = false;
1149 self
1150 }
1151
1152 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1154 self.service_worker_enabled = bypass;
1155 self
1156 }
1157
1158 pub fn set_extra_headers(
1160 mut self,
1161 headers: Option<std::collections::HashMap<String, String>>,
1162 ) -> Self {
1163 self.extra_headers = headers;
1164 self
1165 }
1166
1167 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1169 self.whitelist_patterns = whitelist_patterns;
1170 self
1171 }
1172
1173 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1175 self.blacklist_patterns = blacklist_patterns;
1176 self
1177 }
1178
1179 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1181 let executable = if let Some(e) = self.executable {
1182 e
1183 } else {
1184 detection::default_executable(self.executation_detection)?
1185 };
1186
1187 Ok(BrowserConfig {
1188 headless: self.headless,
1189 sandbox: self.sandbox,
1190 window_size: self.window_size,
1191 port: self.port,
1192 executable,
1193 extensions: self.extensions,
1194 process_envs: self.process_envs,
1195 user_data_dir: self.user_data_dir,
1196 incognito: self.incognito,
1197 launch_timeout: self.launch_timeout,
1198 ignore_https_errors: self.ignore_https_errors,
1199 viewport: self.viewport,
1200 request_timeout: self.request_timeout,
1201 args: self.args,
1202 disable_default_args: self.disable_default_args,
1203 request_intercept: self.request_intercept,
1204 cache_enabled: self.cache_enabled,
1205 ignore_visuals: self.ignore_visuals,
1206 ignore_ads: self.ignore_ads,
1207 ignore_javascript: self.ignore_javascript,
1208 ignore_analytics: self.ignore_analytics,
1209 ignore_stylesheets: self.ignore_stylesheets,
1210 ignore_prefetch: self.ignore_prefetch,
1211 extra_headers: self.extra_headers,
1212 only_html: self.only_html,
1213 intercept_manager: self.intercept_manager,
1214 service_worker_enabled: self.service_worker_enabled,
1215 max_bytes_allowed: self.max_bytes_allowed,
1216 whitelist_patterns: self.whitelist_patterns,
1217 blacklist_patterns: self.blacklist_patterns,
1218 })
1219 }
1220}
1221
1222impl BrowserConfig {
1223 pub fn launch(&self) -> io::Result<Child> {
1224 let mut cmd = async_process::Command::new(&self.executable);
1225
1226 if self.disable_default_args {
1227 cmd.args(&self.args);
1228 } else {
1229 cmd.args(DEFAULT_ARGS).args(&self.args);
1230 }
1231
1232 if !self
1233 .args
1234 .iter()
1235 .any(|arg| arg.contains("--remote-debugging-port="))
1236 {
1237 cmd.arg(format!("--remote-debugging-port={}", self.port));
1238 }
1239
1240 cmd.args(
1241 self.extensions
1242 .iter()
1243 .map(|e| format!("--load-extension={e}")),
1244 );
1245
1246 if let Some(ref user_data) = self.user_data_dir {
1247 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1248 } else {
1249 cmd.arg(format!(
1253 "--user-data-dir={}",
1254 std::env::temp_dir().join("chromiumoxide-runner").display()
1255 ));
1256 }
1257
1258 if let Some((width, height)) = self.window_size {
1259 cmd.arg(format!("--window-size={width},{height}"));
1260 }
1261
1262 if !self.sandbox {
1263 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1264 }
1265
1266 match self.headless {
1267 HeadlessMode::False => (),
1268 HeadlessMode::True => {
1269 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1270 }
1271 HeadlessMode::New => {
1272 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1273 }
1274 }
1275
1276 if self.incognito {
1277 cmd.arg("--incognito");
1278 }
1279
1280 if let Some(ref envs) = self.process_envs {
1281 cmd.envs(envs);
1282 }
1283 cmd.stderr(Stdio::piped()).spawn()
1284 }
1285}
1286
1287#[deprecated(note = "Use detection::default_executable instead")]
1296pub fn default_executable() -> Result<std::path::PathBuf, String> {
1297 let options = DetectionOptions {
1298 msedge: false,
1299 unstable: false,
1300 };
1301 detection::default_executable(options)
1302}
1303
1304static DEFAULT_ARGS: [&str; 26] = [
1307 "--disable-background-networking",
1308 "--enable-features=NetworkService,NetworkServiceInProcess",
1309 "--disable-background-timer-throttling",
1310 "--disable-backgrounding-occluded-windows",
1311 "--disable-breakpad",
1312 "--disable-client-side-phishing-detection",
1313 "--disable-component-extensions-with-background-pages",
1314 "--disable-default-apps",
1315 "--disable-dev-shm-usage",
1316 "--disable-extensions",
1317 "--disable-features=TranslateUI",
1318 "--disable-hang-monitor",
1319 "--disable-ipc-flooding-protection",
1320 "--disable-popup-blocking",
1321 "--disable-prompt-on-repost",
1322 "--disable-renderer-backgrounding",
1323 "--disable-sync",
1324 "--force-color-profile=srgb",
1325 "--metrics-recording-only",
1326 "--no-first-run",
1327 "--enable-automation",
1328 "--password-store=basic",
1329 "--use-mock-keychain",
1330 "--enable-blink-features=IdleDetection",
1331 "--lang=en_US",
1332 "--disable-blink-features=AutomationControlled",
1333];