Implement WebResourceRequested Event. (#34961)

* Implement WebResourceRequested Event on the Embedder Layer

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* fix and add test

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* resolve comments

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* remove sample code in webview

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* remove typo

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* ./mach format

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* fix test fail caused by interception message

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

* update impl for is_for_main_frame

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>

---------

Signed-off-by: zhuhaichao518 <zhuhaichao518@gmail.com>
This commit is contained in:
zhuhaichao518 2025-01-16 15:44:16 +08:00 committed by GitHub
parent 7256590599
commit a1326a7cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 364 additions and 9 deletions

4
Cargo.lock generated
View file

@ -1746,12 +1746,16 @@ dependencies = [
"base",
"cfg-if",
"crossbeam-channel",
"http 1.2.0",
"hyper_serde",
"ipc-channel",
"keyboard-types",
"log",
"malloc_size_of_derive",
"num-derive",
"num-traits",
"serde",
"servo_malloc_size_of",
"servo_url",
"webrender_api",
"webxr-api",

View file

@ -215,6 +215,7 @@ mod from_script {
Self::WebViewClosed(..) => target_variant!("WebViewClosed"),
Self::WebViewFocused(..) => target_variant!("WebViewFocused"),
Self::WebViewBlurred => target_variant!("WebViewBlurred"),
Self::WebResourceRequested(..) => target_variant!("WebResourceRequested"),
Self::AllowUnload(..) => target_variant!("AllowUnload"),
Self::Keyboard(..) => target_variant!("Keyboard"),
Self::ClearClipboardContents => target_variant!("ClearClipboardContents"),

View file

@ -43,6 +43,7 @@ use crate::fetch::headers::determine_nosniff;
use crate::filemanager_thread::FileManager;
use crate::http_loader::{determine_requests_referrer, http_fetch, set_default_accept, HttpState};
use crate::protocols::ProtocolRegistry;
use crate::request_intercepter::RequestIntercepter;
use crate::subresource_integrity::is_response_integrity_valid;
pub type Target<'a> = &'a mut (dyn FetchTaskTarget + Send);
@ -60,6 +61,7 @@ pub struct FetchContext {
pub devtools_chan: Option<Arc<Mutex<Sender<DevtoolsControlMsg>>>>,
pub filemanager: Arc<Mutex<FileManager>>,
pub file_token: FileTokenCheck,
pub request_intercepter: Arc<Mutex<RequestIntercepter>>,
pub cancellation_listener: Arc<CancellationListener>,
pub timing: ServoArc<Mutex<ResourceFetchTiming>>,
pub protocols: Arc<ProtocolRegistry>,
@ -193,6 +195,18 @@ pub fn should_request_be_blocked_by_csp(
.unwrap_or(csp::CheckResult::Allowed)
}
pub fn maybe_intercept_request(
request: &mut Request,
context: &FetchContext,
response: &mut Option<Response>,
) {
context
.request_intercepter
.lock()
.unwrap()
.intercept_request(request, response, context);
}
/// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch)
pub async fn main_fetch(
fetch_params: &mut FetchParams,
@ -299,6 +313,9 @@ pub async fn main_fetch(
let current_url = request.current_url();
let current_scheme = current_url.scheme();
// Intercept the request and maybe override the response.
maybe_intercept_request(request, context, &mut response);
let mut response = match response {
Some(res) => res,
None => {

View file

@ -18,6 +18,7 @@ pub mod image_cache;
pub mod local_directory_listing;
pub mod mime_classifier;
pub mod protocols;
pub mod request_intercepter;
pub mod resource_thread;
mod storage_thread;
pub mod subresource_integrity;

View file

@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use content_security_policy::Destination;
use embedder_traits::{
EmbedderMsg, EmbedderProxy, HttpBodyData, WebResourceRequest, WebResourceResponseMsg,
};
use ipc_channel::ipc;
use net_traits::http_status::HttpStatus;
use net_traits::request::Request;
use net_traits::response::{Response, ResponseBody};
use net_traits::NetworkError;
use crate::fetch::methods::FetchContext;
#[derive(Clone)]
pub struct RequestIntercepter {
embedder_proxy: EmbedderProxy,
}
impl RequestIntercepter {
pub fn new(embedder_proxy: EmbedderProxy) -> RequestIntercepter {
RequestIntercepter { embedder_proxy }
}
pub fn intercept_request(
&self,
request: &mut Request,
response: &mut Option<Response>,
context: &FetchContext,
) {
let (tx, rx) = ipc::channel().unwrap();
let is_for_main_frame = matches!(request.destination, Destination::Document);
let req = WebResourceRequest::new(
request.method.clone(),
request.headers.clone(),
request.url(),
is_for_main_frame,
request.redirect_count > 0,
);
self.embedder_proxy.send((
request.target_browsing_context_id,
EmbedderMsg::WebResourceRequested(req, tx),
));
let mut response_received = false;
// TODO: use done_chan and run in CoreResourceThreadPool.
while let Ok(msg) = rx.recv() {
match msg {
WebResourceResponseMsg::Start(webresource_response) => {
response_received = true;
let timing = context.timing.lock().unwrap().clone();
let mut res = Response::new(webresource_response.url.clone(), timing);
res.headers = webresource_response.headers;
res.status = HttpStatus::new(
webresource_response.status_code,
webresource_response.status_message,
);
*res.body.lock().unwrap() = ResponseBody::Receiving(Vec::new());
*response = Some(res);
},
WebResourceResponseMsg::Body(data) => {
if !response_received {
panic!("Receive body before initializing a Response!");
}
if let Some(ref mut res) = *response {
match data {
HttpBodyData::Chunk(chunk) => {
if let ResponseBody::Receiving(ref mut body) =
*res.body.lock().unwrap()
{
body.extend_from_slice(&chunk);
} else {
panic!("Receive Playload Message when Response body is not in Receiving state!");
}
},
HttpBodyData::Done => {
let mut res_body = res.body.lock().unwrap();
if let ResponseBody::Receiving(ref mut body) = *res_body {
let completed_body = std::mem::take(body);
*res_body = ResponseBody::Done(completed_body);
} else {
panic!("Receive Done Message when Response body is not in Receiving state!");
}
break;
},
HttpBodyData::Cancelled => {
*response =
Some(Response::network_error(NetworkError::LoadCancelled));
break;
},
}
}
},
WebResourceResponseMsg::None => {
// Will not intercept the response. Continue.
break;
},
}
}
}
}

View file

@ -54,6 +54,7 @@ use crate::hsts::HstsList;
use crate::http_cache::HttpCache;
use crate::http_loader::{http_redirect_fetch, HttpState};
use crate::protocols::ProtocolRegistry;
use crate::request_intercepter::RequestIntercepter;
use crate::storage_thread::StorageThreadFactory;
use crate::websocket_loader;
@ -552,6 +553,7 @@ pub struct CoreResourceManager {
devtools_sender: Option<Sender<DevtoolsControlMsg>>,
sw_managers: HashMap<ImmutableOrigin, IpcSender<CustomResponseMediator>>,
filemanager: FileManager,
request_intercepter: RequestIntercepter,
thread_pool: Arc<CoreResourceThreadPool>,
ca_certificates: CACertificates,
ignore_certificate_errors: bool,
@ -703,7 +705,8 @@ impl CoreResourceManager {
user_agent,
devtools_sender,
sw_managers: Default::default(),
filemanager: FileManager::new(embedder_proxy, Arc::downgrade(&pool_handle)),
filemanager: FileManager::new(embedder_proxy.clone(), Arc::downgrade(&pool_handle)),
request_intercepter: RequestIntercepter::new(embedder_proxy),
thread_pool: pool_handle,
ca_certificates,
ignore_certificate_errors,
@ -746,6 +749,7 @@ impl CoreResourceManager {
let ua = self.user_agent.clone();
let dc = self.devtools_sender.clone();
let filemanager = self.filemanager.clone();
let request_intercepter = self.request_intercepter.clone();
let timing_type = match request_builder.destination {
Destination::Document => ResourceTimingType::Navigation,
@ -787,6 +791,7 @@ impl CoreResourceManager {
devtools_chan: dc.map(|dc| Arc::new(Mutex::new(dc))),
filemanager: Arc::new(Mutex::new(filemanager)),
file_token,
request_intercepter: Arc::new(Mutex::new(request_intercepter)),
cancellation_listener,
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(request.timing_type()))),
protocols,

View file

@ -30,6 +30,7 @@ use net::fetch::methods::{self, FetchContext};
use net::filemanager_thread::FileManager;
use net::hsts::HstsEntry;
use net::protocols::ProtocolRegistry;
use net::request_intercepter::RequestIntercepter;
use net::resource_thread::CoreResourceThreadPool;
use net_traits::filemanager_thread::FileTokenCheck;
use net_traits::http_status::HttpStatus;
@ -47,8 +48,9 @@ use uuid::Uuid;
use crate::http_loader::{expect_devtools_http_request, expect_devtools_http_response};
use crate::{
create_embedder_proxy, create_http_state, fetch, fetch_with_context, fetch_with_cors_cache,
make_body, make_server, make_ssl_server, new_fetch_context, DEFAULT_USER_AGENT,
create_embedder_proxy, create_embedder_proxy_and_receiver, create_http_state, fetch,
fetch_with_context, fetch_with_cors_cache, make_body, make_server, make_ssl_server,
new_fetch_context, DEFAULT_USER_AGENT,
};
// TODO write a struct that impls Handler for storing test values
@ -695,15 +697,18 @@ fn test_fetch_with_hsts() {
let (server, url) = make_ssl_server(handler);
let embedder_proxy = create_embedder_proxy();
let mut context = FetchContext {
state: Arc::new(create_http_state(None)),
user_agent: DEFAULT_USER_AGENT.into(),
devtools_chan: None,
filemanager: Arc::new(Mutex::new(FileManager::new(
create_embedder_proxy(),
embedder_proxy.clone(),
Weak::new(),
))),
file_token: FileTokenCheck::NotRequired,
request_intercepter: Arc::new(Mutex::new(RequestIntercepter::new(embedder_proxy))),
cancellation_listener: Arc::new(Default::default()),
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
ResourceTimingType::Navigation,
@ -752,15 +757,18 @@ fn test_load_adds_host_to_hsts_list_when_url_is_https() {
let (server, mut url) = make_ssl_server(handler);
url.as_mut_url().set_scheme("https").unwrap();
let embedder_proxy = create_embedder_proxy();
let mut context = FetchContext {
state: Arc::new(create_http_state(None)),
user_agent: DEFAULT_USER_AGENT.into(),
devtools_chan: None,
filemanager: Arc::new(Mutex::new(FileManager::new(
create_embedder_proxy(),
embedder_proxy.clone(),
Weak::new(),
))),
file_token: FileTokenCheck::NotRequired,
request_intercepter: Arc::new(Mutex::new(RequestIntercepter::new(embedder_proxy))),
cancellation_listener: Arc::new(Default::default()),
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
ResourceTimingType::Navigation,
@ -811,15 +819,18 @@ fn test_fetch_self_signed() {
let (server, mut url) = make_ssl_server(handler);
url.as_mut_url().set_scheme("https").unwrap();
let embedder_proxy = create_embedder_proxy();
let mut context = FetchContext {
state: Arc::new(create_http_state(None)),
user_agent: DEFAULT_USER_AGENT.into(),
devtools_chan: None,
filemanager: Arc::new(Mutex::new(FileManager::new(
create_embedder_proxy(),
embedder_proxy.clone(),
Weak::new(),
))),
file_token: FileTokenCheck::NotRequired,
request_intercepter: Arc::new(Mutex::new(RequestIntercepter::new(embedder_proxy))),
cancellation_listener: Arc::new(Default::default()),
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
ResourceTimingType::Navigation,
@ -1320,3 +1331,102 @@ fn test_fetch_with_devtools() {
assert_eq!(devhttprequest, httprequest);
assert_eq!(devhttpresponse, httpresponse);
}
#[test]
fn test_fetch_request_intercepted() {
static BODY_PART1: &[u8] = b"Request is";
static BODY_PART2: &[u8] = b" intercepted";
static EXPECTED_BODY: &[u8] = b"Request is intercepted";
static HEADERNAME: &str = "custom-header";
static HEADERVALUE: &str = "custom-value";
static STATUS_MESSAGE: &[u8] = b"custom status message";
let (embedder_proxy, mut embedder_receiver) = create_embedder_proxy_and_receiver();
std::thread::spawn(move || {
let (_browser_context_id, embedder_msg) = embedder_receiver.recv_embedder_msg();
match embedder_msg {
embedder_traits::EmbedderMsg::WebResourceRequested(
web_resource_request,
response_sender,
) => {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static(HEADERNAME),
HeaderValue::from_static(HEADERVALUE),
);
let response =
embedder_traits::WebResourceResponse::new(web_resource_request.url.clone())
.headers(headers)
.status_code(StatusCode::FOUND)
.status_message(STATUS_MESSAGE.to_vec());
let msg = embedder_traits::WebResourceResponseMsg::Start(response);
let _ = response_sender.send(msg);
let msg2 = embedder_traits::WebResourceResponseMsg::Body(
embedder_traits::HttpBodyData::Chunk(BODY_PART1.to_vec()),
);
let _ = response_sender.send(msg2);
let msg3 = embedder_traits::WebResourceResponseMsg::Body(
embedder_traits::HttpBodyData::Chunk(BODY_PART2.to_vec()),
);
let _ = response_sender.send(msg3);
let _ = response_sender.send(embedder_traits::WebResourceResponseMsg::Body(
embedder_traits::HttpBodyData::Done,
));
},
_ => unreachable!(),
}
});
let mut context = FetchContext {
state: Arc::new(create_http_state(None)),
user_agent: DEFAULT_USER_AGENT.into(),
devtools_chan: None,
filemanager: Arc::new(Mutex::new(FileManager::new(
embedder_proxy.clone(),
Weak::new(),
))),
file_token: FileTokenCheck::NotRequired,
request_intercepter: Arc::new(Mutex::new(RequestIntercepter::new(embedder_proxy))),
cancellation_listener: Arc::new(Default::default()),
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
ResourceTimingType::Navigation,
))),
protocols: Arc::new(ProtocolRegistry::default()),
};
let url = ServoUrl::parse("http://www.example.org").unwrap();
let request = RequestBuilder::new(url.clone(), Referrer::NoReferrer)
.origin(url.origin())
.build();
let response = fetch_with_context(request, &mut context);
assert!(
response
.headers
.get(HEADERNAME)
.map(|v| v == HEADERVALUE)
.unwrap_or(false),
"The custom header does not exist or has an incorrect value!"
);
let body = response.body.lock().unwrap();
match &*body {
ResponseBody::Done(data) => {
assert_eq!(data, &EXPECTED_BODY, "Body content does not match");
},
_ => panic!("Expected ResponseBody::Done, but got {:?}", *body),
}
assert_eq!(
response.status.code(),
StatusCode::FOUND,
"Status code does not match!"
);
assert_eq!(
response.status.message(),
STATUS_MESSAGE,
"The status_message was not set correctly!"
);
}

View file

@ -42,6 +42,7 @@ use net::fetch::cors_cache::CorsCache;
use net::fetch::methods::{self, FetchContext};
use net::filemanager_thread::FileManager;
use net::protocols::ProtocolRegistry;
use net::request_intercepter::RequestIntercepter;
use net::resource_thread::CoreResourceThreadPool;
use net::test::HttpState;
use net_traits::filemanager_thread::FileTokenCheck;
@ -128,7 +129,7 @@ fn receive_credential_prompt_msgs(
username: Option<String>,
password: Option<String>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
std::thread::spawn(move || loop {
let (_browser_context_id, embedder_msg) = embedder_receiver.recv_embedder_msg();
match embedder_msg {
embedder_traits::EmbedderMsg::Prompt(prompt_definition, _prompt_origin) => {
@ -140,7 +141,9 @@ fn receive_credential_prompt_msgs(
},
_ => unreachable!(),
}
break;
},
embedder_traits::EmbedderMsg::WebResourceRequested(_, _) => {},
_ => unreachable!(),
}
})
@ -179,10 +182,11 @@ fn new_fetch_context(
user_agent: DEFAULT_USER_AGENT.into(),
devtools_chan: dc.map(|dc| Arc::new(Mutex::new(dc))),
filemanager: Arc::new(Mutex::new(FileManager::new(
sender,
sender.clone(),
pool_handle.unwrap_or_else(|| Weak::new()),
))),
file_token: FileTokenCheck::NotRequired,
request_intercepter: Arc::new(Mutex::new(RequestIntercepter::new(sender))),
cancellation_listener: Arc::new(Default::default()),
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
ResourceTimingType::Navigation,

View file

@ -18,10 +18,14 @@ webxr = ["dep:webxr-api"]
base = { workspace = true }
cfg-if = { workspace = true }
crossbeam-channel = { workspace = true }
http = { workspace = true }
hyper_serde = { workspace = true }
ipc-channel = { workspace = true }
keyboard-types = { workspace = true }
log = { workspace = true }
num-derive = "0.4"
malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true }
num-traits = { workspace = true }
serde = { workspace = true }
servo_url = { path = "../../url" }

View file

@ -8,9 +8,11 @@ use std::fmt::{Debug, Error, Formatter};
use base::id::{PipelineId, TopLevelBrowsingContextId, WebViewId};
use crossbeam_channel::{Receiver, Sender};
use http::{HeaderMap, Method, StatusCode};
use ipc_channel::ipc::IpcSender;
use keyboard_types::KeyboardEvent;
use log::warn;
use malloc_size_of_derive::MallocSizeOf;
use num_derive::FromPrimitive;
use serde::{Deserialize, Serialize};
use servo_url::ServoUrl;
@ -212,6 +214,7 @@ pub enum EmbedderMsg {
LoadStart,
/// The load of a page has completed
LoadComplete,
WebResourceRequested(WebResourceRequest, IpcSender<WebResourceResponseMsg>),
/// A pipeline panicked. First string is the reason, second one is the backtrace.
Panic(String, Option<String>),
/// Open dialog to select bluetooth device.
@ -282,6 +285,7 @@ impl Debug for EmbedderMsg {
EmbedderMsg::SetFullscreenState(..) => write!(f, "SetFullscreenState"),
EmbedderMsg::LoadStart => write!(f, "LoadStart"),
EmbedderMsg::LoadComplete => write!(f, "LoadComplete"),
EmbedderMsg::WebResourceRequested(..) => write!(f, "WebResourceRequested"),
EmbedderMsg::Panic(..) => write!(f, "Panic"),
EmbedderMsg::GetSelectedBluetoothDevice(..) => write!(f, "GetSelectedBluetoothDevice"),
EmbedderMsg::SelectFiles(..) => write!(f, "SelectFiles"),
@ -437,3 +441,101 @@ pub struct DualRumbleEffectParams {
pub enum GamepadHapticEffectType {
DualRumble(DualRumbleEffectParams),
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct WebResourceRequest {
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
#[ignore_malloc_size_of = "Defined in hyper"]
pub method: Method,
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
#[ignore_malloc_size_of = "Defined in hyper"]
pub headers: HeaderMap,
pub url: ServoUrl,
pub is_for_main_frame: bool,
pub is_redirect: bool,
}
impl WebResourceRequest {
pub fn new(
method: Method,
headers: HeaderMap,
url: ServoUrl,
is_for_main_frame: bool,
is_redirect: bool,
) -> Self {
WebResourceRequest {
method,
url,
headers,
is_for_main_frame,
is_redirect,
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub enum WebResourceResponseMsg {
// Response of WebResourceRequest, no body included.
Start(WebResourceResponse),
// send a body chunk. It is expected Response sent before body.
Body(HttpBodyData),
// not to override the response.
None,
}
#[derive(Clone, Deserialize, Serialize)]
pub enum HttpBodyData {
Chunk(Vec<u8>),
Done,
Cancelled,
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct WebResourceResponse {
pub url: ServoUrl,
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
#[ignore_malloc_size_of = "Defined in hyper"]
pub headers: HeaderMap,
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
#[ignore_malloc_size_of = "Defined in hyper"]
pub status_code: StatusCode,
pub status_message: Vec<u8>,
}
impl WebResourceResponse {
pub fn new(url: ServoUrl) -> WebResourceResponse {
WebResourceResponse {
url,
headers: HeaderMap::new(),
status_code: StatusCode::OK,
status_message: b"OK".to_vec(),
}
}
pub fn headers(mut self, headers: HeaderMap) -> WebResourceResponse {
self.headers = headers;
self
}
pub fn status_code(mut self, status_code: StatusCode) -> WebResourceResponse {
self.status_code = status_code;
self
}
pub fn status_message(mut self, status_message: Vec<u8>) -> WebResourceResponse {
self.status_message = status_message;
self
}
}

View file

@ -154,6 +154,7 @@ mod from_servo {
Self::WebViewClosed(..) => target!("WebViewClosed"),
Self::WebViewFocused(..) => target!("WebViewFocused"),
Self::WebViewBlurred => target!("WebViewBlurred"),
Self::WebResourceRequested(..) => target!("WebResourceRequested"),
Self::AllowUnload(..) => target!("AllowUnload"),
Self::Keyboard(..) => target!("Keyboard"),
Self::ClearClipboardContents => target!("ClearClipboardContents"),

View file

@ -936,6 +936,7 @@ where
}
}
},
EmbedderMsg::WebResourceRequested(_web_resource_request, _response_sender) => {},
EmbedderMsg::Shutdown => {
self.shutdown_requested = true;
},

View file

@ -645,7 +645,8 @@ impl ServoGlue {
EmbedderMsg::EventDelivered(..) |
EmbedderMsg::PlayGamepadHapticEffect(..) |
EmbedderMsg::StopGamepadHapticEffect(..) |
EmbedderMsg::ClearClipboardContents => {},
EmbedderMsg::ClearClipboardContents |
EmbedderMsg::WebResourceRequested(..) => {},
}
}