1use 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#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
37pub(crate) struct ReportingEndpoint {
38 name: DOMString,
40 url: ServoUrl,
42 failures: u32,
44}
45
46impl ReportingEndpoint {
47 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 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 if parsed_header.is_empty() {
65 return None;
66 }
67 let mut endpoints = Vec::new();
69 for header in parsed_header {
71 let Some(split_index) = header.find('=') else {
74 continue;
75 };
76 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 if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
83 continue;
84 }
85 let endpoint_url_value = &endpoint_url_string[2..length - 1];
87 let Ok(endpoint_url) =
90 ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
91 else {
92 continue;
93 };
94 if !endpoint_url.is_potentially_trustworthy() {
96 continue;
97 }
98 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 fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
113 fn attempt_to_deliver_reports_to_endpoints(
115 &self,
116 endpoint: &ServoUrl,
117 origin: ImmutableOrigin,
118 reports: &[&Report],
119 );
120 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 #[expect(clippy::mutable_key_type)]
132 let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
134 reports.retain(|report| {
136 if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
139 endpoint_map
141 .entry(endpoint)
142 .or_default()
143 .push(report.clone());
144 true
145 } else {
146 false
148 }
149 });
150 for (endpoint, report_list) in endpoint_map.iter() {
152 let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
154 for report in report_list {
156 let Ok(url) = ServoUrl::parse(&report.url.str()) else {
157 continue;
158 };
159 let origin = url.origin();
161 origin_map.entry(origin).or_default().push(report);
163 }
164 for (origin, origin_report_list) in origin_map.iter() {
167 self.attempt_to_deliver_reports_to_endpoints(
170 &endpoint.url,
171 origin.clone(),
172 origin_report_list,
173 );
174 }
185 }
186 }
187
188 fn attempt_to_deliver_reports_to_endpoints(
189 &self,
190 endpoint: &ServoUrl,
191 origin: ImmutableOrigin,
192 reports: &[&Report],
193 ) {
194 let request_body = Self::serialize_list_of_reports(reports);
196 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 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 }
235
236 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
237 let report_body: Vec<SerializedReport> = reports
240 .iter()
241 .map(|r| SerializedReport {
243 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 .collect();
254 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: ServoUrl,
312 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}