diff --git a/Cargo.lock b/Cargo.lock index 98f82a6c126..0aba4f58742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 6c9924341da..6f90737c905 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -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"), diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 76b4da0ed31..cfaa22fc455 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -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>>>, pub filemanager: Arc>, pub file_token: FileTokenCheck, + pub request_intercepter: Arc>, pub cancellation_listener: Arc, pub timing: ServoArc>, pub protocols: Arc, @@ -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, +) { + 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 => { diff --git a/components/net/lib.rs b/components/net/lib.rs index 123ec93cd24..d97cf1ccd1a 100644 --- a/components/net/lib.rs +++ b/components/net/lib.rs @@ -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; diff --git a/components/net/request_intercepter.rs b/components/net/request_intercepter.rs new file mode 100644 index 00000000000..8bb55bd2882 --- /dev/null +++ b/components/net/request_intercepter.rs @@ -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, + 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; + }, + } + } + } +} diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index 12278d4c326..e16c5f541f3 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -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>, sw_managers: HashMap>, filemanager: FileManager, + request_intercepter: RequestIntercepter, thread_pool: Arc, 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, diff --git a/components/net/tests/fetch.rs b/components/net/tests/fetch.rs index fba84589455..ea51fe5d757 100644 --- a/components/net/tests/fetch.rs +++ b/components/net/tests/fetch.rs @@ -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!" + ); +} diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs index 12440e3cc47..a897d5d5bfa 100644 --- a/components/net/tests/main.rs +++ b/components/net/tests/main.rs @@ -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, password: Option, ) -> 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, diff --git a/components/shared/embedder/Cargo.toml b/components/shared/embedder/Cargo.toml index 49c1b783fa6..b4128bfba2b 100644 --- a/components/shared/embedder/Cargo.toml +++ b/components/shared/embedder/Cargo.toml @@ -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" } diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index b03fa1035df..286dc51bff7 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -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), /// A pipeline panicked. First string is the reason, second one is the backtrace. Panic(String, Option), /// 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), + 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, +} + +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) -> WebResourceResponse { + self.status_message = status_message; + self + } +} diff --git a/ports/servoshell/desktop/tracing.rs b/ports/servoshell/desktop/tracing.rs index 91538ae6772..097ec01fbc5 100644 --- a/ports/servoshell/desktop/tracing.rs +++ b/ports/servoshell/desktop/tracing.rs @@ -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"), diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index 771e3f85e2a..2e1a85321c2 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/webview.rs @@ -936,6 +936,7 @@ where } } }, + EmbedderMsg::WebResourceRequested(_web_resource_request, _response_sender) => {}, EmbedderMsg::Shutdown => { self.shutdown_requested = true; }, diff --git a/ports/servoshell/egl/servo_glue.rs b/ports/servoshell/egl/servo_glue.rs index 616a2b47321..58db0b6fa7d 100644 --- a/ports/servoshell/egl/servo_glue.rs +++ b/ports/servoshell/egl/servo_glue.rs @@ -645,7 +645,8 @@ impl ServoGlue { EmbedderMsg::EventDelivered(..) | EmbedderMsg::PlayGamepadHapticEffect(..) | EmbedderMsg::StopGamepadHapticEffect(..) | - EmbedderMsg::ClearClipboardContents => {}, + EmbedderMsg::ClearClipboardContents | + EmbedderMsg::WebResourceRequested(..) => {}, } }