Skip to main content

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