1use std::collections::HashMap;
6
7use headers::{ContentType, HeaderMapExt};
8use http::HeaderMap;
9use hyper_serde::Serde;
10use malloc_size_of_derive::MallocSizeOf;
11use net_traits::request::{
12 CredentialsMode, Destination, RequestBody, RequestId, RequestMode,
13 create_request_body_with_content,
14};
15use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
16use script_bindings::str::DOMString;
17use serde::Serialize;
18use servo_url::{ImmutableOrigin, ServoUrl};
19
20use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
21use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
22use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::SecurityPolicyViolationEventDisposition;
23use crate::dom::bindings::refcounted::Trusted;
24use crate::dom::bindings::root::DomRoot;
25use crate::dom::csp::Violation;
26use crate::dom::csppolicyviolationreport::serialize_disposition;
27use crate::dom::globalscope::GlobalScope;
28use crate::dom::performance::performanceresourcetiming::InitiatorType;
29use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request};
30use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
31use crate::script_runtime::CanGc;
32
33#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
35pub(crate) struct ReportingEndpoint {
36 name: DOMString,
38 url: ServoUrl,
40 failures: u32,
42}
43
44impl ReportingEndpoint {
45 pub(crate) fn parse_reporting_endpoints_header(
47 response_url: &ServoUrl,
48 headers: &Option<Serde<HeaderMap>>,
49 ) -> Option<Vec<ReportingEndpoint>> {
50 let headers = headers.as_ref()?;
51 let reporting_headers = headers.get_all("reporting-endpoints");
52 let mut parsed_header = Vec::new();
55 for header in reporting_headers.iter() {
56 let Some(header_value) = header.to_str().ok() else {
57 continue;
58 };
59 parsed_header.append(&mut header_value.split(",").map(|s| s.trim()).collect());
60 }
61 if parsed_header.is_empty() {
63 return None;
64 }
65 let mut endpoints = Vec::new();
67 for header in parsed_header {
69 let Some(split_index) = header.find('=') else {
72 continue;
73 };
74 let (name, endpoint_url_string) = header.split_at(split_index);
77 let length = endpoint_url_string.len();
78 let endpoint_bytes = endpoint_url_string.as_bytes();
79 if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
81 continue;
82 }
83 let endpoint_url_value = &endpoint_url_string[2..length - 1];
85 let Ok(endpoint_url) =
88 ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
89 else {
90 continue;
91 };
92 if !endpoint_url.is_potentially_trustworthy() {
94 continue;
95 }
96 endpoints.push(ReportingEndpoint {
99 name: name.into(),
100 url: endpoint_url,
101 failures: 0,
102 });
103 }
104 Some(endpoints)
105 }
106}
107
108pub(crate) trait SendReportsToEndpoints {
109 fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
111 fn attempt_to_deliver_reports_to_endpoints(
113 &self,
114 endpoint: &ServoUrl,
115 origin: ImmutableOrigin,
116 reports: &[&Report],
117 );
118 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody>;
120}
121
122impl SendReportsToEndpoints for GlobalScope {
123 fn send_reports_to_endpoints(
124 &self,
125 mut reports: Vec<Report>,
126 endpoints: Vec<ReportingEndpoint>,
127 ) {
128 #[expect(clippy::mutable_key_type)]
130 let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
132 reports.retain(|report| {
134 if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
137 endpoint_map
139 .entry(endpoint)
140 .or_default()
141 .push(report.clone());
142 true
143 } else {
144 false
146 }
147 });
148 for (endpoint, report_list) in endpoint_map.iter() {
150 let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
152 for report in report_list {
154 let Ok(url) = ServoUrl::parse(&report.url.str()) else {
155 continue;
156 };
157 let origin = url.origin();
159 origin_map.entry(origin).or_default().push(report);
161 }
162 for (origin, origin_report_list) in origin_map.iter() {
165 self.attempt_to_deliver_reports_to_endpoints(
168 &endpoint.url,
169 origin.clone(),
170 origin_report_list,
171 );
172 }
183 }
184 }
185
186 fn attempt_to_deliver_reports_to_endpoints(
187 &self,
188 endpoint: &ServoUrl,
189 origin: ImmutableOrigin,
190 reports: &[&Report],
191 ) {
192 let request_body = Self::serialize_list_of_reports(reports);
194 let mut headers = HeaderMap::with_capacity(1);
196 headers.typed_insert(ContentType::from(
197 "application/reports+json".parse::<mime::Mime>().unwrap(),
198 ));
199 let request = create_a_potential_cors_request(
200 None,
201 endpoint.clone(),
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 self.fetch(
217 request,
218 CSPReportEndpointFetchListener {
219 endpoint: endpoint.clone(),
220 global: Trusted::new(self),
221 },
222 self.task_manager().networking_task_source().into(),
223 );
224 }
233
234 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
235 let report_body: Vec<SerializedReport> = reports
238 .iter()
239 .map(|r| SerializedReport {
241 age: 0,
243 type_: r.type_.to_string(),
244 url: r.url.to_string(),
245 user_agent: "".to_owned(),
246 body: r.body.clone().map(|b| b.into()),
247 })
248 .collect();
252 Some(create_request_body_with_content(
255 &serde_json::to_string(&report_body).unwrap_or("".to_owned()),
256 ))
257 }
258}
259
260#[derive(Serialize)]
261struct SerializedReport {
262 age: u64,
263 #[serde(rename = "type")]
264 type_: String,
265 url: String,
266 user_agent: String,
267 body: Option<CSPReportingEndpointBody>,
268}
269
270#[derive(Clone, Debug, Serialize)]
271#[serde(rename_all = "camelCase")]
272pub(crate) struct CSPReportingEndpointBody {
273 sample: Option<String>,
274 #[serde(rename = "blockedURL")]
275 blocked_url: Option<String>,
276 referrer: Option<String>,
277 status_code: u16,
278 #[serde(rename = "documentURL")]
279 document_url: String,
280 source_file: Option<String>,
281 effective_directive: String,
282 line_number: Option<u32>,
283 column_number: Option<u32>,
284 original_policy: String,
285 #[serde(serialize_with = "serialize_disposition")]
286 disposition: SecurityPolicyViolationEventDisposition,
287}
288
289impl From<CSPViolationReportBody> for CSPReportingEndpointBody {
290 fn from(value: CSPViolationReportBody) -> Self {
291 CSPReportingEndpointBody {
292 sample: value.sample.map(|s| s.to_string()),
293 blocked_url: value.blockedURL.map(|s| s.to_string()),
294 referrer: value.referrer.map(|s| s.to_string()),
295 status_code: value.statusCode,
296 document_url: value.documentURL.to_string(),
297 source_file: value.sourceFile.map(|s| s.to_string()),
298 effective_directive: value.effectiveDirective.to_string(),
299 line_number: value.lineNumber,
300 column_number: value.columnNumber,
301 original_policy: value.originalPolicy.into(),
302 disposition: value.disposition,
303 }
304 }
305}
306
307struct CSPReportEndpointFetchListener {
308 endpoint: ServoUrl,
310 global: Trusted<GlobalScope>,
312}
313
314impl FetchResponseListener for CSPReportEndpointFetchListener {
315 fn process_request_body(&mut self, _: RequestId) {}
316
317 fn process_request_eof(&mut self, _: RequestId) {}
318
319 fn process_response(
320 &mut self,
321 _: RequestId,
322 fetch_metadata: Result<FetchMetadata, NetworkError>,
323 ) {
324 _ = fetch_metadata;
325 }
326
327 fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
328 _ = chunk;
329 }
330
331 fn process_response_eof(
332 self,
333 _: RequestId,
334 response: Result<(), NetworkError>,
335 timing: ResourceFetchTiming,
336 ) {
337 submit_timing(&self, &response, &timing, CanGc::note());
338 }
339
340 fn process_csp_violations(&mut self, _request_id: RequestId, _violations: Vec<Violation>) {}
341}
342
343impl ResourceTimingListener for CSPReportEndpointFetchListener {
344 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
345 (InitiatorType::Other, self.endpoint.clone())
346 }
347
348 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
349 self.global.root()
350 }
351}