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 pub embedded_origin: Option<String>,
133}
134
135impl PermissionOverride {
136 pub fn new(descriptor: PermissionDescriptor, setting: PermissionSetting) -> Self {
137 Self {
138 descriptor,
139 setting,
140 origin: None,
141 embedded_origin: None,
142 }
143 }
144
145 pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
146 self.origin = Some(origin.into());
147 self
148 }
149}
150
151#[derive(Default)]
152struct BrowserContextState {
153 closed: bool,
154 emulation_config: Option<EmulationConfig>,
155 pages: Vec<Weak<Page>>,
156}
157
158pub struct Launcher {
174 port: Option<u16>,
175 connect_addr: Option<String>,
176 browser_type: BrowserType,
177 launch_options: BrowserLaunchOptions,
178}
179
180impl Default for Launcher {
181 fn default() -> Self {
182 Self {
183 port: None,
184 connect_addr: None,
185 browser_type: BrowserType::Chrome,
186 launch_options: BrowserLaunchOptions::default(),
187 }
188 }
189}
190
191impl Launcher {
192 pub fn port(mut self, port: u16) -> Self {
193 self.port = Some(port);
194 self
195 }
196
197 pub fn connect_to_existing(mut self, addr: &str) -> Self {
198 self.connect_addr = Some(addr.to_string());
199 self
200 }
201
202 pub fn browser(mut self, browser: BrowserType) -> Self {
203 self.browser_type = browser;
204 self
205 }
206
207 pub fn launch_options(mut self, options: BrowserLaunchOptions) -> Self {
208 self.launch_options = options;
209 self
210 }
211
212 pub fn configure_options(mut self, configure: impl FnOnce(&mut BrowserLaunchOptions)) -> Self {
213 configure(&mut self.launch_options);
214 self
215 }
216
217 pub fn disable_images(mut self, disable: bool) -> Self {
218 self.launch_options.disable_image_loading = disable;
219 self
220 }
221
222 pub fn mute_audio(mut self, mute: bool) -> Self {
223 self.launch_options.mute_audio = mute;
224 self
225 }
226
227 pub fn incognito(mut self, incognito: bool) -> Self {
228 self.launch_options.incognito = incognito;
229 self
230 }
231
232 pub fn user_data_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
233 self.launch_options.user_data_dir = Some(path.into());
234 self
235 }
236
237 pub fn clear_user_data_dir(mut self) -> Self {
238 self.launch_options.user_data_dir = None;
239 self
240 }
241
242 pub fn profile_directory<S: Into<String>>(mut self, profile: S) -> Self {
243 self.launch_options.profile_directory = Some(profile.into());
244 self
245 }
246
247 pub fn clear_profile_directory(mut self) -> Self {
248 self.launch_options.profile_directory = None;
249 self
250 }
251
252 pub fn add_extension<P: Into<PathBuf>>(mut self, path: P) -> Self {
253 self.launch_options.add_extension(path);
254 self
255 }
256
257 pub fn remove_extension<P: AsRef<Path>>(mut self, path: P) -> Self {
258 self.launch_options.remove_extension(path);
259 self
260 }
261
262 pub fn clear_extensions(mut self) -> Self {
263 self.launch_options.clear_extensions();
264 self
265 }
266
267 pub fn disable_extensions_except<I, S>(mut self, ids: I) -> Self
268 where
269 I: IntoIterator<Item = S>,
270 S: Into<String>,
271 {
272 self.launch_options.disable_extensions_except(ids);
273 self
274 }
275
276 pub fn remove_default_flag<S: Into<String>>(mut self, flag: S) -> Self {
277 self.launch_options.remove_default_flag(flag);
278 self
279 }
280
281 pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
282 self.launch_options.add_arg(arg);
283 self
284 }
285
286 pub fn set_switch_flag<S: Into<String>>(mut self, switch: S) -> Self {
287 self.launch_options.set_switch_flag(switch);
288 self
289 }
290
291 pub fn set_switch_value<S, V>(mut self, switch: S, value: V) -> Self
292 where
293 S: Into<String>,
294 V: Into<String>,
295 {
296 self.launch_options.set_switch_value(switch, value);
297 self
298 }
299
300 pub fn clear_switch<S: Into<String>>(mut self, switch: S) -> Self {
301 self.launch_options.clear_switch(switch);
302 self
303 }
304
305 pub fn enable_feature<S: Into<String>>(mut self, feature: S) -> Self {
306 self.launch_options.enable_feature(feature);
307 self
308 }
309
310 pub fn disable_feature<S: Into<String>>(mut self, feature: S) -> Self {
311 self.launch_options.disable_feature(feature);
312 self
313 }
314
315 pub fn force_field_trial<S: Into<String>>(mut self, trial: S) -> Self {
316 self.launch_options.force_field_trial(trial);
317 self
318 }
319
320 pub async fn launch(self) -> Result<Arc<Browser>> {
321 let Launcher {
322 port,
323 connect_addr,
324 browser_type,
325 launch_options,
326 } = self;
327
328 let (ws_url, process) = if let Some(addr) = connect_addr {
329 let url = resolve_browser_ws_url(&addr).await?;
331 (url, None)
332 } else {
333 match find_running_browser_port(browser_type) {
335 Ok(found_port) => {
336 let addr = format!("http://127.0.0.1:{}", found_port);
337 tracing::info!("Connecting to existing browser at {}", addr);
338 let url = resolve_browser_ws_url(&addr).await?;
339 (url, None)
340 }
341 Err(_) => {
342 let selected_port = if let Some(p) = port {
344 p
345 } else {
346 find_available_port().await?
347 };
348 let launched =
349 browser_type.launch_with_options(selected_port, launch_options)?;
350 let addr = format!("http://127.0.0.1:{}", launched.debug_port);
351 tracing::info!("Launched new browser at {}", addr);
352 let url = resolve_browser_ws_url(&addr).await?;
353 (url, Some(ChromeProcess(launched)))
354 }
355 }
356 };
357
358 Browser::connect(ws_url, process).await
359 }
360}
361
362struct ChromeProcess(LaunchedBrowser);
364impl Drop for ChromeProcess {
365 fn drop(&mut self) {
366 self.0.child.kill().ok();
367 }
368}
369
370pub struct Browser {
374 internals: Arc<ConnectionInternals>,
375 session_event_senders: Arc<Mutex<HashMap<String, mpsc::Sender<CdpEvent>>>>,
376 active_pages: Arc<Mutex<HashMap<String, Arc<Page>>>>, browser_contexts: Arc<Mutex<HashMap<String, Weak<BrowserContext>>>>,
378 _chrome_process: Option<ChromeProcess>,
379}
380
381impl Browser {
382 pub fn launcher() -> Launcher {
395 Launcher::default()
396 }
397
398 async fn connect(ws_url: Url, process: Option<ChromeProcess>) -> Result<Arc<Browser>> {
399 let (ws_stream, _) = connect_async(ws_url.as_str()).await?;
400 let (writer, reader) = ws_stream.split();
401 let connection = Arc::new(ConnectionInternals::new(writer));
402 let active_pages = Arc::new(Mutex::new(HashMap::new()));
403 let session_event_senders = Arc::new(Mutex::new(HashMap::new()));
404 let browser_contexts = Arc::new(Mutex::new(HashMap::new()));
405 tokio::spawn(central_message_dispatcher(
406 reader,
407 Arc::clone(&connection),
408 Arc::clone(&active_pages),
409 Arc::clone(&session_event_senders),
410 ));
411 let browser = Browser {
412 internals: connection,
413 session_event_senders,
414 active_pages,
415 browser_contexts,
416 _chrome_process: process,
417 };
418 let browser_arc = Arc::new(browser);
419 Ok(browser_arc)
420 }
421
422 pub async fn new_context(self: &Arc<Self>) -> Result<Arc<BrowserContext>> {
437 self.new_context_with_options(BrowserContextOptions::default())
438 .await
439 }
440
441 pub async fn new_context_with_options(
459 self: &Arc<Self>,
460 options: BrowserContextOptions,
461 ) -> Result<Arc<BrowserContext>> {
462 let method = CreateBrowserContext {
463 dispose_on_detach: options.dispose_on_detach,
464 proxy_server: options.proxy_server.clone(),
465 proxy_bypass_list: options.proxy_bypass_list.clone(),
466 origins_with_universal_network_access: options
467 .origins_with_universal_network_access
468 .clone(),
469 };
470 let obj = self
471 .send_command::<_, CreateBrowserContextReturnObject>(method, None)
472 .await?;
473 let context_id = obj.browser_context_id;
474 println!("Created new BrowserContext with ID: {}", context_id);
475
476 let context = Arc::new(BrowserContext::new(context_id.clone(), Arc::clone(self)));
477 self.register_context(&context).await;
478
479 if let Err(err) = context.apply_options(&options).await {
480 self.unregister_context(&context_id).await;
481 return Err(err);
482 }
483
484 Ok(context)
485 }
486
487 pub async fn contexts(&self) -> Vec<Arc<BrowserContext>> {
489 let mut contexts = Vec::new();
490 let mut guard = self.browser_contexts.lock().await;
491 guard.retain(|_, weak| match weak.upgrade() {
492 Some(context) => {
493 contexts.push(context);
494 true
495 }
496 None => false,
497 });
498 contexts
499 }
500
501 pub async fn get_context(&self, id: &str) -> Option<Arc<BrowserContext>> {
503 let mut guard = self.browser_contexts.lock().await;
504 if let Some(weak) = guard.get(id)
505 && let Some(context) = weak.upgrade()
506 {
507 return Some(context);
508 }
509 guard.remove(id);
510 None
511 }
512
513 async fn register_context(&self, context: &Arc<BrowserContext>) {
514 let id = context.id().to_string();
515 self.browser_contexts
516 .lock()
517 .await
518 .insert(id, Arc::downgrade(context));
519 }
520
521 async fn unregister_context(&self, id: &str) {
522 self.browser_contexts.lock().await.remove(id);
523 }
524
525 pub async fn new_page(self: &Arc<Self>) -> Result<Arc<Page>> {
539 self.new_page_in_context(None).await
541 }
542
543 pub(crate) async fn new_page_in_context(&self, context_id: Option<&str>) -> Result<Arc<Page>> {
545 let (event_sender, event_receiver) = mpsc::channel(crate::DEFAULT_CHANNEL_CAPACITY);
546 let method = CreateTarget {
547 url: "about:blank".to_string(),
548 left: None,
549 top: None,
550 width: None,
551 height: None,
552 window_state: None,
553 browser_context_id: context_id.map(|s| s.to_string()),
554 enable_begin_frame_control: None,
555 new_window: None,
556 background: None,
557 for_tab: None,
558 hidden: None,
559 };
560 let obj = self
561 .send_command::<_, CreateTargetReturnObject>(method, None)
562 .await?;
563 let method = cdp_protocol::target::AttachToTarget {
564 target_id: obj.target_id,
565 flatten: Some(true),
566 };
567 let obj = self
568 .send_command::<_, AttachToTargetReturnObject>(method, None)
569 .await?;
570 self.session_event_senders
571 .lock()
572 .await
573 .insert(obj.session_id.clone(), event_sender);
574
575 let session = Session::new(
576 Some(obj.session_id.clone()),
577 Arc::clone(&self.internals),
578 event_receiver,
579 );
580 let page = Arc::new(Page::new_from_browser(Arc::new(session)));
581
582 page.domain_manager.enable_required_domains().await?;
584
585 self.active_pages
586 .lock()
587 .await
588 .insert(obj.session_id.clone(), Arc::clone(&page));
589 Ok(page)
590 }
591
592 pub(crate) async fn send_command<
593 M: serde::Serialize + std::fmt::Debug + cdp_protocol::types::Method,
594 R: for<'de> Deserialize<'de>,
595 >(
596 &self,
597 method: M,
598 timeout: Option<Duration>,
599 ) -> Result<R> {
600 self.internals.send(method, None, timeout).await
601 }
602}
603
604pub struct BrowserContext {
606 id: String, browser: Arc<Browser>, state: Mutex<BrowserContextState>,
609}
610
611impl BrowserContext {
612 fn new(id: String, browser: Arc<Browser>) -> Self {
613 Self {
614 id,
615 browser,
616 state: Mutex::new(BrowserContextState::default()),
617 }
618 }
619
620 pub fn id(&self) -> &str {
621 &self.id
622 }
623
624 async fn apply_options(&self, options: &BrowserContextOptions) -> Result<()> {
625 for grant in &options.permission_grants {
626 self.apply_permission_grant(grant).await?;
627 }
628
629 for permission in &options.permission_overrides {
630 self.set_permission_override(permission).await?;
631 }
632
633 if let Some(download) = &options.download {
634 self.set_download_behavior(download).await?;
635 }
636
637 if let Some(emulation) = &options.emulation {
638 self.set_emulation_config(emulation.clone()).await?;
639 }
640
641 Ok(())
642 }
643
644 pub async fn new_page(&self) -> Result<Arc<Page>> {
659 let page = self.browser.new_page_in_context(Some(&self.id)).await?;
660 self.register_page(&page).await;
661 self.apply_emulation_to_page(&page).await?;
662 Ok(page)
663 }
664
665 pub async fn grant_permissions(
667 &self,
668 origin: Option<&str>,
669 permissions: &[PermissionType],
670 ) -> Result<()> {
671 if permissions.is_empty() {
672 return Ok(());
673 }
674
675 let method = GrantPermissions {
676 permissions: permissions.to_vec(),
677 origin: origin.map(|value| value.to_string()),
678 browser_context_id: Some(self.id.clone()),
679 };
680 let _: GrantPermissionsReturnObject = self.browser.send_command(method, None).await?;
681 Ok(())
682 }
683
684 pub async fn apply_permission_grant(&self, grant: &PermissionGrant) -> Result<()> {
686 self.grant_permissions(grant.origin.as_deref(), &grant.permissions)
687 .await
688 }
689
690 pub async fn set_permission_override(&self, permission: &PermissionOverride) -> Result<()> {
692 let method = SetPermission {
693 permission: permission.descriptor.clone(),
694 setting: permission.setting.clone(),
695 origin: permission.origin.clone(),
696 embedded_origin: permission.embedded_origin.clone(),
697 browser_context_id: Some(self.id.clone()),
698 };
699 let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
700 Ok(())
701 }
702
703 pub async fn reset_permissions(&self) -> Result<()> {
705 let method = ResetPermissions {
706 browser_context_id: Some(self.id.clone()),
707 };
708 let _: ResetPermissionsReturnObject = self.browser.send_command(method, None).await?;
709 Ok(())
710 }
711
712 pub async fn set_download_behavior(&self, options: &DownloadOptions) -> Result<()> {
714 let download_path = options
715 .download_path
716 .as_ref()
717 .map(|path| path.to_string_lossy().into_owned());
718 let method = SetDownloadBehavior {
719 behavior: options.behavior.as_cdp_behavior(),
720 browser_context_id: Some(self.id.clone()),
721 download_path,
722 events_enabled: options.events_enabled,
723 };
724 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
725 Ok(())
726 }
727
728 pub async fn clear_download_behavior(&self) -> Result<()> {
730 let method = SetDownloadBehavior {
731 behavior: SetDownloadBehaviorBehaviorOption::Default,
732 browser_context_id: Some(self.id.clone()),
733 download_path: None,
734 events_enabled: None,
735 };
736 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
737 Ok(())
738 }
739
740 pub async fn set_emulation_config(&self, config: EmulationConfig) -> Result<()> {
742 let pages = {
743 let mut state = self.state.lock().await;
744 state.emulation_config = Some(config.clone());
745 let mut pages = Vec::new();
746 state.pages.retain(|weak| match weak.upgrade() {
747 Some(page) => {
748 pages.push(page);
749 true
750 }
751 None => false,
752 });
753 pages
754 };
755
756 for page in pages {
757 page.emulation().apply_config(&config).await?;
758 }
759
760 Ok(())
761 }
762
763 pub async fn clear_emulation_config(&self) -> Result<()> {
765 let (pages, previous) = {
766 let mut state = self.state.lock().await;
767 let previous = state.emulation_config.take();
768 let mut pages = Vec::new();
769 state.pages.retain(|weak| match weak.upgrade() {
770 Some(page) => {
771 pages.push(page);
772 true
773 }
774 None => false,
775 });
776 (pages, previous)
777 };
778
779 if let Some(config) = previous {
780 for page in pages {
781 let controller = page.emulation();
782 if config.geolocation.is_some() {
783 controller.clear_geolocation().await?;
784 }
785 if config.timezone_id.is_some() {
786 controller.reset_timezone().await?;
787 }
788 if config.locale.is_some() {
789 controller.set_locale(None).await?;
790 }
791 if config.media.is_some() {
792 controller.clear_media().await?;
793 }
794 }
796 }
797
798 Ok(())
799 }
800
801 pub async fn close(&self) -> Result<()> {
803 {
804 let mut state = self.state.lock().await;
805 if state.closed {
806 return Ok(());
807 }
808 state.closed = true;
809 }
810
811 let method = DisposeBrowserContext {
812 browser_context_id: self.id.clone(),
813 };
814 let result: Result<Value> = self.browser.send_command(method, None).await;
815
816 if let Err(err) = result {
817 let mut state = self.state.lock().await;
818 state.closed = false;
819 return Err(err);
820 }
821
822 {
823 let mut state = self.state.lock().await;
824 state.pages.clear();
825 state.emulation_config = None;
826 }
827
828 self.browser.unregister_context(&self.id).await;
829 Ok(())
830 }
831
832 async fn register_page(&self, page: &Arc<Page>) {
833 let mut state = self.state.lock().await;
834 state.pages.retain(|weak| weak.upgrade().is_some());
835 let weak = Arc::downgrade(page);
836 if !state
837 .pages
838 .iter()
839 .any(|existing| Weak::ptr_eq(existing, &weak))
840 {
841 state.pages.push(weak);
842 }
843 }
844
845 async fn apply_emulation_to_page(&self, page: &Arc<Page>) -> Result<()> {
846 let config = {
847 let state = self.state.lock().await;
848 state.emulation_config.clone()
849 };
850
851 if let Some(config) = config {
852 page.emulation().apply_config(&config).await?;
853 }
854
855 Ok(())
856 }
857}