use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use dpi::PhysicalSize;
use servo::{
AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
DeviceIntSize, EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, LoadStatus,
NavigationRequest, PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, TraversalId,
WebView, WebViewDelegate,
};
use bao_cdp::servo_bridge::main_frame_id_for_target;
use bao_cdp::{BaoEvent, ConsoleMessage};
use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServiceWorkerRegistrationId {
pub script_url: String,
pub scope: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceWorkerRegistrationState {
Idle,
Installing,
Installed,
Activating,
Activated,
Redundant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceWorkerFetchInterceptMode {
None,
Intercepting,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerHandle {
pub script_url: String,
pub scope: String,
pub closing: Arc<AtomicBool>,
pub terminated: Arc<AtomicBool>,
pub state: Arc<std::sync::Mutex<ServiceWorkerRegistrationState>>,
pub fetch_intercept_mode: Arc<std::sync::Mutex<ServiceWorkerFetchInterceptMode>>,
pub stealth_profile: Option<bao_stealth::StealthProfile>,
}
impl ServiceWorkerHandle {
pub fn new(
script_url: String,
scope: String,
stealth_profile: Option<bao_stealth::StealthProfile>,
) -> Self {
ServiceWorkerHandle {
script_url,
scope,
closing: Arc::new(AtomicBool::new(false)),
terminated: Arc::new(AtomicBool::new(false)),
state: Arc::new(std::sync::Mutex::new(
ServiceWorkerRegistrationState::Installing,
)),
fetch_intercept_mode: Arc::new(std::sync::Mutex::new(
ServiceWorkerFetchInterceptMode::None,
)),
stealth_profile,
}
}
pub fn id(&self) -> ServiceWorkerRegistrationId {
ServiceWorkerRegistrationId {
script_url: self.script_url.clone(),
scope: self.scope.clone(),
}
}
pub fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
pub fn is_terminated(&self) -> bool {
self.terminated.load(Ordering::Acquire)
}
pub fn registration_state(&self) -> ServiceWorkerRegistrationState {
self.state
.lock()
.expect("ServiceWorkerHandle state lock poisoned")
.clone()
}
pub fn fetch_intercept_mode(&self) -> ServiceWorkerFetchInterceptMode {
self.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned")
.clone()
}
pub fn is_intercepting_fetch(&self) -> bool {
matches!(
self.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
)
}
pub fn transition_state(&self, new_state: ServiceWorkerRegistrationState) {
let mut state = self
.state
.lock()
.expect("ServiceWorkerHandle state lock poisoned");
*state = new_state;
}
pub fn enable_fetch_interception(&self) {
let mut mode = self
.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
*mode = ServiceWorkerFetchInterceptMode::Intercepting;
}
pub fn disable_fetch_interception(&self) {
let mut mode = self
.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
*mode = ServiceWorkerFetchInterceptMode::None;
}
pub fn terminate(&self) {
self.closing.store(true, Ordering::Release);
self.disable_fetch_interception();
}
pub fn mark_terminated(&self) {
self.terminated.store(true, Ordering::Release);
}
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerRegistrationTracking {
pub registration_id: ServiceWorkerRegistrationId,
pub state: ServiceWorkerRegistrationState,
pub fetch_intercept_active: bool,
pub registering_page_url: String,
pub has_fetch_handler: bool,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerGlobalScopeState {
pub scope: WorkerGlobalScopeState,
pub registration_id: ServiceWorkerRegistrationId,
pub has_fetch_handler: bool,
pub has_activate_handler: bool,
pub has_install_handler: bool,
pub has_message_handler: bool,
pub scope_url: String,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerScopeConfig {
pub stealth_profile: Option<bao_stealth::StealthProfile>,
pub user_agent: String,
pub platform: String,
pub hardware_concurrency: usize,
pub language: String,
pub languages: Vec<String>,
pub registering_page_url: String,
}
impl Default for ServiceWorkerScopeConfig {
fn default() -> Self {
ServiceWorkerScopeConfig {
stealth_profile: None,
user_agent: String::new(),
platform: String::new(),
hardware_concurrency: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
registering_page_url: String::new(),
}
}
}
impl From<&bao_stealth::StealthProfile> for ServiceWorkerScopeConfig {
fn from(profile: &bao_stealth::StealthProfile) -> Self {
ServiceWorkerScopeConfig {
stealth_profile: Some(profile.clone()),
user_agent: profile.navigator.user_agent.clone(),
platform: profile.navigator.platform.clone(),
hardware_concurrency: profile.navigator.hardware_concurrency as usize,
language: profile.navigator.language.clone(),
languages: profile.navigator.languages.clone(),
registering_page_url: String::new(),
}
}
}
impl WorkerGlobalScopeState {
pub fn new_service(worker_url: String, config: &ServiceWorkerScopeConfig) -> Self {
Self::from_scope_config(worker_url, config)
}
}
impl ServiceWorkerGlobalScopeState {
pub fn new(
registration_id: ServiceWorkerRegistrationId,
config: &ServiceWorkerScopeConfig,
) -> Self {
let worker_url = registration_id.script_url.clone();
let scope_url = registration_id.scope.clone();
ServiceWorkerGlobalScopeState {
scope: WorkerGlobalScopeState::new_service(worker_url, config),
registration_id,
has_fetch_handler: false,
has_activate_handler: false,
has_install_handler: false,
has_message_handler: false,
scope_url,
}
}
pub fn location(&self) -> Option<&WorkerLocation> {
self.scope.location.as_ref()
}
pub fn navigator(&self) -> &WorkerNavigator {
&self.scope.navigator
}
pub fn set_fetch_handler(&mut self) {
self.has_fetch_handler = true;
}
pub fn set_activate_handler(&mut self) {
self.has_activate_handler = true;
}
pub fn set_install_handler(&mut self) {
self.has_install_handler = true;
}
pub fn set_message_handler(&mut self) {
self.has_message_handler = true;
}
pub fn is_url_in_scope(&self, url: &str) -> bool {
url.starts_with(&self.scope_url)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_worker_registration_id_equality() {
let id1 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let id2 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let id3 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_service_worker_handle_lifecycle() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert!(!handle.is_closing());
assert!(!handle.is_terminated());
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installing
);
assert!(!handle.is_intercepting_fetch());
}
#[test]
fn test_service_worker_handle_state_transitions() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
handle.transition_state(ServiceWorkerRegistrationState::Installed);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installed
);
handle.transition_state(ServiceWorkerRegistrationState::Activating);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activating
);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activated
);
assert!(handle.is_intercepting_fetch());
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
);
}
#[test]
fn test_service_worker_handle_terminate_disables_interception() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
handle.enable_fetch_interception();
assert!(handle.is_intercepting_fetch());
handle.terminate();
assert!(handle.is_closing());
assert!(!handle.is_intercepting_fetch());
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
}
#[test]
fn test_service_worker_scope_config_from_stealth_profile() {
let profile = bao_stealth::StealthProfile::chrome_default();
let config = ServiceWorkerScopeConfig::from(&profile);
assert!(config.stealth_profile.is_some());
assert_eq!(config.user_agent, profile.navigator.user_agent);
assert_eq!(config.platform, profile.navigator.platform);
assert_eq!(
config.hardware_concurrency,
profile.navigator.hardware_concurrency as usize
);
assert_eq!(config.language, profile.navigator.language);
}
#[test]
fn test_service_worker_global_scope_state() {
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id.clone(), &config);
assert!(!scope.has_fetch_handler);
assert!(!scope.has_activate_handler);
assert!(!scope.has_install_handler);
assert!(!scope.has_message_handler);
assert_eq!(scope.scope_url, "/app/");
assert!(scope.is_url_in_scope("/app/page1"));
assert!(scope.is_url_in_scope("/app/sub/page2"));
assert!(!scope.is_url_in_scope("/other/page"));
}
#[test]
fn test_service_worker_global_scope_fetch_handler() {
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let mut scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
scope.set_fetch_handler();
assert!(scope.has_fetch_handler);
assert!(scope.is_url_in_scope("/anything"));
}
#[test]
fn test_webview_state_service_worker_control() {
let mut state = BaoWebViewState::default();
assert!(!state.is_controlled_by_service_worker());
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
state.set_controlling_service_worker(handle);
assert!(state.is_controlled_by_service_worker());
state.clear_controlling_service_worker();
assert!(!state.is_controlled_by_service_worker());
}
#[test]
fn test_webview_state_service_worker_scope_matching() {
let mut state = BaoWebViewState::default();
assert!(!state.is_url_in_service_worker_scope("/app/page1"));
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/app/".to_string(), None);
state.set_controlling_service_worker(handle);
state.register_service_worker_scope(scope);
assert!(state.is_url_in_service_worker_scope("/app/page1"));
assert!(state.is_url_in_service_worker_scope("/app/sub/page2"));
assert!(!state.is_url_in_service_worker_scope("/other/page"));
}
#[test]
fn test_delegate_service_worker_registration() {
let delegate = BaoServoDelegate::new();
assert_eq!(delegate.service_worker_count(), 0);
let (handle, is_new) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert!(is_new);
assert_eq!(delegate.service_worker_count(), 1);
let (handle2, is_new2) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert!(!is_new2);
assert_eq!(delegate.service_worker_count(), 1);
}
#[test]
fn test_delegate_find_service_worker_for_url() {
let delegate = BaoServoDelegate::new();
let handle = delegate
.get_or_create_service_worker("sw.js", "/app/", None)
.0;
assert!(delegate.find_service_worker_for_url("/app/page1").is_none());
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
let found = delegate.find_service_worker_for_url("/app/page1");
assert!(found.is_some());
assert_eq!(found.unwrap().script_url, "sw.js");
assert!(delegate
.find_service_worker_for_url("/other/page")
.is_none());
}
#[test]
fn test_delegate_service_worker_unregistration() {
let delegate = BaoServoDelegate::new();
let (handle, _) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert_eq!(delegate.service_worker_count(), 1);
let id = handle.id();
assert!(delegate.unregister_service_worker(&id));
assert_eq!(delegate.service_worker_count(), 0);
assert!(!delegate.unregister_service_worker(&id));
}
#[test]
fn test_delegate_service_worker_stealth_consistency_no_violations() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let handle =
ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
delegate.register_service_worker(handle);
let all = delegate.all_service_workers();
all[0].transition_state(ServiceWorkerRegistrationState::Activated);
all[0].enable_fetch_interception();
let violations = delegate.verify_service_worker_stealth_consistency(&profile);
assert!(violations.is_empty());
}
#[test]
fn test_delegate_service_worker_stealth_consistency_violation_no_profile() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
delegate.register_service_worker(handle);
let all = delegate.all_service_workers();
all[0].transition_state(ServiceWorkerRegistrationState::Activated);
all[0].enable_fetch_interception();
let violations = delegate.verify_service_worker_stealth_consistency(&profile);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_service_worker_persistent_lifecycle_across_page_navigation() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let (handle, is_new) =
delegate.get_or_create_service_worker("sw.js", "/", Some(profile.clone()));
assert!(is_new);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
let mut page_state = BaoWebViewState::default();
page_state.set_controlling_service_worker(handle.clone());
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::from(&profile);
page_state
.register_service_worker_scope(ServiceWorkerGlobalScopeState::new(reg_id, &config));
assert!(page_state.is_controlled_by_service_worker());
page_state.clear_controlling_service_worker();
assert!(!page_state.is_controlled_by_service_worker());
assert_eq!(delegate.service_worker_count(), 1);
assert!(delegate.find_service_worker_for_url("/page2").is_some());
let mut page2_state = BaoWebViewState::default();
page2_state.set_controlling_service_worker(handle.clone());
assert!(page2_state.is_controlled_by_service_worker());
}
#[test]
fn test_service_worker_fetch_intercept_mode() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
handle.enable_fetch_interception();
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
);
handle.disable_fetch_interception();
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
}
#[test]
fn test_service_worker_registration_state_all_transitions() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installing
);
handle.transition_state(ServiceWorkerRegistrationState::Installed);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installed
);
handle.transition_state(ServiceWorkerRegistrationState::Activating);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activating
);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activated
);
handle.transition_state(ServiceWorkerRegistrationState::Redundant);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Redundant
);
}
#[test]
fn test_service_worker_navigator_from_scope_config() {
let config = ServiceWorkerScopeConfig {
stealth_profile: None,
user_agent: "Mozilla/5.0 Test".to_string(),
platform: "Linux x86_64".to_string(),
hardware_concurrency: 4,
language: "zh-CN".to_string(),
languages: vec!["zh-CN".to_string(), "zh".to_string()],
registering_page_url: "https://example.com/".to_string(),
};
let nav = WorkerNavigator::from_scope_config(&config);
assert_eq!(nav.user_agent, "Mozilla/5.0 Test");
assert_eq!(nav.platform, "Linux x86_64");
assert_eq!(nav.hardware_concurrency, 4);
assert_eq!(nav.language, "zh-CN");
assert_eq!(nav.languages, vec!["zh-CN".to_string(), "zh".to_string()]);
}
#[test]
fn test_webview_state_service_worker_scope_config() {
let mut state = BaoWebViewState::default();
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
state.set_controlling_service_worker(handle);
state.register_service_worker_scope(scope);
let new_config = ServiceWorkerScopeConfig {
user_agent: "Updated Agent".to_string(),
..ServiceWorkerScopeConfig::default()
};
state.set_service_worker_scope_config(&new_config);
assert_eq!(
state.service_worker_scope().unwrap().navigator().user_agent,
"Updated Agent"
);
}
}