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 PermissionDescriptor, PermissionSetting, PermissionType, ResetPermissions,
14 ResetPermissionsReturnObject, SetDownloadBehavior, SetDownloadBehaviorBehaviorOption,
15 SetDownloadBehaviorReturnObject, SetPermission, SetPermissionReturnObject,
16};
17use cdp_protocol::target::{
18 AttachToTargetReturnObject, CreateBrowserContext, CreateBrowserContextReturnObject,
19 CreateTarget, CreateTargetReturnObject, DisposeBrowserContext,
20};
21use futures_util::StreamExt;
22use serde::Deserialize;
23use serde_json::Value;
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26use std::sync::{Arc, Weak};
27use std::time::Duration;
28use tokio::sync::{Mutex, mpsc};
29use tokio_tungstenite::connect_async;
30use url::Url;
31
32#[derive(Clone, Debug, Default)]
34pub struct BrowserContextOptions {
35 pub dispose_on_detach: Option<bool>,
36 pub proxy_server: Option<String>,
37 pub proxy_bypass_list: Option<String>,
38 pub origins_with_universal_network_access: Option<Vec<String>>,
39 pub download: Option<DownloadOptions>,
40 pub permission_grants: Vec<PermissionGrant>,
41 pub permission_overrides: Vec<PermissionOverride>,
42 pub emulation: Option<EmulationConfig>,
43}
44
45impl BrowserContextOptions {
46 pub fn with_emulation(mut self, emulation: EmulationConfig) -> Self {
47 self.emulation = Some(emulation);
48 self
49 }
50}
51
52#[derive(Clone, Debug)]
54pub struct DownloadOptions {
55 pub behavior: DownloadBehavior,
56 pub download_path: Option<PathBuf>,
57 pub events_enabled: Option<bool>,
58}
59
60impl DownloadOptions {
61 pub fn new(behavior: DownloadBehavior) -> Self {
62 Self {
63 behavior,
64 download_path: None,
65 events_enabled: None,
66 }
67 }
68
69 pub fn with_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
70 self.download_path = Some(path.into());
71 self
72 }
73
74 pub fn with_events_enabled(mut self, enabled: bool) -> Self {
75 self.events_enabled = Some(enabled);
76 self
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
82pub enum DownloadBehavior {
83 Deny,
84 #[default]
85 Allow,
86 AllowAndName,
87 Default,
88}
89
90impl DownloadBehavior {
91 fn as_cdp_behavior(self) -> SetDownloadBehaviorBehaviorOption {
92 match self {
93 DownloadBehavior::Deny => SetDownloadBehaviorBehaviorOption::Deny,
94 DownloadBehavior::Allow => SetDownloadBehaviorBehaviorOption::Allow,
95 DownloadBehavior::AllowAndName => SetDownloadBehaviorBehaviorOption::AllowAndName,
96 DownloadBehavior::Default => SetDownloadBehaviorBehaviorOption::Default,
97 }
98 }
99}
100
101#[derive(Clone, Debug, Default)]
103pub struct PermissionGrant {
104 pub origin: Option<String>,
105 pub permissions: Vec<PermissionType>,
106}
107
108impl PermissionGrant {
109 pub fn new<I>(permissions: I) -> Self
110 where
111 I: IntoIterator<Item = PermissionType>,
112 {
113 Self {
114 origin: None,
115 permissions: permissions.into_iter().collect(),
116 }
117 }
118
119 pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
120 self.origin = Some(origin.into());
121 self
122 }
123}
124
125#[derive(Clone, Debug)]
127pub struct PermissionOverride {
128 pub descriptor: PermissionDescriptor,
129 pub setting: PermissionSetting,
130 pub origin: Option<String>,
131 pub embedded_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 embedded_origin: None,
141 }
142 }
143
144 pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
145 self.origin = Some(origin.into());
146 self
147 }
148}
149
150#[derive(Default)]
151struct BrowserContextState {
152 closed: bool,
153 emulation_config: Option<EmulationConfig>,
154 pages: Vec<Weak<Page>>,
155}
156
157pub struct Launcher {
173 port: Option<u16>,
174 connect_addr: Option<String>,
175 browser_type: BrowserType,
176 launch_options: BrowserLaunchOptions,
177}
178
179impl Default for Launcher {
180 fn default() -> Self {
181 Self {
182 port: None,
183 connect_addr: None,
184 browser_type: BrowserType::Chrome,
185 launch_options: BrowserLaunchOptions::default(),
186 }
187 }
188}
189
190impl Launcher {
191 pub fn port(mut self, port: u16) -> Self {
192 self.port = Some(port);
193 self
194 }
195
196 pub fn connect_to_existing(mut self, addr: &str) -> Self {
197 self.connect_addr = Some(addr.to_string());
198 self
199 }
200
201 pub fn browser(mut self, browser: BrowserType) -> Self {
202 self.browser_type = browser;
203 self
204 }
205
206 pub fn launch_options(mut self, options: BrowserLaunchOptions) -> Self {
207 self.launch_options = options;
208 self
209 }
210
211 pub fn configure_options(mut self, configure: impl FnOnce(&mut BrowserLaunchOptions)) -> Self {
212 configure(&mut self.launch_options);
213 self
214 }
215
216 pub fn disable_images(mut self, disable: bool) -> Self {
217 self.launch_options.disable_image_loading = disable;
218 self
219 }
220
221 pub fn mute_audio(mut self, mute: bool) -> Self {
222 self.launch_options.mute_audio = mute;
223 self
224 }
225
226 pub fn incognito(mut self, incognito: bool) -> Self {
227 self.launch_options.incognito = incognito;
228 self
229 }
230
231 pub fn user_data_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
232 self.launch_options.user_data_dir = Some(path.into());
233 self
234 }
235
236 pub fn clear_user_data_dir(mut self) -> Self {
237 self.launch_options.user_data_dir = None;
238 self
239 }
240
241 pub fn profile_directory<S: Into<String>>(mut self, profile: S) -> Self {
242 self.launch_options.profile_directory = Some(profile.into());
243 self
244 }
245
246 pub fn clear_profile_directory(mut self) -> Self {
247 self.launch_options.profile_directory = None;
248 self
249 }
250
251 pub fn add_extension<P: Into<PathBuf>>(mut self, path: P) -> Self {
252 self.launch_options.add_extension(path);
253 self
254 }
255
256 pub fn remove_extension<P: AsRef<Path>>(mut self, path: P) -> Self {
257 self.launch_options.remove_extension(path);
258 self
259 }
260
261 pub fn clear_extensions(mut self) -> Self {
262 self.launch_options.clear_extensions();
263 self
264 }
265
266 pub fn disable_extensions_except<I, S>(mut self, ids: I) -> Self
267 where
268 I: IntoIterator<Item = S>,
269 S: Into<String>,
270 {
271 self.launch_options.disable_extensions_except(ids);
272 self
273 }
274
275 pub fn remove_default_flag<S: Into<String>>(mut self, flag: S) -> Self {
276 self.launch_options.remove_default_flag(flag);
277 self
278 }
279
280 pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
281 self.launch_options.add_arg(arg);
282 self
283 }
284
285 pub fn set_switch_flag<S: Into<String>>(mut self, switch: S) -> Self {
286 self.launch_options.set_switch_flag(switch);
287 self
288 }
289
290 pub fn set_switch_value<S, V>(mut self, switch: S, value: V) -> Self
291 where
292 S: Into<String>,
293 V: Into<String>,
294 {
295 self.launch_options.set_switch_value(switch, value);
296 self
297 }
298
299 pub fn clear_switch<S: Into<String>>(mut self, switch: S) -> Self {
300 self.launch_options.clear_switch(switch);
301 self
302 }
303
304 pub fn enable_feature<S: Into<String>>(mut self, feature: S) -> Self {
305 self.launch_options.enable_feature(feature);
306 self
307 }
308
309 pub fn disable_feature<S: Into<String>>(mut self, feature: S) -> Self {
310 self.launch_options.disable_feature(feature);
311 self
312 }
313
314 pub fn force_field_trial<S: Into<String>>(mut self, trial: S) -> Self {
315 self.launch_options.force_field_trial(trial);
316 self
317 }
318
319 pub async fn launch(self) -> Result<Arc<Browser>> {
320 let Launcher {
321 port,
322 connect_addr,
323 browser_type,
324 launch_options,
325 } = self;
326
327 let (ws_url, process) = if let Some(addr) = connect_addr {
328 let url = resolve_browser_ws_url(&addr).await?;
330 (url, None)
331 } else {
332 match find_running_browser_port(browser_type) {
334 Ok(found_port) => {
335 let addr = format!("http://127.0.0.1:{}", found_port);
336 tracing::info!("Connecting to existing browser at {}", addr);
337 let url = resolve_browser_ws_url(&addr).await?;
338 (url, None)
339 }
340 Err(_) => {
341 let selected_port = if let Some(p) = port {
343 p
344 } else {
345 find_available_port().await?
346 };
347 let launched =
348 browser_type.launch_with_options(selected_port, launch_options)?;
349 let addr = format!("http://127.0.0.1:{}", launched.debug_port);
350 tracing::info!("Launched new browser at {}", addr);
351 let url = resolve_browser_ws_url(&addr).await?;
352 (url, Some(ChromeProcess(launched)))
353 }
354 }
355 };
356
357 Browser::connect(ws_url, process).await
358 }
359}
360
361struct ChromeProcess(LaunchedBrowser);
363impl Drop for ChromeProcess {
364 fn drop(&mut self) {
365 self.0.child.kill().ok();
366 }
367}
368
369pub struct Browser {
373 internals: Arc<ConnectionInternals>,
374 session_event_senders: Arc<Mutex<HashMap<String, mpsc::Sender<CdpEvent>>>>,
375 active_pages: Arc<Mutex<HashMap<String, Arc<Page>>>>, browser_contexts: Arc<Mutex<HashMap<String, Weak<BrowserContext>>>>,
377 _chrome_process: Option<ChromeProcess>,
378}
379
380impl Browser {
381 pub fn launcher() -> Launcher {
394 Launcher::default()
395 }
396
397 async fn connect(ws_url: Url, process: Option<ChromeProcess>) -> Result<Arc<Browser>> {
398 let (ws_stream, _) = connect_async(ws_url.as_str()).await?;
399 let (writer, reader) = ws_stream.split();
400 let connection = Arc::new(ConnectionInternals::new(writer));
401 let active_pages = Arc::new(Mutex::new(HashMap::new()));
402 let session_event_senders = Arc::new(Mutex::new(HashMap::new()));
403 let browser_contexts = Arc::new(Mutex::new(HashMap::new()));
404 tokio::spawn(central_message_dispatcher(
405 reader,
406 Arc::clone(&connection),
407 Arc::clone(&active_pages),
408 Arc::clone(&session_event_senders),
409 ));
410 let browser = Browser {
411 internals: connection,
412 session_event_senders,
413 active_pages,
414 browser_contexts,
415 _chrome_process: process,
416 };
417 let browser_arc = Arc::new(browser);
418 Ok(browser_arc)
419 }
420
421 pub async fn new_context(self: &Arc<Self>) -> Result<Arc<BrowserContext>> {
436 self.new_context_with_options(BrowserContextOptions::default())
437 .await
438 }
439
440 pub async fn new_context_with_options(
458 self: &Arc<Self>,
459 options: BrowserContextOptions,
460 ) -> Result<Arc<BrowserContext>> {
461 let method = CreateBrowserContext {
462 dispose_on_detach: options.dispose_on_detach,
463 proxy_server: options.proxy_server.clone(),
464 proxy_bypass_list: options.proxy_bypass_list.clone(),
465 origins_with_universal_network_access: options
466 .origins_with_universal_network_access
467 .clone(),
468 };
469 let obj = self
470 .send_command::<_, CreateBrowserContextReturnObject>(method, None)
471 .await?;
472 let context_id = obj.browser_context_id;
473 println!("Created new BrowserContext with ID: {}", context_id);
474
475 let context = Arc::new(BrowserContext::new(context_id.clone(), Arc::clone(self)));
476 self.register_context(&context).await;
477
478 if let Err(err) = context.apply_options(&options).await {
479 self.unregister_context(&context_id).await;
480 return Err(err);
481 }
482
483 Ok(context)
484 }
485
486 pub async fn contexts(&self) -> Vec<Arc<BrowserContext>> {
488 let mut contexts = Vec::new();
489 let mut guard = self.browser_contexts.lock().await;
490 guard.retain(|_, weak| match weak.upgrade() {
491 Some(context) => {
492 contexts.push(context);
493 true
494 }
495 None => false,
496 });
497 contexts
498 }
499
500 pub async fn get_context(&self, id: &str) -> Option<Arc<BrowserContext>> {
502 let mut guard = self.browser_contexts.lock().await;
503 if let Some(weak) = guard.get(id)
504 && let Some(context) = weak.upgrade()
505 {
506 return Some(context);
507 }
508 guard.remove(id);
509 None
510 }
511
512 async fn register_context(&self, context: &Arc<BrowserContext>) {
513 let id = context.id().to_string();
514 self.browser_contexts
515 .lock()
516 .await
517 .insert(id, Arc::downgrade(context));
518 }
519
520 async fn unregister_context(&self, id: &str) {
521 self.browser_contexts.lock().await.remove(id);
522 }
523
524 pub async fn new_page(self: &Arc<Self>) -> Result<Arc<Page>> {
538 self.new_page_in_context(None).await
540 }
541
542 pub(crate) async fn new_page_in_context(&self, context_id: Option<&str>) -> Result<Arc<Page>> {
544 let (event_sender, event_receiver) = mpsc::channel(crate::DEFAULT_CHANNEL_CAPACITY);
545 let method = CreateTarget {
546 url: "about:blank".to_string(),
547 left: None,
548 top: None,
549 width: None,
550 height: None,
551 window_state: None,
552 browser_context_id: context_id.map(|s| s.to_string()),
553 enable_begin_frame_control: None,
554 new_window: None,
555 background: None,
556 for_tab: None,
557 hidden: None,
558 focus: Some(true),
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(
670 &self,
671 origin: Option<&str>,
672 permissions: &[PermissionType],
673 ) -> Result<()> {
674 for permission in permissions {
675 let name = serde_json::to_value(permission)
677 .ok()
678 .and_then(|v| v.as_str().map(|s| s.to_string()))
679 .unwrap_or_else(|| format!("{:?}", permission));
680
681 let method = SetPermission {
682 permission: PermissionDescriptor {
683 name,
684 sysex: None,
685 user_visible_only: None,
686 allow_without_sanitization: None,
687 allow_without_gesture: None,
688 pan_tilt_zoom: None,
689 },
690 origin: origin.map(|v| v.to_string()),
691 setting: PermissionSetting::Granted,
692 browser_context_id: Some(self.id.clone()),
693 embedded_origin: None,
694 };
695 let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
696 }
697 Ok(())
698 }
699
700 pub async fn apply_permission_grant(&self, grant: &PermissionGrant) -> Result<()> {
702 self.grant_permissions(grant.origin.as_deref(), &grant.permissions)
703 .await
704 }
705
706 pub async fn set_permission_override(&self, permission: &PermissionOverride) -> Result<()> {
708 let method = SetPermission {
709 permission: permission.descriptor.clone(),
710 setting: permission.setting.clone(),
711 origin: permission.origin.clone(),
712 embedded_origin: permission.embedded_origin.clone(),
713 browser_context_id: Some(self.id.clone()),
714 };
715 let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
716 Ok(())
717 }
718
719 pub async fn reset_permissions(&self) -> Result<()> {
721 let method = ResetPermissions {
722 browser_context_id: Some(self.id.clone()),
723 };
724 let _: ResetPermissionsReturnObject = self.browser.send_command(method, None).await?;
725 Ok(())
726 }
727
728 pub async fn set_download_behavior(&self, options: &DownloadOptions) -> Result<()> {
730 let download_path = options
731 .download_path
732 .as_ref()
733 .map(|path| path.to_string_lossy().into_owned());
734 let method = SetDownloadBehavior {
735 behavior: options.behavior.as_cdp_behavior(),
736 browser_context_id: Some(self.id.clone()),
737 download_path,
738 events_enabled: options.events_enabled,
739 };
740 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
741 Ok(())
742 }
743
744 pub async fn clear_download_behavior(&self) -> Result<()> {
746 let method = SetDownloadBehavior {
747 behavior: SetDownloadBehaviorBehaviorOption::Default,
748 browser_context_id: Some(self.id.clone()),
749 download_path: None,
750 events_enabled: None,
751 };
752 let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
753 Ok(())
754 }
755
756 pub async fn set_emulation_config(&self, config: EmulationConfig) -> Result<()> {
758 let pages = {
759 let mut state = self.state.lock().await;
760 state.emulation_config = Some(config.clone());
761 let mut pages = Vec::new();
762 state.pages.retain(|weak| match weak.upgrade() {
763 Some(page) => {
764 pages.push(page);
765 true
766 }
767 None => false,
768 });
769 pages
770 };
771
772 for page in pages {
773 page.emulation().apply_config(&config).await?;
774 }
775
776 Ok(())
777 }
778
779 pub async fn clear_emulation_config(&self) -> Result<()> {
781 let (pages, previous) = {
782 let mut state = self.state.lock().await;
783 let previous = state.emulation_config.take();
784 let mut pages = Vec::new();
785 state.pages.retain(|weak| match weak.upgrade() {
786 Some(page) => {
787 pages.push(page);
788 true
789 }
790 None => false,
791 });
792 (pages, previous)
793 };
794
795 if let Some(config) = previous {
796 for page in pages {
797 let controller = page.emulation();
798 if config.geolocation.is_some() {
799 controller.clear_geolocation().await?;
800 }
801 if config.timezone_id.is_some() {
802 controller.reset_timezone().await?;
803 }
804 if config.locale.is_some() {
805 controller.set_locale(None).await?;
806 }
807 if config.media.is_some() {
808 controller.clear_media().await?;
809 }
810 }
812 }
813
814 Ok(())
815 }
816
817 pub async fn close(&self) -> Result<()> {
819 {
820 let mut state = self.state.lock().await;
821 if state.closed {
822 return Ok(());
823 }
824 state.closed = true;
825 }
826
827 let method = DisposeBrowserContext {
828 browser_context_id: self.id.clone(),
829 };
830 let result: Result<Value> = self.browser.send_command(method, None).await;
831
832 if let Err(err) = result {
833 let mut state = self.state.lock().await;
834 state.closed = false;
835 return Err(err);
836 }
837
838 {
839 let mut state = self.state.lock().await;
840 state.pages.clear();
841 state.emulation_config = None;
842 }
843
844 self.browser.unregister_context(&self.id).await;
845 Ok(())
846 }
847
848 async fn register_page(&self, page: &Arc<Page>) {
849 let mut state = self.state.lock().await;
850 state.pages.retain(|weak| weak.upgrade().is_some());
851 let weak = Arc::downgrade(page);
852 if !state
853 .pages
854 .iter()
855 .any(|existing| Weak::ptr_eq(existing, &weak))
856 {
857 state.pages.push(weak);
858 }
859 }
860
861 async fn apply_emulation_to_page(&self, page: &Arc<Page>) -> Result<()> {
862 let config = {
863 let state = self.state.lock().await;
864 state.emulation_config.clone()
865 };
866
867 if let Some(config) = config {
868 page.emulation().apply_config(&config).await?;
869 }
870
871 Ok(())
872 }
873}