1use super::{
2 discovery::{find_available_port, find_running_browser_port},
3 launcher::{BrowserLaunchOptions, BrowserType, LaunchedBrowser},
4 ws_endpoints::resolve_browser_ws_url,
5};
6use crate::emulation::EmulationConfig;
7use crate::error::Result;
8use crate::page::Page;
9use crate::session::Session;
10use crate::transport::{cdp_protocol::*, websocket_connection::*};
11
12use cdp_protocol::browser::{
13 GrantPermissions, GrantPermissionsReturnObject, PermissionDescriptor, PermissionSetting,
14 PermissionType, ResetPermissions, ResetPermissionsReturnObject, SetDownloadBehavior,
15 SetDownloadBehaviorBehaviorOption, SetDownloadBehaviorReturnObject, SetPermission,
16 SetPermissionReturnObject,
17};
18use cdp_protocol::target::{
19 AttachToTargetReturnObject, CreateBrowserContext, CreateBrowserContextReturnObject,
20 CreateTarget, CreateTargetReturnObject, DisposeBrowserContext,
21};
22use futures_util::StreamExt;
23use serde::Deserialize;
24use serde_json::Value;
25use std::collections::HashMap;
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, Weak};
28use std::time::Duration;
29use tokio::sync::{Mutex, mpsc};
30use tokio_tungstenite::connect_async;
31use url::Url;
32
33#[derive(Clone, Debug, Default)]
35pub struct BrowserContextOptions {
36 pub dispose_on_detach: Option<bool>,
37 pub proxy_server: Option<String>,
38 pub proxy_bypass_list: Option<String>,
39 pub origins_with_universal_network_access: Option<Vec<String>>,
40 pub download: Option<DownloadOptions>,
41 pub permission_grants: Vec<PermissionGrant>,
42 pub permission_overrides: Vec<PermissionOverride>,
43 pub emulation: Option<EmulationConfig>,
44}
45
46impl BrowserContextOptions {
47 pub fn with_emulation(mut self, emulation: EmulationConfig) -> Self {
48 self.emulation = Some(emulation);
49 self
50 }
51}
52
53#[derive(Clone, Debug)]
55pub struct DownloadOptions {
56 pub behavior: DownloadBehavior,
57 pub download_path: Option<PathBuf>,
58 pub events_enabled: Option<bool>,
59}
60
61impl DownloadOptions {
62 pub fn new(behavior: DownloadBehavior) -> Self {
63 Self {
64 behavior,
65 download_path: None,
66 events_enabled: None,
67 }
68 }
69
70 pub fn with_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
71 self.download_path = Some(path.into());
72 self
73 }
74
75 pub fn with_events_enabled(mut self, enabled: bool) -> Self {
76 self.events_enabled = Some(enabled);
77 self
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
83pub enum DownloadBehavior {
84 Deny,
85 #[default]
86 Allow,
87 AllowAndName,
88 Default,
89}
90
91impl DownloadBehavior {
92 fn as_cdp_behavior(self) -> SetDownloadBehaviorBehaviorOption {
93 match self {
94 DownloadBehavior::Deny => SetDownloadBehaviorBehaviorOption::Deny,
95 DownloadBehavior::Allow => SetDownloadBehaviorBehaviorOption::Allow,
96 DownloadBehavior::AllowAndName => SetDownloadBehaviorBehaviorOption::AllowAndName,
97 DownloadBehavior::Default => SetDownloadBehaviorBehaviorOption::Default,
98 }
99 }
100}
101
102#[derive(Clone, Debug, Default)]
104pub struct PermissionGrant {
105 pub origin: Option<String>,
106 pub permissions: Vec<PermissionType>,
107}
108
109impl PermissionGrant {
110 pub fn new<I>(permissions: I) -> Self
111 where
112 I: IntoIterator<Item = PermissionType>,
113 {
114 Self {
115 origin: None,
116 permissions: permissions.into_iter().collect(),
117 }
118 }
119
120 pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
121 self.origin = Some(origin.into());
122 self
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct PermissionOverride {
129 pub descriptor: PermissionDescriptor,
130 pub setting: PermissionSetting,
131 pub origin: Option<String>,
132}
133
134impl PermissionOverride {
135 pub fn new(descriptor: PermissionDescriptor, setting: PermissionSetting) -> Self {
136 Self {
137 descriptor,
138 setting,
139 origin: None,
140 }
141 }
142
143 pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
144 self.origin = Some(origin.into());
145 self
146 }
147}
148
149#[derive(Default)]
150struct BrowserContextState {
151 closed: bool,
152 emulation_config: Option<EmulationConfig>,
153 pages: Vec<Weak<Page>>,
154}
155
156pub struct Launcher {
172 port: Option<u16>,
173 connect_addr: Option<String>,
174 browser_type: BrowserType,
175 launch_options: BrowserLaunchOptions,
176}
177
178impl Default for Launcher {
179 fn default() -> Self {
180 Self {
181 port: None,
182 connect_addr: None,
183 browser_type: BrowserType::Chrome,
184 launch_options: BrowserLaunchOptions::default(),
185 }
186 }
187}
188
189impl Launcher {
190 pub fn port(mut self, port: u16) -> Self {
191 self.port = Some(port);
192 self
193 }
194
195 pub fn connect_to_existing(mut self, addr: &str) -> Self {
196 self.connect_addr = Some(addr.to_string());
197 self
198 }
199
200 pub fn browser(mut self, browser: BrowserType) -> Self {
201 self.browser_type = browser;
202 self
203 }
204
205 pub fn launch_options(mut self, options: BrowserLaunchOptions) -> Self {
206 self.launch_options = options;
207 self
208 }
209
210 pub fn configure_options(mut self, configure: impl FnOnce(&mut BrowserLaunchOptions)) -> Self {
211 configure(&mut self.launch_options);
212 self
213 }
214
215 pub fn disable_images(mut self, disable: bool) -> Self {
216 self.launch_options.disable_image_loading = disable;
217 self
218 }
219
220 pub fn mute_audio(mut self, mute: bool) -> Self {
221 self.launch_options.mute_audio = mute;
222 self
223 }
224
225 pub fn incognito(mut self, incognito: bool) -> Self {
226 self.launch_options.incognito = incognito;
227 self
228 }
229
230 pub fn user_data_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
231 self.launch_options.user_data_dir = Some(path.into());
232 self
233 }
234
235 pub fn clear_user_data_dir(mut self) -> Self {
236 self.launch_options.user_data_dir = None;
237 self
238 }
239
240 pub fn profile_directory<S: Into<String>>(mut self, profile: S) -> Self {
241 self.launch_options.profile_directory = Some(profile.into());
242 self
243 }
244
245 pub fn clear_profile_directory(mut self) -> Self {
246 self.launch_options.profile_directory = None;
247 self
248 }
249
250 pub fn add_extension<P: Into<PathBuf>>(mut self, path: P) -> Self {
251 self.launch_options.add_extension(path);
252 self
253 }
254
255 pub fn remove_extension<P: AsRef<Path>>(mut self, path: P) -> Self {
256 self.launch_options.remove_extension(path);
257 self
258 }
259
260 pub fn clear_extensions(mut self) -> Self {
261 self.launch_options.clear_extensions();
262 self
263 }
264
265 pub fn disable_extensions_except<I, S>(mut self, ids: I) -> Self
266 where
267 I: IntoIterator<Item = S>,
268 S: Into<String>,
269 {
270 self.launch_options.disable_extensions_except(ids);
271 self
272 }
273
274 pub fn remove_default_flag<S: Into<String>>(mut self, flag: S) -> Self {
275 self.launch_options.remove_default_flag(flag);
276 self
277 }
278
279 pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
280 self.launch_options.add_arg(arg);
281 self
282 }
283
284 pub fn set_switch_flag<S: Into<String>>(mut self, switch: S) -> Self {
285 self.launch_options.set_switch_flag(switch);
286 self
287 }
288
289 pub fn set_switch_value<S, V>(mut self, switch: S, value: V) -> Self
290 where
291 S: Into<String>,
292 V: Into<String>,
293 {
294 self.launch_options.set_switch_value(switch, value);
295 self
296 }
297
298 pub fn clear_switch<S: Into<String>>(mut self, switch: S) -> Self {
299 self.launch_options.clear_switch(switch);
300 self
301 }
302
303 pub fn enable_feature<S: Into<String>>(mut self, feature: S) -> Self {
304 self.launch_options.enable_feature(feature);
305 self
306 }
307
308 pub fn disable_feature<S: Into<String>>(mut self, feature: S) -> Self {
309 self.launch_options.disable_feature(feature);
310 self
311 }
312
313 pub fn force_field_trial<S: Into<String>>(mut self, trial: S) -> Self {
314 self.launch_options.force_field_trial(trial);
315 self
316 }
317
318 pub async fn launch(self) -> Result<Arc<Browser>> {
319 let Launcher {
320 port,
321 connect_addr,
322 browser_type,
323 launch_options,
324 } = self;
325
326 let (ws_url, process) = if let Some(addr) = connect_addr {
327 let url = resolve_browser_ws_url(&addr).await?;
329 (url, None)
330 } else {
331 match find_running_browser_port(browser_type) {
333 Ok(found_port) => {
334 let addr = format!("http://127.0.0.1:{}", found_port);
335 tracing::info!("Connecting to existing browser at {}", addr);
336 let url = resolve_browser_ws_url(&addr).await?;
337 (url, None)
338 }
339 Err(_) => {
340 let selected_port = if let Some(p) = port {
342 p
343 } else {
344 find_available_port().await?
345 };
346 let launched =
347 browser_type.launch_with_options(selected_port, launch_options)?;
348 let addr = format!("http://127.0.0.1:{}", launched.debug_port);
349 tracing::info!("Launched new browser at {}", addr);
350 let url = resolve_browser_ws_url(&addr).await?;
351 (url, Some(ChromeProcess(launched)))
352 }
353 }
354 };
355
356 Browser::connect(ws_url, process).await
357 }
358}
359
360struct ChromeProcess(LaunchedBrowser);
362impl Drop for ChromeProcess {
363 fn drop(&mut self) {
364 self.0.child.kill().ok();
365 }
366}
367
368pub struct Browser {
372 internals: Arc<ConnectionInternals>,
373 session_event_senders: Arc<Mutex<HashMap<String, mpsc::Sender<CdpEvent>>>>,
374 active_pages: Arc<Mutex<HashMap<String, Arc<Page>>>>, browser_contexts: Arc<Mutex<HashMap<String, Weak<BrowserContext>>>>,
376 _chrome_process: Option<ChromeProcess>,
377}
378
379impl Browser {
380 pub fn launcher() -> Launcher {
393 Launcher::default()
394 }
395
396 async fn connect(ws_url: Url, process: Option<ChromeProcess>) -> Result<Arc<Browser>> {
397 let (ws_stream, _) = connect_async(ws_url.as_str()).await?;
398 let (writer, reader) = ws_stream.split();
399 let connection = Arc::new(ConnectionInternals::new(writer));
400 let active_pages = Arc::new(Mutex::new(HashMap::new()));
401 let session_event_senders = Arc::new(Mutex::new(HashMap::new()));
402 let browser_contexts = Arc::new(Mutex::new(HashMap::new()));
403 tokio::spawn(central_message_dispatcher(
404 reader,
405 Arc::clone(&connection),
406 Arc::clone(&active_pages),
407 Arc::clone(&session_event_senders),
408 ));
409 let browser = Browser {
410 internals: connection,
411 session_event_senders,
412 active_pages,
413 browser_contexts,
414 _chrome_process: process,
415 };
416 let browser_arc = Arc::new(browser);
417 Ok(browser_arc)
418 }
419
420 pub async fn new_context(self: &Arc<Self>) -> Result<Arc<BrowserContext>> {
435 self.new_context_with_options(BrowserContextOptions::default())
436 .await
437 }
438
439 pub async fn new_context_with_options(
457 self: &Arc<Self>,
458 options: BrowserContextOptions,
459 ) -> Result<Arc<BrowserContext>> {
460 let method = CreateBrowserContext {
461 dispose_on_detach: options.dispose_on_detach,
462 proxy_server: options.proxy_server.clone(),
463 proxy_bypass_list: options.proxy_bypass_list.clone(),
464 origins_with_universal_network_access: options
465 .origins_with_universal_network_access
466 .clone(),
467 };
468 let obj = self
469 .send_command::<_, CreateBrowserContextReturnObject>(method, None)
470 .await?;
471 let context_id = obj.browser_context_id;
472 println!("Created new BrowserContext with ID: {}", context_id);
473
474 let context = Arc::new(BrowserContext::new(context_id.clone(), Arc::clone(self)));
475 self.register_context(&context).await;
476
477 if let Err(err) = context.apply_options(&options).await {
478 self.unregister_context(&context_id).await;
479 return Err(err);
480 }
481
482 Ok(context)
483 }
484
485 pub async fn contexts(&self) -> Vec<Arc<BrowserContext>> {
487 let mut contexts = Vec::new();
488 let mut guard = self.browser_contexts.lock().await;
489 guard.retain(|_, weak| match weak.upgrade() {
490 Some(context) => {
491 contexts.push(context);
492 true
493 }
494 None => false,
495 });
496 contexts
497 }
498
499 pub async fn get_context(&self, id: &str) -> Option<Arc<BrowserContext>> {
501 let mut guard = self.browser_contexts.lock().await;
502 if let Some(weak) = guard.get(id)
503 && let Some(context) = weak.upgrade()
504 {
505 return Some(context);
506 }
507 guard.remove(id);
508 None
509 }
510
511 async fn register_context(&self, context: &Arc<BrowserContext>) {
512 let id = context.id().to_string();
513 self.browser_contexts
514 .lock()
515 .await
516 .insert(id, Arc::downgrade(context));
517 }
518
519 async fn unregister_context(&self, id: &str) {
520 self.browser_contexts.lock().await.remove(id);
521 }
522
523 pub async fn new_page(self: &Arc<Self>) -> Result<Arc<Page>> {
537 self.new_page_in_context(None).await
539 }
540
541 pub(crate) async fn new_page_in_context(&self, context_id: Option<&str>) -> Result<Arc<Page>> {
543 let (event_sender, event_receiver) = mpsc::channel(crate::DEFAULT_CHANNEL_CAPACITY);
544 let method = CreateTarget {
545 url: "about:blank".to_string(),
546 left: None,
547 top: None,
548 width: None,
549 height: None,
550 window_state: None,
551 browser_context_id: context_id.map(|s| s.to_string()),
552 enable_begin_frame_control: None,
553 new_window: None,
554 background: None,
555 for_tab: None,
556 hidden: None,
557 };
558 let obj = self
559 .send_command::<_, CreateTargetReturnObject>(method, None)
560 .await?;
561 let method = cdp_protocol::target::AttachToTarget {
562 target_id: obj.target_id,
563 flatten: Some(true),
564 };
565 let obj = self
566 .send_command::<_, AttachToTargetReturnObject>(method, None)
567 .await?;
568 self.session_event_senders
569 .lock()
570 .await
571 .insert(obj.session_id.clone(), event_sender);
572
573 let session = Session::new(
574 Some(obj.session_id.clone()),
575 Arc::clone(&self.internals),
576 event_receiver,
577 );
578 let page = Arc::new(Page::new_from_browser(Arc::new(session)));
579
580 page.domain_manager.enable_required_domains().await?;
582
583 self.active_pages
584 .lock()
585 .await
586 .insert(obj.session_id.clone(), Arc::clone(&page));
587 Ok(page)
588 }
589
590 pub(crate) async fn send_command<
591 M: serde::Serialize + std::fmt::Debug + cdp_protocol::types::Method,
592 R: for<'de> Deserialize<'de>,
593 >(
594 &self,
595 method: M,
596 timeout: Option<Duration>,
597 ) -> Result<R> {
598 self.internals.send(method, None, timeout).await
599 }
600}
601
602pub struct BrowserContext {
604 id: String, browser: Arc<Browser>, state: Mutex<BrowserContextState>,
607}
608
609impl BrowserContext {
610 fn new(id: String, browser: Arc<Browser>) -> Self {
611 Self {
612 id,
613 browser,
614 state: Mutex::new(BrowserContextState::default()),
615 }
616 }
617
618 pub fn id(&self) -> &str {
619 &self.id
620 }
621
622 async fn apply_options(&self, options: &BrowserContextOptions) -> Result<()> {
623 for grant in &options.permission_grants {
624 self.apply_permission_grant(grant).await?;
625 }
626
627 for permission in &options.permission_overrides {
628 self.set_permission_override(permission).await?;
629 }
630
631 if let Some(download) = &options.download {
632 self.set_download_behavior(download).await?;
633 }
634
635 if let Some(emulation) = &options.emulation {
636 self.set_emulation_config(emulation.clone()).await?;
637 }
638
639 Ok(())
640 }
641
642 pub async fn new_page(&self) -> Result<Arc<Page>> {
657 let page = self.browser.new_page_in_context(Some(&self.id)).await?;
658 self.register_page(&page).await;
659 self.apply_emulation_to_page(&page).await?;
660 Ok(page)
661 }
662
663 pub async fn grant_permissions(
665 &self,
666 origin: Option<&str>,
667 permissions: &[PermissionType],
668 ) -> Result<()> {
669 if permissions.is_empty() {
670 return Ok(());
671 }
672
673 let method = GrantPermissions {
674 permissions: permissions.to_vec(),
675 origin: origin.map(|value| value.to_string()),
676 browser_context_id: Some(self.id.clone()),
677 };
678 let _: GrantPermissionsReturnObject = self.browser.send_command(method, None).await?;
679 Ok(())
680 }
681
682 pub async fn apply_permission_grant(&self, grant: &PermissionGrant) -> Result<()> {
684 self.grant_permissions(grant.origin.as_deref(), &grant.permissions)
685 .await
686 }
687
688 pub async fn set_permission_override(&self, permission: &PermissionOverride) -> Result<()> {
690 let method = SetPermission {
691 permission: permission.descriptor.clone(),
692 setting: permission.setting.clone(),
693 origin: permission.origin.clone(),
694 browser_context_id: Some(self.id.clone()),
695 };
696 let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
697 Ok(())
698 }
699
700 pub async fn reset_permissions(&self) -> Result<()> {
702 let method = ResetPermissions {
703 browser_context_id: Some(self.id.clone()),
704 };
705 let _: ResetPermissionsReturnObject = self.browser.send_command(method, None).await?;
706 Ok(())
707 }
708
709 pub async fn set_download_behavior(&self, options: &DownloadOptions) -> Result<()> {
711 let download_path = options
712 .download_path
713 .as_ref()
714 .map(|path| path.to_string_lossy().into_owned());
715 let method = SetDownloadBehavior {
716 behavior: options.behavior.as_cdp_behavior(),
717 browser_context_id: Some(self.id.clone()),
718 download_path,
719 events_enabled: options.events_enabled,
720 };
721 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
722 Ok(())
723 }
724
725 pub async fn clear_download_behavior(&self) -> Result<()> {
727 let method = SetDownloadBehavior {
728 behavior: SetDownloadBehaviorBehaviorOption::Default,
729 browser_context_id: Some(self.id.clone()),
730 download_path: None,
731 events_enabled: None,
732 };
733 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
734 Ok(())
735 }
736
737 pub async fn set_emulation_config(&self, config: EmulationConfig) -> Result<()> {
739 let pages = {
740 let mut state = self.state.lock().await;
741 state.emulation_config = Some(config.clone());
742 let mut pages = Vec::new();
743 state.pages.retain(|weak| match weak.upgrade() {
744 Some(page) => {
745 pages.push(page);
746 true
747 }
748 None => false,
749 });
750 pages
751 };
752
753 for page in pages {
754 page.emulation().apply_config(&config).await?;
755 }
756
757 Ok(())
758 }
759
760 pub async fn clear_emulation_config(&self) -> Result<()> {
762 let (pages, previous) = {
763 let mut state = self.state.lock().await;
764 let previous = state.emulation_config.take();
765 let mut pages = Vec::new();
766 state.pages.retain(|weak| match weak.upgrade() {
767 Some(page) => {
768 pages.push(page);
769 true
770 }
771 None => false,
772 });
773 (pages, previous)
774 };
775
776 if let Some(config) = previous {
777 for page in pages {
778 let controller = page.emulation();
779 if config.geolocation.is_some() {
780 controller.clear_geolocation().await?;
781 }
782 if config.timezone_id.is_some() {
783 controller.reset_timezone().await?;
784 }
785 if config.locale.is_some() {
786 controller.set_locale(None).await?;
787 }
788 if config.media.is_some() {
789 controller.clear_media().await?;
790 }
791 }
793 }
794
795 Ok(())
796 }
797
798 pub async fn close(&self) -> Result<()> {
800 {
801 let mut state = self.state.lock().await;
802 if state.closed {
803 return Ok(());
804 }
805 state.closed = true;
806 }
807
808 let method = DisposeBrowserContext {
809 browser_context_id: self.id.clone(),
810 };
811 let result: Result<Value> = self.browser.send_command(method, None).await;
812
813 if let Err(err) = result {
814 let mut state = self.state.lock().await;
815 state.closed = false;
816 return Err(err);
817 }
818
819 {
820 let mut state = self.state.lock().await;
821 state.pages.clear();
822 state.emulation_config = None;
823 }
824
825 self.browser.unregister_context(&self.id).await;
826 Ok(())
827 }
828
829 async fn register_page(&self, page: &Arc<Page>) {
830 let mut state = self.state.lock().await;
831 state.pages.retain(|weak| weak.upgrade().is_some());
832 let weak = Arc::downgrade(page);
833 if !state
834 .pages
835 .iter()
836 .any(|existing| Weak::ptr_eq(existing, &weak))
837 {
838 state.pages.push(weak);
839 }
840 }
841
842 async fn apply_emulation_to_page(&self, page: &Arc<Page>) -> Result<()> {
843 let config = {
844 let state = self.state.lock().await;
845 state.emulation_config.clone()
846 };
847
848 if let Some(config) = config {
849 page.emulation().apply_config(&config).await?;
850 }
851
852 Ok(())
853 }
854}