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