mirror of
https://github.com/servo/servo.git
synced 2025-07-15 11:23:39 +01:00
Does not yet handle failures of endpoints, which requires us to update metadata. I don't see that metadata being used anywhere, so I am not sure if there is WPT coverage for it. Part of #37238 Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
374 lines
15 KiB
Rust
374 lines
15 KiB
Rust
/* 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 std::collections::HashMap;
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
use headers::{ContentType, HeaderMapExt};
|
||
use http::HeaderMap;
|
||
use hyper_serde::Serde;
|
||
use malloc_size_of_derive::MallocSizeOf;
|
||
use net_traits::request::{
|
||
CredentialsMode, Destination, RequestBody, RequestId, RequestMode,
|
||
create_request_body_with_content,
|
||
};
|
||
use net_traits::{
|
||
FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType,
|
||
};
|
||
use script_bindings::str::DOMString;
|
||
use serde::Serialize;
|
||
use servo_url::{ImmutableOrigin, ServoUrl};
|
||
|
||
use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
|
||
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
|
||
use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::SecurityPolicyViolationEventDisposition;
|
||
use crate::dom::bindings::refcounted::Trusted;
|
||
use crate::dom::bindings::root::DomRoot;
|
||
use crate::dom::csp::Violation;
|
||
use crate::dom::csppolicyviolationreport::serialize_disposition;
|
||
use crate::dom::globalscope::GlobalScope;
|
||
use crate::dom::performanceresourcetiming::InitiatorType;
|
||
use crate::fetch::create_a_potential_cors_request;
|
||
use crate::network_listener::{PreInvoke, ResourceTimingListener, submit_timing};
|
||
use crate::script_runtime::CanGc;
|
||
|
||
/// <https://w3c.github.io/reporting/#endpoint>
|
||
#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
|
||
pub(crate) struct ReportingEndpoint {
|
||
/// <https://w3c.github.io/reporting/#dom-endpoint-name>
|
||
name: DOMString,
|
||
/// <https://w3c.github.io/reporting/#dom-endpoint-url>
|
||
url: ServoUrl,
|
||
/// <https://w3c.github.io/reporting/#dom-endpoint-failures>
|
||
failures: u32,
|
||
}
|
||
|
||
impl ReportingEndpoint {
|
||
/// <https://w3c.github.io/reporting/#process-header>
|
||
pub(crate) fn parse_reporting_endpoints_header(
|
||
response_url: &ServoUrl,
|
||
headers: &Option<Serde<HeaderMap>>,
|
||
) -> Option<Vec<ReportingEndpoint>> {
|
||
let headers = headers.as_ref()?;
|
||
let reporting_headers = headers.get_all("reporting-endpoints");
|
||
// Step 2. Let parsed header be the result of executing get a structured field value
|
||
// given "Reporting-Endpoints" and "dictionary" from response’s header list.
|
||
let mut parsed_header = Vec::new();
|
||
for header in reporting_headers.iter() {
|
||
let Some(header_value) = header.to_str().ok() else {
|
||
continue;
|
||
};
|
||
parsed_header.append(&mut header_value.split(",").map(|s| s.trim()).collect());
|
||
}
|
||
// Step 3. If parsed header is null, abort these steps.
|
||
if parsed_header.is_empty() {
|
||
return None;
|
||
}
|
||
// Step 4. Let endpoints be an empty list.
|
||
let mut endpoints = Vec::new();
|
||
// Step 5. For each name → value_and_parameters of parsed header:
|
||
for header in parsed_header {
|
||
// There could be a '=' in the URL itself (for example query parameters). Therefore, we can't
|
||
// split on '=', but instead look for the first one.
|
||
let Some(split_index) = header.find('=') else {
|
||
continue;
|
||
};
|
||
// Step 5.1. Let endpoint url string be the first element of the tuple value_and_parameters.
|
||
// If endpoint url string is not a string, then continue.
|
||
let (name, endpoint_url_string) = header.split_at(split_index);
|
||
let length = endpoint_url_string.len();
|
||
let endpoint_bytes = endpoint_url_string.as_bytes();
|
||
// Note that the first character is the '=' and we check for the next and last character to be '"'
|
||
if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
|
||
continue;
|
||
}
|
||
// The '="' at the start and '"' at the end removed
|
||
let endpoint_url_value = &endpoint_url_string[2..length - 1];
|
||
// Step 5.2. Let endpoint url be the result of executing the URL parser on endpoint url string,
|
||
// with base URL set to response’s url. If endpoint url is failure, then continue.
|
||
let Ok(endpoint_url) =
|
||
ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
|
||
else {
|
||
continue;
|
||
};
|
||
// Step 5.3. If endpoint url’s origin is not potentially trustworthy, then continue.
|
||
if !endpoint_url.is_potentially_trustworthy() {
|
||
continue;
|
||
}
|
||
// Step 5.4. Let endpoint be a new endpoint whose properties are set as follows:
|
||
// Step 5.5. Add endpoint to endpoints.
|
||
endpoints.push(ReportingEndpoint {
|
||
name: name.into(),
|
||
url: endpoint_url,
|
||
failures: 0,
|
||
});
|
||
}
|
||
Some(endpoints)
|
||
}
|
||
}
|
||
|
||
pub(crate) trait SendReportsToEndpoints {
|
||
/// <https://w3c.github.io/reporting/#send-reports>
|
||
fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
|
||
/// <https://w3c.github.io/reporting/#try-delivery>
|
||
fn attempt_to_deliver_reports_to_endpoints(
|
||
&self,
|
||
endpoint: &ServoUrl,
|
||
origin: ImmutableOrigin,
|
||
reports: &[&Report],
|
||
);
|
||
/// <https://w3c.github.io/reporting/#serialize-a-list-of-reports-to-json>
|
||
fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody>;
|
||
}
|
||
|
||
impl SendReportsToEndpoints for GlobalScope {
|
||
fn send_reports_to_endpoints(
|
||
&self,
|
||
mut reports: Vec<Report>,
|
||
endpoints: Vec<ReportingEndpoint>,
|
||
) {
|
||
// Step 1. Let endpoint map be an empty map of endpoint objects to lists of report objects.
|
||
let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
|
||
// Step 2. For each report in reports:
|
||
reports.retain(|report| {
|
||
// Step 2.1. If there exists an endpoint (endpoint) in context’s endpoints
|
||
// list whose name is report’s destination:
|
||
if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
|
||
// Step 2.1.1. Append report to endpoint map’s list of reports for endpoint.
|
||
endpoint_map
|
||
.entry(endpoint)
|
||
.or_default()
|
||
.push(report.clone());
|
||
true
|
||
} else {
|
||
// Step 2.1.2. Otherwise, remove report from reports.
|
||
false
|
||
}
|
||
});
|
||
// Step 3. For each (endpoint, report list) pair in endpoint map:
|
||
for (endpoint, report_list) in endpoint_map.iter() {
|
||
// Step 3.1. Let origin map be an empty map of origins to lists of report objects.
|
||
let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
|
||
// Step 3.2. For each report in report list:
|
||
for report in report_list {
|
||
let Ok(url) = ServoUrl::parse(&report.url) else {
|
||
continue;
|
||
};
|
||
// Step 3.2.1. Let origin be the origin of report’s url.
|
||
let origin = url.origin();
|
||
// Step 3.2.2. Append report to origin map’s list of reports for origin.
|
||
origin_map.entry(origin).or_default().push(report);
|
||
}
|
||
// Step 3.3. For each (origin, per-origin reports) pair in origin map,
|
||
// execute the following steps asynchronously:
|
||
for (origin, origin_report_list) in origin_map.iter() {
|
||
// Step 3.3.1. Let result be the result of executing
|
||
// § 3.5.2 Attempt to deliver reports to endpoint on endpoint, origin, and per-origin reports.
|
||
self.attempt_to_deliver_reports_to_endpoints(
|
||
&endpoint.url,
|
||
origin.clone(),
|
||
origin_report_list,
|
||
);
|
||
// Step 3.3.2. If result is "Failure":
|
||
// TODO(37238)
|
||
// Step 3.3.2.1. Increment endpoint’s failures.
|
||
// TODO(37238)
|
||
// Step 3.3.3. If result is "Remove Endpoint":
|
||
// TODO(37238)
|
||
// Step 3.3.3.1 Remove endpoint from context’s endpoints list.
|
||
// TODO(37238)
|
||
// Step 3.3.4. Remove each report from reports.
|
||
// TODO(37238)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn attempt_to_deliver_reports_to_endpoints(
|
||
&self,
|
||
endpoint: &ServoUrl,
|
||
origin: ImmutableOrigin,
|
||
reports: &[&Report],
|
||
) {
|
||
// Step 1. Let body be the result of executing serialize a list of reports to JSON on reports.
|
||
let request_body = Self::serialize_list_of_reports(reports);
|
||
// Step 2. Let request be a new request with the following properties [FETCH]:
|
||
let mut headers = HeaderMap::with_capacity(1);
|
||
headers.typed_insert(ContentType::from(
|
||
"application/reports+json".parse::<mime::Mime>().unwrap(),
|
||
));
|
||
let request = create_a_potential_cors_request(
|
||
None,
|
||
endpoint.clone(),
|
||
Destination::Report,
|
||
None,
|
||
None,
|
||
self.get_referrer(),
|
||
self.insecure_requests_policy(),
|
||
self.has_trustworthy_ancestor_or_current_origin(),
|
||
self.policy_container(),
|
||
)
|
||
.method(http::Method::POST)
|
||
.body(request_body)
|
||
.origin(origin)
|
||
.mode(RequestMode::CorsMode)
|
||
.credentials_mode(CredentialsMode::CredentialsSameOrigin)
|
||
.unsafe_request(true)
|
||
.headers(headers);
|
||
// Step 3. Queue a task to fetch request.
|
||
self.fetch(
|
||
request,
|
||
Arc::new(Mutex::new(CSPReportEndpointFetchListener {
|
||
endpoint: endpoint.clone(),
|
||
global: Trusted::new(self),
|
||
resource_timing: ResourceFetchTiming::new(ResourceTimingType::None),
|
||
})),
|
||
self.task_manager().networking_task_source().into(),
|
||
);
|
||
// Step 4. Wait for a response (response).
|
||
// TODO(37238)
|
||
// Step 5. If response’s status is an OK status (200-299), return "Success".
|
||
// TODO(37238)
|
||
// Step 6. If response’s status is 410 Gone [RFC9110], return "Remove Endpoint".
|
||
// TODO(37238)
|
||
// Step 7. Return "Failure".
|
||
// TODO(37238)
|
||
}
|
||
|
||
fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
|
||
// Step 1. Let collection be an empty list.
|
||
// Step 2. For each report in reports:
|
||
let report_body: Vec<SerializedReport> = reports
|
||
.iter()
|
||
// Step 2.1. Let data be a map with the following key/value pairs:
|
||
.map(|r| SerializedReport {
|
||
// TODO(37238)
|
||
age: 0,
|
||
type_: r.type_.to_string(),
|
||
url: r.url.to_string(),
|
||
user_agent: "".to_owned(),
|
||
body: r.body.clone().map(|b| b.into()),
|
||
})
|
||
// Step 2.2. Increment report’s attempts.
|
||
// TODO(37238)
|
||
// Step 2.3. Append data to collection.
|
||
.collect();
|
||
// Step 3. Return the byte sequence resulting from executing serialize an
|
||
// Infra value to JSON bytes on collection.
|
||
Some(create_request_body_with_content(
|
||
&serde_json::to_string(&report_body).unwrap_or("".to_owned()),
|
||
))
|
||
}
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
struct SerializedReport {
|
||
age: u64,
|
||
#[serde(rename = "type")]
|
||
type_: String,
|
||
url: String,
|
||
user_agent: String,
|
||
body: Option<CSPReportingEndpointBody>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub(crate) struct CSPReportingEndpointBody {
|
||
sample: Option<String>,
|
||
#[serde(rename = "blockedURL")]
|
||
blocked_url: Option<String>,
|
||
referrer: Option<String>,
|
||
status_code: u16,
|
||
#[serde(rename = "documentURL")]
|
||
document_url: String,
|
||
source_file: Option<String>,
|
||
effective_directive: String,
|
||
line_number: Option<u32>,
|
||
column_number: Option<u32>,
|
||
original_policy: String,
|
||
#[serde(serialize_with = "serialize_disposition")]
|
||
disposition: SecurityPolicyViolationEventDisposition,
|
||
}
|
||
|
||
impl From<CSPViolationReportBody> for CSPReportingEndpointBody {
|
||
fn from(value: CSPViolationReportBody) -> Self {
|
||
CSPReportingEndpointBody {
|
||
sample: value.sample.map(|s| s.to_string()),
|
||
blocked_url: value.blockedURL.map(|s| s.to_string()),
|
||
referrer: value.referrer.map(|s| s.to_string()),
|
||
status_code: value.statusCode,
|
||
document_url: value.documentURL.to_string(),
|
||
source_file: value.sourceFile.map(|s| s.to_string()),
|
||
effective_directive: value.effectiveDirective.to_string(),
|
||
line_number: value.lineNumber,
|
||
column_number: value.columnNumber,
|
||
original_policy: value.originalPolicy.into(),
|
||
disposition: value.disposition,
|
||
}
|
||
}
|
||
}
|
||
|
||
struct CSPReportEndpointFetchListener {
|
||
/// Endpoint URL of this request.
|
||
endpoint: ServoUrl,
|
||
/// Timing data for this resource.
|
||
resource_timing: ResourceFetchTiming,
|
||
/// The global object fetching the report uri violation
|
||
global: Trusted<GlobalScope>,
|
||
}
|
||
|
||
impl FetchResponseListener for CSPReportEndpointFetchListener {
|
||
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>) {}
|
||
}
|
||
|
||
impl ResourceTimingListener for CSPReportEndpointFetchListener {
|
||
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
|
||
(InitiatorType::Other, self.endpoint.clone())
|
||
}
|
||
|
||
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
|
||
self.global.root()
|
||
}
|
||
}
|
||
|
||
impl PreInvoke for CSPReportEndpointFetchListener {
|
||
fn should_invoke(&self) -> bool {
|
||
true
|
||
}
|
||
}
|