1use hashbrown::HashMap;
2use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
3use std::future::Future;
4use std::time::Duration;
5use std::{
6 io,
7 path::{Path, PathBuf},
8};
9
10use tokio::sync::mpsc::{channel, unbounded_channel, Sender};
11use tokio::sync::oneshot::channel as oneshot_channel;
12
13use crate::async_process::{self, Child, ExitStatus, Stdio};
14use crate::cmd::{to_command_response, CommandMessage};
15use crate::conn::Connection;
16use crate::detection::{self, DetectionOptions};
17use crate::error::{BrowserStderr, CdpError, Result};
18use crate::handler::browser::BrowserContext;
19use crate::handler::viewport::Viewport;
20use crate::handler::{Handler, HandlerConfig, HandlerMessage, REQUEST_TIMEOUT};
21use crate::listeners::{EventListenerRequest, EventStream};
22use crate::page::Page;
23use crate::utils;
24use chromiumoxide_cdp::cdp::browser_protocol::browser::{
25 BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
26};
27use chromiumoxide_cdp::cdp::browser_protocol::browser::{
28 PermissionDescriptor, PermissionSetting, SetPermissionParams,
29};
30use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
31use chromiumoxide_cdp::cdp::browser_protocol::storage::{
32 ClearCookiesParams, GetCookiesParams, SetCookiesParams,
33};
34use chromiumoxide_cdp::cdp::browser_protocol::target::{
35 CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
36 GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
37};
38
39use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
40use chromiumoxide_types::*;
41use spider_network_blocker::intercept_manager::NetworkInterceptManager;
42
43pub const LAUNCH_TIMEOUT: u64 = 20_000;
45
46lazy_static::lazy_static! {
47 static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
49 .timeout(Duration::from_secs(60))
50 .default_headers({
51 let mut m = HeaderMap::new();
52
53 m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
54
55 m
56 })
57 .tcp_keepalive(Some(Duration::from_secs(5)))
58 .pool_idle_timeout(Some(Duration::from_secs(60)))
59 .pool_max_idle_per_host(10)
60 .build()
61 .expect("client to build");
62}
63
64#[derive(Debug)]
66pub struct Browser {
67 pub(crate) sender: Sender<HandlerMessage>,
70 config: Option<BrowserConfig>,
72 child: Option<Child>,
74 debug_ws_url: String,
76 pub browser_context: BrowserContext,
78}
79
80#[derive(serde::Deserialize, Debug, Default)]
82pub struct BrowserConnection {
83 #[serde(rename = "Browser")]
84 pub browser: String,
86 #[serde(rename = "Protocol-Version")]
87 pub protocol_version: String,
89 #[serde(rename = "User-Agent")]
90 pub user_agent: String,
92 #[serde(rename = "V8-Version")]
93 pub v8_version: String,
95 #[serde(rename = "WebKit-Version")]
96 pub webkit_version: String,
98 #[serde(rename = "webSocketDebuggerUrl")]
99 pub web_socket_debugger_url: String,
101}
102
103impl Browser {
104 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
108 Self::connect_with_config(url, HandlerConfig::default()).await
109 }
110
111 pub async fn connect_with_config(
115 url: impl Into<String>,
116 config: HandlerConfig,
117 ) -> Result<(Self, Handler)> {
118 let mut debug_ws_url = url.into();
119 let retries = config.connection_retries;
120
121 if debug_ws_url.starts_with("http") {
122 let version_url = if debug_ws_url.ends_with("/json/version")
123 || debug_ws_url.ends_with("/json/version/")
124 {
125 debug_ws_url.to_owned()
126 } else {
127 format!(
128 "{}{}json/version",
129 &debug_ws_url,
130 if debug_ws_url.ends_with('/') { "" } else { "/" }
131 )
132 };
133
134 let mut discovered = false;
135
136 for attempt in 0..=retries {
137 match REQUEST_CLIENT.get(&version_url).send().await {
138 Ok(req) => {
139 if let Ok(b) = req.bytes().await {
140 if let Ok(connection) =
141 crate::serde_json::from_slice::<Box<BrowserConnection>>(&b)
142 {
143 if !connection.web_socket_debugger_url.is_empty() {
144 debug_ws_url = connection.web_socket_debugger_url;
145 }
146 }
147 }
148 discovered = true;
149 break;
150 }
151 Err(_) => {
152 if attempt < retries {
153 let backoff_ms = 50u64 * 3u64.saturating_pow(attempt);
154 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
155 }
156 }
157 }
158 }
159
160 if !discovered {
161 return Err(CdpError::NoResponse);
162 }
163 }
164
165 let conn =
166 Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
167
168 let (tx, rx) = channel(config.channel_capacity);
169
170 let handler_config = BrowserConfig {
171 ignore_https_errors: config.ignore_https_errors,
172 viewport: config.viewport.clone(),
173 request_timeout: config.request_timeout,
174 request_intercept: config.request_intercept,
175 cache_enabled: config.cache_enabled,
176 ignore_visuals: config.ignore_visuals,
177 ignore_stylesheets: config.ignore_stylesheets,
178 ignore_javascript: config.ignore_javascript,
179 ignore_analytics: config.ignore_analytics,
180 ignore_prefetch: config.ignore_prefetch,
181 ignore_ads: config.ignore_ads,
182 extra_headers: config.extra_headers.clone(),
183 only_html: config.only_html,
184 service_worker_enabled: config.service_worker_enabled,
185 intercept_manager: config.intercept_manager,
186 max_bytes_allowed: config.max_bytes_allowed,
187 whitelist_patterns: config.whitelist_patterns.clone(),
188 blacklist_patterns: config.blacklist_patterns.clone(),
189 ..Default::default()
190 };
191
192 let fut = Handler::new(conn, rx, config);
193 let browser_context = fut.default_browser_context().clone();
194
195 let browser = Self {
196 sender: tx,
197 config: Some(handler_config),
198 child: None,
199 debug_ws_url,
200 browser_context,
201 };
202
203 Ok((browser, fut))
204 }
205
206 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
215 config.executable = utils::canonicalize_except_snap(config.executable).await?;
217
218 let mut child = config.launch()?;
220
221 async fn with_child(
226 config: &BrowserConfig,
227 child: &mut Child,
228 ) -> Result<(String, Connection<CdpEventMessage>)> {
229 let dur = config.launch_timeout;
230 let timeout_fut = Box::pin(tokio::time::sleep(dur));
231
232 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
234 let conn = Connection::<CdpEventMessage>::connect_with_retries(
235 &debug_ws_url,
236 config.connection_retries,
237 )
238 .await?;
239 Ok((debug_ws_url, conn))
240 }
241
242 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
243 Ok(conn) => conn,
244 Err(e) => {
245 if let Ok(Some(_)) = child.try_wait() {
247 } else {
249 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
251 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
252 }
253 return Err(e);
254 }
255 };
256
257 let (tx, rx) = channel(config.channel_capacity);
261
262 let handler_config = HandlerConfig {
263 ignore_https_errors: config.ignore_https_errors,
264 viewport: config.viewport.clone(),
265 context_ids: Vec::new(),
266 request_timeout: config.request_timeout,
267 request_intercept: config.request_intercept,
268 cache_enabled: config.cache_enabled,
269 ignore_visuals: config.ignore_visuals,
270 ignore_stylesheets: config.ignore_stylesheets,
271 ignore_javascript: config.ignore_javascript,
272 ignore_analytics: config.ignore_analytics,
273 ignore_prefetch: config.ignore_prefetch,
274 ignore_ads: config.ignore_ads,
275 extra_headers: config.extra_headers.clone(),
276 only_html: config.only_html,
277 service_worker_enabled: config.service_worker_enabled,
278 created_first_target: false,
279 intercept_manager: config.intercept_manager,
280 max_bytes_allowed: config.max_bytes_allowed,
281 whitelist_patterns: config.whitelist_patterns.clone(),
282 blacklist_patterns: config.blacklist_patterns.clone(),
283 channel_capacity: config.channel_capacity,
284 connection_retries: config.connection_retries,
285 };
286
287 let fut = Handler::new(conn, rx, handler_config);
288 let browser_context = fut.default_browser_context().clone();
289
290 let browser = Self {
291 sender: tx,
292 config: Some(config),
293 child: Some(child),
294 debug_ws_url,
295 browser_context,
296 };
297
298 Ok((browser, fut))
299 }
300
301 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
311 let (tx, rx) = oneshot_channel();
312
313 self.sender
314 .send(HandlerMessage::FetchTargets(tx))
315 .await?;
316
317 rx.await?
318 }
319
320 pub async fn close(&self) -> Result<CloseReturns> {
327 let (tx, rx) = oneshot_channel();
328
329 self.sender
330 .send(HandlerMessage::CloseBrowser(tx))
331 .await?;
332
333 rx.await?
334 }
335
336 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
345 if let Some(child) = self.child.as_mut() {
346 Ok(Some(child.wait().await?))
347 } else {
348 Ok(None)
349 }
350 }
351
352 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
361 if let Some(child) = self.child.as_mut() {
362 child.try_wait()
363 } else {
364 Ok(None)
365 }
366 }
367
368 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
379 self.child.as_mut()
380 }
381
382 pub fn has_child(&self) -> bool {
384 self.child.is_some()
385 }
386
387 pub async fn kill(&mut self) -> Option<io::Result<()>> {
398 match self.child.as_mut() {
399 Some(child) => Some(child.kill().await),
400 None => None,
401 }
402 }
403
404 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
410 if !self.is_incognito_configured() {
411 let browser_context_id = self
412 .create_browser_context(CreateBrowserContextParams::default())
413 .await?;
414 self.browser_context = BrowserContext::from(browser_context_id);
415 self.sender
416 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
417 .await?;
418 }
419
420 Ok(self)
421 }
422
423 pub async fn quit_incognito_context_base(
429 &self,
430 browser_context_id: BrowserContextId,
431 ) -> Result<&Self> {
432 self.dispose_browser_context(browser_context_id.clone())
433 .await?;
434 self.sender
435 .send(HandlerMessage::DisposeContext(BrowserContext::from(
436 browser_context_id,
437 )))
438 .await?;
439 Ok(self)
440 }
441
442 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
448 if let Some(id) = self.browser_context.take() {
449 let _ = self.quit_incognito_context_base(id).await;
450 }
451 Ok(self)
452 }
453
454 fn is_incognito_configured(&self) -> bool {
456 self.config
457 .as_ref()
458 .map(|c| c.incognito)
459 .unwrap_or_default()
460 }
461
462 pub fn websocket_address(&self) -> &String {
464 &self.debug_ws_url
465 }
466
467 pub fn is_incognito(&self) -> bool {
469 self.is_incognito_configured() || self.browser_context.is_incognito()
470 }
471
472 pub fn config(&self) -> Option<&BrowserConfig> {
474 self.config.as_ref()
475 }
476
477 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
479 let (tx, rx) = oneshot_channel();
480 let mut params = params.into();
481
482 if let Some(id) = self.browser_context.id() {
483 if params.browser_context_id.is_none() {
484 params.browser_context_id = Some(id.clone());
485 }
486 }
487
488 let _ = self
489 .sender
490 .send(HandlerMessage::CreatePage(params, tx))
491 .await;
492
493 rx.await?
494 }
495
496 pub async fn version(&self) -> Result<GetVersionReturns> {
498 Ok(self.execute(GetVersionParams::default()).await?.result)
499 }
500
501 pub async fn user_agent(&self) -> Result<String> {
503 Ok(self.version().await?.user_agent)
504 }
505
506 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
508 let (tx, rx) = oneshot_channel();
509 let method = cmd.identifier();
510 let msg = CommandMessage::new(cmd, tx)?;
511
512 self.sender
513 .send(HandlerMessage::Command(msg))
514 .await?;
515 let resp = rx.await??;
516 to_command_response::<T>(resp, method)
517 }
518
519 pub async fn set_permission(
523 &self,
524 permission: PermissionDescriptor,
525 setting: PermissionSetting,
526 origin: Option<impl Into<String>>,
527 embedded_origin: Option<impl Into<String>>,
528 browser_context_id: Option<BrowserContextId>,
529 ) -> Result<&Self> {
530 self.execute(SetPermissionParams {
531 permission,
532 setting,
533 origin: origin.map(Into::into),
534 embedded_origin: embedded_origin.map(Into::into),
535 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
536 })
537 .await?;
538 Ok(self)
539 }
540
541 pub async fn set_permission_for_origin(
543 &self,
544 origin: impl Into<String>,
545 embedded_origin: Option<impl Into<String>>,
546 permission: PermissionDescriptor,
547 setting: PermissionSetting,
548 ) -> Result<&Self> {
549 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
550 .await
551 }
552
553 pub async fn reset_permission_for_origin(
555 &self,
556 origin: impl Into<String>,
557 embedded_origin: Option<impl Into<String>>,
558 permission: PermissionDescriptor,
559 ) -> Result<&Self> {
560 self.set_permission_for_origin(
561 origin,
562 embedded_origin,
563 permission,
564 PermissionSetting::Prompt,
565 )
566 .await
567 }
568
569 pub async fn grant_all_permission_for_origin(
571 &self,
572 origin: impl Into<String>,
573 embedded_origin: Option<impl Into<String>>,
574 permission: PermissionDescriptor,
575 ) -> Result<&Self> {
576 self.set_permission_for_origin(
577 origin,
578 embedded_origin,
579 permission,
580 PermissionSetting::Granted,
581 )
582 .await
583 }
584
585 pub async fn deny_all_permission_for_origin(
587 &self,
588 origin: impl Into<String>,
589 embedded_origin: Option<impl Into<String>>,
590 permission: PermissionDescriptor,
591 ) -> Result<&Self> {
592 self.set_permission_for_origin(
593 origin,
594 embedded_origin,
595 permission,
596 PermissionSetting::Denied,
597 )
598 .await
599 }
600
601 pub async fn pages(&self) -> Result<Vec<Page>> {
603 let (tx, rx) = oneshot_channel();
604 self.sender
605 .send(HandlerMessage::GetPages(tx))
606 .await?;
607 Ok(rx.await?)
608 }
609
610 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
612 let (tx, rx) = oneshot_channel();
613 self.sender
614 .send(HandlerMessage::GetPage(target_id, tx))
615 .await?;
616 rx.await?.ok_or(CdpError::NotFound)
617 }
618
619 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
621 let (tx, rx) = unbounded_channel();
622 self.sender
623 .send(HandlerMessage::AddEventListener(
624 EventListenerRequest::new::<T>(tx),
625 ))
626 .await?;
627
628 Ok(EventStream::new(rx))
629 }
630
631 pub async fn create_browser_context(
633 &mut self,
634 params: CreateBrowserContextParams,
635 ) -> Result<BrowserContextId> {
636 let response = self.execute(params).await?;
637
638 Ok(response.result.browser_context_id)
639 }
640
641 pub async fn get_browser_contexts(
643 &mut self,
644 params: GetBrowserContextsParams,
645 ) -> Result<GetBrowserContextsReturns> {
646 let response = self.execute(params).await?;
647 Ok(response.result)
648 }
649
650 pub async fn send_new_context(
652 &mut self,
653 browser_context_id: BrowserContextId,
654 ) -> Result<&Self> {
655 self.browser_context = BrowserContext::from(browser_context_id);
656 self.sender
657 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
658 .await?;
659 Ok(self)
660 }
661
662 pub async fn dispose_browser_context(
664 &self,
665 browser_context_id: impl Into<BrowserContextId>,
666 ) -> Result<&Self> {
667 self.execute(DisposeBrowserContextParams::new(browser_context_id))
668 .await?;
669
670 Ok(self)
671 }
672
673 pub async fn clear_cookies(&self) -> Result<&Self> {
675 self.execute(ClearCookiesParams::default()).await?;
676 Ok(self)
677 }
678
679 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
681 let cmd = GetCookiesParams {
682 browser_context_id: self.browser_context.id.clone(),
683 };
684
685 Ok(self.execute(cmd).await?.result.cookies)
686 }
687
688 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
690 for cookie in &mut cookies {
691 if let Some(url) = cookie.url.as_ref() {
692 crate::page::validate_cookie_url(url)?;
693 }
694 }
695
696 let mut cookies_param = SetCookiesParams::new(cookies);
697
698 cookies_param.browser_context_id = self.browser_context.id.clone();
699
700 self.execute(cookies_param).await?;
701 Ok(self)
702 }
703}
704
705impl Drop for Browser {
706 fn drop(&mut self) {
707 if let Some(child) = self.child.as_mut() {
708 if let Ok(Some(_)) = child.try_wait() {
709 } else {
711 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
719 }
720 }
721 }
722}
723
724async fn ws_url_from_output(
734 child_process: &mut Child,
735 timeout_fut: impl Future<Output = ()> + Unpin,
736) -> Result<String> {
737 use tokio::io::AsyncBufReadExt;
738 let stderr = child_process.stderr.take().expect("no stderror");
739 let mut stderr_bytes = Vec::<u8>::new();
740 let mut buf = tokio::io::BufReader::new(stderr);
741 let mut timeout_fut = timeout_fut;
742 loop {
743 tokio::select! {
744 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
745 exit_status = child_process.wait() => {
746 return Err(match exit_status {
747 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
748 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
749 })
750 },
751 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
752 match read_res {
753 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
754 Ok(byte_count) => {
755 if byte_count == 0 {
756 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
757 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
758 }
759 let start_offset = stderr_bytes.len() - byte_count;
760 let new_bytes = &stderr_bytes[start_offset..];
761 match std::str::from_utf8(new_bytes) {
762 Err(_) => {
763 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
764 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
765 }
766 Ok(line) => {
767 if let Some((_, ws)) = line.rsplit_once("listening on ") {
768 if ws.starts_with("ws") && ws.contains("devtools/browser") {
769 return Ok(ws.trim().to_string());
770 }
771 }
772 }
773 }
774 }
775 }
776 }
777 }
778 }
779}
780
781#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
782pub enum HeadlessMode {
783 False,
785 #[default]
787 True,
788 New,
790}
791
792#[derive(Debug, Clone, Default)]
793pub struct BrowserConfig {
794 headless: HeadlessMode,
797 sandbox: bool,
799 window_size: Option<(u32, u32)>,
801 port: u16,
803 executable: std::path::PathBuf,
808
809 extensions: Vec<String>,
817
818 pub process_envs: Option<HashMap<String, String>>,
821
822 pub user_data_dir: Option<PathBuf>,
824
825 incognito: bool,
827
828 launch_timeout: Duration,
830
831 ignore_https_errors: bool,
833 pub viewport: Option<Viewport>,
834 request_timeout: Duration,
836
837 args: Vec<String>,
839
840 disable_default_args: bool,
842
843 pub request_intercept: bool,
845
846 pub cache_enabled: bool,
848 pub service_worker_enabled: bool,
851 pub ignore_visuals: bool,
854 pub ignore_stylesheets: bool,
857 pub ignore_javascript: bool,
860 pub ignore_analytics: bool,
862 pub ignore_prefetch: bool,
864 pub ignore_ads: bool,
866 pub extra_headers: Option<std::collections::HashMap<String, String>>,
868 pub only_html: bool,
870 pub intercept_manager: NetworkInterceptManager,
872 pub max_bytes_allowed: Option<u64>,
874 pub whitelist_patterns: Option<Vec<String>>,
876 pub blacklist_patterns: Option<Vec<String>>,
878 pub channel_capacity: usize,
881 pub connection_retries: u32,
884}
885
886#[derive(Debug, Clone)]
887pub struct BrowserConfigBuilder {
888 headless: HeadlessMode,
890 sandbox: bool,
892 window_size: Option<(u32, u32)>,
894 port: u16,
896 executable: Option<PathBuf>,
899 executation_detection: DetectionOptions,
901 extensions: Vec<String>,
903 process_envs: Option<HashMap<String, String>>,
905 user_data_dir: Option<PathBuf>,
907 incognito: bool,
909 launch_timeout: Duration,
911 ignore_https_errors: bool,
913 viewport: Option<Viewport>,
915 request_timeout: Duration,
917 args: Vec<String>,
919 disable_default_args: bool,
921 request_intercept: bool,
923 cache_enabled: bool,
925 service_worker_enabled: bool,
927 ignore_visuals: bool,
929 ignore_ads: bool,
931 ignore_javascript: bool,
933 ignore_stylesheets: bool,
935 ignore_prefetch: bool,
937 ignore_analytics: bool,
939 only_html: bool,
941 extra_headers: Option<std::collections::HashMap<String, String>>,
943 intercept_manager: NetworkInterceptManager,
945 max_bytes_allowed: Option<u64>,
947 whitelist_patterns: Option<Vec<String>>,
949 blacklist_patterns: Option<Vec<String>>,
951 channel_capacity: usize,
953 connection_retries: u32,
955}
956
957impl BrowserConfig {
958 pub fn builder() -> BrowserConfigBuilder {
960 BrowserConfigBuilder::default()
961 }
962
963 pub fn with_executable(path: impl AsRef<Path>) -> Self {
965 Self::builder()
966 .chrome_executable(path)
967 .build()
968 .expect("path to executable exist")
969 }
970}
971
972impl Default for BrowserConfigBuilder {
973 fn default() -> Self {
974 Self {
975 headless: HeadlessMode::True,
976 sandbox: true,
977 window_size: None,
978 port: 0,
979 executable: None,
980 executation_detection: DetectionOptions::default(),
981 extensions: Vec::new(),
982 process_envs: None,
983 user_data_dir: None,
984 incognito: false,
985 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
986 ignore_https_errors: true,
987 viewport: Some(Default::default()),
988 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
989 args: Vec::new(),
990 disable_default_args: false,
991 request_intercept: false,
992 cache_enabled: true,
993 ignore_visuals: false,
994 ignore_ads: false,
995 ignore_javascript: false,
996 ignore_analytics: false,
997 ignore_stylesheets: false,
998 ignore_prefetch: true,
999 only_html: false,
1000 extra_headers: Default::default(),
1001 service_worker_enabled: true,
1002 intercept_manager: NetworkInterceptManager::Unknown,
1003 max_bytes_allowed: None,
1004 whitelist_patterns: None,
1005 blacklist_patterns: None,
1006 channel_capacity: 1000,
1007 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1008 }
1009 }
1010}
1011
1012impl BrowserConfigBuilder {
1013 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1015 self.window_size = Some((width, height));
1016 self
1017 }
1018 pub fn no_sandbox(mut self) -> Self {
1020 self.sandbox = false;
1021 self
1022 }
1023 pub fn with_head(mut self) -> Self {
1025 self.headless = HeadlessMode::False;
1026 self
1027 }
1028 pub fn new_headless_mode(mut self) -> Self {
1030 self.headless = HeadlessMode::New;
1031 self
1032 }
1033 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1035 self.headless = mode;
1036 self
1037 }
1038 pub fn incognito(mut self) -> Self {
1040 self.incognito = true;
1041 self
1042 }
1043
1044 pub fn respect_https_errors(mut self) -> Self {
1045 self.ignore_https_errors = false;
1046 self
1047 }
1048
1049 pub fn port(mut self, port: u16) -> Self {
1050 self.port = port;
1051 self
1052 }
1053
1054 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1055 self.max_bytes_allowed = max_bytes_allowed;
1056 self
1057 }
1058
1059 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1060 self.launch_timeout = timeout;
1061 self
1062 }
1063
1064 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1065 self.request_timeout = timeout;
1066 self
1067 }
1068
1069 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1075 self.viewport = viewport.into();
1076 self
1077 }
1078
1079 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1080 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1081 self
1082 }
1083
1084 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1085 self.executable = Some(path.as_ref().to_path_buf());
1086 self
1087 }
1088
1089 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1090 self.executation_detection = options;
1091 self
1092 }
1093
1094 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1095 self.extensions.push(extension.into());
1096 self
1097 }
1098
1099 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1100 where
1101 I: IntoIterator<Item = S>,
1102 S: Into<String>,
1103 {
1104 for ext in extensions {
1105 self.extensions.push(ext.into());
1106 }
1107 self
1108 }
1109
1110 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1111 self.process_envs
1112 .get_or_insert(HashMap::new())
1113 .insert(key.into(), val.into());
1114 self
1115 }
1116
1117 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1118 where
1119 I: IntoIterator<Item = (K, V)>,
1120 K: Into<String>,
1121 V: Into<String>,
1122 {
1123 self.process_envs
1124 .get_or_insert(HashMap::new())
1125 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1126 self
1127 }
1128
1129 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1130 self.args.push(arg.into());
1131 self
1132 }
1133
1134 pub fn args<I, S>(mut self, args: I) -> Self
1135 where
1136 I: IntoIterator<Item = S>,
1137 S: Into<String>,
1138 {
1139 for arg in args {
1140 self.args.push(arg.into());
1141 }
1142 self
1143 }
1144
1145 pub fn disable_default_args(mut self) -> Self {
1146 self.disable_default_args = true;
1147 self
1148 }
1149
1150 pub fn enable_request_intercept(mut self) -> Self {
1151 self.request_intercept = true;
1152 self
1153 }
1154
1155 pub fn disable_request_intercept(mut self) -> Self {
1156 self.request_intercept = false;
1157 self
1158 }
1159
1160 pub fn enable_cache(mut self) -> Self {
1161 self.cache_enabled = true;
1162 self
1163 }
1164
1165 pub fn disable_cache(mut self) -> Self {
1166 self.cache_enabled = false;
1167 self
1168 }
1169
1170 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1172 self.service_worker_enabled = bypass;
1173 self
1174 }
1175
1176 pub fn set_extra_headers(
1178 mut self,
1179 headers: Option<std::collections::HashMap<String, String>>,
1180 ) -> Self {
1181 self.extra_headers = headers;
1182 self
1183 }
1184
1185 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1187 self.whitelist_patterns = whitelist_patterns;
1188 self
1189 }
1190
1191 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1193 self.blacklist_patterns = blacklist_patterns;
1194 self
1195 }
1196
1197 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1200 self.channel_capacity = capacity;
1201 self
1202 }
1203
1204 pub fn connection_retries(mut self, retries: u32) -> Self {
1207 self.connection_retries = retries;
1208 self
1209 }
1210
1211 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1213 let executable = if let Some(e) = self.executable {
1214 e
1215 } else {
1216 detection::default_executable(self.executation_detection)?
1217 };
1218
1219 Ok(BrowserConfig {
1220 headless: self.headless,
1221 sandbox: self.sandbox,
1222 window_size: self.window_size,
1223 port: self.port,
1224 executable,
1225 extensions: self.extensions,
1226 process_envs: self.process_envs,
1227 user_data_dir: self.user_data_dir,
1228 incognito: self.incognito,
1229 launch_timeout: self.launch_timeout,
1230 ignore_https_errors: self.ignore_https_errors,
1231 viewport: self.viewport,
1232 request_timeout: self.request_timeout,
1233 args: self.args,
1234 disable_default_args: self.disable_default_args,
1235 request_intercept: self.request_intercept,
1236 cache_enabled: self.cache_enabled,
1237 ignore_visuals: self.ignore_visuals,
1238 ignore_ads: self.ignore_ads,
1239 ignore_javascript: self.ignore_javascript,
1240 ignore_analytics: self.ignore_analytics,
1241 ignore_stylesheets: self.ignore_stylesheets,
1242 ignore_prefetch: self.ignore_prefetch,
1243 extra_headers: self.extra_headers,
1244 only_html: self.only_html,
1245 intercept_manager: self.intercept_manager,
1246 service_worker_enabled: self.service_worker_enabled,
1247 max_bytes_allowed: self.max_bytes_allowed,
1248 whitelist_patterns: self.whitelist_patterns,
1249 blacklist_patterns: self.blacklist_patterns,
1250 channel_capacity: self.channel_capacity,
1251 connection_retries: self.connection_retries,
1252 })
1253 }
1254}
1255
1256impl BrowserConfig {
1257 pub fn launch(&self) -> io::Result<Child> {
1258 let mut cmd = async_process::Command::new(&self.executable);
1259
1260 if self.disable_default_args {
1261 cmd.args(&self.args);
1262 } else {
1263 cmd.args(DEFAULT_ARGS).args(&self.args);
1264 }
1265
1266 if !self
1267 .args
1268 .iter()
1269 .any(|arg| arg.contains("--remote-debugging-port="))
1270 {
1271 cmd.arg(format!("--remote-debugging-port={}", self.port));
1272 }
1273
1274 cmd.args(
1275 self.extensions
1276 .iter()
1277 .map(|e| format!("--load-extension={e}")),
1278 );
1279
1280 if let Some(ref user_data) = self.user_data_dir {
1281 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1282 } else {
1283 cmd.arg(format!(
1287 "--user-data-dir={}",
1288 std::env::temp_dir().join("chromiumoxide-runner").display()
1289 ));
1290 }
1291
1292 if let Some((width, height)) = self.window_size {
1293 cmd.arg(format!("--window-size={width},{height}"));
1294 }
1295
1296 if !self.sandbox {
1297 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1298 }
1299
1300 match self.headless {
1301 HeadlessMode::False => (),
1302 HeadlessMode::True => {
1303 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1304 }
1305 HeadlessMode::New => {
1306 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1307 }
1308 }
1309
1310 if self.incognito {
1311 cmd.arg("--incognito");
1312 }
1313
1314 if let Some(ref envs) = self.process_envs {
1315 cmd.envs(envs);
1316 }
1317 cmd.stderr(Stdio::piped()).spawn()
1318 }
1319}
1320
1321#[deprecated(note = "Use detection::default_executable instead")]
1330pub fn default_executable() -> Result<std::path::PathBuf, String> {
1331 let options = DetectionOptions {
1332 msedge: false,
1333 unstable: false,
1334 };
1335 detection::default_executable(options)
1336}
1337
1338static DEFAULT_ARGS: [&str; 26] = [
1341 "--disable-background-networking",
1342 "--enable-features=NetworkService,NetworkServiceInProcess",
1343 "--disable-background-timer-throttling",
1344 "--disable-backgrounding-occluded-windows",
1345 "--disable-breakpad",
1346 "--disable-client-side-phishing-detection",
1347 "--disable-component-extensions-with-background-pages",
1348 "--disable-default-apps",
1349 "--disable-dev-shm-usage",
1350 "--disable-extensions",
1351 "--disable-features=TranslateUI",
1352 "--disable-hang-monitor",
1353 "--disable-ipc-flooding-protection",
1354 "--disable-popup-blocking",
1355 "--disable-prompt-on-repost",
1356 "--disable-renderer-backgrounding",
1357 "--disable-sync",
1358 "--force-color-profile=srgb",
1359 "--metrics-recording-only",
1360 "--no-first-run",
1361 "--enable-automation",
1362 "--password-store=basic",
1363 "--use-mock-keychain",
1364 "--enable-blink-features=IdleDetection",
1365 "--lang=en_US",
1366 "--disable-blink-features=AutomationControlled",
1367];