1use hashbrown::HashMap;
2use reqwest::header::{HeaderMap, HeaderName, 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::{ConnectHeaderError, ConnectHeaders, 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
64pub fn request_client() -> &'static reqwest::Client {
67 &REQUEST_CLIENT
68}
69
70#[derive(Debug)]
72pub struct Browser {
73 pub(crate) sender: Sender<HandlerMessage>,
76 config: Option<BrowserConfig>,
78 child: Option<Child>,
80 debug_ws_url: String,
82 pub browser_context: BrowserContext,
84}
85
86#[derive(serde::Deserialize, Debug, Default)]
88pub struct BrowserConnection {
89 #[serde(rename = "Browser")]
90 pub browser: String,
92 #[serde(rename = "Protocol-Version")]
93 pub protocol_version: String,
95 #[serde(rename = "User-Agent")]
96 pub user_agent: String,
98 #[serde(rename = "V8-Version")]
99 pub v8_version: String,
101 #[serde(rename = "WebKit-Version")]
102 pub webkit_version: String,
104 #[serde(rename = "webSocketDebuggerUrl")]
105 pub web_socket_debugger_url: String,
107}
108
109impl Browser {
110 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
114 Self::connect_with_config(url, HandlerConfig::default()).await
115 }
116
117 pub async fn connect_with_config(
121 url: impl Into<String>,
122 config: HandlerConfig,
123 ) -> Result<(Self, Handler)> {
124 let mut debug_ws_url = url.into();
125 let retries = config.connection_retries;
126
127 if debug_ws_url.starts_with("http") {
128 let version_url = if debug_ws_url.ends_with("/json/version")
129 || debug_ws_url.ends_with("/json/version/")
130 {
131 debug_ws_url.to_owned()
132 } else {
133 format!(
134 "{}{}json/version",
135 &debug_ws_url,
136 if debug_ws_url.ends_with('/') { "" } else { "/" }
137 )
138 };
139
140 let mut discovered = false;
141
142 for attempt in 0..=retries {
143 let retry = || async {
144 if attempt < retries {
145 let backoff_ms = 50u64
150 .saturating_mul(3u64.saturating_pow(attempt))
151 .min(crate::conn::MAX_BACKOFF_MS);
152 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
153 }
154 };
155
156 match REQUEST_CLIENT.get(&version_url).send().await {
157 Ok(req) => match req.bytes().await {
158 Ok(b) => {
159 match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
160 Ok(connection)
161 if !connection.web_socket_debugger_url.is_empty() =>
162 {
163 debug_ws_url = connection.web_socket_debugger_url;
164 discovered = true;
165 break;
166 }
167 _ => {
168 retry().await;
170 }
171 }
172 }
173 Err(_) => {
174 retry().await;
175 }
176 },
177 Err(_) => {
178 retry().await;
179 }
180 }
181 }
182
183 if !discovered {
184 return Err(CdpError::NoResponse);
185 }
186 }
187
188 let conn = Connection::<CdpEventMessage>::connect_with_retries_and_headers(
189 &debug_ws_url,
190 retries,
191 &config.connect_headers,
192 )
193 .await?;
194
195 let (tx, rx) = channel(config.channel_capacity);
196
197 let handler_config = BrowserConfig {
198 ignore_https_errors: config.ignore_https_errors,
199 viewport: config.viewport.clone(),
200 request_timeout: config.request_timeout,
201 request_intercept: config.request_intercept,
202 cache_enabled: config.cache_enabled,
203 ignore_visuals: config.ignore_visuals,
204 ignore_stylesheets: config.ignore_stylesheets,
205 ignore_javascript: config.ignore_javascript,
206 ignore_analytics: config.ignore_analytics,
207 ignore_prefetch: config.ignore_prefetch,
208 ignore_ads: config.ignore_ads,
209 allow_first_party_stylesheets: config.allow_first_party_stylesheets,
210 allow_first_party_javascript: config.allow_first_party_javascript,
211 allow_first_party_visuals: config.allow_first_party_visuals,
212 extra_headers: config.extra_headers.clone(),
213 only_html: config.only_html,
214 service_worker_enabled: config.service_worker_enabled,
215 intercept_manager: config.intercept_manager,
216 max_bytes_allowed: config.max_bytes_allowed,
217 max_redirects: config.max_redirects,
218 max_main_frame_navigations: config.max_main_frame_navigations,
219 whitelist_patterns: config.whitelist_patterns.clone(),
220 blacklist_patterns: config.blacklist_patterns.clone(),
221 connect_headers: config.connect_headers.clone(),
222 ..Default::default()
223 };
224
225 let fut = Handler::new(conn, rx, config);
226 let browser_context = fut.default_browser_context().clone();
227
228 let browser = Self {
229 sender: tx,
230 config: Some(handler_config),
231 child: None,
232 debug_ws_url,
233 browser_context,
234 };
235
236 Ok((browser, fut))
237 }
238
239 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
248 crate::bg_cleanup::init_worker();
254
255 config.executable = utils::canonicalize_except_snap(config.executable).await?;
257
258 let mut child = config.launch()?;
260
261 async fn with_child(
266 config: &BrowserConfig,
267 child: &mut Child,
268 ) -> Result<(String, Connection<CdpEventMessage>)> {
269 let dur = config.launch_timeout;
270 let timeout_fut = Box::pin(tokio::time::sleep(dur));
271
272 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
274 let conn = Connection::<CdpEventMessage>::connect_with_retries_and_headers(
275 &debug_ws_url,
276 config.connection_retries,
277 &config.connect_headers,
278 )
279 .await?;
280 Ok((debug_ws_url, conn))
281 }
282
283 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
284 Ok(conn) => conn,
285 Err(e) => {
286 if let Ok(Some(_)) = child.try_wait() {
288 } else {
290 let _ = child.kill().await;
292 let _ = child.wait().await;
293 }
294 return Err(e);
295 }
296 };
297
298 let (tx, rx) = channel(config.channel_capacity);
302
303 let handler_config = HandlerConfig {
304 ignore_https_errors: config.ignore_https_errors,
305 viewport: config.viewport.clone(),
306 context_ids: Vec::new(),
307 request_timeout: config.request_timeout,
308 request_intercept: config.request_intercept,
309 cache_enabled: config.cache_enabled,
310 ignore_visuals: config.ignore_visuals,
311 ignore_stylesheets: config.ignore_stylesheets,
312 ignore_javascript: config.ignore_javascript,
313 ignore_analytics: config.ignore_analytics,
314 ignore_prefetch: config.ignore_prefetch,
315 ignore_ads: config.ignore_ads,
316 allow_first_party_stylesheets: config.allow_first_party_stylesheets,
317 allow_first_party_javascript: config.allow_first_party_javascript,
318 allow_first_party_visuals: config.allow_first_party_visuals,
319 extra_headers: config.extra_headers.clone(),
320 only_html: config.only_html,
321 service_worker_enabled: config.service_worker_enabled,
322 created_first_target: false,
323 intercept_manager: config.intercept_manager,
324 max_bytes_allowed: config.max_bytes_allowed,
325 max_redirects: config.max_redirects,
326 max_main_frame_navigations: config.max_main_frame_navigations,
327 whitelist_patterns: config.whitelist_patterns.clone(),
328 blacklist_patterns: config.blacklist_patterns.clone(),
329 remote_local_policy: false,
333 #[cfg(feature = "adblock")]
334 adblock_filter_rules: config.adblock_filter_rules.clone(),
335 channel_capacity: config.channel_capacity,
336 page_channel_capacity: config.page_channel_capacity,
337 connection_retries: config.connection_retries,
338 connect_headers: config.connect_headers.clone(),
339 };
340
341 let fut = Handler::new(conn, rx, handler_config);
342 let browser_context = fut.default_browser_context().clone();
343
344 let browser = Self {
345 sender: tx,
346 config: Some(config),
347 child: Some(child),
348 debug_ws_url,
349 browser_context,
350 };
351
352 Ok((browser, fut))
353 }
354
355 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
365 let (tx, rx) = oneshot_channel();
366
367 self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
368
369 rx.await?
370 }
371
372 pub async fn close(&self) -> Result<CloseReturns> {
379 let (tx, rx) = oneshot_channel();
380
381 self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
382
383 rx.await?
384 }
385
386 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
395 if let Some(child) = self.child.as_mut() {
396 Ok(Some(child.wait().await?))
397 } else {
398 Ok(None)
399 }
400 }
401
402 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
411 if let Some(child) = self.child.as_mut() {
412 child.try_wait()
413 } else {
414 Ok(None)
415 }
416 }
417
418 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
429 self.child.as_mut()
430 }
431
432 pub fn has_child(&self) -> bool {
434 self.child.is_some()
435 }
436
437 pub async fn kill(&mut self) -> Option<io::Result<()>> {
448 match self.child.as_mut() {
449 Some(child) => Some(child.kill().await),
450 None => None,
451 }
452 }
453
454 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
460 if !self.is_incognito_configured() {
461 let browser_context_id = self
462 .create_browser_context(CreateBrowserContextParams::default())
463 .await?;
464 self.browser_context = BrowserContext::from(browser_context_id);
465 self.sender
466 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
467 .await?;
468 }
469
470 Ok(self)
471 }
472
473 pub async fn quit_incognito_context_base(
479 &self,
480 browser_context_id: BrowserContextId,
481 ) -> Result<&Self> {
482 self.dispose_browser_context(browser_context_id.clone())
483 .await?;
484 self.sender
485 .send(HandlerMessage::DisposeContext(BrowserContext::from(
486 browser_context_id,
487 )))
488 .await?;
489 Ok(self)
490 }
491
492 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
498 if let Some(id) = self.browser_context.take() {
499 let _ = self.quit_incognito_context_base(id).await;
500 }
501 Ok(self)
502 }
503
504 fn is_incognito_configured(&self) -> bool {
506 self.config
507 .as_ref()
508 .map(|c| c.incognito)
509 .unwrap_or_default()
510 }
511
512 pub fn websocket_address(&self) -> &String {
514 &self.debug_ws_url
515 }
516
517 pub fn is_incognito(&self) -> bool {
519 self.is_incognito_configured() || self.browser_context.is_incognito()
520 }
521
522 pub fn config(&self) -> Option<&BrowserConfig> {
524 self.config.as_ref()
525 }
526
527 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
529 let (tx, rx) = oneshot_channel();
530 let mut params = params.into();
531
532 if let Some(id) = self.browser_context.id() {
533 if params.browser_context_id.is_none() {
534 params.browser_context_id = Some(id.clone());
535 }
536 }
537
538 let _ = self
539 .sender
540 .send(HandlerMessage::CreatePage(params, tx))
541 .await;
542
543 rx.await?
544 }
545
546 pub async fn version(&self) -> Result<GetVersionReturns> {
548 Ok(self.execute(GetVersionParams::default()).await?.result)
549 }
550
551 pub async fn user_agent(&self) -> Result<String> {
553 Ok(self.version().await?.user_agent)
554 }
555
556 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
558 let (tx, rx) = oneshot_channel();
559 let method = cmd.identifier();
560 let msg = CommandMessage::new(cmd, tx)?;
561
562 self.sender.send(HandlerMessage::Command(msg)).await?;
563 let resp = rx.await??;
564 to_command_response::<T>(resp, method)
565 }
566
567 pub async fn set_permission(
571 &self,
572 permission: PermissionDescriptor,
573 setting: PermissionSetting,
574 origin: Option<impl Into<String>>,
575 embedded_origin: Option<impl Into<String>>,
576 browser_context_id: Option<BrowserContextId>,
577 ) -> Result<&Self> {
578 self.execute(SetPermissionParams {
579 permission,
580 setting,
581 origin: origin.map(Into::into),
582 embedded_origin: embedded_origin.map(Into::into),
583 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
584 })
585 .await?;
586 Ok(self)
587 }
588
589 pub async fn set_permission_for_origin(
591 &self,
592 origin: impl Into<String>,
593 embedded_origin: Option<impl Into<String>>,
594 permission: PermissionDescriptor,
595 setting: PermissionSetting,
596 ) -> Result<&Self> {
597 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
598 .await
599 }
600
601 pub async fn reset_permission_for_origin(
603 &self,
604 origin: impl Into<String>,
605 embedded_origin: Option<impl Into<String>>,
606 permission: PermissionDescriptor,
607 ) -> Result<&Self> {
608 self.set_permission_for_origin(
609 origin,
610 embedded_origin,
611 permission,
612 PermissionSetting::Prompt,
613 )
614 .await
615 }
616
617 pub async fn grant_all_permission_for_origin(
619 &self,
620 origin: impl Into<String>,
621 embedded_origin: Option<impl Into<String>>,
622 permission: PermissionDescriptor,
623 ) -> Result<&Self> {
624 self.set_permission_for_origin(
625 origin,
626 embedded_origin,
627 permission,
628 PermissionSetting::Granted,
629 )
630 .await
631 }
632
633 pub async fn deny_all_permission_for_origin(
635 &self,
636 origin: impl Into<String>,
637 embedded_origin: Option<impl Into<String>>,
638 permission: PermissionDescriptor,
639 ) -> Result<&Self> {
640 self.set_permission_for_origin(
641 origin,
642 embedded_origin,
643 permission,
644 PermissionSetting::Denied,
645 )
646 .await
647 }
648
649 pub async fn pages(&self) -> Result<Vec<Page>> {
651 let (tx, rx) = oneshot_channel();
652 self.sender.send(HandlerMessage::GetPages(tx)).await?;
653 Ok(rx.await?)
654 }
655
656 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
658 let (tx, rx) = oneshot_channel();
659 self.sender
660 .send(HandlerMessage::GetPage(target_id, tx))
661 .await?;
662 rx.await?.ok_or(CdpError::NotFound)
663 }
664
665 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
667 let (tx, rx) = unbounded_channel();
668 self.sender
669 .send(HandlerMessage::AddEventListener(
670 EventListenerRequest::new::<T>(tx),
671 ))
672 .await?;
673
674 Ok(EventStream::new(rx))
675 }
676
677 pub async fn create_browser_context(
679 &mut self,
680 params: CreateBrowserContextParams,
681 ) -> Result<BrowserContextId> {
682 let response = self.execute(params).await?;
683
684 Ok(response.result.browser_context_id)
685 }
686
687 pub async fn get_browser_contexts(
689 &mut self,
690 params: GetBrowserContextsParams,
691 ) -> Result<GetBrowserContextsReturns> {
692 let response = self.execute(params).await?;
693 Ok(response.result)
694 }
695
696 pub async fn send_new_context(
698 &mut self,
699 browser_context_id: BrowserContextId,
700 ) -> Result<&Self> {
701 self.browser_context = BrowserContext::from(browser_context_id);
702 self.sender
703 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
704 .await?;
705 Ok(self)
706 }
707
708 pub async fn dispose_browser_context(
710 &self,
711 browser_context_id: impl Into<BrowserContextId>,
712 ) -> Result<&Self> {
713 self.execute(DisposeBrowserContextParams::new(browser_context_id))
714 .await?;
715
716 Ok(self)
717 }
718
719 pub async fn clear_cookies(&self) -> Result<&Self> {
721 self.execute(ClearCookiesParams::default()).await?;
722 Ok(self)
723 }
724
725 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
727 let cmd = GetCookiesParams {
728 browser_context_id: self.browser_context.id.clone(),
729 };
730
731 Ok(self.execute(cmd).await?.result.cookies)
732 }
733
734 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
736 for cookie in &mut cookies {
737 if let Some(url) = cookie.url.as_ref() {
738 crate::page::validate_cookie_url(url)?;
739 }
740 }
741
742 let mut cookies_param = SetCookiesParams::new(cookies);
743
744 cookies_param.browser_context_id = self.browser_context.id.clone();
745
746 self.execute(cookies_param).await?;
747 Ok(self)
748 }
749}
750
751impl Drop for Browser {
752 fn drop(&mut self) {
753 if let Some(child) = self.child.as_mut() {
754 if let Ok(Some(_)) = child.try_wait() {
755 } else {
757 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
765 }
766 }
767 }
768}
769
770async fn ws_url_from_output(
780 child_process: &mut Child,
781 timeout_fut: impl Future<Output = ()> + Unpin,
782) -> Result<String> {
783 use tokio::io::AsyncBufReadExt;
784 let stderr = match child_process.stderr.take() {
785 Some(stderr) => stderr,
786 None => {
787 return Err(CdpError::LaunchIo(
788 io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
789 BrowserStderr::new(Vec::new()),
790 ));
791 }
792 };
793 let mut stderr_bytes = Vec::<u8>::new();
794 let mut buf = tokio::io::BufReader::new(stderr);
795 let mut timeout_fut = timeout_fut;
796 loop {
797 tokio::select! {
798 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
799 exit_status = child_process.wait() => {
800 return Err(match exit_status {
801 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
802 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
803 })
804 },
805 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
806 match read_res {
807 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
808 Ok(byte_count) => {
809 if byte_count == 0 {
810 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
811 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
812 }
813 let start_offset = stderr_bytes.len() - byte_count;
814 let new_bytes = &stderr_bytes[start_offset..];
815 match std::str::from_utf8(new_bytes) {
816 Err(_) => {
817 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
818 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
819 }
820 Ok(line) => {
821 if let Some((_, ws)) = line.rsplit_once("listening on ") {
822 if ws.starts_with("ws") && ws.contains("devtools/browser") {
823 return Ok(ws.trim().to_string());
824 }
825 }
826 }
827 }
828 }
829 }
830 }
831 }
832 }
833}
834
835#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
836pub enum HeadlessMode {
837 False,
839 #[default]
841 True,
842 New,
844}
845
846#[derive(Debug, Clone, Default)]
847pub struct BrowserConfig {
848 headless: HeadlessMode,
851 sandbox: bool,
853 window_size: Option<(u32, u32)>,
855 port: u16,
857 executable: std::path::PathBuf,
862
863 extensions: Vec<String>,
871
872 pub process_envs: Option<HashMap<String, String>>,
875
876 pub user_data_dir: Option<PathBuf>,
878
879 incognito: bool,
881
882 launch_timeout: Duration,
884
885 ignore_https_errors: bool,
887 pub viewport: Option<Viewport>,
888 request_timeout: Duration,
890
891 args: Vec<String>,
893
894 disable_default_args: bool,
896
897 pub request_intercept: bool,
899
900 pub cache_enabled: bool,
902 pub service_worker_enabled: bool,
905 pub ignore_visuals: bool,
908 pub ignore_stylesheets: bool,
911 pub ignore_javascript: bool,
914 pub allow_first_party_stylesheets: bool,
918 pub allow_first_party_javascript: bool,
921 pub allow_first_party_visuals: bool,
924 pub ignore_analytics: bool,
926 pub ignore_prefetch: bool,
928 pub ignore_ads: bool,
930 pub extra_headers: Option<std::collections::HashMap<String, String>>,
932 pub only_html: bool,
934 pub intercept_manager: NetworkInterceptManager,
936 pub max_bytes_allowed: Option<u64>,
938 pub max_redirects: Option<usize>,
941 pub max_main_frame_navigations: Option<u32>,
945 pub whitelist_patterns: Option<Vec<String>>,
947 pub blacklist_patterns: Option<Vec<String>>,
949 #[cfg(feature = "adblock")]
953 pub adblock_filter_rules: Option<Vec<String>>,
954 pub channel_capacity: usize,
957 pub page_channel_capacity: usize,
962 pub connection_retries: u32,
965 pub connect_headers: ConnectHeaders,
968}
969
970#[derive(Debug, Clone)]
971pub struct BrowserConfigBuilder {
972 headless: HeadlessMode,
974 sandbox: bool,
976 window_size: Option<(u32, u32)>,
978 port: u16,
980 executable: Option<PathBuf>,
983 executation_detection: DetectionOptions,
985 extensions: Vec<String>,
987 process_envs: Option<HashMap<String, String>>,
989 user_data_dir: Option<PathBuf>,
991 incognito: bool,
993 launch_timeout: Duration,
995 ignore_https_errors: bool,
997 viewport: Option<Viewport>,
999 request_timeout: Duration,
1001 args: Vec<String>,
1003 disable_default_args: bool,
1005 request_intercept: bool,
1007 cache_enabled: bool,
1009 service_worker_enabled: bool,
1011 ignore_visuals: bool,
1013 ignore_ads: bool,
1015 ignore_javascript: bool,
1017 ignore_stylesheets: bool,
1019 allow_first_party_stylesheets: bool,
1022 allow_first_party_javascript: bool,
1025 allow_first_party_visuals: bool,
1028 ignore_prefetch: bool,
1030 ignore_analytics: bool,
1032 only_html: bool,
1034 extra_headers: Option<std::collections::HashMap<String, String>>,
1036 intercept_manager: NetworkInterceptManager,
1038 max_bytes_allowed: Option<u64>,
1040 max_redirects: Option<usize>,
1042 max_main_frame_navigations: Option<u32>,
1044 whitelist_patterns: Option<Vec<String>>,
1046 blacklist_patterns: Option<Vec<String>>,
1048 #[cfg(feature = "adblock")]
1050 adblock_filter_rules: Option<Vec<String>>,
1051 channel_capacity: usize,
1053 page_channel_capacity: usize,
1055 connection_retries: u32,
1057 connect_headers: ConnectHeaders,
1059}
1060
1061impl BrowserConfig {
1062 pub fn builder() -> BrowserConfigBuilder {
1064 BrowserConfigBuilder::default()
1065 }
1066
1067 pub fn with_executable(path: impl AsRef<Path>) -> Self {
1069 Self::builder().chrome_executable(path).build().unwrap()
1072 }
1073}
1074
1075impl Default for BrowserConfigBuilder {
1076 fn default() -> Self {
1077 Self {
1078 headless: HeadlessMode::True,
1079 sandbox: true,
1080 window_size: None,
1081 port: 0,
1082 executable: None,
1083 executation_detection: DetectionOptions::default(),
1084 extensions: Vec::new(),
1085 process_envs: None,
1086 user_data_dir: None,
1087 incognito: false,
1088 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1089 ignore_https_errors: true,
1090 viewport: Some(Default::default()),
1091 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1092 args: Vec::new(),
1093 disable_default_args: false,
1094 request_intercept: false,
1095 cache_enabled: true,
1096 ignore_visuals: false,
1097 ignore_ads: false,
1098 ignore_javascript: false,
1099 ignore_analytics: false,
1100 ignore_stylesheets: false,
1101 allow_first_party_stylesheets: true,
1102 allow_first_party_javascript: true,
1103 allow_first_party_visuals: true,
1104 ignore_prefetch: true,
1105 only_html: false,
1106 extra_headers: Default::default(),
1107 service_worker_enabled: true,
1108 intercept_manager: NetworkInterceptManager::Unknown,
1109 max_bytes_allowed: None,
1110 max_redirects: None,
1111 max_main_frame_navigations: None,
1112 whitelist_patterns: None,
1113 blacklist_patterns: None,
1114 #[cfg(feature = "adblock")]
1115 adblock_filter_rules: None,
1116 channel_capacity: 4096,
1117 page_channel_capacity: crate::handler::page::DEFAULT_PAGE_CHANNEL_CAPACITY,
1118 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1119 connect_headers: ConnectHeaders::new(),
1120 }
1121 }
1122}
1123
1124impl BrowserConfigBuilder {
1125 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1127 self.window_size = Some((width, height));
1128 self
1129 }
1130 pub fn no_sandbox(mut self) -> Self {
1132 self.sandbox = false;
1133 self
1134 }
1135 pub fn with_head(mut self) -> Self {
1137 self.headless = HeadlessMode::False;
1138 self
1139 }
1140 pub fn new_headless_mode(mut self) -> Self {
1142 self.headless = HeadlessMode::New;
1143 self
1144 }
1145 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1147 self.headless = mode;
1148 self
1149 }
1150 pub fn incognito(mut self) -> Self {
1152 self.incognito = true;
1153 self
1154 }
1155
1156 pub fn respect_https_errors(mut self) -> Self {
1157 self.ignore_https_errors = false;
1158 self
1159 }
1160
1161 pub fn port(mut self, port: u16) -> Self {
1162 self.port = port;
1163 self
1164 }
1165
1166 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1167 self.max_bytes_allowed = max_bytes_allowed;
1168 self
1169 }
1170
1171 pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
1177 self.max_redirects = max_redirects;
1178 self
1179 }
1180
1181 pub fn with_max_main_frame_navigations(mut self, cap: Option<u32>) -> Self {
1189 self.max_main_frame_navigations = cap;
1190 self
1191 }
1192
1193 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1194 self.launch_timeout = timeout;
1195 self
1196 }
1197
1198 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1199 self.request_timeout = timeout;
1200 self
1201 }
1202
1203 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1209 self.viewport = viewport.into();
1210 self
1211 }
1212
1213 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1214 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1215 self
1216 }
1217
1218 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1219 self.executable = Some(path.as_ref().to_path_buf());
1220 self
1221 }
1222
1223 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1224 self.executation_detection = options;
1225 self
1226 }
1227
1228 pub fn prefer_edge(mut self) -> Self {
1234 self.executation_detection.msedge = true;
1235 self.executation_detection.prefer_msedge = true;
1236 self
1237 }
1238
1239 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1240 self.extensions.push(extension.into());
1241 self
1242 }
1243
1244 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1245 where
1246 I: IntoIterator<Item = S>,
1247 S: Into<String>,
1248 {
1249 for ext in extensions {
1250 self.extensions.push(ext.into());
1251 }
1252 self
1253 }
1254
1255 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1256 self.process_envs
1257 .get_or_insert(HashMap::new())
1258 .insert(key.into(), val.into());
1259 self
1260 }
1261
1262 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1263 where
1264 I: IntoIterator<Item = (K, V)>,
1265 K: Into<String>,
1266 V: Into<String>,
1267 {
1268 self.process_envs
1269 .get_or_insert(HashMap::new())
1270 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1271 self
1272 }
1273
1274 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1275 self.args.push(arg.into());
1276 self
1277 }
1278
1279 pub fn args<I, S>(mut self, args: I) -> Self
1280 where
1281 I: IntoIterator<Item = S>,
1282 S: Into<String>,
1283 {
1284 for arg in args {
1285 self.args.push(arg.into());
1286 }
1287 self
1288 }
1289
1290 pub fn disable_default_args(mut self) -> Self {
1291 self.disable_default_args = true;
1292 self
1293 }
1294
1295 pub fn enable_request_intercept(mut self) -> Self {
1296 self.request_intercept = true;
1297 self
1298 }
1299
1300 pub fn disable_request_intercept(mut self) -> Self {
1301 self.request_intercept = false;
1302 self
1303 }
1304
1305 pub fn enable_cache(mut self) -> Self {
1306 self.cache_enabled = true;
1307 self
1308 }
1309
1310 pub fn disable_cache(mut self) -> Self {
1311 self.cache_enabled = false;
1312 self
1313 }
1314
1315 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1317 self.service_worker_enabled = bypass;
1318 self
1319 }
1320
1321 pub fn allow_first_party_stylesheets(mut self, allow: bool) -> Self {
1324 self.allow_first_party_stylesheets = allow;
1325 self
1326 }
1327
1328 pub fn allow_first_party_javascript(mut self, allow: bool) -> Self {
1331 self.allow_first_party_javascript = allow;
1332 self
1333 }
1334
1335 pub fn allow_first_party_visuals(mut self, allow: bool) -> Self {
1339 self.allow_first_party_visuals = allow;
1340 self
1341 }
1342
1343 pub fn set_extra_headers(
1345 mut self,
1346 headers: Option<std::collections::HashMap<String, String>>,
1347 ) -> Self {
1348 self.extra_headers = headers;
1349 self
1350 }
1351
1352 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1354 self.whitelist_patterns = whitelist_patterns;
1355 self
1356 }
1357
1358 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1360 self.blacklist_patterns = blacklist_patterns;
1361 self
1362 }
1363
1364 #[cfg(feature = "adblock")]
1367 pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1368 self.adblock_filter_rules = Some(rules);
1369 self
1370 }
1371
1372 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1375 self.channel_capacity = capacity;
1376 self
1377 }
1378
1379 pub fn page_channel_capacity(mut self, capacity: usize) -> Self {
1388 self.page_channel_capacity = capacity;
1389 self
1390 }
1391
1392 pub fn connection_retries(mut self, retries: u32) -> Self {
1395 self.connection_retries = retries;
1396 self
1397 }
1398
1399 pub fn connect_header<N, V>(
1412 mut self,
1413 name: N,
1414 value: V,
1415 ) -> std::result::Result<Self, ConnectHeaderError>
1416 where
1417 N: TryInto<HeaderName>,
1418 N::Error: std::fmt::Display,
1419 V: TryInto<HeaderValue>,
1420 V::Error: std::fmt::Display,
1421 {
1422 self.connect_headers.insert(name, value)?;
1423 Ok(self)
1424 }
1425
1426 pub fn connect_headers<H>(mut self, headers: H) -> std::result::Result<Self, ConnectHeaderError>
1432 where
1433 H: TryInto<ConnectHeaders, Error = ConnectHeaderError>,
1434 {
1435 self.connect_headers = headers.try_into()?;
1436 Ok(self)
1437 }
1438
1439 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1441 let executable = if let Some(e) = self.executable {
1442 e
1443 } else {
1444 detection::default_executable(self.executation_detection)?
1445 };
1446
1447 Ok(BrowserConfig {
1448 headless: self.headless,
1449 sandbox: self.sandbox,
1450 window_size: self.window_size,
1451 port: self.port,
1452 executable,
1453 extensions: self.extensions,
1454 process_envs: self.process_envs,
1455 user_data_dir: self.user_data_dir,
1456 incognito: self.incognito,
1457 launch_timeout: self.launch_timeout,
1458 ignore_https_errors: self.ignore_https_errors,
1459 viewport: self.viewport,
1460 request_timeout: self.request_timeout,
1461 args: self.args,
1462 disable_default_args: self.disable_default_args,
1463 request_intercept: self.request_intercept,
1464 cache_enabled: self.cache_enabled,
1465 ignore_visuals: self.ignore_visuals,
1466 ignore_ads: self.ignore_ads,
1467 ignore_javascript: self.ignore_javascript,
1468 ignore_analytics: self.ignore_analytics,
1469 ignore_stylesheets: self.ignore_stylesheets,
1470 allow_first_party_stylesheets: self.allow_first_party_stylesheets,
1471 allow_first_party_javascript: self.allow_first_party_javascript,
1472 allow_first_party_visuals: self.allow_first_party_visuals,
1473 ignore_prefetch: self.ignore_prefetch,
1474 extra_headers: self.extra_headers,
1475 only_html: self.only_html,
1476 intercept_manager: self.intercept_manager,
1477 service_worker_enabled: self.service_worker_enabled,
1478 max_bytes_allowed: self.max_bytes_allowed,
1479 max_redirects: self.max_redirects,
1480 max_main_frame_navigations: self.max_main_frame_navigations,
1481 whitelist_patterns: self.whitelist_patterns,
1482 blacklist_patterns: self.blacklist_patterns,
1483 #[cfg(feature = "adblock")]
1484 adblock_filter_rules: self.adblock_filter_rules,
1485 channel_capacity: self.channel_capacity,
1486 page_channel_capacity: self.page_channel_capacity,
1487 connection_retries: self.connection_retries,
1488 connect_headers: self.connect_headers,
1489 })
1490 }
1491}
1492
1493impl BrowserConfig {
1494 pub fn launch(&self) -> io::Result<Child> {
1495 let mut cmd = async_process::Command::new(&self.executable);
1496
1497 if self.disable_default_args {
1498 cmd.args(&self.args);
1499 } else {
1500 cmd.args(DEFAULT_ARGS).args(&self.args);
1501 }
1502
1503 if !self
1504 .args
1505 .iter()
1506 .any(|arg| arg.contains("--remote-debugging-port="))
1507 {
1508 cmd.arg(format!("--remote-debugging-port={}", self.port));
1509 }
1510
1511 cmd.args(
1512 self.extensions
1513 .iter()
1514 .map(|e| format!("--load-extension={e}")),
1515 );
1516
1517 if let Some(ref user_data) = self.user_data_dir {
1518 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1519 } else {
1520 cmd.arg(format!(
1524 "--user-data-dir={}",
1525 std::env::temp_dir().join("chromiumoxide-runner").display()
1526 ));
1527 }
1528
1529 if let Some((width, height)) = self.window_size {
1530 cmd.arg(format!("--window-size={width},{height}"));
1531 }
1532
1533 if !self.sandbox {
1534 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1535 }
1536
1537 match self.headless {
1538 HeadlessMode::False => (),
1539 HeadlessMode::True => {
1540 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1541 }
1542 HeadlessMode::New => {
1543 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1544 }
1545 }
1546
1547 if self.incognito {
1548 cmd.arg("--incognito");
1549 }
1550
1551 if let Some(ref envs) = self.process_envs {
1552 cmd.envs(envs);
1553 }
1554 cmd.stderr(Stdio::piped()).spawn()
1555 }
1556}
1557
1558#[deprecated(note = "Use detection::default_executable instead")]
1567pub fn default_executable() -> Result<std::path::PathBuf, String> {
1568 let options = DetectionOptions {
1569 msedge: false,
1570 unstable: false,
1571 prefer_msedge: false,
1572 };
1573 detection::default_executable(options)
1574}
1575
1576static DEFAULT_ARGS: [&str; 26] = [
1579 "--disable-background-networking",
1580 "--enable-features=NetworkService,NetworkServiceInProcess",
1581 "--disable-background-timer-throttling",
1582 "--disable-backgrounding-occluded-windows",
1583 "--disable-breakpad",
1584 "--disable-client-side-phishing-detection",
1585 "--disable-component-extensions-with-background-pages",
1586 "--disable-default-apps",
1587 "--disable-dev-shm-usage",
1588 "--disable-extensions",
1589 "--disable-features=TranslateUI",
1590 "--disable-hang-monitor",
1591 "--disable-ipc-flooding-protection",
1592 "--disable-popup-blocking",
1593 "--disable-prompt-on-repost",
1594 "--disable-renderer-backgrounding",
1595 "--disable-sync",
1596 "--force-color-profile=srgb",
1597 "--metrics-recording-only",
1598 "--no-first-run",
1599 "--enable-automation",
1600 "--password-store=basic",
1601 "--use-mock-keychain",
1602 "--enable-blink-features=IdleDetection",
1603 "--lang=en_US",
1604 "--disable-blink-features=AutomationControlled",
1605];