mirror of
https://github.com/servo/servo.git
synced 2025-08-05 05:30:08 +01:00
Implement initial version of navigator.sendBeacon
(#38301)
Gated behind the feature flag `dom_navigator_sendbeacon_enabled` as the `keep-alive` fetch parameter is crucial for real-life use cases such as analytics requests. Part of #4577 Part of #38302 Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
parent
181f97879d
commit
dbb886fad2
22 changed files with 237 additions and 29 deletions
|
@ -99,6 +99,7 @@ pub struct Preferences {
|
|||
pub dom_microdata_testing_enabled: bool,
|
||||
pub dom_mouse_event_which_enabled: bool,
|
||||
pub dom_mutation_observer_enabled: bool,
|
||||
pub dom_navigator_sendbeacon_enabled: bool,
|
||||
pub dom_notification_enabled: bool,
|
||||
pub dom_offscreen_canvas_enabled: bool,
|
||||
pub dom_permissions_enabled: bool,
|
||||
|
@ -279,6 +280,7 @@ impl Preferences {
|
|||
dom_microdata_testing_enabled: false,
|
||||
dom_mouse_event_which_enabled: false,
|
||||
dom_mutation_observer_enabled: true,
|
||||
dom_navigator_sendbeacon_enabled: false,
|
||||
dom_notification_enabled: false,
|
||||
dom_offscreen_canvas_enabled: false,
|
||||
dom_permissions_enabled: false,
|
||||
|
|
|
@ -4,28 +4,45 @@
|
|||
|
||||
use std::cell::Cell;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
use dom_struct::dom_struct;
|
||||
use headers::HeaderMap;
|
||||
use http::header::{self, HeaderValue};
|
||||
use js::rust::MutableHandleValue;
|
||||
use net_traits::request::{
|
||||
CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
|
||||
is_cors_safelisted_request_content_type,
|
||||
};
|
||||
use net_traits::{
|
||||
FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType,
|
||||
};
|
||||
use servo_config::pref;
|
||||
use servo_url::ServoUrl;
|
||||
|
||||
use crate::body::Extractable;
|
||||
use crate::dom::bindings::cell::DomRefCell;
|
||||
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
|
||||
use crate::dom::bindings::error::{Error, Fallible};
|
||||
use crate::dom::bindings::refcounted::Trusted;
|
||||
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
|
||||
use crate::dom::bindings::root::{DomRoot, MutNullableDom};
|
||||
use crate::dom::bindings::str::DOMString;
|
||||
use crate::dom::bindings::str::{DOMString, USVString};
|
||||
use crate::dom::bindings::utils::to_frozen_array;
|
||||
#[cfg(feature = "bluetooth")]
|
||||
use crate::dom::bluetooth::Bluetooth;
|
||||
use crate::dom::clipboard::Clipboard;
|
||||
use crate::dom::csp::{GlobalCspReporting, Violation};
|
||||
use crate::dom::gamepad::Gamepad;
|
||||
use crate::dom::gamepadevent::GamepadEventType;
|
||||
use crate::dom::globalscope::GlobalScope;
|
||||
use crate::dom::mediadevices::MediaDevices;
|
||||
use crate::dom::mediasession::MediaSession;
|
||||
use crate::dom::mimetypearray::MimeTypeArray;
|
||||
use crate::dom::navigatorinfo;
|
||||
use crate::dom::performanceresourcetiming::InitiatorType;
|
||||
use crate::dom::permissions::Permissions;
|
||||
use crate::dom::pluginarray::PluginArray;
|
||||
use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
|
||||
|
@ -35,6 +52,7 @@ use crate::dom::webgpu::gpu::GPU;
|
|||
use crate::dom::window::Window;
|
||||
#[cfg(feature = "webxr")]
|
||||
use crate::dom::xrsystem::XRSystem;
|
||||
use crate::network_listener::{PreInvoke, ResourceTimingListener, submit_timing};
|
||||
use crate::script_runtime::{CanGc, JSContext};
|
||||
|
||||
pub(super) fn hardware_concurrency() -> u64 {
|
||||
|
@ -320,9 +338,159 @@ impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
|
|||
.or_init(|| Clipboard::new(&self.global(), CanGc::note()))
|
||||
}
|
||||
|
||||
/// <https://w3c.github.io/beacon/#sec-processing-model>
|
||||
fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
|
||||
let global = self.global();
|
||||
// Step 1. Set base to this's relevant settings object's API base URL.
|
||||
let base = global.api_base_url();
|
||||
// Step 2. Set origin to this's relevant settings object's origin.
|
||||
let origin = global.origin().immutable().clone();
|
||||
// Step 3. Set parsedUrl to the result of the URL parser steps with url and base.
|
||||
// If the algorithm returns an error, or if parsedUrl's scheme is not "http" or "https",
|
||||
// throw a "TypeError" exception and terminate these steps.
|
||||
let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
|
||||
return Err(Error::Type("Cannot parse URL".to_owned()));
|
||||
};
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return Err(Error::Type("URL is not http(s)".to_owned()));
|
||||
}
|
||||
let mut request_body = None;
|
||||
// Step 4. Let headerList be an empty list.
|
||||
let mut headers = HeaderMap::with_capacity(1);
|
||||
// Step 5. Let corsMode be "no-cors".
|
||||
let mut cors_mode = RequestMode::NoCors;
|
||||
// Step 6. If data is not null:
|
||||
if let Some(data) = data {
|
||||
// Step 6.1. Set transmittedData and contentType to the result of extracting data's byte stream
|
||||
// with the keepalive flag set.
|
||||
let extracted_body = data.extract(&global, can_gc)?;
|
||||
// Step 6.2. If the amount of data that can be queued to be sent by keepalive enabled requests
|
||||
// is exceeded by the size of transmittedData (as defined in HTTP-network-or-cache fetch),
|
||||
// set the return value to false and terminate these steps.
|
||||
if let Some(total_bytes) = extracted_body.total_bytes {
|
||||
if total_bytes > 64 * 1024 {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
// Step 6.3. If contentType is not null:
|
||||
if let Some(content_type) = extracted_body.content_type.as_ref() {
|
||||
// Set corsMode to "cors".
|
||||
cors_mode = RequestMode::CorsMode;
|
||||
// If contentType value is a CORS-safelisted request-header value for the Content-Type header,
|
||||
// set corsMode to "no-cors".
|
||||
if is_cors_safelisted_request_content_type(content_type.as_bytes()) {
|
||||
cors_mode = RequestMode::NoCors;
|
||||
}
|
||||
// Append a Content-Type header with value contentType to headerList.
|
||||
//
|
||||
// We cannot use typed header insertion with `mime::Mime` parsing here,
|
||||
// since it lowercases `charset=UTF-8`: https://github.com/hyperium/mime/issues/116
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(content_type).unwrap(),
|
||||
);
|
||||
}
|
||||
request_body = Some(extracted_body.into_net_request_body().0);
|
||||
}
|
||||
// Step 7.1. Let req be a new request, initialized as follows:
|
||||
let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
|
||||
.mode(cors_mode)
|
||||
.destination(Destination::None)
|
||||
.policy_container(global.policy_container())
|
||||
.insecure_requests_policy(global.insecure_requests_policy())
|
||||
.has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
|
||||
.method(http::Method::POST)
|
||||
.body(request_body)
|
||||
.origin(origin)
|
||||
// TODO: Set keep-alive flag
|
||||
.credentials_mode(CredentialsMode::Include)
|
||||
.headers(headers);
|
||||
// Step 7.2. Fetch req.
|
||||
global.fetch(
|
||||
request,
|
||||
Arc::new(Mutex::new(BeaconFetchListener {
|
||||
url,
|
||||
global: Trusted::new(&global),
|
||||
resource_timing: ResourceFetchTiming::new(ResourceTimingType::None),
|
||||
})),
|
||||
global.task_manager().networking_task_source().into(),
|
||||
);
|
||||
// Step 7. Set the return value to true, return the sendBeacon() call,
|
||||
// and continue to run the following steps in parallel:
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// <https://servo.org/internal-no-spec>
|
||||
fn Servo(&self) -> DomRoot<ServoInternals> {
|
||||
self.servo_internals
|
||||
.or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
|
||||
}
|
||||
}
|
||||
|
||||
struct BeaconFetchListener {
|
||||
/// URL of this request.
|
||||
url: ServoUrl,
|
||||
/// Timing data for this resource.
|
||||
resource_timing: ResourceFetchTiming,
|
||||
/// The global object fetching the report uri violation
|
||||
global: Trusted<GlobalScope>,
|
||||
}
|
||||
|
||||
impl FetchResponseListener for BeaconFetchListener {
|
||||
fn process_request_body(&mut self, _: RequestId) {}
|
||||
|
||||
fn process_request_eof(&mut self, _: RequestId) {}
|
||||
|
||||
fn process_response(
|
||||
&mut self,
|
||||
_: RequestId,
|
||||
fetch_metadata: Result<FetchMetadata, NetworkError>,
|
||||
) {
|
||||
_ = fetch_metadata;
|
||||
}
|
||||
|
||||
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
|
||||
_ = chunk;
|
||||
}
|
||||
|
||||
fn process_response_eof(
|
||||
&mut self,
|
||||
_: RequestId,
|
||||
response: Result<ResourceFetchTiming, NetworkError>,
|
||||
) {
|
||||
_ = response;
|
||||
}
|
||||
|
||||
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
|
||||
&mut self.resource_timing
|
||||
}
|
||||
|
||||
fn resource_timing(&self) -> &ResourceFetchTiming {
|
||||
&self.resource_timing
|
||||
}
|
||||
|
||||
fn submit_resource_timing(&mut self) {
|
||||
submit_timing(self, CanGc::note())
|
||||
}
|
||||
|
||||
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
|
||||
let global = self.resource_timing_global();
|
||||
global.report_csp_violations(violations, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceTimingListener for BeaconFetchListener {
|
||||
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
|
||||
(InitiatorType::Beacon, self.url.clone())
|
||||
}
|
||||
|
||||
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
|
||||
self.global.root()
|
||||
}
|
||||
}
|
||||
|
||||
impl PreInvoke for BeaconFetchListener {
|
||||
fn should_invoke(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,10 @@ use crate::script_runtime::CanGc;
|
|||
// TODO Cross origin resources MUST BE INCLUDED as PerformanceResourceTiming objects
|
||||
// https://w3c.github.io/resource-timing/#sec-cross-origin-resources
|
||||
|
||||
// TODO CSS, Beacon
|
||||
// TODO CSS
|
||||
#[derive(Debug, JSTraceable, MallocSizeOf, PartialEq)]
|
||||
pub(crate) enum InitiatorType {
|
||||
Beacon,
|
||||
LocalName(String),
|
||||
Navigation,
|
||||
XMLHttpRequest,
|
||||
|
@ -191,6 +192,7 @@ impl PerformanceResourceTimingMethods<crate::DomTypeHolder> for PerformanceResou
|
|||
// https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-initiatortype
|
||||
fn InitiatorType(&self) -> DOMString {
|
||||
match self.initiator_type {
|
||||
InitiatorType::Beacon => DOMString::from("beacon"),
|
||||
InitiatorType::LocalName(ref n) => DOMString::from(n.clone()),
|
||||
InitiatorType::Navigation => DOMString::from("navigation"),
|
||||
InitiatorType::XMLHttpRequest => DOMString::from("xmlhttprequest"),
|
||||
|
|
|
@ -503,7 +503,7 @@ DOMInterfaces = {
|
|||
|
||||
'Navigator': {
|
||||
'inRealms': ['GetVRDisplays'],
|
||||
'canGc': ['Languages'],
|
||||
'canGc': ['Languages', 'SendBeacon'],
|
||||
},
|
||||
|
||||
'Node': {
|
||||
|
|
|
@ -81,3 +81,9 @@ interface mixin NavigatorConcurrentHardware {
|
|||
partial interface Navigator {
|
||||
[SecureContext, SameObject, Pref="dom_async_clipboard_enabled"] readonly attribute Clipboard clipboard;
|
||||
};
|
||||
|
||||
// https://w3c.github.io/beacon/#sendbeacon-method
|
||||
partial interface Navigator {
|
||||
[Throws, Pref="dom_navigator_sendbeacon_enabled"]
|
||||
boolean sendBeacon(USVString url, optional BodyInit? data = null);
|
||||
};
|
||||
|
|
|
@ -801,7 +801,7 @@ fn is_cors_safelisted_language(value: &[u8]) -> bool {
|
|||
|
||||
// https://fetch.spec.whatwg.org/#cors-safelisted-request-header
|
||||
// subclause `content-type`
|
||||
fn is_cors_safelisted_request_content_type(value: &[u8]) -> bool {
|
||||
pub fn is_cors_safelisted_request_content_type(value: &[u8]) -> bool {
|
||||
// step 1
|
||||
if value.iter().any(is_cors_unsafe_request_header_byte) {
|
||||
return false;
|
||||
|
|
|
@ -591,6 +591,7 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing
|
|||
"dom_fontface_enabled",
|
||||
"dom_intersection_observer_enabled",
|
||||
"dom_mouse_event_which_enabled",
|
||||
"dom_navigator_sendbeacon_enabled",
|
||||
"dom_notification_enabled",
|
||||
"dom_offscreen_canvas_enabled",
|
||||
"dom_permissions_enabled",
|
||||
|
|
2
tests/wpt/include.ini
vendored
2
tests/wpt/include.ini
vendored
|
@ -3,6 +3,8 @@ skip: true
|
|||
skip: false
|
||||
[_webgl]
|
||||
skip: false
|
||||
[beacon]
|
||||
skip: false
|
||||
[clipboard-apis]
|
||||
skip: false
|
||||
[console]
|
||||
|
|
12
tests/wpt/meta/beacon/beacon-basic.https.window.js.ini
vendored
Normal file
12
tests/wpt/meta/beacon/beacon-basic.https.window.js.ini
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[beacon-basic.https.window.html]
|
||||
[Payload size restriction should be accumulated: type = string]
|
||||
expected: FAIL
|
||||
|
||||
[Payload size restriction should be accumulated: type = arraybuffer]
|
||||
expected: FAIL
|
||||
|
||||
[Payload size restriction should be accumulated: type = blob]
|
||||
expected: FAIL
|
||||
|
||||
[sendBeacon() with a stream does not work due to the keepalive flag being set]
|
||||
expected: FAIL
|
6
tests/wpt/meta/beacon/beacon-cors.https.window.js.ini
vendored
Normal file
6
tests/wpt/meta/beacon/beacon-cors.https.window.js.ini
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[beacon-cors.https.window.html]
|
||||
[cross-origin, non-CORS-safelisted: failure case (without redirect)]
|
||||
expected: FAIL
|
||||
|
||||
[cross-origin, non-CORS-safelisted[credentials=false\]]
|
||||
expected: FAIL
|
4
tests/wpt/meta/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html.ini
vendored
Normal file
4
tests/wpt/meta/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html.ini
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[header-referrer-no-referrer-when-downgrade.https.html]
|
||||
expected: TIMEOUT
|
||||
[Test referer header http://web-platform.test:8000/beacon/resources/]
|
||||
expected: TIMEOUT
|
4
tests/wpt/meta/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html.ini
vendored
Normal file
4
tests/wpt/meta/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html.ini
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[header-referrer-strict-origin-when-cross-origin.https.html]
|
||||
expected: TIMEOUT
|
||||
[Test referer header http://www1.web-platform.test:8000/beacon/resources/]
|
||||
expected: TIMEOUT
|
4
tests/wpt/meta/beacon/headers/header-referrer-strict-origin.https.html.ini
vendored
Normal file
4
tests/wpt/meta/beacon/headers/header-referrer-strict-origin.https.html.ini
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[header-referrer-strict-origin.https.html]
|
||||
expected: TIMEOUT
|
||||
[Test referer header http://web-platform.test:8000/beacon/resources/]
|
||||
expected: TIMEOUT
|
4
tests/wpt/meta/beacon/headers/header-referrer-unsafe-url.https.html.ini
vendored
Normal file
4
tests/wpt/meta/beacon/headers/header-referrer-unsafe-url.https.html.ini
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[header-referrer-unsafe-url.https.html]
|
||||
expected: TIMEOUT
|
||||
[Test referer header http://web-platform.test:8000/beacon/resources/]
|
||||
expected: TIMEOUT
|
|
@ -1,3 +0,0 @@
|
|||
[connect-src-beacon-allowed.sub.html]
|
||||
[Expecting logs: ["Pass"\]]
|
||||
expected: NOTRUN
|
|
@ -1,3 +0,0 @@
|
|||
[connect-src-beacon-blocked.sub.html]
|
||||
[Expecting logs: ["Pass", "violated-directive=connect-src"\]]
|
||||
expected: NOTRUN
|
|
@ -1,3 +0,0 @@
|
|||
[connect-src-beacon-redirect-to-blocked.sub.html]
|
||||
[Expecting logs: ["violated-directive=connect-src"\]]
|
||||
expected: NOTRUN
|
|
@ -14,9 +14,6 @@
|
|||
[source : unpaired surrogate codepoint should be replaced with U+FFFD]
|
||||
expected: FAIL
|
||||
|
||||
[sendBeacon URL: unpaired surrogate codepoint should not make any exceptions.]
|
||||
expected: FAIL
|
||||
|
||||
[RegisterProtocolHandler URL: unpaired surrogate codepoint should not make any exceptions.]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[beacon.https.html]
|
||||
[Mixed-Content: Expects allowed for beacon to same-https origin and keep-scheme redirection from https context.]
|
||||
[Mixed-Content: Expects blocked for beacon to cross-https origin and swap-scheme redirection from https context.]
|
||||
expected: FAIL
|
||||
|
||||
[Mixed-Content: Expects allowed for beacon to same-https origin and no-redirect redirection from https context.]
|
||||
[Mixed-Content: Expects blocked for beacon to same-https origin and swap-scheme redirection from https context.]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[beacon.https.html]
|
||||
[Mixed-Content: Expects allowed for beacon to same-https origin and no-redirect redirection from https context.]
|
||||
expected: FAIL
|
|
@ -1,6 +0,0 @@
|
|||
[beacon.https.html]
|
||||
[Mixed-Content: Expects allowed for beacon to same-https origin and keep-scheme redirection from https context.]
|
||||
expected: FAIL
|
||||
|
||||
[Mixed-Content: Expects allowed for beacon to same-https origin and no-redirect redirection from https context.]
|
||||
expected: FAIL
|
|
@ -1,2 +1,16 @@
|
|||
[misc.html]
|
||||
expected: ERROR
|
||||
expected: TIMEOUT
|
||||
[The initiator type for <body background> must be 'body']
|
||||
expected: FAIL
|
||||
|
||||
[The initiator type for <input type='image'> must be 'input']
|
||||
expected: FAIL
|
||||
|
||||
[The initiator type for <object type='image/png'> must be 'object']
|
||||
expected: FAIL
|
||||
|
||||
[The initiator type for for fetch() must be 'fetch']
|
||||
expected: FAIL
|
||||
|
||||
[The initiator type for new EventSource() must be 'other']
|
||||
expected: TIMEOUT
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue