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
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 * 3u64.saturating_pow(attempt);
146 tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
147 }
148 };
149
150 match REQUEST_CLIENT.get(&version_url).send().await {
151 Ok(req) => match req.bytes().await {
152 Ok(b) => {
153 match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
154 Ok(connection)
155 if !connection.web_socket_debugger_url.is_empty() =>
156 {
157 debug_ws_url = connection.web_socket_debugger_url;
158 discovered = true;
159 break;
160 }
161 _ => {
162 retry().await;
164 }
165 }
166 }
167 Err(_) => {
168 retry().await;
169 }
170 },
171 Err(_) => {
172 retry().await;
173 }
174 }
175 }
176
177 if !discovered {
178 return Err(CdpError::NoResponse);
179 }
180 }
181
182 let conn =
183 Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
184
185 let (tx, rx) = channel(config.channel_capacity);
186
187 let handler_config = BrowserConfig {
188 ignore_https_errors: config.ignore_https_errors,
189 viewport: config.viewport.clone(),
190 request_timeout: config.request_timeout,
191 request_intercept: config.request_intercept,
192 cache_enabled: config.cache_enabled,
193 ignore_visuals: config.ignore_visuals,
194 ignore_stylesheets: config.ignore_stylesheets,
195 ignore_javascript: config.ignore_javascript,
196 ignore_analytics: config.ignore_analytics,
197 ignore_prefetch: config.ignore_prefetch,
198 ignore_ads: config.ignore_ads,
199 extra_headers: config.extra_headers.clone(),
200 only_html: config.only_html,
201 service_worker_enabled: config.service_worker_enabled,
202 intercept_manager: config.intercept_manager,
203 max_bytes_allowed: config.max_bytes_allowed,
204 max_redirects: config.max_redirects,
205 whitelist_patterns: config.whitelist_patterns.clone(),
206 blacklist_patterns: config.blacklist_patterns.clone(),
207 ..Default::default()
208 };
209
210 let fut = Handler::new(conn, rx, config);
211 let browser_context = fut.default_browser_context().clone();
212
213 let browser = Self {
214 sender: tx,
215 config: Some(handler_config),
216 child: None,
217 debug_ws_url,
218 browser_context,
219 };
220
221 Ok((browser, fut))
222 }
223
224 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
233 config.executable = utils::canonicalize_except_snap(config.executable).await?;
235
236 let mut child = config.launch()?;
238
239 async fn with_child(
244 config: &BrowserConfig,
245 child: &mut Child,
246 ) -> Result<(String, Connection<CdpEventMessage>)> {
247 let dur = config.launch_timeout;
248 let timeout_fut = Box::pin(tokio::time::sleep(dur));
249
250 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
252 let conn = Connection::<CdpEventMessage>::connect_with_retries(
253 &debug_ws_url,
254 config.connection_retries,
255 )
256 .await?;
257 Ok((debug_ws_url, conn))
258 }
259
260 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
261 Ok(conn) => conn,
262 Err(e) => {
263 if let Ok(Some(_)) = child.try_wait() {
265 } else {
267 let _ = child.kill().await;
269 let _ = child.wait().await;
270 }
271 return Err(e);
272 }
273 };
274
275 let (tx, rx) = channel(config.channel_capacity);
279
280 let handler_config = HandlerConfig {
281 ignore_https_errors: config.ignore_https_errors,
282 viewport: config.viewport.clone(),
283 context_ids: Vec::new(),
284 request_timeout: config.request_timeout,
285 request_intercept: config.request_intercept,
286 cache_enabled: config.cache_enabled,
287 ignore_visuals: config.ignore_visuals,
288 ignore_stylesheets: config.ignore_stylesheets,
289 ignore_javascript: config.ignore_javascript,
290 ignore_analytics: config.ignore_analytics,
291 ignore_prefetch: config.ignore_prefetch,
292 ignore_ads: config.ignore_ads,
293 extra_headers: config.extra_headers.clone(),
294 only_html: config.only_html,
295 service_worker_enabled: config.service_worker_enabled,
296 created_first_target: false,
297 intercept_manager: config.intercept_manager,
298 max_bytes_allowed: config.max_bytes_allowed,
299 max_redirects: config.max_redirects,
300 whitelist_patterns: config.whitelist_patterns.clone(),
301 blacklist_patterns: config.blacklist_patterns.clone(),
302 #[cfg(feature = "adblock")]
303 adblock_filter_rules: config.adblock_filter_rules.clone(),
304 channel_capacity: config.channel_capacity,
305 connection_retries: config.connection_retries,
306 };
307
308 let fut = Handler::new(conn, rx, handler_config);
309 let browser_context = fut.default_browser_context().clone();
310
311 let browser = Self {
312 sender: tx,
313 config: Some(config),
314 child: Some(child),
315 debug_ws_url,
316 browser_context,
317 };
318
319 Ok((browser, fut))
320 }
321
322 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
332 let (tx, rx) = oneshot_channel();
333
334 self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
335
336 rx.await?
337 }
338
339 pub async fn close(&self) -> Result<CloseReturns> {
346 let (tx, rx) = oneshot_channel();
347
348 self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
349
350 rx.await?
351 }
352
353 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
362 if let Some(child) = self.child.as_mut() {
363 Ok(Some(child.wait().await?))
364 } else {
365 Ok(None)
366 }
367 }
368
369 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
378 if let Some(child) = self.child.as_mut() {
379 child.try_wait()
380 } else {
381 Ok(None)
382 }
383 }
384
385 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
396 self.child.as_mut()
397 }
398
399 pub fn has_child(&self) -> bool {
401 self.child.is_some()
402 }
403
404 pub async fn kill(&mut self) -> Option<io::Result<()>> {
415 match self.child.as_mut() {
416 Some(child) => Some(child.kill().await),
417 None => None,
418 }
419 }
420
421 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
427 if !self.is_incognito_configured() {
428 let browser_context_id = self
429 .create_browser_context(CreateBrowserContextParams::default())
430 .await?;
431 self.browser_context = BrowserContext::from(browser_context_id);
432 self.sender
433 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
434 .await?;
435 }
436
437 Ok(self)
438 }
439
440 pub async fn quit_incognito_context_base(
446 &self,
447 browser_context_id: BrowserContextId,
448 ) -> Result<&Self> {
449 self.dispose_browser_context(browser_context_id.clone())
450 .await?;
451 self.sender
452 .send(HandlerMessage::DisposeContext(BrowserContext::from(
453 browser_context_id,
454 )))
455 .await?;
456 Ok(self)
457 }
458
459 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
465 if let Some(id) = self.browser_context.take() {
466 let _ = self.quit_incognito_context_base(id).await;
467 }
468 Ok(self)
469 }
470
471 fn is_incognito_configured(&self) -> bool {
473 self.config
474 .as_ref()
475 .map(|c| c.incognito)
476 .unwrap_or_default()
477 }
478
479 pub fn websocket_address(&self) -> &String {
481 &self.debug_ws_url
482 }
483
484 pub fn is_incognito(&self) -> bool {
486 self.is_incognito_configured() || self.browser_context.is_incognito()
487 }
488
489 pub fn config(&self) -> Option<&BrowserConfig> {
491 self.config.as_ref()
492 }
493
494 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
496 let (tx, rx) = oneshot_channel();
497 let mut params = params.into();
498
499 if let Some(id) = self.browser_context.id() {
500 if params.browser_context_id.is_none() {
501 params.browser_context_id = Some(id.clone());
502 }
503 }
504
505 let _ = self
506 .sender
507 .send(HandlerMessage::CreatePage(params, tx))
508 .await;
509
510 rx.await?
511 }
512
513 pub async fn version(&self) -> Result<GetVersionReturns> {
515 Ok(self.execute(GetVersionParams::default()).await?.result)
516 }
517
518 pub async fn user_agent(&self) -> Result<String> {
520 Ok(self.version().await?.user_agent)
521 }
522
523 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
525 let (tx, rx) = oneshot_channel();
526 let method = cmd.identifier();
527 let msg = CommandMessage::new(cmd, tx)?;
528
529 self.sender.send(HandlerMessage::Command(msg)).await?;
530 let resp = rx.await??;
531 to_command_response::<T>(resp, method)
532 }
533
534 pub async fn set_permission(
538 &self,
539 permission: PermissionDescriptor,
540 setting: PermissionSetting,
541 origin: Option<impl Into<String>>,
542 embedded_origin: Option<impl Into<String>>,
543 browser_context_id: Option<BrowserContextId>,
544 ) -> Result<&Self> {
545 self.execute(SetPermissionParams {
546 permission,
547 setting,
548 origin: origin.map(Into::into),
549 embedded_origin: embedded_origin.map(Into::into),
550 browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
551 })
552 .await?;
553 Ok(self)
554 }
555
556 pub async fn set_permission_for_origin(
558 &self,
559 origin: impl Into<String>,
560 embedded_origin: Option<impl Into<String>>,
561 permission: PermissionDescriptor,
562 setting: PermissionSetting,
563 ) -> Result<&Self> {
564 self.set_permission(permission, setting, Some(origin), embedded_origin, None)
565 .await
566 }
567
568 pub async fn reset_permission_for_origin(
570 &self,
571 origin: impl Into<String>,
572 embedded_origin: Option<impl Into<String>>,
573 permission: PermissionDescriptor,
574 ) -> Result<&Self> {
575 self.set_permission_for_origin(
576 origin,
577 embedded_origin,
578 permission,
579 PermissionSetting::Prompt,
580 )
581 .await
582 }
583
584 pub async fn grant_all_permission_for_origin(
586 &self,
587 origin: impl Into<String>,
588 embedded_origin: Option<impl Into<String>>,
589 permission: PermissionDescriptor,
590 ) -> Result<&Self> {
591 self.set_permission_for_origin(
592 origin,
593 embedded_origin,
594 permission,
595 PermissionSetting::Granted,
596 )
597 .await
598 }
599
600 pub async fn deny_all_permission_for_origin(
602 &self,
603 origin: impl Into<String>,
604 embedded_origin: Option<impl Into<String>>,
605 permission: PermissionDescriptor,
606 ) -> Result<&Self> {
607 self.set_permission_for_origin(
608 origin,
609 embedded_origin,
610 permission,
611 PermissionSetting::Denied,
612 )
613 .await
614 }
615
616 pub async fn pages(&self) -> Result<Vec<Page>> {
618 let (tx, rx) = oneshot_channel();
619 self.sender.send(HandlerMessage::GetPages(tx)).await?;
620 Ok(rx.await?)
621 }
622
623 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
625 let (tx, rx) = oneshot_channel();
626 self.sender
627 .send(HandlerMessage::GetPage(target_id, tx))
628 .await?;
629 rx.await?.ok_or(CdpError::NotFound)
630 }
631
632 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
634 let (tx, rx) = unbounded_channel();
635 self.sender
636 .send(HandlerMessage::AddEventListener(
637 EventListenerRequest::new::<T>(tx),
638 ))
639 .await?;
640
641 Ok(EventStream::new(rx))
642 }
643
644 pub async fn create_browser_context(
646 &mut self,
647 params: CreateBrowserContextParams,
648 ) -> Result<BrowserContextId> {
649 let response = self.execute(params).await?;
650
651 Ok(response.result.browser_context_id)
652 }
653
654 pub async fn get_browser_contexts(
656 &mut self,
657 params: GetBrowserContextsParams,
658 ) -> Result<GetBrowserContextsReturns> {
659 let response = self.execute(params).await?;
660 Ok(response.result)
661 }
662
663 pub async fn send_new_context(
665 &mut self,
666 browser_context_id: BrowserContextId,
667 ) -> Result<&Self> {
668 self.browser_context = BrowserContext::from(browser_context_id);
669 self.sender
670 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
671 .await?;
672 Ok(self)
673 }
674
675 pub async fn dispose_browser_context(
677 &self,
678 browser_context_id: impl Into<BrowserContextId>,
679 ) -> Result<&Self> {
680 self.execute(DisposeBrowserContextParams::new(browser_context_id))
681 .await?;
682
683 Ok(self)
684 }
685
686 pub async fn clear_cookies(&self) -> Result<&Self> {
688 self.execute(ClearCookiesParams::default()).await?;
689 Ok(self)
690 }
691
692 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
694 let cmd = GetCookiesParams {
695 browser_context_id: self.browser_context.id.clone(),
696 };
697
698 Ok(self.execute(cmd).await?.result.cookies)
699 }
700
701 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
703 for cookie in &mut cookies {
704 if let Some(url) = cookie.url.as_ref() {
705 crate::page::validate_cookie_url(url)?;
706 }
707 }
708
709 let mut cookies_param = SetCookiesParams::new(cookies);
710
711 cookies_param.browser_context_id = self.browser_context.id.clone();
712
713 self.execute(cookies_param).await?;
714 Ok(self)
715 }
716}
717
718impl Drop for Browser {
719 fn drop(&mut self) {
720 if let Some(child) = self.child.as_mut() {
721 if let Ok(Some(_)) = child.try_wait() {
722 } else {
724 tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
732 }
733 }
734 }
735}
736
737async fn ws_url_from_output(
747 child_process: &mut Child,
748 timeout_fut: impl Future<Output = ()> + Unpin,
749) -> Result<String> {
750 use tokio::io::AsyncBufReadExt;
751 let stderr = match child_process.stderr.take() {
752 Some(stderr) => stderr,
753 None => {
754 return Err(CdpError::LaunchIo(
755 io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
756 BrowserStderr::new(Vec::new()),
757 ));
758 }
759 };
760 let mut stderr_bytes = Vec::<u8>::new();
761 let mut buf = tokio::io::BufReader::new(stderr);
762 let mut timeout_fut = timeout_fut;
763 loop {
764 tokio::select! {
765 _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
766 exit_status = child_process.wait() => {
767 return Err(match exit_status {
768 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
769 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
770 })
771 },
772 read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
773 match read_res {
774 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
775 Ok(byte_count) => {
776 if byte_count == 0 {
777 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
778 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
779 }
780 let start_offset = stderr_bytes.len() - byte_count;
781 let new_bytes = &stderr_bytes[start_offset..];
782 match std::str::from_utf8(new_bytes) {
783 Err(_) => {
784 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
785 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
786 }
787 Ok(line) => {
788 if let Some((_, ws)) = line.rsplit_once("listening on ") {
789 if ws.starts_with("ws") && ws.contains("devtools/browser") {
790 return Ok(ws.trim().to_string());
791 }
792 }
793 }
794 }
795 }
796 }
797 }
798 }
799 }
800}
801
802#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
803pub enum HeadlessMode {
804 False,
806 #[default]
808 True,
809 New,
811}
812
813#[derive(Debug, Clone, Default)]
814pub struct BrowserConfig {
815 headless: HeadlessMode,
818 sandbox: bool,
820 window_size: Option<(u32, u32)>,
822 port: u16,
824 executable: std::path::PathBuf,
829
830 extensions: Vec<String>,
838
839 pub process_envs: Option<HashMap<String, String>>,
842
843 pub user_data_dir: Option<PathBuf>,
845
846 incognito: bool,
848
849 launch_timeout: Duration,
851
852 ignore_https_errors: bool,
854 pub viewport: Option<Viewport>,
855 request_timeout: Duration,
857
858 args: Vec<String>,
860
861 disable_default_args: bool,
863
864 pub request_intercept: bool,
866
867 pub cache_enabled: bool,
869 pub service_worker_enabled: bool,
872 pub ignore_visuals: bool,
875 pub ignore_stylesheets: bool,
878 pub ignore_javascript: bool,
881 pub ignore_analytics: bool,
883 pub ignore_prefetch: bool,
885 pub ignore_ads: bool,
887 pub extra_headers: Option<std::collections::HashMap<String, String>>,
889 pub only_html: bool,
891 pub intercept_manager: NetworkInterceptManager,
893 pub max_bytes_allowed: Option<u64>,
895 pub max_redirects: Option<usize>,
898 pub whitelist_patterns: Option<Vec<String>>,
900 pub blacklist_patterns: Option<Vec<String>>,
902 #[cfg(feature = "adblock")]
906 pub adblock_filter_rules: Option<Vec<String>>,
907 pub channel_capacity: usize,
910 pub connection_retries: u32,
913}
914
915#[derive(Debug, Clone)]
916pub struct BrowserConfigBuilder {
917 headless: HeadlessMode,
919 sandbox: bool,
921 window_size: Option<(u32, u32)>,
923 port: u16,
925 executable: Option<PathBuf>,
928 executation_detection: DetectionOptions,
930 extensions: Vec<String>,
932 process_envs: Option<HashMap<String, String>>,
934 user_data_dir: Option<PathBuf>,
936 incognito: bool,
938 launch_timeout: Duration,
940 ignore_https_errors: bool,
942 viewport: Option<Viewport>,
944 request_timeout: Duration,
946 args: Vec<String>,
948 disable_default_args: bool,
950 request_intercept: bool,
952 cache_enabled: bool,
954 service_worker_enabled: bool,
956 ignore_visuals: bool,
958 ignore_ads: bool,
960 ignore_javascript: bool,
962 ignore_stylesheets: bool,
964 ignore_prefetch: bool,
966 ignore_analytics: bool,
968 only_html: bool,
970 extra_headers: Option<std::collections::HashMap<String, String>>,
972 intercept_manager: NetworkInterceptManager,
974 max_bytes_allowed: Option<u64>,
976 max_redirects: Option<usize>,
978 whitelist_patterns: Option<Vec<String>>,
980 blacklist_patterns: Option<Vec<String>>,
982 #[cfg(feature = "adblock")]
984 adblock_filter_rules: Option<Vec<String>>,
985 channel_capacity: usize,
987 connection_retries: u32,
989}
990
991impl BrowserConfig {
992 pub fn builder() -> BrowserConfigBuilder {
994 BrowserConfigBuilder::default()
995 }
996
997 pub fn with_executable(path: impl AsRef<Path>) -> Self {
999 Self::builder().chrome_executable(path).build().unwrap()
1002 }
1003}
1004
1005impl Default for BrowserConfigBuilder {
1006 fn default() -> Self {
1007 Self {
1008 headless: HeadlessMode::True,
1009 sandbox: true,
1010 window_size: None,
1011 port: 0,
1012 executable: None,
1013 executation_detection: DetectionOptions::default(),
1014 extensions: Vec::new(),
1015 process_envs: None,
1016 user_data_dir: None,
1017 incognito: false,
1018 launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1019 ignore_https_errors: true,
1020 viewport: Some(Default::default()),
1021 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1022 args: Vec::new(),
1023 disable_default_args: false,
1024 request_intercept: false,
1025 cache_enabled: true,
1026 ignore_visuals: false,
1027 ignore_ads: false,
1028 ignore_javascript: false,
1029 ignore_analytics: false,
1030 ignore_stylesheets: false,
1031 ignore_prefetch: true,
1032 only_html: false,
1033 extra_headers: Default::default(),
1034 service_worker_enabled: true,
1035 intercept_manager: NetworkInterceptManager::Unknown,
1036 max_bytes_allowed: None,
1037 max_redirects: None,
1038 whitelist_patterns: None,
1039 blacklist_patterns: None,
1040 #[cfg(feature = "adblock")]
1041 adblock_filter_rules: None,
1042 channel_capacity: 4096,
1043 connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1044 }
1045 }
1046}
1047
1048impl BrowserConfigBuilder {
1049 pub fn window_size(mut self, width: u32, height: u32) -> Self {
1051 self.window_size = Some((width, height));
1052 self
1053 }
1054 pub fn no_sandbox(mut self) -> Self {
1056 self.sandbox = false;
1057 self
1058 }
1059 pub fn with_head(mut self) -> Self {
1061 self.headless = HeadlessMode::False;
1062 self
1063 }
1064 pub fn new_headless_mode(mut self) -> Self {
1066 self.headless = HeadlessMode::New;
1067 self
1068 }
1069 pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1071 self.headless = mode;
1072 self
1073 }
1074 pub fn incognito(mut self) -> Self {
1076 self.incognito = true;
1077 self
1078 }
1079
1080 pub fn respect_https_errors(mut self) -> Self {
1081 self.ignore_https_errors = false;
1082 self
1083 }
1084
1085 pub fn port(mut self, port: u16) -> Self {
1086 self.port = port;
1087 self
1088 }
1089
1090 pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1091 self.max_bytes_allowed = max_bytes_allowed;
1092 self
1093 }
1094
1095 pub fn with_max_redirects(mut self, max_redirects: Option<usize>) -> Self {
1101 self.max_redirects = max_redirects;
1102 self
1103 }
1104
1105 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1106 self.launch_timeout = timeout;
1107 self
1108 }
1109
1110 pub fn request_timeout(mut self, timeout: Duration) -> Self {
1111 self.request_timeout = timeout;
1112 self
1113 }
1114
1115 pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1121 self.viewport = viewport.into();
1122 self
1123 }
1124
1125 pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1126 self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1127 self
1128 }
1129
1130 pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1131 self.executable = Some(path.as_ref().to_path_buf());
1132 self
1133 }
1134
1135 pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1136 self.executation_detection = options;
1137 self
1138 }
1139
1140 pub fn extension(mut self, extension: impl Into<String>) -> Self {
1141 self.extensions.push(extension.into());
1142 self
1143 }
1144
1145 pub fn extensions<I, S>(mut self, extensions: I) -> Self
1146 where
1147 I: IntoIterator<Item = S>,
1148 S: Into<String>,
1149 {
1150 for ext in extensions {
1151 self.extensions.push(ext.into());
1152 }
1153 self
1154 }
1155
1156 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1157 self.process_envs
1158 .get_or_insert(HashMap::new())
1159 .insert(key.into(), val.into());
1160 self
1161 }
1162
1163 pub fn envs<I, K, V>(mut self, envs: I) -> Self
1164 where
1165 I: IntoIterator<Item = (K, V)>,
1166 K: Into<String>,
1167 V: Into<String>,
1168 {
1169 self.process_envs
1170 .get_or_insert(HashMap::new())
1171 .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1172 self
1173 }
1174
1175 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1176 self.args.push(arg.into());
1177 self
1178 }
1179
1180 pub fn args<I, S>(mut self, args: I) -> Self
1181 where
1182 I: IntoIterator<Item = S>,
1183 S: Into<String>,
1184 {
1185 for arg in args {
1186 self.args.push(arg.into());
1187 }
1188 self
1189 }
1190
1191 pub fn disable_default_args(mut self) -> Self {
1192 self.disable_default_args = true;
1193 self
1194 }
1195
1196 pub fn enable_request_intercept(mut self) -> Self {
1197 self.request_intercept = true;
1198 self
1199 }
1200
1201 pub fn disable_request_intercept(mut self) -> Self {
1202 self.request_intercept = false;
1203 self
1204 }
1205
1206 pub fn enable_cache(mut self) -> Self {
1207 self.cache_enabled = true;
1208 self
1209 }
1210
1211 pub fn disable_cache(mut self) -> Self {
1212 self.cache_enabled = false;
1213 self
1214 }
1215
1216 pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1218 self.service_worker_enabled = bypass;
1219 self
1220 }
1221
1222 pub fn set_extra_headers(
1224 mut self,
1225 headers: Option<std::collections::HashMap<String, String>>,
1226 ) -> Self {
1227 self.extra_headers = headers;
1228 self
1229 }
1230
1231 pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1233 self.whitelist_patterns = whitelist_patterns;
1234 self
1235 }
1236
1237 pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1239 self.blacklist_patterns = blacklist_patterns;
1240 self
1241 }
1242
1243 #[cfg(feature = "adblock")]
1246 pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1247 self.adblock_filter_rules = Some(rules);
1248 self
1249 }
1250
1251 pub fn channel_capacity(mut self, capacity: usize) -> Self {
1254 self.channel_capacity = capacity;
1255 self
1256 }
1257
1258 pub fn connection_retries(mut self, retries: u32) -> Self {
1261 self.connection_retries = retries;
1262 self
1263 }
1264
1265 pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1267 let executable = if let Some(e) = self.executable {
1268 e
1269 } else {
1270 detection::default_executable(self.executation_detection)?
1271 };
1272
1273 Ok(BrowserConfig {
1274 headless: self.headless,
1275 sandbox: self.sandbox,
1276 window_size: self.window_size,
1277 port: self.port,
1278 executable,
1279 extensions: self.extensions,
1280 process_envs: self.process_envs,
1281 user_data_dir: self.user_data_dir,
1282 incognito: self.incognito,
1283 launch_timeout: self.launch_timeout,
1284 ignore_https_errors: self.ignore_https_errors,
1285 viewport: self.viewport,
1286 request_timeout: self.request_timeout,
1287 args: self.args,
1288 disable_default_args: self.disable_default_args,
1289 request_intercept: self.request_intercept,
1290 cache_enabled: self.cache_enabled,
1291 ignore_visuals: self.ignore_visuals,
1292 ignore_ads: self.ignore_ads,
1293 ignore_javascript: self.ignore_javascript,
1294 ignore_analytics: self.ignore_analytics,
1295 ignore_stylesheets: self.ignore_stylesheets,
1296 ignore_prefetch: self.ignore_prefetch,
1297 extra_headers: self.extra_headers,
1298 only_html: self.only_html,
1299 intercept_manager: self.intercept_manager,
1300 service_worker_enabled: self.service_worker_enabled,
1301 max_bytes_allowed: self.max_bytes_allowed,
1302 max_redirects: self.max_redirects,
1303 whitelist_patterns: self.whitelist_patterns,
1304 blacklist_patterns: self.blacklist_patterns,
1305 #[cfg(feature = "adblock")]
1306 adblock_filter_rules: self.adblock_filter_rules,
1307 channel_capacity: self.channel_capacity,
1308 connection_retries: self.connection_retries,
1309 })
1310 }
1311}
1312
1313impl BrowserConfig {
1314 pub fn launch(&self) -> io::Result<Child> {
1315 let mut cmd = async_process::Command::new(&self.executable);
1316
1317 if self.disable_default_args {
1318 cmd.args(&self.args);
1319 } else {
1320 cmd.args(DEFAULT_ARGS).args(&self.args);
1321 }
1322
1323 if !self
1324 .args
1325 .iter()
1326 .any(|arg| arg.contains("--remote-debugging-port="))
1327 {
1328 cmd.arg(format!("--remote-debugging-port={}", self.port));
1329 }
1330
1331 cmd.args(
1332 self.extensions
1333 .iter()
1334 .map(|e| format!("--load-extension={e}")),
1335 );
1336
1337 if let Some(ref user_data) = self.user_data_dir {
1338 cmd.arg(format!("--user-data-dir={}", user_data.display()));
1339 } else {
1340 cmd.arg(format!(
1344 "--user-data-dir={}",
1345 std::env::temp_dir().join("chromiumoxide-runner").display()
1346 ));
1347 }
1348
1349 if let Some((width, height)) = self.window_size {
1350 cmd.arg(format!("--window-size={width},{height}"));
1351 }
1352
1353 if !self.sandbox {
1354 cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1355 }
1356
1357 match self.headless {
1358 HeadlessMode::False => (),
1359 HeadlessMode::True => {
1360 cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1361 }
1362 HeadlessMode::New => {
1363 cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1364 }
1365 }
1366
1367 if self.incognito {
1368 cmd.arg("--incognito");
1369 }
1370
1371 if let Some(ref envs) = self.process_envs {
1372 cmd.envs(envs);
1373 }
1374 cmd.stderr(Stdio::piped()).spawn()
1375 }
1376}
1377
1378#[deprecated(note = "Use detection::default_executable instead")]
1387pub fn default_executable() -> Result<std::path::PathBuf, String> {
1388 let options = DetectionOptions {
1389 msedge: false,
1390 unstable: false,
1391 };
1392 detection::default_executable(options)
1393}
1394
1395static DEFAULT_ARGS: [&str; 26] = [
1398 "--disable-background-networking",
1399 "--enable-features=NetworkService,NetworkServiceInProcess",
1400 "--disable-background-timer-throttling",
1401 "--disable-backgrounding-occluded-windows",
1402 "--disable-breakpad",
1403 "--disable-client-side-phishing-detection",
1404 "--disable-component-extensions-with-background-pages",
1405 "--disable-default-apps",
1406 "--disable-dev-shm-usage",
1407 "--disable-extensions",
1408 "--disable-features=TranslateUI",
1409 "--disable-hang-monitor",
1410 "--disable-ipc-flooding-protection",
1411 "--disable-popup-blocking",
1412 "--disable-prompt-on-repost",
1413 "--disable-renderer-backgrounding",
1414 "--disable-sync",
1415 "--force-color-profile=srgb",
1416 "--metrics-recording-only",
1417 "--no-first-run",
1418 "--enable-automation",
1419 "--password-store=basic",
1420 "--use-mock-keychain",
1421 "--enable-blink-features=IdleDetection",
1422 "--lang=en_US",
1423 "--disable-blink-features=AutomationControlled",
1424];