script/dom/
reportingendpoint.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8use headers::{ContentType, HeaderMapExt};
9use http::HeaderMap;
10use hyper_serde::Serde;
11use malloc_size_of_derive::MallocSizeOf;
12use net_traits::request::{
13    CredentialsMode, Destination, RequestBody, RequestId, RequestMode,
14    create_request_body_with_content,
15};
16use net_traits::{
17    FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType,
18};
19use script_bindings::str::DOMString;
20use serde::Serialize;
21use servo_url::{ImmutableOrigin, ServoUrl};
22
23use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
24use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
25use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::SecurityPolicyViolationEventDisposition;
26use crate::dom::bindings::refcounted::Trusted;
27use crate::dom::bindings::root::DomRoot;
28use crate::dom::csp::Violation;
29use crate::dom::csppolicyviolationreport::serialize_disposition;
30use crate::dom::globalscope::GlobalScope;
31use crate::dom::performanceresourcetiming::InitiatorType;
32use crate::fetch::create_a_potential_cors_request;
33use crate::network_listener::{PreInvoke, ResourceTimingListener, submit_timing};
34use crate::script_runtime::CanGc;
35
36/// <https://w3c.github.io/reporting/#endpoint>
37#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
38pub(crate) struct ReportingEndpoint {
39    /// <https://w3c.github.io/reporting/#dom-endpoint-name>
40    name: DOMString,
41    /// <https://w3c.github.io/reporting/#dom-endpoint-url>
42    url: ServoUrl,
43    /// <https://w3c.github.io/reporting/#dom-endpoint-failures>
44    failures: u32,
45}
46
47impl ReportingEndpoint {
48    /// <https://w3c.github.io/reporting/#process-header>
49    pub(crate) fn parse_reporting_endpoints_header(
50        response_url: &ServoUrl,
51        headers: &Option<Serde<HeaderMap>>,
52    ) -> Option<Vec<ReportingEndpoint>> {
53        let headers = headers.as_ref()?;
54        let reporting_headers = headers.get_all("reporting-endpoints");
55        // Step 2. Let parsed header be the result of executing get a structured field value
56        // given "Reporting-Endpoints" and "dictionary" from response’s header list.
57        let mut parsed_header = Vec::new();
58        for header in reporting_headers.iter() {
59            let Some(header_value) = header.to_str().ok() else {
60                continue;
61            };
62            parsed_header.append(&mut header_value.split(",").map(|s| s.trim()).collect());
63        }
64        // Step 3. If parsed header is null, abort these steps.
65        if parsed_header.is_empty() {
66            return None;
67        }
68        // Step 4. Let endpoints be an empty list.
69        let mut endpoints = Vec::new();
70        // Step 5. For each name → value_and_parameters of parsed header:
71        for header in parsed_header {
72            // There could be a '=' in the URL itself (for example query parameters). Therefore, we can't
73            // split on '=', but instead look for the first one.
74            let Some(split_index) = header.find('=') else {
75                continue;
76            };
77            // Step 5.1. Let endpoint url string be the first element of the tuple value_and_parameters.
78            // If endpoint url string is not a string, then continue.
79            let (name, endpoint_url_string) = header.split_at(split_index);
80            let length = endpoint_url_string.len();
81            let endpoint_bytes = endpoint_url_string.as_bytes();
82            // Note that the first character is the '=' and we check for the next and last character to be '"'
83            if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
84                continue;
85            }
86            // The '="' at the start and '"' at the end removed
87            let endpoint_url_value = &endpoint_url_string[2..length - 1];
88            // Step 5.2. Let endpoint url be the result of executing the URL parser on endpoint url string,
89            // with base URL set to response’s url. If endpoint url is failure, then continue.
90            let Ok(endpoint_url) =
91                ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
92            else {
93                continue;
94            };
95            // Step 5.3. If endpoint url’s origin is not potentially trustworthy, then continue.
96            if !endpoint_url.is_potentially_trustworthy() {
97                continue;
98            }
99            // Step 5.4. Let endpoint be a new endpoint whose properties are set as follows:
100            // Step 5.5. Add endpoint to endpoints.
101            endpoints.push(ReportingEndpoint {
102                name: name.into(),
103                url: endpoint_url,
104                failures: 0,
105            });
106        }
107        Some(endpoints)
108    }
109}
110
111pub(crate) trait SendReportsToEndpoints {
112    /// <https://w3c.github.io/reporting/#send-reports>
113    fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
114    /// <https://w3c.github.io/reporting/#try-delivery>
115    fn attempt_to_deliver_reports_to_endpoints(
116        &self,
117        endpoint: &ServoUrl,
118        origin: ImmutableOrigin,
119        reports: &[&Report],
120    );
121    /// <https://w3c.github.io/reporting/#serialize-a-list-of-reports-to-json>
122    fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody>;
123}
124
125impl SendReportsToEndpoints for GlobalScope {
126    fn send_reports_to_endpoints(
127        &self,
128        mut reports: Vec<Report>,
129        endpoints: Vec<ReportingEndpoint>,
130    ) {
131        // Step 1. Let endpoint map be an empty map of endpoint objects to lists of report objects.
132        let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
133        // Step 2. For each report in reports:
134        reports.retain(|report| {
135            // Step 2.1. If there exists an endpoint (endpoint) in context’s endpoints
136            // list whose name is report’s destination:
137            if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
138                // Step 2.1.1. Append report to endpoint map’s list of reports for endpoint.
139                endpoint_map
140                    .entry(endpoint)
141                    .or_default()
142                    .push(report.clone());
143                true
144            } else {
145                // Step 2.1.2. Otherwise, remove report from reports.
146                false
147            }
148        });
149        // Step 3. For each (endpoint, report list) pair in endpoint map:
150        for (endpoint, report_list) in endpoint_map.iter() {
151            // Step 3.1. Let origin map be an empty map of origins to lists of report objects.
152            let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
153            // Step 3.2. For each report in report list:
154            for report in report_list {
155                let Ok(url) = ServoUrl::parse(&report.url) else {
156                    continue;
157                };
158                // Step 3.2.1. Let origin be the origin of report’s url.
159                let origin = url.origin();
160                // Step 3.2.2. Append report to origin map’s list of reports for origin.
161                origin_map.entry(origin).or_default().push(report);
162            }
163            // Step 3.3. For each (origin, per-origin reports) pair in origin map,
164            // execute the following steps asynchronously:
165            for (origin, origin_report_list) in origin_map.iter() {
166                // Step 3.3.1. Let result be the result of executing
167                // § 3.5.2 Attempt to deliver reports to endpoint on endpoint, origin, and per-origin reports.
168                self.attempt_to_deliver_reports_to_endpoints(
169                    &endpoint.url,
170                    origin.clone(),
171                    origin_report_list,
172                );
173                // Step 3.3.2. If result is "Failure":
174                // TODO(37238)
175                // Step 3.3.2.1. Increment endpoint’s failures.
176                // TODO(37238)
177                // Step 3.3.3. If result is "Remove Endpoint":
178                // TODO(37238)
179                // Step 3.3.3.1 Remove endpoint from context’s endpoints list.
180                // TODO(37238)
181                // Step 3.3.4. Remove each report from reports.
182                // TODO(37238)
183            }
184        }
185    }
186
187    fn attempt_to_deliver_reports_to_endpoints(
188        &self,
189        endpoint: &ServoUrl,
190        origin: ImmutableOrigin,
191        reports: &[&Report],
192    ) {
193        // Step 1. Let body be the result of executing serialize a list of reports to JSON on reports.
194        let request_body = Self::serialize_list_of_reports(reports);
195        // Step 2. Let request be a new request with the following properties [FETCH]:
196        let mut headers = HeaderMap::with_capacity(1);
197        headers.typed_insert(ContentType::from(
198            "application/reports+json".parse::<mime::Mime>().unwrap(),
199        ));
200        let request = create_a_potential_cors_request(
201            None,
202            endpoint.clone(),
203            Destination::Report,
204            None,
205            None,
206            self.get_referrer(),
207            self.insecure_requests_policy(),
208            self.has_trustworthy_ancestor_or_current_origin(),
209            self.policy_container(),
210        )
211        .method(http::Method::POST)
212        .body(request_body)
213        .origin(origin)
214        .mode(RequestMode::CorsMode)
215        .credentials_mode(CredentialsMode::CredentialsSameOrigin)
216        .unsafe_request(true)
217        .headers(headers);
218        // Step 3. Queue a task to fetch request.
219        self.fetch(
220            request,
221            Arc::new(Mutex::new(CSPReportEndpointFetchListener {
222                endpoint: endpoint.clone(),
223                global: Trusted::new(self),
224                resource_timing: ResourceFetchTiming::new(ResourceTimingType::None),
225            })),
226            self.task_manager().networking_task_source().into(),
227        );
228        // Step 4. Wait for a response (response).
229        // TODO(37238)
230        // Step 5. If response’s status is an OK status (200-299), return "Success".
231        // TODO(37238)
232        // Step 6. If response’s status is 410 Gone [RFC9110], return "Remove Endpoint".
233        // TODO(37238)
234        // Step 7. Return "Failure".
235        // TODO(37238)
236    }
237
238    fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
239        // Step 1. Let collection be an empty list.
240        // Step 2. For each report in reports:
241        let report_body: Vec<SerializedReport> = reports
242            .iter()
243            // Step 2.1. Let data be a map with the following key/value pairs:
244            .map(|r| SerializedReport {
245                // TODO(37238)
246                age: 0,
247                type_: r.type_.to_string(),
248                url: r.url.to_string(),
249                user_agent: "".to_owned(),
250                body: r.body.clone().map(|b| b.into()),
251            })
252            // Step 2.2. Increment report’s attempts.
253            // TODO(37238)
254            // Step 2.3. Append data to collection.
255            .collect();
256        // Step 3. Return the byte sequence resulting from executing serialize an
257        // Infra value to JSON bytes on collection.
258        Some(create_request_body_with_content(
259            &serde_json::to_string(&report_body).unwrap_or("".to_owned()),
260        ))
261    }
262}
263
264#[derive(Serialize)]
265struct SerializedReport {
266    age: u64,
267    #[serde(rename = "type")]
268    type_: String,
269    url: String,
270    user_agent: String,
271    body: Option<CSPReportingEndpointBody>,
272}
273
274#[derive(Clone, Debug, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub(crate) struct CSPReportingEndpointBody {
277    sample: Option<String>,
278    #[serde(rename = "blockedURL")]
279    blocked_url: Option<String>,
280    referrer: Option<String>,
281    status_code: u16,
282    #[serde(rename = "documentURL")]
283    document_url: String,
284    source_file: Option<String>,
285    effective_directive: String,
286    line_number: Option<u32>,
287    column_number: Option<u32>,
288    original_policy: String,
289    #[serde(serialize_with = "serialize_disposition")]
290    disposition: SecurityPolicyViolationEventDisposition,
291}
292
293impl From<CSPViolationReportBody> for CSPReportingEndpointBody {
294    fn from(value: CSPViolationReportBody) -> Self {
295        CSPReportingEndpointBody {
296            sample: value.sample.map(|s| s.to_string()),
297            blocked_url: value.blockedURL.map(|s| s.to_string()),
298            referrer: value.referrer.map(|s| s.to_string()),
299            status_code: value.statusCode,
300            document_url: value.documentURL.to_string(),
301            source_file: value.sourceFile.map(|s| s.to_string()),
302            effective_directive: value.effectiveDirective.to_string(),
303            line_number: value.lineNumber,
304            column_number: value.columnNumber,
305            original_policy: value.originalPolicy.into(),
306            disposition: value.disposition,
307        }
308    }
309}
310
311struct CSPReportEndpointFetchListener {
312    /// Endpoint URL of this request.
313    endpoint: ServoUrl,
314    /// Timing data for this resource.
315    resource_timing: ResourceFetchTiming,
316    /// The global object fetching the report uri violation
317    global: Trusted<GlobalScope>,
318}
319
320impl FetchResponseListener for CSPReportEndpointFetchListener {
321    fn process_request_body(&mut self, _: RequestId) {}
322
323    fn process_request_eof(&mut self, _: RequestId) {}
324
325    fn process_response(
326        &mut self,
327        _: RequestId,
328        fetch_metadata: Result<FetchMetadata, NetworkError>,
329    ) {
330        _ = fetch_metadata;
331    }
332
333    fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
334        _ = chunk;
335    }
336
337    fn process_response_eof(
338        &mut self,
339        _: RequestId,
340        response: Result<ResourceFetchTiming, NetworkError>,
341    ) {
342        _ = response;
343    }
344
345    fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
346        &mut self.resource_timing
347    }
348
349    fn resource_timing(&self) -> &ResourceFetchTiming {
350        &self.resource_timing
351    }
352
353    fn submit_resource_timing(&mut self) {
354        submit_timing(self, CanGc::note())
355    }
356
357    fn process_csp_violations(&mut self, _request_id: RequestId, _violations: Vec<Violation>) {}
358}
359
360impl ResourceTimingListener for CSPReportEndpointFetchListener {
361    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
362        (InitiatorType::Other, self.endpoint.clone())
363    }
364
365    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
366        self.global.root()
367    }
368}
369
370impl PreInvoke for CSPReportEndpointFetchListener {
371    fn should_invoke(&self) -> bool {
372        true
373    }
374}