mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
1090 lines
38 KiB
Rust
1090 lines
38 KiB
Rust
mod auth;
|
|
mod body_stream;
|
|
mod commands;
|
|
mod helpers;
|
|
mod navigation;
|
|
mod params;
|
|
mod patterns;
|
|
mod state;
|
|
mod subresource;
|
|
|
|
use crate::conn::{
|
|
BackgroundProtocolEvent, CdpConnection, Cmd, CommandOwnerScope, DevToolsCommandExecutionOutput,
|
|
FetchInterceptionPattern, FetchRequestStage as ConnFetchRequestStage,
|
|
};
|
|
use crate::devtools_runtime::{
|
|
DevToolsAddNetworkInterceptCommand, DevToolsAddNetworkInterceptResult, DevToolsCommand,
|
|
DevToolsCommandResult, DevToolsError, DevToolsErrorKind, DevToolsNetworkInterceptPhase,
|
|
DevToolsProtocol,
|
|
};
|
|
use crate::domains::actions::FetchAction;
|
|
use crate::domains::command_output::{CommandOutputPlan, devtools_error_from_cdp_error_parts};
|
|
use crate::domains::{activity, network, page};
|
|
use serde_json::json;
|
|
|
|
#[cfg(test)]
|
|
pub(crate) use crate::conn::FetchAuthChallenge;
|
|
#[cfg(test)]
|
|
pub(crate) use crate::conn::FetchRequestStage;
|
|
#[cfg(test)]
|
|
pub(crate) use crate::conn::FetchResourceTypeFilter;
|
|
#[cfg(test)]
|
|
pub(crate) use crate::conn::PendingFetchAuthNavigation;
|
|
#[cfg(test)]
|
|
pub(crate) use crate::conn::PendingFetchNavigation;
|
|
#[cfg(test)]
|
|
pub(crate) use helpers::encode_basic_auth;
|
|
pub(crate) use helpers::request_paused_background_event;
|
|
#[cfg(test)]
|
|
use helpers::response_headers_from_params;
|
|
#[cfg(test)]
|
|
pub(crate) use helpers::{emit_auth_required, extract_auth_challenge, request_auth_for_challenge};
|
|
pub(crate) use helpers::{
|
|
pending_subresource_auth_required_event,
|
|
pending_subresource_response_stage_request_paused_event, populate_auth_challenge_origin,
|
|
};
|
|
#[cfg(test)]
|
|
pub(crate) use moli_fetch::url_pattern_matches;
|
|
pub(crate) use navigation::continue_navigation_without_request_pause_into_buffer_async;
|
|
use params::EnableParams;
|
|
use patterns::supported_pattern_config;
|
|
pub(crate) use subresource::{
|
|
detached_parser_script_fetch_pause_prepared_outputs_for_renderer_record_async,
|
|
emit_subresource_fetch_pause_outputs,
|
|
subresource_fetch_pause_prepared_outputs_for_renderer_record_async,
|
|
};
|
|
|
|
/// Disables the Fetch handler owned by one DevTools session and drains every
|
|
/// request that was paused by that handler before its binding is removed.
|
|
pub(in crate::domains) async fn dispose_session_async(
|
|
conn: &mut CdpConnection,
|
|
out: &mut Vec<BackgroundProtocolEvent>,
|
|
session_id: &str,
|
|
) -> anyhow::Result<Option<moli_core::RendererOutputFence>> {
|
|
dispose_owner_async(conn, out, Some(session_id)).await
|
|
}
|
|
|
|
pub(in crate::domains) async fn dispose_owner_async(
|
|
conn: &mut CdpConnection,
|
|
out: &mut Vec<BackgroundProtocolEvent>,
|
|
session_id: Option<&str>,
|
|
) -> anyhow::Result<Option<moli_core::RendererOutputFence>> {
|
|
let Some((pending_fetch_state, pending_page_command)) = conn
|
|
.start_disable_fetch_for_session_owner(session_id)
|
|
.map_err(anyhow::Error::msg)?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let mut renderer_cleanup_error = None;
|
|
if let Some(pending_page_command) = pending_page_command {
|
|
match pending_page_command.wait().await {
|
|
Ok(completion) => match conn.loaded_page_mut_for_protocol_access(session_id) {
|
|
Ok(page) => {
|
|
if let Err(error) = page.finish_set_fetch_subresource_interception(completion) {
|
|
renderer_cleanup_error = Some(error.context(
|
|
"failed to finish Fetch interception disable while disposing session",
|
|
));
|
|
}
|
|
}
|
|
Err(message) if message == "NoDocumentLoaded" => {}
|
|
Err(message) => {
|
|
renderer_cleanup_error = Some(anyhow::anyhow!(
|
|
"failed to find Page while disposing Fetch handler: {message}"
|
|
));
|
|
}
|
|
},
|
|
Err(error) => {
|
|
renderer_cleanup_error =
|
|
Some(error.context(
|
|
"renderer failed Fetch interception disable while disposing session",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let (
|
|
pending_navigations,
|
|
pending_auth_navigations,
|
|
pending_response_navigations,
|
|
pending_subresource_fetches,
|
|
pending_subresource_auths,
|
|
pending_subresource_responses,
|
|
) = pending_fetch_state;
|
|
// Session teardown has no command response to fence. The concrete
|
|
// renderer publication remains ordered on its own stream and will reach
|
|
// protocol ingress independently.
|
|
let predecessor = page::fail_pending_fetch_state_background_events_async(
|
|
conn,
|
|
out,
|
|
session_id,
|
|
"Target detached",
|
|
"Target detached",
|
|
pending_navigations,
|
|
pending_auth_navigations,
|
|
pending_response_navigations,
|
|
pending_subresource_fetches,
|
|
pending_subresource_auths,
|
|
pending_subresource_responses,
|
|
)
|
|
.await;
|
|
if let Some(error) = renderer_cleanup_error {
|
|
return Err(error);
|
|
}
|
|
Ok(predecessor)
|
|
}
|
|
|
|
pub(crate) struct PendingFetchCommandDispatch {
|
|
command_id: Option<u64>,
|
|
owner_scope: CommandOwnerScope,
|
|
kind: PendingFetchCommandKind,
|
|
pending: PendingFetchCommandOperation,
|
|
}
|
|
|
|
pub(crate) struct CompletedFetchCommandDispatch {
|
|
command_id: Option<u64>,
|
|
owner_scope: CommandOwnerScope,
|
|
kind: PendingFetchCommandKind,
|
|
completed: CompletedFetchCommandOperation,
|
|
}
|
|
|
|
pub(crate) enum FetchCommandTaskStep {
|
|
Pending(PendingFetchCommandDispatch),
|
|
Complete(CommandOutputPlan),
|
|
}
|
|
|
|
enum PendingFetchCommandKind {
|
|
Enable,
|
|
AddNetworkIntercept {
|
|
intercept_id: String,
|
|
},
|
|
RemoveNetworkIntercept,
|
|
Disable {
|
|
pending_fetch_state: Box<FetchDisablePendingState>,
|
|
},
|
|
ContinueRequest {
|
|
state: Box<commands::PendingContinueRequestState>,
|
|
},
|
|
ContinueWithAuth {
|
|
state: Box<auth::PendingContinueWithAuthState>,
|
|
},
|
|
FailRequest {
|
|
state: Box<commands::PendingFailRequestState>,
|
|
},
|
|
FulfillRequest {
|
|
state: Box<commands::PendingFulfillRequestState>,
|
|
},
|
|
DispatchWebSocketMessage {
|
|
operation: commands::PendingWebSocketCommandOperation,
|
|
},
|
|
CloseWebSocket,
|
|
ContinueResponse {
|
|
state: Box<commands::PendingContinueResponseState>,
|
|
},
|
|
GetResponseBody,
|
|
}
|
|
|
|
enum PendingFetchCommandOperation {
|
|
Ready,
|
|
Page(moli_core::page::PendingPageCommand),
|
|
MaterializeResponseBody {
|
|
request_id: String,
|
|
transfer: Box<crate::conn::PausedDocumentTransfer>,
|
|
limit: usize,
|
|
},
|
|
}
|
|
|
|
enum CompletedFetchCommandOperation {
|
|
Ready,
|
|
Page(Box<Result<moli_core::page::CompletedPageCommand, String>>),
|
|
MaterializeResponseBody {
|
|
request_id: String,
|
|
result: Box<
|
|
Result<
|
|
(Option<Vec<u8>>, crate::conn::PausedDocumentTransfer),
|
|
(anyhow::Error, crate::conn::PausedDocumentTransfer),
|
|
>,
|
|
>,
|
|
},
|
|
}
|
|
|
|
type FetchDisablePendingState = (
|
|
Vec<crate::conn::PendingFetchNavigation>,
|
|
Vec<crate::conn::PendingFetchAuthNavigation>,
|
|
Vec<crate::conn::PausedDocumentTransfer>,
|
|
Vec<(String, crate::conn::PendingSubresourceFetchRequest)>,
|
|
Vec<(String, crate::conn::PendingSubresourceFetchAuthRequest)>,
|
|
Vec<(String, crate::conn::PendingSubresourceFetchResponseRequest)>,
|
|
);
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct FetchCommandOutput {
|
|
plan: CommandOutputPlan,
|
|
command_status: Option<Result<(), DevToolsError>>,
|
|
}
|
|
|
|
impl FetchCommandOutput {
|
|
fn push_success(&mut self) {
|
|
self.record_command_status(Ok(()));
|
|
self.plan.push_result(json!({}));
|
|
}
|
|
|
|
fn push_error(&mut self, code: i32, message: impl AsRef<str>) {
|
|
let message = message.as_ref();
|
|
self.record_command_status(Err(devtools_error_from_cdp_error_parts(
|
|
Some(i64::from(code)),
|
|
message,
|
|
)));
|
|
self.plan.push_error(code, message);
|
|
}
|
|
|
|
fn extend_plan_as_command_response(&mut self, plan: CommandOutputPlan) {
|
|
if let Some(status) = plan.command_status() {
|
|
self.record_command_status(status);
|
|
}
|
|
self.plan.extend(plan);
|
|
}
|
|
|
|
fn extend_plan_as_background_events(
|
|
&mut self,
|
|
plan: CommandOutputPlan,
|
|
command_id: Option<u64>,
|
|
session_id: Option<&str>,
|
|
) {
|
|
self.plan
|
|
.extend(plan.into_background_event_plan(command_id, session_id));
|
|
}
|
|
|
|
fn set_renderer_output_predecessor(&mut self, predecessor: moli_core::RendererOutputFence) {
|
|
self.plan.set_renderer_output_predecessor(predecessor);
|
|
}
|
|
|
|
fn extend_background_events(
|
|
&mut self,
|
|
events: impl IntoIterator<Item = BackgroundProtocolEvent>,
|
|
) {
|
|
self.plan.extend_background_events(events);
|
|
}
|
|
|
|
fn into_output_plan(self) -> CommandOutputPlan {
|
|
self.plan
|
|
}
|
|
|
|
fn into_devtools_result_and_background_events(
|
|
mut self,
|
|
success_result: DevToolsCommandResult,
|
|
) -> DevToolsCommandExecutionOutput {
|
|
let status = self.command_status.unwrap_or_else(|| {
|
|
Err(DevToolsError::new(
|
|
DevToolsErrorKind::Internal,
|
|
"MissingFetchCommandResponse",
|
|
))
|
|
});
|
|
let renderer_output_predecessor = self.plan.take_renderer_output_predecessor();
|
|
let (_, events) = self.plan.into_command_status_and_background_events();
|
|
DevToolsCommandExecutionOutput::from_parts(
|
|
status.map(|()| success_result),
|
|
events,
|
|
renderer_output_predecessor,
|
|
)
|
|
}
|
|
|
|
fn record_command_status(&mut self, status: Result<(), DevToolsError>) {
|
|
if self.command_status.is_none() {
|
|
self.command_status = Some(status);
|
|
} else {
|
|
tracing::warn!("fetch command produced multiple command responses");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PendingFetchCommandDispatch {
|
|
fn new(
|
|
conn: &CdpConnection,
|
|
command_id: Option<u64>,
|
|
session_id: Option<&str>,
|
|
kind: PendingFetchCommandKind,
|
|
pending: PendingFetchCommandOperation,
|
|
) -> Self {
|
|
let owner = CommandOwnerScope::capture(conn, session_id);
|
|
Self::new_for_owner(command_id, owner, kind, pending)
|
|
}
|
|
|
|
fn new_for_owner(
|
|
command_id: Option<u64>,
|
|
owner_scope: CommandOwnerScope,
|
|
kind: PendingFetchCommandKind,
|
|
pending: PendingFetchCommandOperation,
|
|
) -> Self {
|
|
Self {
|
|
command_id,
|
|
owner_scope,
|
|
kind,
|
|
pending,
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn wait(self) -> CompletedFetchCommandDispatch {
|
|
let completed = match self.pending {
|
|
PendingFetchCommandOperation::Ready => CompletedFetchCommandOperation::Ready,
|
|
PendingFetchCommandOperation::Page(pending) => CompletedFetchCommandOperation::Page(
|
|
Box::new(pending.wait().await.map_err(|error| error.to_string())),
|
|
),
|
|
PendingFetchCommandOperation::MaterializeResponseBody {
|
|
request_id,
|
|
transfer,
|
|
limit,
|
|
} => CompletedFetchCommandOperation::MaterializeResponseBody {
|
|
request_id,
|
|
result: Box::new(transfer.materialize_body_limited_async(limit).await),
|
|
},
|
|
};
|
|
CompletedFetchCommandDispatch {
|
|
command_id: self.command_id,
|
|
owner_scope: self.owner_scope,
|
|
kind: self.kind,
|
|
completed,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletedFetchCommandDispatch {
|
|
pub(crate) fn command_id(&self) -> Option<u64> {
|
|
self.command_id
|
|
}
|
|
|
|
pub(crate) fn session_id(&self) -> Option<&str> {
|
|
self.owner_scope.session_id()
|
|
}
|
|
}
|
|
|
|
impl CompletedFetchCommandOperation {
|
|
fn renderer_output_predecessor(&self) -> Option<moli_core::RendererOutputFence> {
|
|
match self {
|
|
Self::Page(completed) => completed
|
|
.as_ref()
|
|
.as_ref()
|
|
.ok()
|
|
.and_then(moli_core::page::CompletedPageCommand::renderer_output_predecessor),
|
|
Self::Ready | Self::MaterializeResponseBody { .. } => None,
|
|
}
|
|
}
|
|
|
|
fn into_page_completion(self) -> Option<Result<moli_core::page::CompletedPageCommand, String>> {
|
|
match self {
|
|
Self::Page(completed) => Some(*completed),
|
|
Self::Ready | Self::MaterializeResponseBody { .. } => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn try_start_fetch_command_dispatch(
|
|
conn: &mut CdpConnection,
|
|
cmd: &Cmd<'_>,
|
|
) -> Option<FetchCommandTaskStep> {
|
|
match cmd.parse_action::<FetchAction>() {
|
|
Some(FetchAction::Enable) => Some(start_enable_command(conn, cmd)),
|
|
Some(FetchAction::Disable) => Some(start_disable_command(conn, cmd)),
|
|
Some(FetchAction::ContinueRequest) => {
|
|
Some(commands::start_continue_request_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::ContinueWithAuth) => {
|
|
Some(auth::start_continue_with_auth_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::FailRequest) => Some(commands::start_fail_request_command(conn, cmd)),
|
|
Some(FetchAction::FulfillRequest) => {
|
|
Some(commands::start_fulfill_request_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::ContinueResponse) => {
|
|
Some(commands::start_continue_response_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::DispatchWebSocketMessage) => Some(
|
|
commands::start_dispatch_websocket_message_command(conn, cmd),
|
|
),
|
|
Some(FetchAction::CloseWebSocket) => {
|
|
Some(commands::start_close_websocket_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::GetResponseBody) => {
|
|
Some(body_stream::start_get_response_body_command(conn, cmd))
|
|
}
|
|
Some(FetchAction::TakeResponseBodyAsStream) => Some(FetchCommandTaskStep::Complete(
|
|
body_stream::take_response_body_as_stream_command(conn, cmd),
|
|
)),
|
|
None => Some(FetchCommandTaskStep::Complete(CommandOutputPlan::error(
|
|
-32601,
|
|
"UnknownMethod",
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn execute_devtools_fetch_command_async_with_protocol_events(
|
|
conn: &mut CdpConnection,
|
|
command: DevToolsCommand,
|
|
) -> DevToolsCommandExecutionOutput {
|
|
let success_result = devtools_fetch_success_result(&command);
|
|
let owner = match fetch_devtools_command_owner(conn, &command) {
|
|
Ok(owner) => owner,
|
|
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
|
|
};
|
|
let step = start_devtools_fetch_command_for_owner(conn, None, &owner, command);
|
|
match step {
|
|
FetchCommandTaskStep::Complete(mut plan) => {
|
|
let renderer_output_predecessor = plan.take_renderer_output_predecessor();
|
|
let (status, events) = plan.into_command_status_and_background_events();
|
|
DevToolsCommandExecutionOutput::from_parts(
|
|
status
|
|
.unwrap_or_else(|| {
|
|
Err(DevToolsError::new(
|
|
DevToolsErrorKind::Internal,
|
|
"MissingFetchCommandResponse",
|
|
))
|
|
})
|
|
.map(|()| success_result),
|
|
events,
|
|
renderer_output_predecessor,
|
|
)
|
|
}
|
|
FetchCommandTaskStep::Pending(pending) => {
|
|
let completed = pending.wait().await;
|
|
complete_pending_devtools_fetch_command(conn, completed)
|
|
.await
|
|
.into_devtools_result_and_background_events(success_result)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn devtools_fetch_success_result(command: &DevToolsCommand) -> DevToolsCommandResult {
|
|
match command {
|
|
DevToolsCommand::AddNetworkIntercept(command) => {
|
|
DevToolsCommandResult::AddNetworkIntercept(DevToolsAddNetworkInterceptResult {
|
|
intercept_id: command.intercept_id.clone(),
|
|
})
|
|
}
|
|
_ => DevToolsCommandResult::Empty,
|
|
}
|
|
}
|
|
|
|
fn start_devtools_fetch_command_for_owner(
|
|
conn: &mut CdpConnection,
|
|
command_id: Option<u64>,
|
|
owner: &CommandOwnerScope,
|
|
command: DevToolsCommand,
|
|
) -> FetchCommandTaskStep {
|
|
match &command {
|
|
DevToolsCommand::AddNetworkIntercept(command) => {
|
|
start_devtools_add_network_intercept_command(conn, command_id, owner, command)
|
|
}
|
|
DevToolsCommand::RemoveNetworkIntercept(command) => {
|
|
start_devtools_remove_network_intercept_command(
|
|
conn,
|
|
command_id,
|
|
owner,
|
|
command.intercept_id.as_str(),
|
|
command.context.protocol != DevToolsProtocol::Cdp
|
|
&& command.context.target_id.is_none(),
|
|
)
|
|
}
|
|
_ => commands::start_devtools_fetch_command_for_owner(conn, command_id, owner, command),
|
|
}
|
|
}
|
|
|
|
fn fetch_devtools_command_owner(
|
|
conn: &CdpConnection,
|
|
command: &DevToolsCommand,
|
|
) -> Result<CommandOwnerScope, DevToolsError> {
|
|
let (context, request_id) = match command {
|
|
DevToolsCommand::AddNetworkIntercept(command) => {
|
|
return fetch_config_devtools_command_owner(conn, &command.context);
|
|
}
|
|
DevToolsCommand::RemoveNetworkIntercept(command) => {
|
|
return fetch_config_devtools_command_owner(conn, &command.context);
|
|
}
|
|
DevToolsCommand::ContinueInterceptedRequest(command) => {
|
|
(&command.context, command.request_id.as_str())
|
|
}
|
|
DevToolsCommand::ContinueInterceptedResponse(command) => {
|
|
(&command.context, command.request_id.as_str())
|
|
}
|
|
DevToolsCommand::ContinueWithAuth(command) => {
|
|
(&command.context, command.request_id.as_str())
|
|
}
|
|
DevToolsCommand::FailInterceptedRequest(command) => {
|
|
(&command.context, command.request_id.as_str())
|
|
}
|
|
DevToolsCommand::FulfillInterceptedRequest(command) => {
|
|
(&command.context, command.request_id.as_str())
|
|
}
|
|
_ => return Ok(CommandOwnerScope::capture(conn, None)),
|
|
};
|
|
if context.protocol == DevToolsProtocol::Cdp {
|
|
return Ok(CommandOwnerScope::capture(
|
|
conn,
|
|
context.session_id.as_ref().map(|session| session.as_str()),
|
|
));
|
|
}
|
|
Ok(conn
|
|
.pending_fetch_request_session_route(request_id)
|
|
.map(CommandOwnerScope::for_route)
|
|
.unwrap_or_else(|| CommandOwnerScope::capture(conn, None)))
|
|
}
|
|
|
|
fn fetch_config_devtools_command_owner(
|
|
conn: &CdpConnection,
|
|
context: &crate::devtools_runtime::DevToolsCommandContext,
|
|
) -> Result<CommandOwnerScope, DevToolsError> {
|
|
if context.protocol == DevToolsProtocol::Cdp {
|
|
return Ok(CommandOwnerScope::capture(
|
|
conn,
|
|
context.session_id.as_ref().map(|session| session.as_str()),
|
|
));
|
|
}
|
|
if let Some(target_id) = context.target_id.as_ref() {
|
|
let route = conn
|
|
.target_session_route_for_target_id(target_id.as_str())
|
|
.ok_or_else(|| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget"))?;
|
|
Ok(CommandOwnerScope::for_route(route))
|
|
} else {
|
|
Ok(CommandOwnerScope::capture(conn, None))
|
|
}
|
|
}
|
|
|
|
fn start_enable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchCommandTaskStep {
|
|
let params: EnableParams = match cmd.get_params() {
|
|
Ok(Some(params)) => params,
|
|
Ok(None) => EnableParams::default(),
|
|
Err(_) => {
|
|
return FetchCommandTaskStep::Complete(CommandOutputPlan::error(
|
|
-32602,
|
|
"InvalidParams",
|
|
));
|
|
}
|
|
};
|
|
|
|
let patterns = match supported_pattern_config(¶ms.patterns) {
|
|
Ok(patterns) => patterns,
|
|
Err(()) => {
|
|
return FetchCommandTaskStep::Complete(CommandOutputPlan::error(
|
|
-32602,
|
|
"InvalidParams",
|
|
));
|
|
}
|
|
};
|
|
|
|
match conn.start_enable_fetch_for_session_owner(
|
|
cmd.session_id,
|
|
params.handle_auth_requests,
|
|
patterns,
|
|
) {
|
|
Ok(Some(pending)) => FetchCommandTaskStep::Pending(PendingFetchCommandDispatch::new(
|
|
conn,
|
|
cmd.id,
|
|
cmd.session_id,
|
|
PendingFetchCommandKind::Enable,
|
|
PendingFetchCommandOperation::Page(pending),
|
|
)),
|
|
Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::success()),
|
|
Err(message) if message == "BrowserContextNotLoaded" => FetchCommandTaskStep::Complete(
|
|
CommandOutputPlan::error(-31998, "BrowserContextNotLoaded"),
|
|
),
|
|
Err(message) => FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)),
|
|
}
|
|
}
|
|
|
|
fn start_devtools_add_network_intercept_command(
|
|
conn: &mut CdpConnection,
|
|
command_id: Option<u64>,
|
|
owner: &CommandOwnerScope,
|
|
command: &DevToolsAddNetworkInterceptCommand,
|
|
) -> FetchCommandTaskStep {
|
|
let (handle_auth_requests, auth_url_patterns, patterns) =
|
|
network_intercept_fetch_config(command);
|
|
let intercept_session_id = if command.context.protocol == DevToolsProtocol::Cdp {
|
|
owner.session_id().map(str::to_owned)
|
|
} else {
|
|
command
|
|
.context
|
|
.session_id
|
|
.as_ref()
|
|
.map(|session_id| session_id.as_str().to_owned())
|
|
};
|
|
match conn.start_add_network_intercept_for_owner(
|
|
owner,
|
|
intercept_session_id,
|
|
command.intercept_id.as_str().to_owned(),
|
|
handle_auth_requests,
|
|
auth_url_patterns,
|
|
patterns,
|
|
) {
|
|
Ok(Some(pending)) => {
|
|
FetchCommandTaskStep::Pending(PendingFetchCommandDispatch::new_for_owner(
|
|
command_id,
|
|
owner.clone(),
|
|
PendingFetchCommandKind::AddNetworkIntercept {
|
|
intercept_id: command.intercept_id.as_str().to_owned(),
|
|
},
|
|
PendingFetchCommandOperation::Page(pending),
|
|
))
|
|
}
|
|
Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::from_devtools_result(
|
|
DevToolsCommandResult::AddNetworkIntercept(DevToolsAddNetworkInterceptResult {
|
|
intercept_id: command.intercept_id.clone(),
|
|
}),
|
|
)),
|
|
Err(message) if message == "BrowserContextNotLoaded" => FetchCommandTaskStep::Complete(
|
|
CommandOutputPlan::error(-31998, "BrowserContextNotLoaded"),
|
|
),
|
|
Err(message) => FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)),
|
|
}
|
|
}
|
|
|
|
fn start_devtools_remove_network_intercept_command(
|
|
conn: &mut CdpConnection,
|
|
command_id: Option<u64>,
|
|
owner: &CommandOwnerScope,
|
|
intercept_id: &str,
|
|
allow_global_lookup: bool,
|
|
) -> FetchCommandTaskStep {
|
|
match conn.start_remove_network_intercept_for_owner(owner, intercept_id, allow_global_lookup) {
|
|
Ok(Some(pending)) => {
|
|
FetchCommandTaskStep::Pending(PendingFetchCommandDispatch::new_for_owner(
|
|
command_id,
|
|
owner.clone(),
|
|
PendingFetchCommandKind::RemoveNetworkIntercept,
|
|
PendingFetchCommandOperation::Page(pending),
|
|
))
|
|
}
|
|
Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::success()),
|
|
Err(message) if message == "NetworkInterceptNotFound" => FetchCommandTaskStep::Complete(
|
|
CommandOutputPlan::error(-32000, "NetworkInterceptNotFound"),
|
|
),
|
|
Err(message) if message == "BrowserContextNotLoaded" => FetchCommandTaskStep::Complete(
|
|
CommandOutputPlan::error(-31998, "BrowserContextNotLoaded"),
|
|
),
|
|
Err(message) => FetchCommandTaskStep::Complete(CommandOutputPlan::error(-32000, message)),
|
|
}
|
|
}
|
|
|
|
fn network_intercept_fetch_config(
|
|
command: &DevToolsAddNetworkInterceptCommand,
|
|
) -> (bool, Vec<String>, Vec<FetchInterceptionPattern>) {
|
|
let handle_auth_requests = command
|
|
.phases
|
|
.contains(&DevToolsNetworkInterceptPhase::AuthRequired);
|
|
let auth_url_patterns = if handle_auth_requests {
|
|
if command.url_patterns.is_empty() {
|
|
vec!["*".to_owned()]
|
|
} else {
|
|
command
|
|
.url_patterns
|
|
.iter()
|
|
.map(|pattern| pattern.url_pattern.clone())
|
|
.collect()
|
|
}
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let mut patterns = Vec::new();
|
|
for request_stage in [
|
|
ConnFetchRequestStage::Request,
|
|
ConnFetchRequestStage::Response,
|
|
] {
|
|
let phase = match request_stage {
|
|
ConnFetchRequestStage::Request => DevToolsNetworkInterceptPhase::BeforeRequestSent,
|
|
ConnFetchRequestStage::Response => DevToolsNetworkInterceptPhase::ResponseStarted,
|
|
};
|
|
if !command.phases.contains(&phase) {
|
|
continue;
|
|
}
|
|
if command.url_patterns.is_empty() {
|
|
patterns.push(FetchInterceptionPattern {
|
|
url_pattern: "*".to_owned(),
|
|
resource_type_filter: None,
|
|
request_stage,
|
|
});
|
|
continue;
|
|
}
|
|
patterns.extend(
|
|
command
|
|
.url_patterns
|
|
.iter()
|
|
.map(|pattern| FetchInterceptionPattern {
|
|
url_pattern: pattern.url_pattern.clone(),
|
|
resource_type_filter: None,
|
|
request_stage,
|
|
}),
|
|
);
|
|
}
|
|
(handle_auth_requests, auth_url_patterns, patterns)
|
|
}
|
|
|
|
pub(crate) async fn complete_pending_fetch_command(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
) -> CommandOutputPlan {
|
|
complete_pending_fetch_command_output(conn, completed)
|
|
.await
|
|
.into_output_plan()
|
|
}
|
|
|
|
async fn complete_pending_devtools_fetch_command(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
) -> FetchCommandOutput {
|
|
complete_pending_fetch_command_output(conn, completed).await
|
|
}
|
|
|
|
async fn complete_pending_fetch_command_output(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
) -> FetchCommandOutput {
|
|
complete_pending_fetch_command_inner(conn, completed).await
|
|
}
|
|
|
|
async fn complete_pending_fetch_command_inner(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
) -> FetchCommandOutput {
|
|
let mut out = FetchCommandOutput::default();
|
|
let owner_scope = completed.owner_scope.clone();
|
|
// Every Fetch operation that crossed the renderer Page boundary must make
|
|
// its concrete publication a predecessor of the frontend response. Keep
|
|
// this at the one dispatch join point: command-specific finish helpers
|
|
// consume CompletedPageCommand and must not each recreate the ordering
|
|
// contract.
|
|
if let Some(predecessor) = completed.completed.renderer_output_predecessor() {
|
|
out.set_renderer_output_predecessor(predecessor);
|
|
}
|
|
match completed.kind {
|
|
PendingFetchCommandKind::Enable => {
|
|
out.extend_plan_as_command_response(complete_enable_command(conn, completed));
|
|
}
|
|
PendingFetchCommandKind::AddNetworkIntercept { ref intercept_id } => {
|
|
let result_intercept_id = intercept_id.clone();
|
|
out.extend_plan_as_command_response(complete_fetch_config_update_command(
|
|
conn,
|
|
completed,
|
|
DevToolsCommandResult::AddNetworkIntercept(DevToolsAddNetworkInterceptResult {
|
|
intercept_id: result_intercept_id.into(),
|
|
}),
|
|
));
|
|
}
|
|
PendingFetchCommandKind::RemoveNetworkIntercept => {
|
|
out.extend_plan_as_command_response(complete_fetch_config_update_command(
|
|
conn,
|
|
completed,
|
|
DevToolsCommandResult::Empty,
|
|
));
|
|
}
|
|
PendingFetchCommandKind::Disable {
|
|
pending_fetch_state,
|
|
} => {
|
|
complete_disable_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*pending_fetch_state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::ContinueRequest { state } => {
|
|
commands::complete_continue_request_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::ContinueWithAuth { state } => {
|
|
auth::complete_continue_with_auth_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::FailRequest { state } => {
|
|
commands::complete_fail_request_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::FulfillRequest { state } => {
|
|
commands::complete_fulfill_request_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::DispatchWebSocketMessage { operation } => {
|
|
commands::complete_websocket_page_command(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
operation,
|
|
&mut out,
|
|
);
|
|
}
|
|
PendingFetchCommandKind::CloseWebSocket => {
|
|
commands::complete_websocket_page_command(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
commands::PendingWebSocketCommandOperation::Close,
|
|
&mut out,
|
|
);
|
|
}
|
|
PendingFetchCommandKind::ContinueResponse { state } => {
|
|
commands::complete_continue_response_command_async(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed.into_page_completion(),
|
|
*state,
|
|
&mut out,
|
|
)
|
|
.await;
|
|
}
|
|
PendingFetchCommandKind::GetResponseBody => {
|
|
body_stream::complete_get_response_body_from_transfer(
|
|
conn,
|
|
&owner_scope,
|
|
completed.completed,
|
|
&mut out,
|
|
);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn complete_enable_command(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
) -> CommandOutputPlan {
|
|
complete_fetch_config_update_command(conn, completed, DevToolsCommandResult::Empty)
|
|
}
|
|
|
|
fn complete_fetch_config_update_command(
|
|
conn: &mut CdpConnection,
|
|
completed: CompletedFetchCommandDispatch,
|
|
result: DevToolsCommandResult,
|
|
) -> CommandOutputPlan {
|
|
let owner_scope = completed.owner_scope.clone();
|
|
let Some(completed_page_command) = completed.completed.into_page_completion() else {
|
|
return CommandOutputPlan::error(-32000, "Missing renderer completion");
|
|
};
|
|
let completion = match completed_page_command {
|
|
Ok(completion) => completion,
|
|
Err(error) => return CommandOutputPlan::error(-32000, error),
|
|
};
|
|
let page = match conn.loaded_page_mut_for_protocol_access_for_owner(&owner_scope) {
|
|
Ok(page) => page,
|
|
Err(message) if message == "NoDocumentLoaded" => {
|
|
return CommandOutputPlan::from_devtools_result(result);
|
|
}
|
|
Err(message) => return CommandOutputPlan::error(-32000, message),
|
|
};
|
|
match page.finish_set_fetch_subresource_interception(completion) {
|
|
Ok(()) => CommandOutputPlan::from_devtools_result(result),
|
|
Err(error) => CommandOutputPlan::error(-32000, error.to_string()),
|
|
}
|
|
}
|
|
|
|
fn start_disable_command(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> FetchCommandTaskStep {
|
|
match conn.start_disable_fetch_for_session_owner(cmd.session_id) {
|
|
Ok(Some((pending_fetch_state, pending))) => {
|
|
FetchCommandTaskStep::Pending(PendingFetchCommandDispatch::new(
|
|
conn,
|
|
cmd.id,
|
|
cmd.session_id,
|
|
PendingFetchCommandKind::Disable {
|
|
pending_fetch_state: Box::new(pending_fetch_state),
|
|
},
|
|
pending
|
|
.map(PendingFetchCommandOperation::Page)
|
|
.unwrap_or(PendingFetchCommandOperation::Ready),
|
|
))
|
|
}
|
|
Ok(None) => FetchCommandTaskStep::Complete(CommandOutputPlan::error(
|
|
-31998,
|
|
"BrowserContextNotLoaded",
|
|
)),
|
|
Err(error) => FetchCommandTaskStep::Complete(CommandOutputPlan::error(
|
|
-32000,
|
|
format!("failed to clear page fetch interception: {error}"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn complete_disable_command_async(
|
|
conn: &mut CdpConnection,
|
|
owner: &CommandOwnerScope,
|
|
completed: Option<Result<moli_core::page::CompletedPageCommand, String>>,
|
|
pending_fetch_state: FetchDisablePendingState,
|
|
out: &mut FetchCommandOutput,
|
|
) {
|
|
if let Some(completed) = completed {
|
|
let completion = match completed {
|
|
Ok(completion) => completion,
|
|
Err(error) => {
|
|
out.push_error(
|
|
-32000,
|
|
format!("failed to clear page fetch interception: {error}"),
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
match conn.loaded_page_mut_for_protocol_access_for_owner(owner) {
|
|
Ok(page) => {
|
|
if let Err(error) = page.finish_set_fetch_subresource_interception(completion) {
|
|
out.push_error(
|
|
-32000,
|
|
format!("failed to clear page fetch interception: {error}"),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
Err(message) if message == "NoDocumentLoaded" => {}
|
|
Err(message) => {
|
|
out.push_error(-32000, message);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let (
|
|
pending_navigations,
|
|
pending_auth_navigations,
|
|
pending_response_navigations,
|
|
pending_subresource_fetches,
|
|
pending_subresource_auths,
|
|
pending_subresource_responses,
|
|
) = pending_fetch_state;
|
|
|
|
out.push_success();
|
|
for pending in pending_navigations {
|
|
let token = pending.document_navigation_token;
|
|
let navigation_state = pending.navigation;
|
|
let navigation = network::materialize_navigation_load_result(
|
|
conn,
|
|
&navigation_state,
|
|
Err(anyhow::anyhow!("Fetch interception disabled")),
|
|
);
|
|
navigation::complete_tokened_materialized_navigation_as_background_events_async(
|
|
conn,
|
|
out,
|
|
token,
|
|
navigation_state,
|
|
navigation,
|
|
)
|
|
.await;
|
|
}
|
|
for pending in pending_auth_navigations {
|
|
let token = pending.document_navigation_token;
|
|
let navigation_state = pending.navigation;
|
|
let navigation = network::materialize_navigation_load_result(
|
|
conn,
|
|
&navigation_state,
|
|
Err(anyhow::anyhow!("Fetch interception disabled")),
|
|
);
|
|
navigation::complete_tokened_materialized_navigation_as_background_events_async(
|
|
conn,
|
|
out,
|
|
token,
|
|
navigation_state,
|
|
navigation,
|
|
)
|
|
.await;
|
|
}
|
|
for pending in pending_response_navigations {
|
|
let (token, navigation, result) = pending.fail("Fetch interception disabled".to_owned());
|
|
let result = network::materialize_navigation_load_result(conn, &navigation, result);
|
|
navigation::complete_tokened_materialized_navigation_as_background_events_async(
|
|
conn, out, token, navigation, result,
|
|
)
|
|
.await;
|
|
}
|
|
for (_, pending) in pending_subresource_fetches {
|
|
if let Ok(predecessor) = conn
|
|
.fail_pending_subresource_fetch_for_owner_async(
|
|
owner,
|
|
pending.internal_id,
|
|
"Fetch interception disabled".to_owned(),
|
|
)
|
|
.await
|
|
{
|
|
if let Some(predecessor) = predecessor {
|
|
out.set_renderer_output_predecessor(predecessor);
|
|
}
|
|
let mut events = Vec::new();
|
|
activity::flush_post_subresource_fetch_request_activity_background_events_async(
|
|
conn,
|
|
&mut events,
|
|
owner.session_id(),
|
|
&pending,
|
|
)
|
|
.await;
|
|
out.extend_background_events(events);
|
|
}
|
|
}
|
|
for (_, pending) in pending_subresource_auths {
|
|
if let Ok(predecessor) = conn
|
|
.fail_pending_subresource_auth_for_owner_async(
|
|
owner,
|
|
pending.internal_id,
|
|
"Fetch interception disabled".to_owned(),
|
|
)
|
|
.await
|
|
{
|
|
if let Some(predecessor) = predecessor {
|
|
out.set_renderer_output_predecessor(predecessor);
|
|
}
|
|
let mut events = Vec::new();
|
|
activity::flush_post_subresource_auth_activity_background_events_async(
|
|
conn,
|
|
&mut events,
|
|
owner.session_id(),
|
|
&pending,
|
|
)
|
|
.await;
|
|
out.extend_background_events(events);
|
|
}
|
|
}
|
|
for (_, pending) in pending_subresource_responses {
|
|
if let Ok(predecessor) = conn
|
|
.fail_pending_subresource_response_for_owner_async(
|
|
owner,
|
|
pending.internal_id,
|
|
"Fetch interception disabled".to_owned(),
|
|
)
|
|
.await
|
|
{
|
|
if let Some(predecessor) = predecessor {
|
|
out.set_renderer_output_predecessor(predecessor);
|
|
}
|
|
let mut events = Vec::new();
|
|
activity::flush_post_subresource_response_activity_background_events_async(
|
|
conn,
|
|
&mut events,
|
|
owner.session_id(),
|
|
&pending,
|
|
)
|
|
.await;
|
|
out.extend_background_events(events);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|