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::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#[derive(Clone, Eq, Hash, MallocSizeOf, PartialEq)]
36pub(crate) struct ReportingEndpoint {
37 name: DOMString,
39 url: ServoUrl,
41 failures: u32,
43}
44
45impl ReportingEndpoint {
46 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 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 if parsed_header.is_empty() {
64 return None;
65 }
66 let mut endpoints = Vec::new();
68 for header in parsed_header {
70 let Some(split_index) = header.find('=') else {
73 continue;
74 };
75 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 if length < 3 || endpoint_bytes[1] != b'"' || endpoint_bytes[length - 1] != b'"' {
82 continue;
83 }
84 let endpoint_url_value = &endpoint_url_string[2..length - 1];
86 let Ok(endpoint_url) =
89 ServoUrl::parse_with_base(Some(response_url), endpoint_url_value)
90 else {
91 continue;
92 };
93 if !endpoint_url.is_potentially_trustworthy() {
95 continue;
96 }
97 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 fn send_reports_to_endpoints(&self, reports: Vec<Report>, endpoints: Vec<ReportingEndpoint>);
112 fn attempt_to_deliver_reports_to_endpoints(
114 &self,
115 endpoint: &ServoUrl,
116 origin: ImmutableOrigin,
117 reports: &[&Report],
118 );
119 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 let mut endpoint_map: HashMap<&ReportingEndpoint, Vec<Report>> = HashMap::new();
131 reports.retain(|report| {
133 if let Some(endpoint) = endpoints.iter().find(|e| e.name == report.destination) {
136 endpoint_map
138 .entry(endpoint)
139 .or_default()
140 .push(report.clone());
141 true
142 } else {
143 false
145 }
146 });
147 for (endpoint, report_list) in endpoint_map.iter() {
149 let mut origin_map: HashMap<ImmutableOrigin, Vec<&Report>> = HashMap::new();
151 for report in report_list {
153 let Ok(url) = ServoUrl::parse(&report.url.str()) else {
154 continue;
155 };
156 let origin = url.origin();
158 origin_map.entry(origin).or_default().push(report);
160 }
161 for (origin, origin_report_list) in origin_map.iter() {
164 self.attempt_to_deliver_reports_to_endpoints(
167 &endpoint.url,
168 origin.clone(),
169 origin_report_list,
170 );
171 }
182 }
183 }
184
185 fn attempt_to_deliver_reports_to_endpoints(
186 &self,
187 endpoint: &ServoUrl,
188 origin: ImmutableOrigin,
189 reports: &[&Report],
190 ) {
191 let request_body = Self::serialize_list_of_reports(reports);
193 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 self.fetch(
217 request,
218 CSPReportEndpointFetchListener {
219 endpoint: endpoint.clone(),
220 },
221 self.task_manager().networking_task_source().into(),
222 );
223 }
232
233 fn serialize_list_of_reports(reports: &[&Report]) -> Option<RequestBody> {
234 let report_body: Vec<SerializedReport> = reports
237 .iter()
238 .map(|r| SerializedReport {
240 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 .collect();
251 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: 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}