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 let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
133 reports.retain(|report| {
135 if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
138 endpoint_map
140 .entry(endpoint)
141 .or_default()
142 .push(report.clone());
143 true
144 } else {
145 false
147 }
148 });
149 for (endpoint, report_list) in endpoint_map.iter() {
151 let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
153 for report in report_list {
155 let Ok(url) = ServoUrl::parse(&report.url) else {
156 continue;
157 };
158 let origin = url.origin();
160 origin_map.entry(origin).or_default().push(report);
162 }
163 for (origin, origin_report_list) in origin_map.iter() {
166 self.attempt_to_deliver_reports_to_endpoints(
169 &endpoint.url,
170 origin.clone(),
171 origin_report_list,
172 );
173 }
184 }
185 }
186
187 fn attempt_to_deliver_reports_to_endpoints(
188 &self,
189 endpoint: &ServoUrl,
190 origin: ImmutableOrigin,
191 reports: &[&Report],
192 ) {
193 let request_body = Self::serialize_list_of_reports(reports);
195 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 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 }
237
238 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
239 let report_body: Vec<SerializedReport> = reports
242 .iter()
243 .map(|r| SerializedReport {
245 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 .collect();
256 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: ServoUrl,
314 resource_timing: ResourceFetchTiming,
316 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}