1use 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#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
38pub(crate) struct ReportingEndpoint {
39 name: DOMString,
41 url: ServoUrl,
43 failures: u32,
45}
46
47impl ReportingEndpoint {
48 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 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 if parsed_header.is_empty() {
66 return None;
67 }
68 let mut endpoints = Vec::new();
70 for header in parsed_header {
72 let Some(split_index) = header.find('=') else {
75 continue;
76 };
77 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 if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
84 continue;
85 }
86 let endpoint_url_value = &endpoint_url_string[2..length - 1];
88 let Ok(endpoint_url) =
91 ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
92 else {
93 continue;
94 };
95 if !endpoint_url.is_potentially_trustworthy() {
97 continue;
98 }
99 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 fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
114 fn attempt_to_deliver_reports_to_endpoints(
116 &self,
117 endpoint: &ServoUrl,
118 origin: ImmutableOrigin,
119 reports: &[&Report],
120 );
121 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 #[allow(clippy::mutable_key_type)]
133 let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
135 reports.retain(|report| {
137 if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
140 endpoint_map
142 .entry(endpoint)
143 .or_default()
144 .push(report.clone());
145 true
146 } else {
147 false
149 }
150 });
151 for (endpoint, report_list) in endpoint_map.iter() {
153 let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
155 for report in report_list {
157 let Ok(url) = ServoUrl::parse(&report.url.str()) else {
158 continue;
159 };
160 let origin = url.origin();
162 origin_map.entry(origin).or_default().push(report);
164 }
165 for (origin, origin_report_list) in origin_map.iter() {
168 self.attempt_to_deliver_reports_to_endpoints(
171 &endpoint.url,
172 origin.clone(),
173 origin_report_list,
174 );
175 }
186 }
187 }
188
189 fn attempt_to_deliver_reports_to_endpoints(
190 &self,
191 endpoint: &ServoUrl,
192 origin: ImmutableOrigin,
193 reports: &[&Report],
194 ) {
195 let request_body = Self::serialize_list_of_reports(reports);
197 let mut headers = HeaderMap::with_capacity(1);
199 headers.typed_insert(ContentType::from(
200 "application/reports+json".parse::<mime::Mime>().unwrap(),
201 ));
202 let request = create_a_potential_cors_request(
203 None,
204 endpoint.clone(),
205 Destination::Report,
206 None,
207 None,
208 self.get_referrer(),
209 self.insecure_requests_policy(),
210 self.has_trustworthy_ancestor_or_current_origin(),
211 self.policy_container(),
212 )
213 .method(http::Method::POST)
214 .body(request_body)
215 .origin(origin)
216 .mode(RequestMode::CorsMode)
217 .credentials_mode(CredentialsMode::CredentialsSameOrigin)
218 .unsafe_request(true)
219 .headers(headers);
220 self.fetch(
222 request,
223 Arc::new(Mutex::new(CSPReportEndpointFetchListener {
224 endpoint: endpoint.clone(),
225 global: Trusted::new(self),
226 resource_timing: ResourceFetchTiming::new(ResourceTimingType::None),
227 })),
228 self.task_manager().networking_task_source().into(),
229 );
230 }
239
240 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
241 let report_body: Vec<SerializedReport> = reports
244 .iter()
245 .map(|r| SerializedReport {
247 age: 0,
249 type_: r.type_.to_string(),
250 url: r.url.to_string(),
251 user_agent: "".to_owned(),
252 body: r.body.clone().map(|b| b.into()),
253 })
254 .collect();
258 Some(create_request_body_with_content(
261 &serde_json::to_string(&report_body).unwrap_or("".to_owned()),
262 ))
263 }
264}
265
266#[derive(Serialize)]
267struct SerializedReport {
268 age: u64,
269 #[serde(rename = "type")]
270 type_: String,
271 url: String,
272 user_agent: String,
273 body: Option<CSPReportingEndpointBody>,
274}
275
276#[derive(Clone, Debug, Serialize)]
277#[serde(rename_all = "camelCase")]
278pub(crate) struct CSPReportingEndpointBody {
279 sample: Option<String>,
280 #[serde(rename = "blockedURL")]
281 blocked_url: Option<String>,
282 referrer: Option<String>,
283 status_code: u16,
284 #[serde(rename = "documentURL")]
285 document_url: String,
286 source_file: Option<String>,
287 effective_directive: String,
288 line_number: Option<u32>,
289 column_number: Option<u32>,
290 original_policy: String,
291 #[serde(serialize_with = "serialize_disposition")]
292 disposition: SecurityPolicyViolationEventDisposition,
293}
294
295impl From<CSPViolationReportBody> for CSPReportingEndpointBody {
296 fn from(value: CSPViolationReportBody) -> Self {
297 CSPReportingEndpointBody {
298 sample: value.sample.map(|s| s.to_string()),
299 blocked_url: value.blockedURL.map(|s| s.to_string()),
300 referrer: value.referrer.map(|s| s.to_string()),
301 status_code: value.statusCode,
302 document_url: value.documentURL.to_string(),
303 source_file: value.sourceFile.map(|s| s.to_string()),
304 effective_directive: value.effectiveDirective.to_string(),
305 line_number: value.lineNumber,
306 column_number: value.columnNumber,
307 original_policy: value.originalPolicy.into(),
308 disposition: value.disposition,
309 }
310 }
311}
312
313struct CSPReportEndpointFetchListener {
314 endpoint: ServoUrl,
316 resource_timing: ResourceFetchTiming,
318 global: Trusted<GlobalScope>,
320}
321
322impl FetchResponseListener for CSPReportEndpointFetchListener {
323 fn process_request_body(&mut self, _: RequestId) {}
324
325 fn process_request_eof(&mut self, _: RequestId) {}
326
327 fn process_response(
328 &mut self,
329 _: RequestId,
330 fetch_metadata: Result<FetchMetadata, NetworkError>,
331 ) {
332 _ = fetch_metadata;
333 }
334
335 fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
336 _ = chunk;
337 }
338
339 fn process_response_eof(
340 &mut self,
341 _: RequestId,
342 response: Result<ResourceFetchTiming, NetworkError>,
343 ) {
344 _ = response;
345 }
346
347 fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
348 &mut self.resource_timing
349 }
350
351 fn resource_timing(&self) -> &ResourceFetchTiming {
352 &self.resource_timing
353 }
354
355 fn submit_resource_timing(&mut self) {
356 submit_timing(self, CanGc::note())
357 }
358
359 fn process_csp_violations(&mut self, _request_id: RequestId, _violations: Vec<Violation>) {}
360}
361
362impl ResourceTimingListener for CSPReportEndpointFetchListener {
363 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
364 (InitiatorType::Other, self.endpoint.clone())
365 }
366
367 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
368 self.global.root()
369 }
370}
371
372impl PreInvoke for CSPReportEndpointFetchListener {
373 fn should_invoke(&self) -> bool {
374 true
375 }
376}