mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 08:01:00 +00:00
1015 lines
36 KiB
Rust
1015 lines
36 KiB
Rust
#[cfg(test)]
|
|
use std::path::PathBuf;
|
|
use std::{
|
|
net::SocketAddr,
|
|
sync::{Arc, atomic::AtomicU64},
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use anyhow::{Context, anyhow};
|
|
use axum::{
|
|
Router,
|
|
body::Body,
|
|
http::{HeaderValue, Request, header},
|
|
middleware::{self, Next},
|
|
response::Response,
|
|
routing::{any, get, post, put},
|
|
serve::ListenerExt,
|
|
};
|
|
use moli_cookie_jar::StoredCookie;
|
|
use moli_core::{
|
|
LayoutConfiguration, LayoutPolicy, OptionalResourceFetchMask,
|
|
runtime::{NavigationRuntimeConfig, storage_partition::StoragePartitionState},
|
|
};
|
|
use moli_fetch::FetchConfig;
|
|
#[cfg(test)]
|
|
use moli_protocol::DEFAULT_CDP_TAB_TARGET_ID;
|
|
use moli_protocol::{CdpInitialStoragePartition, DEFAULT_CDP_PAGE_TARGET_ID};
|
|
#[cfg(test)]
|
|
use parking_lot::Mutex;
|
|
use tokio::net::TcpListener;
|
|
use tracing::info;
|
|
|
|
#[cfg(test)]
|
|
use moli_cookie_cache as cookie_cache;
|
|
|
|
pub use crate::config::ServerConfig;
|
|
|
|
mod cdp;
|
|
mod cdp_agent_host;
|
|
mod cdp_owner;
|
|
mod cdp_socket;
|
|
mod protocol_local_executor;
|
|
mod tcp_options;
|
|
mod webdriver_bidi;
|
|
mod webdriver_classic;
|
|
mod webdriver_files;
|
|
|
|
use cdp_agent_host::SharedCdpAgentHostDirectory;
|
|
use cdp_owner::SharedCdpOwnerRegistry;
|
|
use webdriver_bidi::SharedBidiSessionRegistry;
|
|
use webdriver_classic::SharedClassicSessionRegistry;
|
|
|
|
const DEFAULT_BROWSER_ID: &str = "moli-browser";
|
|
const DEFAULT_TARGET_ID: &str = DEFAULT_CDP_PAGE_TARGET_ID;
|
|
#[cfg(test)]
|
|
const DEFAULT_TAB_TARGET_ID: &str = DEFAULT_CDP_TAB_TARGET_ID;
|
|
const DEFAULT_TARGET_URL: &str = "about:blank";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ProtocolServer {
|
|
config: ServerConfig,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
}
|
|
|
|
impl ProtocolServer {
|
|
pub fn new(config: ServerConfig) -> Self {
|
|
Self::new_with_initial_cookies(config, Vec::new())
|
|
}
|
|
|
|
pub fn new_with_initial_cookies(
|
|
config: ServerConfig,
|
|
initial_cookies: Vec<StoredCookie>,
|
|
) -> Self {
|
|
let storage_partition = Arc::new(
|
|
StoragePartitionState::open(None).expect("in-memory storage partition should open"),
|
|
);
|
|
storage_partition
|
|
.import_cookies(initial_cookies)
|
|
.expect("in-memory cookie import should succeed");
|
|
Self::new_with_storage_partition(config, storage_partition)
|
|
}
|
|
|
|
pub fn new_with_storage_partition(
|
|
config: ServerConfig,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
) -> Self {
|
|
Self::new_with_storage_partition_fetch_config_and_resource_loading(
|
|
config,
|
|
storage_partition,
|
|
FetchConfig::default(),
|
|
OptionalResourceFetchMask::NONE,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn new_with_storage_partition_fetch_config_and_resource_loading(
|
|
config: ServerConfig,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
fetch_config: FetchConfig,
|
|
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
|
subframe_loading_enabled: bool,
|
|
) -> Self {
|
|
Self::new_with_storage_partition_and_runtime_config(
|
|
config,
|
|
storage_partition,
|
|
NavigationRuntimeConfig::new(
|
|
fetch_config,
|
|
optional_resource_fetch_mask,
|
|
subframe_loading_enabled,
|
|
LayoutPolicy::default(),
|
|
),
|
|
)
|
|
}
|
|
|
|
pub fn new_with_storage_partition_and_runtime_config(
|
|
config: ServerConfig,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
mut navigation_runtime_config: NavigationRuntimeConfig,
|
|
) -> Self {
|
|
if navigation_runtime_config
|
|
.fetch_config()
|
|
.http_cache_dir()
|
|
.is_none()
|
|
&& let Some(http_cache_root) = storage_partition.http_cache_root()
|
|
{
|
|
navigation_runtime_config
|
|
.fetch_config_mut()
|
|
.set_http_cache_dir(Some(http_cache_root.display().to_string()));
|
|
}
|
|
Self {
|
|
config,
|
|
storage_partition,
|
|
navigation_runtime_config,
|
|
}
|
|
}
|
|
|
|
pub async fn serve(&self) -> Result<()> {
|
|
let listener = TcpListener::bind(self.config.bind_target())
|
|
.await
|
|
.with_context(|| {
|
|
anyhow!(
|
|
"failed to bind protocol server to {}:{}",
|
|
self.config.host,
|
|
self.config.port
|
|
)
|
|
})?;
|
|
let addr = listener
|
|
.local_addr()
|
|
.context("failed to read bound protocol socket address")?;
|
|
|
|
info!(%addr, "protocol server listening");
|
|
|
|
let app_state = AppState::new_with_storage_partition_and_runtime_config(
|
|
addr,
|
|
self.storage_partition.clone(),
|
|
self.navigation_runtime_config.clone(),
|
|
self.config.screencast_interval_ms,
|
|
)?;
|
|
|
|
let cdp_owner_registry = app_state.cdp_owner_registry.clone();
|
|
let app = build_router(app_state);
|
|
|
|
let listener = listener.tap_io(|tcp_stream| {
|
|
tcp_options::configure_accepted_protocol_stream(tcp_stream);
|
|
});
|
|
let result = axum::serve(listener, app).await;
|
|
cdp_owner_registry.shutdown().await;
|
|
result.context("protocol server failed")
|
|
}
|
|
}
|
|
|
|
fn build_router(app_state: AppState) -> Router {
|
|
Router::new()
|
|
.route("/status", get(webdriver_classic::webdriver_classic_status))
|
|
.route("/status/", get(webdriver_classic::webdriver_classic_status))
|
|
.route("/json", any(cdp::json_list))
|
|
.route("/json/list", any(cdp::json_list))
|
|
.route("/json/list/", any(cdp::json_list))
|
|
.route("/json/version", get(cdp::json_version))
|
|
.route("/json/version/", get(cdp::json_version))
|
|
.route("/json/protocol", get(cdp::json_protocol))
|
|
.route("/json/protocol/", get(cdp::json_protocol))
|
|
.route("/json/new", put(cdp::json_new_target))
|
|
.route("/json/new/", put(cdp::json_new_target))
|
|
.route("/json/activate/{target_id}", get(cdp::json_activate_target))
|
|
.route(
|
|
"/json/activate/{target_id}/",
|
|
get(cdp::json_activate_target),
|
|
)
|
|
.route("/json/close/{target_id}", get(cdp::json_close_target))
|
|
.route("/json/close/{target_id}/", get(cdp::json_close_target))
|
|
.route(
|
|
"/devtools/browser/{browser_id}",
|
|
get(cdp::ws_browser_upgrade_handler),
|
|
)
|
|
.route(
|
|
"/devtools/browser/{browser_id}/",
|
|
get(cdp::ws_browser_upgrade_handler),
|
|
)
|
|
.route(
|
|
"/devtools/page/{target_id}",
|
|
get(cdp::ws_target_upgrade_handler),
|
|
)
|
|
.route(
|
|
"/devtools/page/{target_id}/",
|
|
get(cdp::ws_target_upgrade_handler),
|
|
)
|
|
.route(
|
|
"/session",
|
|
get(webdriver_bidi::ws_bidi_session_upgrade_handler)
|
|
.post(webdriver_classic::webdriver_classic_new_session),
|
|
)
|
|
.route(
|
|
"/session/",
|
|
get(webdriver_bidi::ws_bidi_session_upgrade_handler)
|
|
.post(webdriver_classic::webdriver_classic_new_session),
|
|
)
|
|
.route(
|
|
"/session/{session_id}",
|
|
get(webdriver_bidi::ws_bidi_existing_session_upgrade_handler)
|
|
.delete(webdriver_classic::webdriver_classic_delete_session),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/",
|
|
get(webdriver_bidi::ws_bidi_existing_session_upgrade_handler)
|
|
.delete(webdriver_classic::webdriver_classic_delete_session),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/url",
|
|
get(webdriver_classic::webdriver_classic_get_url)
|
|
.post(webdriver_classic::webdriver_classic_navigate),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/url/",
|
|
get(webdriver_classic::webdriver_classic_get_url)
|
|
.post(webdriver_classic::webdriver_classic_navigate),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/title",
|
|
get(webdriver_classic::webdriver_classic_get_title),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/title/",
|
|
get(webdriver_classic::webdriver_classic_get_title),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/timeouts",
|
|
get(webdriver_classic::webdriver_classic_get_timeouts)
|
|
.post(webdriver_classic::webdriver_classic_set_timeouts),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/timeouts/",
|
|
get(webdriver_classic::webdriver_classic_get_timeouts)
|
|
.post(webdriver_classic::webdriver_classic_set_timeouts),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/source",
|
|
get(webdriver_classic::webdriver_classic_get_source),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/source/",
|
|
get(webdriver_classic::webdriver_classic_get_source),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/screenshot",
|
|
get(webdriver_classic::webdriver_classic_take_screenshot),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/screenshot/",
|
|
get(webdriver_classic::webdriver_classic_take_screenshot),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/print",
|
|
post(webdriver_classic::webdriver_classic_print_page),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/print/",
|
|
post(webdriver_classic::webdriver_classic_print_page),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/file",
|
|
post(webdriver_classic::webdriver_classic_upload_file),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/file/",
|
|
post(webdriver_classic::webdriver_classic_upload_file),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/se/file",
|
|
post(webdriver_classic::webdriver_classic_upload_file),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/se/file/",
|
|
post(webdriver_classic::webdriver_classic_upload_file),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/se/files",
|
|
get(webdriver_classic::webdriver_classic_get_downloadable_files)
|
|
.post(webdriver_classic::webdriver_classic_download_file)
|
|
.delete(webdriver_classic::webdriver_classic_delete_downloadable_files),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/se/files/",
|
|
get(webdriver_classic::webdriver_classic_get_downloadable_files)
|
|
.post(webdriver_classic::webdriver_classic_download_file)
|
|
.delete(webdriver_classic::webdriver_classic_delete_downloadable_files),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/moli/service-workers",
|
|
get(webdriver_classic::webdriver_classic_get_service_workers),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/moli/service-workers/",
|
|
get(webdriver_classic::webdriver_classic_get_service_workers),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window",
|
|
get(webdriver_classic::webdriver_classic_get_window)
|
|
.post(webdriver_classic::webdriver_classic_switch_window)
|
|
.delete(webdriver_classic::webdriver_classic_close_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/",
|
|
get(webdriver_classic::webdriver_classic_get_window)
|
|
.post(webdriver_classic::webdriver_classic_switch_window)
|
|
.delete(webdriver_classic::webdriver_classic_close_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/handles",
|
|
get(webdriver_classic::webdriver_classic_get_window_handles),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/handles/",
|
|
get(webdriver_classic::webdriver_classic_get_window_handles),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/rect",
|
|
get(webdriver_classic::webdriver_classic_get_window_rect)
|
|
.post(webdriver_classic::webdriver_classic_set_window_rect),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/rect/",
|
|
get(webdriver_classic::webdriver_classic_get_window_rect)
|
|
.post(webdriver_classic::webdriver_classic_set_window_rect),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/maximize",
|
|
post(webdriver_classic::webdriver_classic_maximize_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/maximize/",
|
|
post(webdriver_classic::webdriver_classic_maximize_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/minimize",
|
|
post(webdriver_classic::webdriver_classic_minimize_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/minimize/",
|
|
post(webdriver_classic::webdriver_classic_minimize_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/fullscreen",
|
|
post(webdriver_classic::webdriver_classic_fullscreen_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/fullscreen/",
|
|
post(webdriver_classic::webdriver_classic_fullscreen_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/new",
|
|
post(webdriver_classic::webdriver_classic_new_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/window/new/",
|
|
post(webdriver_classic::webdriver_classic_new_window),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/frame",
|
|
post(webdriver_classic::webdriver_classic_switch_frame),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/frame/",
|
|
post(webdriver_classic::webdriver_classic_switch_frame),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/frame/parent",
|
|
post(webdriver_classic::webdriver_classic_switch_parent_frame),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/frame/parent/",
|
|
post(webdriver_classic::webdriver_classic_switch_parent_frame),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/text",
|
|
get(webdriver_classic::webdriver_classic_get_alert_text)
|
|
.post(webdriver_classic::webdriver_classic_send_alert_text),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/text/",
|
|
get(webdriver_classic::webdriver_classic_get_alert_text)
|
|
.post(webdriver_classic::webdriver_classic_send_alert_text),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/accept",
|
|
post(webdriver_classic::webdriver_classic_accept_alert),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/accept/",
|
|
post(webdriver_classic::webdriver_classic_accept_alert),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/dismiss",
|
|
post(webdriver_classic::webdriver_classic_dismiss_alert),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/alert/dismiss/",
|
|
post(webdriver_classic::webdriver_classic_dismiss_alert),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/refresh",
|
|
post(webdriver_classic::webdriver_classic_refresh),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/refresh/",
|
|
post(webdriver_classic::webdriver_classic_refresh),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/back",
|
|
post(webdriver_classic::webdriver_classic_back),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/back/",
|
|
post(webdriver_classic::webdriver_classic_back),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/forward",
|
|
post(webdriver_classic::webdriver_classic_forward),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/forward/",
|
|
post(webdriver_classic::webdriver_classic_forward),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/execute/sync",
|
|
post(webdriver_classic::webdriver_classic_execute_sync),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/execute/sync/",
|
|
post(webdriver_classic::webdriver_classic_execute_sync),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/execute/async",
|
|
post(webdriver_classic::webdriver_classic_execute_async),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/execute/async/",
|
|
post(webdriver_classic::webdriver_classic_execute_async),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element",
|
|
post(webdriver_classic::webdriver_classic_find_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/",
|
|
post(webdriver_classic::webdriver_classic_find_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/elements",
|
|
post(webdriver_classic::webdriver_classic_find_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/elements/",
|
|
post(webdriver_classic::webdriver_classic_find_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/active",
|
|
get(webdriver_classic::webdriver_classic_get_active_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/active/",
|
|
get(webdriver_classic::webdriver_classic_get_active_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/element",
|
|
post(webdriver_classic::webdriver_classic_find_child_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/element/",
|
|
post(webdriver_classic::webdriver_classic_find_child_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/elements",
|
|
post(webdriver_classic::webdriver_classic_find_child_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/elements/",
|
|
post(webdriver_classic::webdriver_classic_find_child_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/shadow",
|
|
get(webdriver_classic::webdriver_classic_get_element_shadow_root),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/shadow/",
|
|
get(webdriver_classic::webdriver_classic_get_element_shadow_root),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/shadow/{shadow_root_id}/element",
|
|
post(webdriver_classic::webdriver_classic_find_shadow_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/shadow/{shadow_root_id}/element/",
|
|
post(webdriver_classic::webdriver_classic_find_shadow_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/shadow/{shadow_root_id}/elements",
|
|
post(webdriver_classic::webdriver_classic_find_shadow_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/shadow/{shadow_root_id}/elements/",
|
|
post(webdriver_classic::webdriver_classic_find_shadow_elements),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/attribute/{name}",
|
|
get(webdriver_classic::webdriver_classic_get_element_attribute),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/attribute/{name}/",
|
|
get(webdriver_classic::webdriver_classic_get_element_attribute),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/text",
|
|
get(webdriver_classic::webdriver_classic_get_element_text),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/text/",
|
|
get(webdriver_classic::webdriver_classic_get_element_text),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/name",
|
|
get(webdriver_classic::webdriver_classic_get_element_tag_name),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/name/",
|
|
get(webdriver_classic::webdriver_classic_get_element_tag_name),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/equals/{other_element_id}",
|
|
get(webdriver_classic::webdriver_classic_element_equals),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/equals/{other_element_id}/",
|
|
get(webdriver_classic::webdriver_classic_element_equals),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/enabled",
|
|
get(webdriver_classic::webdriver_classic_is_element_enabled),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/enabled/",
|
|
get(webdriver_classic::webdriver_classic_is_element_enabled),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/displayed",
|
|
get(webdriver_classic::webdriver_classic_is_element_displayed),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/displayed/",
|
|
get(webdriver_classic::webdriver_classic_is_element_displayed),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/selected",
|
|
get(webdriver_classic::webdriver_classic_is_element_selected),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/selected/",
|
|
get(webdriver_classic::webdriver_classic_is_element_selected),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/rect",
|
|
get(webdriver_classic::webdriver_classic_get_element_rect),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/rect/",
|
|
get(webdriver_classic::webdriver_classic_get_element_rect),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/screenshot",
|
|
get(webdriver_classic::webdriver_classic_take_element_screenshot),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/screenshot/",
|
|
get(webdriver_classic::webdriver_classic_take_element_screenshot),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/css/{property_name}",
|
|
get(webdriver_classic::webdriver_classic_get_element_css_value),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/css/{property_name}/",
|
|
get(webdriver_classic::webdriver_classic_get_element_css_value),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/computedlabel",
|
|
get(webdriver_classic::webdriver_classic_get_element_computed_label),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/computedlabel/",
|
|
get(webdriver_classic::webdriver_classic_get_element_computed_label),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/computedrole",
|
|
get(webdriver_classic::webdriver_classic_get_element_computed_role),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/computedrole/",
|
|
get(webdriver_classic::webdriver_classic_get_element_computed_role),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/property/{name}",
|
|
get(webdriver_classic::webdriver_classic_get_element_property),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/property/{name}/",
|
|
get(webdriver_classic::webdriver_classic_get_element_property),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/clear",
|
|
post(webdriver_classic::webdriver_classic_clear_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/clear/",
|
|
post(webdriver_classic::webdriver_classic_clear_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/click",
|
|
post(webdriver_classic::webdriver_classic_click_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/click/",
|
|
post(webdriver_classic::webdriver_classic_click_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/value",
|
|
post(webdriver_classic::webdriver_classic_send_keys_to_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/element/{element_id}/value/",
|
|
post(webdriver_classic::webdriver_classic_send_keys_to_element),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/actions",
|
|
post(webdriver_classic::webdriver_classic_perform_actions)
|
|
.delete(webdriver_classic::webdriver_classic_release_actions),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/actions/",
|
|
post(webdriver_classic::webdriver_classic_perform_actions)
|
|
.delete(webdriver_classic::webdriver_classic_release_actions),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/cookie",
|
|
get(webdriver_classic::webdriver_classic_get_cookies)
|
|
.post(webdriver_classic::webdriver_classic_add_cookie)
|
|
.delete(webdriver_classic::webdriver_classic_delete_all_cookies),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/cookie/",
|
|
get(webdriver_classic::webdriver_classic_get_cookies)
|
|
.post(webdriver_classic::webdriver_classic_add_cookie)
|
|
.delete(webdriver_classic::webdriver_classic_delete_all_cookies),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/cookie/{name}",
|
|
get(webdriver_classic::webdriver_classic_get_named_cookie)
|
|
.delete(webdriver_classic::webdriver_classic_delete_cookie),
|
|
)
|
|
.route(
|
|
"/session/{session_id}/cookie/{name}/",
|
|
get(webdriver_classic::webdriver_classic_get_named_cookie)
|
|
.delete(webdriver_classic::webdriver_classic_delete_cookie),
|
|
)
|
|
.with_state(app_state)
|
|
.layer(middleware::from_fn(
|
|
classic_webdriver_response_headers_middleware,
|
|
))
|
|
}
|
|
|
|
async fn classic_webdriver_response_headers_middleware(
|
|
request: Request<Body>,
|
|
next: Next,
|
|
) -> Response {
|
|
let should_apply = is_classic_webdriver_http_path(request.uri().path())
|
|
&& !is_websocket_upgrade_request(&request);
|
|
let mut response = next.run(request).await;
|
|
if should_apply && is_classic_webdriver_json_response(&response) {
|
|
let headers = response.headers_mut();
|
|
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
|
headers.insert(
|
|
header::CONTENT_TYPE,
|
|
HeaderValue::from_static("application/json; charset=utf-8"),
|
|
);
|
|
}
|
|
response
|
|
}
|
|
|
|
fn is_classic_webdriver_json_response(response: &Response) -> bool {
|
|
response
|
|
.headers()
|
|
.get(header::CONTENT_TYPE)
|
|
.and_then(|value| value.to_str().ok())
|
|
.is_some_and(|value| {
|
|
value
|
|
.split_once(';')
|
|
.map_or(value, |(media_type, _)| media_type)
|
|
.trim()
|
|
.eq_ignore_ascii_case("application/json")
|
|
})
|
|
}
|
|
|
|
fn is_classic_webdriver_http_path(path: &str) -> bool {
|
|
path == "/status"
|
|
|| path == "/status/"
|
|
|| path == "/session"
|
|
|| path == "/session/"
|
|
|| path.starts_with("/session/")
|
|
}
|
|
|
|
fn is_websocket_upgrade_request(request: &Request<Body>) -> bool {
|
|
request
|
|
.headers()
|
|
.get(header::UPGRADE)
|
|
.is_some_and(|value| value.as_bytes().eq_ignore_ascii_case(b"websocket"))
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
browser_ws_url: String,
|
|
page_ws_url: String,
|
|
bidi_ws_url: String,
|
|
bidi_session_registry: SharedBidiSessionRegistry,
|
|
classic_session_registry: SharedClassicSessionRegistry,
|
|
cdp_agent_host_directory: SharedCdpAgentHostDirectory,
|
|
cdp_owner_registry: SharedCdpOwnerRegistry,
|
|
devtools_frontend_url: String,
|
|
cookie_profile: SharedCookieProfile,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
fetch_config: FetchConfig,
|
|
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
|
subframe_loading_enabled: bool,
|
|
layout_configuration: LayoutConfiguration,
|
|
}
|
|
|
|
impl AppState {
|
|
#[cfg(test)]
|
|
fn new_with_storage_partition(
|
|
addr: SocketAddr,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
fetch_config: FetchConfig,
|
|
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
|
subframe_loading_enabled: bool,
|
|
) -> anyhow::Result<Self> {
|
|
Self::new_with_storage_partition_and_runtime_config(
|
|
addr,
|
|
storage_partition,
|
|
NavigationRuntimeConfig::new(
|
|
fetch_config,
|
|
optional_resource_fetch_mask,
|
|
subframe_loading_enabled,
|
|
LayoutPolicy::default(),
|
|
),
|
|
crate::config::DEFAULT_SCREENCAST_INTERVAL_MS,
|
|
)
|
|
}
|
|
|
|
fn new_with_storage_partition_and_runtime_config(
|
|
addr: SocketAddr,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
screencast_interval_ms: u32,
|
|
) -> anyhow::Result<Self> {
|
|
Ok(Self::from_parts(
|
|
addr,
|
|
SharedCookieProfile::from_storage_partition(storage_partition.clone()),
|
|
storage_partition,
|
|
navigation_runtime_config,
|
|
screencast_interval_ms,
|
|
))
|
|
}
|
|
|
|
fn from_parts(
|
|
addr: SocketAddr,
|
|
cookie_profile: SharedCookieProfile,
|
|
storage_partition: Arc<StoragePartitionState>,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
screencast_interval_ms: u32,
|
|
) -> Self {
|
|
let cdp_agent_host_directory = SharedCdpAgentHostDirectory::default();
|
|
let cdp_target_id_allocator = Arc::new(AtomicU64::new(0));
|
|
let cdp_tab_target_id_allocator = Arc::new(AtomicU64::new(0));
|
|
let cdp_owner_registry = SharedCdpOwnerRegistry::new(
|
|
cdp_agent_host_directory.clone(),
|
|
cdp_target_id_allocator,
|
|
cdp_tab_target_id_allocator,
|
|
cookie_profile.clone(),
|
|
storage_partition.clone(),
|
|
navigation_runtime_config.clone(),
|
|
screencast_interval_ms,
|
|
);
|
|
Self {
|
|
browser_ws_url: format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}"),
|
|
page_ws_url: format!("ws://{addr}/devtools/page/{DEFAULT_TARGET_ID}"),
|
|
bidi_ws_url: format!("ws://{addr}/session"),
|
|
bidi_session_registry: SharedBidiSessionRegistry::default(),
|
|
classic_session_registry: SharedClassicSessionRegistry::default(),
|
|
cdp_agent_host_directory,
|
|
cdp_owner_registry,
|
|
devtools_frontend_url: format!(
|
|
"/devtools/inspector.html?ws={addr}/devtools/page/{DEFAULT_TARGET_ID}"
|
|
),
|
|
cookie_profile,
|
|
storage_partition,
|
|
fetch_config: navigation_runtime_config.fetch_config().clone(),
|
|
optional_resource_fetch_mask: navigation_runtime_config.optional_resource_fetch_mask(),
|
|
subframe_loading_enabled: navigation_runtime_config.subframe_loading_enabled(),
|
|
layout_configuration: navigation_runtime_config.layout_configuration(),
|
|
}
|
|
}
|
|
|
|
fn initial_storage_partition(
|
|
&self,
|
|
initial_cookies: Vec<StoredCookie>,
|
|
) -> CdpInitialStoragePartition {
|
|
CdpInitialStoragePartition::from_storage_partition(
|
|
initial_cookies,
|
|
self.storage_partition.as_ref(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct SharedCookieProfile {
|
|
backing: SharedCookieProfileBacking,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum SharedCookieProfileBacking {
|
|
#[cfg(test)]
|
|
Legacy(Arc<Mutex<CookieProfile>>),
|
|
StoragePartition(Arc<StoragePartitionState>),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
struct CookieProfileCommit {
|
|
initial_cookies: Vec<StoredCookie>,
|
|
final_cookies: Option<Vec<StoredCookie>>,
|
|
}
|
|
|
|
impl CookieProfileCommit {
|
|
fn new(initial_cookies: Vec<StoredCookie>, final_cookies: Vec<StoredCookie>) -> Self {
|
|
Self {
|
|
initial_cookies,
|
|
final_cookies: Some(final_cookies),
|
|
}
|
|
}
|
|
|
|
fn unchanged() -> Self {
|
|
Self {
|
|
initial_cookies: Vec::new(),
|
|
final_cookies: None,
|
|
}
|
|
}
|
|
|
|
fn from_optional_profile_backed_snapshot(
|
|
initial_cookies: Vec<StoredCookie>,
|
|
final_cookies: Option<Vec<StoredCookie>>,
|
|
) -> Self {
|
|
match final_cookies {
|
|
Some(final_cookies) => Self::new(initial_cookies, final_cookies),
|
|
None => Self::unchanged(),
|
|
}
|
|
}
|
|
|
|
fn is_unchanged(&self) -> bool {
|
|
self.final_cookies.is_none()
|
|
}
|
|
}
|
|
|
|
impl SharedCookieProfile {
|
|
#[cfg(test)]
|
|
fn new(initial_cookies: Vec<StoredCookie>, cache_paths: Vec<PathBuf>) -> Self {
|
|
Self {
|
|
backing: SharedCookieProfileBacking::Legacy(Arc::new(Mutex::new(CookieProfile {
|
|
cookies: initial_cookies,
|
|
cache_paths,
|
|
}))),
|
|
}
|
|
}
|
|
|
|
fn from_storage_partition(storage_partition: Arc<StoragePartitionState>) -> Self {
|
|
Self {
|
|
backing: SharedCookieProfileBacking::StoragePartition(storage_partition),
|
|
}
|
|
}
|
|
|
|
fn snapshot(&self) -> Vec<StoredCookie> {
|
|
match &self.backing {
|
|
#[cfg(test)]
|
|
SharedCookieProfileBacking::Legacy(inner) => inner.lock().cookies.clone(),
|
|
SharedCookieProfileBacking::StoragePartition(storage_partition) => storage_partition
|
|
.cookies()
|
|
.expect("storage partition cookie snapshot should succeed"),
|
|
}
|
|
}
|
|
|
|
fn commit_and_save(&self, commit: CookieProfileCommit) -> Result<()> {
|
|
if commit.is_unchanged() {
|
|
return Ok(());
|
|
}
|
|
|
|
match &self.backing {
|
|
#[cfg(test)]
|
|
SharedCookieProfileBacking::Legacy(inner) => {
|
|
let (cookies, cache_paths) = {
|
|
let mut profile = inner.lock();
|
|
commit_cookie_profile(&mut profile.cookies, commit);
|
|
(profile.cookies.clone(), profile.cache_paths.clone())
|
|
};
|
|
|
|
for path in cache_paths {
|
|
cookie_cache::save_cookie_cache(&path, cookies.clone()).with_context(|| {
|
|
anyhow!("failed to save cookie cache `{}`", path.display())
|
|
})?;
|
|
}
|
|
Ok(())
|
|
}
|
|
SharedCookieProfileBacking::StoragePartition(storage_partition) => {
|
|
storage_partition.commit_cookie_delta(&commit.initial_cookies, commit.final_cookies)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn merge_and_save(&self, cookies: Vec<StoredCookie>) -> Result<()> {
|
|
self.commit_and_save(CookieProfileCommit::new(Vec::new(), cookies))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug)]
|
|
struct CookieProfile {
|
|
cookies: Vec<StoredCookie>,
|
|
cache_paths: Vec<PathBuf>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn merge_cookie_profiles(base: &mut Vec<StoredCookie>, updates: Vec<StoredCookie>) {
|
|
for cookie in updates.into_iter().filter(|cookie| !cookie.is_expired()) {
|
|
if let Some(existing) = base
|
|
.iter_mut()
|
|
.find(|existing| same_cookie_key(existing, &cookie))
|
|
{
|
|
*existing = cookie;
|
|
} else {
|
|
base.push(cookie);
|
|
}
|
|
}
|
|
base.retain(|cookie| !cookie.is_expired());
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn commit_cookie_profile(base: &mut Vec<StoredCookie>, commit: CookieProfileCommit) {
|
|
let Some(final_cookies) = commit.final_cookies else {
|
|
return;
|
|
};
|
|
let final_cookies = final_cookies
|
|
.into_iter()
|
|
.filter(|cookie| !cookie.is_expired())
|
|
.collect::<Vec<_>>();
|
|
|
|
for initial in commit
|
|
.initial_cookies
|
|
.iter()
|
|
.filter(|cookie| !cookie.is_expired())
|
|
{
|
|
let cookie_still_exists_in_session = final_cookies
|
|
.iter()
|
|
.any(|cookie| same_cookie_key(cookie, initial));
|
|
if cookie_still_exists_in_session {
|
|
continue;
|
|
}
|
|
if let Some(position) = base
|
|
.iter()
|
|
.position(|cookie| same_cookie_key(cookie, initial) && cookie == initial)
|
|
{
|
|
base.remove(position);
|
|
}
|
|
}
|
|
|
|
merge_cookie_profiles(base, final_cookies);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn same_cookie_key(left: &StoredCookie, right: &StoredCookie) -> bool {
|
|
left.name == right.name
|
|
&& left.domain == right.domain
|
|
&& left.path == right.path
|
|
&& left.partition_key == right.partition_key
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|