reporting_api/
lib.rs

1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2019, rs-reporting-api authors.
4//
5// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
6// in compliance with the License.  You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software distributed under the
11// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12// express or implied.  See the License for the specific language governing permissions and
13// limitations under the License.
14// ------------------------------------------------------------------------------------------------
15
16//! This crate provides some useful Rust code for working with the [Reporting API][] and [Network
17//! Error Logging][] W3C draft specifications.
18//!
19//! [Reporting API]: https://w3c.github.io/reporting/
20//! [Network Error Logging]: https://w3c.github.io/network-error-logging/
21//!
22//! # Overview
23//!
24//! The core of the [Reporting API][] is pretty simple: reports are uploaded via a `POST` to a URL
25//! of your choosing.  The payload of the `POST` request is a JSON-encoded array of reports, and
26//! the report schema is defined by the spec.
27//!
28//! The [Reporting API][] can be used to upload many different _kinds_ of reports.  For instance,
29//! Reporting itself defines [crash reports][], [deprecations][], and [interventions][], all of
30//! which come from the JavaScript environment running in the browser.  Other report types are
31//! complex enough that they need to be defined in their own specs, such as [Network Error
32//! Logging][] and [Content Security Policy][].  Regardless of where they're defined, each report
33//! type defines some fields specific to that type (the **_body_**), and the [Reporting API][]
34//! defines some fields that are common to all types.
35//!
36//! [crash reports]: https://w3c.github.io/reporting/#crash-report
37//! [deprecations]: https://w3c.github.io/reporting/#deprecation-report
38//! [interventions]: https://w3c.github.io/reporting/#intervention-report
39//! [Content Security Policy]: https://www.w3.org/TR/CSP3/
40//!
41//! This library provides a definition of all of these schemas as regular Rust types, along with
42//! the ability to use [serde][] to serialize and deserialize them.  We've carefully defined
43//! everything so that [serde_json][] will automatically do The Right Thing and use a JSON
44//! serialization that lines up with the various specifications.  We also provide way to define
45//! body schemas for new report types, and have them seamlessly fit in with the rest of the
46//! serialization logic.
47//!
48//! [serde]: https://docs.rs/serde/
49//! [serde_json]: https://docs.rs/serde_json/
50//!
51//! # Collecting reports
52//!
53//! The simplest way to use this library is if you just want to receive reports from somewhere
54//! (you're implementing a collector, for instance, and we've already defined Rust types for all of
55//! the report types that you care about).
56//!
57//! To do that, you just need to use `serde_json` to deserialize the content of the JSON string
58//! that you've received:
59//!
60//! ```
61//! # use reporting_api::BareReport;
62//! # let payload = r#"[{"age":500,"type":"network-error","url":"https://example.com/about/","user_agent":"Mozilla/5.0","body":{"referrer":"https://example.com/","sampling_fraction":0.5,"server_ip":"203.0.113.75","protocol":"h2","method":"POST","status_code":200,"elapsed_time":45,"phase":"application","type":"ok"}}]"#;
63//! let reports: Vec<BareReport> = serde_json::from_str(payload).unwrap();
64//! ```
65//!
66//! That's it!  The elements of the vector will represent each of the reports in this upload batch.
67//! Each one is a "bare" report, which means that we haven't tried to figure out what type of
68//! report this is, or which Rust type corresponds with that report type.  Instead, the raw body of
69//! the report is available (in the [`body`][] field) as a `serde_json` [`Value`][].
70//!
71//! If you know which particular kind of report you want to process, you can use the bare report's
72//! [`parse`][] method to convert it into a "parsed" report.  For instance, if you know you only
73//! care about [Network Error Logging][] reports:
74//!
75//! ```
76//! # use reporting_api::BareReport;
77//! # use reporting_api::Report;
78//! # use reporting_api::NEL;
79//! # let payload = r#"[{"age":500,"type":"network-error","url":"https://example.com/about/","user_agent":"Mozilla/5.0","body":{"referrer":"https://example.com/","sampling_fraction":0.5,"server_ip":"203.0.113.75","protocol":"h2","method":"POST","status_code":200,"elapsed_time":45,"phase":"application","type":"ok"}}]"#;
80//! # let reports: Vec<BareReport> = serde_json::from_str(payload).unwrap();
81//! // Ignore both kinds of failure, returning a Vec<Report<NEL>>.
82//! let nel_reports = reports
83//!     .into_iter()
84//!     .filter_map(BareReport::parse::<NEL>)
85//!     .filter_map(Result::ok)
86//!     .collect::<Vec<Report<NEL>>>();
87//! ```
88//!
89//! [`BareReport`]: struct.BareReport.html
90//! [`body`]: struct.BareReport.html#structfield.body
91//! [`Value`]: https://docs.rs/serde_json/*/serde_json/value/enum.Value.html
92//! [`parse`]: struct.BareReport.html#method.parse
93//!
94//! Note that [`parse`][]'s return value is wrapped in _both_ [`Option`][] _and_ [`Result`][].  The
95//! outer [`Option`][] tells you whether or not the report is of the expected type.  If it is, the
96//! inner [`Result`][] tells you whether we were able to parse the reports `body` field according
97//! to that type's expected schema.  In this example, we therefore need two `filter_map` calls to
98//! strip away any mismatches and errors, leaving us with a vector of `Report<NEL>` instances.
99//!
100//! [`Option`]: https://doc.rust-lang.org/std/option/enum.Option.html
101//! [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
102//!
103//! # Creating a new report type
104//!
105//! This should be a relatively rare occurrence, but consider a new report type that uses the
106//! [Reporting API][] but that isn't covered here.  For instance, let's say there's a new `lint`
107//! report type whose body content looks like:
108//!
109//! ``` json
110//! {
111//!     "source_file": "foo.js",
112//!     "line": 10,
113//!     "column": 12,
114//!     "finding": "Indentation doesn't match the rest of the file"
115//! }
116//! ```
117//!
118//! First you'll define a Rust type to hold the body content:
119//!
120//! ```
121//! # use serde::Deserialize;
122//! # use serde::Serialize;
123//! #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
124//! pub struct Lint {
125//!     pub source_file: String,
126//!     pub line: u32,
127//!     pub column: u32,
128//!     pub finding: String,
129//! }
130//! ```
131//!
132//! Lastly, you must implement the [`ReportType`][] trait for your new type, which defines the
133//! value of the `type` field in the report payload that corresponds to this new report type.
134//!
135//! [`ReportType`]: trait.ReportType.html
136//!
137//! ```
138//! # use reporting_api::ReportType;
139//! # pub struct Lint;
140//! impl ReportType for Lint {
141//!     fn report_type() -> &'static str {
142//!         "lint"
143//!     }
144//! }
145//! ```
146//!
147//! And that's it!  The [`parse`][] method will now work with your new report type.
148
149use std::time::Duration;
150
151use serde::Deserialize;
152use serde::Serialize;
153use serde_json::Value;
154
155/// Represents a single report uploaded via the Reporting API, whose body is still a JSON object
156/// and has not yet been parsed into a more specific Rust type.
157#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
158pub struct BareReport {
159    /// The amount of time between when the report was generated by the user agent and when it was
160    /// uploaded.
161    #[serde(with = "parse_milliseconds")]
162    pub age: Duration,
163    /// The URL of the request that this report describes.
164    pub url: String,
165    /// The value of the `User-Agent` header of the request that this report describes.
166    pub user_agent: String,
167    /// The type of report
168    #[serde(rename = "type")]
169    pub report_type: String,
170    /// The body of the report, still encoded as a JSON object.
171    pub body: Value,
172}
173
174impl BareReport {
175    /// Verifies that a bare report has a particular type, and tries to parse the report body using
176    /// the corresponding Rust type.  Returns `Some(Ok(...))` if everything goes well.  Returns
177    /// `None` if the report has a different type, and `Some(Err(...))` if the report has the right
178    /// type but we can't parse the report body using that type's schema.
179    pub fn parse<C>(self) -> Option<Result<Report<C>, serde_json::Error>>
180    where
181        C: ReportType + for<'de> Deserialize<'de>,
182    {
183        if self.report_type != C::report_type() {
184            return None;
185        }
186        Some(self.parse_body())
187    }
188
189    fn parse_body<C>(self) -> Result<Report<C>, serde_json::Error>
190    where
191        C: for<'de> Deserialize<'de>,
192    {
193        Ok(Report {
194            age: self.age,
195            url: self.url,
196            user_agent: self.user_agent,
197            body: serde_json::from_value(self.body)?,
198        })
199    }
200}
201
202/// Represents a single report, after having parsed the body into the Rust type specific to this
203/// type of report.
204#[derive(Clone, Debug, Default, PartialEq)]
205pub struct Report<C> {
206    /// The amount of time between when the report was generated by the user agent and when it was
207    /// uploaded.
208    pub age: Duration,
209    /// The URL of the request that this report describes.
210    pub url: String,
211    /// The value of the `User-Agent` header of the request that this report describes.
212    pub user_agent: String,
213    /// The body of the report.
214    pub body: C,
215}
216
217/// A trait that maps each Rust report type to the corresponding `type` value that appears in a
218/// JSON report payload.
219pub trait ReportType {
220    /// The value of the report's `type` field for reports of this type.
221    fn report_type() -> &'static str;
222}
223
224/// The body of a single Network Error Logging report.
225#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
226pub struct NEL {
227    /// The referrer information for the request, as determined by the referrer policy associated
228    /// with its client.
229    pub referrer: String,
230    /// The sampling rate that was in effect for this request, expressed as a frcation between 0.0
231    /// and 1.0 (inclusive).
232    pub sampling_fraction: f32,
233    /// The IP address of the host to which the user agent sent the request.
234    pub server_ip: String,
235    /// The ALPN ID of the network protocol used to fetch the resource.
236    pub protocol: String,
237    /// The method of the HTTP request (e.g., `GET`, `POST`)
238    pub method: String,
239    /// The status code of the HTTP response, if available.
240    pub status_code: Option<u16>,
241    /// The elapsed time between the start of the resource fetch and when it was completed or
242    /// aborted by the user agent.
243    #[serde(with = "parse_opt_milliseconds")]
244    pub elapsed_time: Option<Duration>,
245    /// The phase of the request in which the failure occurred, if any.  One of `dns`,
246    /// `connection`, or `application`.  A successful request always has a phase of `application`.
247    pub phase: String,
248    /// The code describing the error that occurred, or `ok` if the request was successful.  See
249    /// the NEL spec for the [authoritative
250    /// list](https://w3c.github.io/network-error-logging/#predefined-network-error-types) of
251    /// possible codes.
252    #[serde(rename = "type")]
253    pub status: String,
254}
255
256impl ReportType for NEL {
257    fn report_type() -> &'static str {
258        "network-error"
259    }
260}
261
262/// A serde parsing module that can be used to parse durations expressed as an integer number of
263/// milliseconds.
264pub mod parse_milliseconds {
265    use std::time::Duration;
266
267    use serde::Deserialize;
268    use serde::Deserializer;
269    use serde::Serializer;
270
271    pub fn serialize<S>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error>
272    where
273        S: Serializer,
274    {
275        serializer.serialize_u64(value.as_millis() as u64)
276    }
277
278    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
279    where
280        D: Deserializer<'de>,
281    {
282        Ok(Duration::from_millis(u64::deserialize(deserializer)?))
283    }
284}
285
286/// A serde parsing module that can be used to parse _optional_ durations expressed as an integer
287/// number of milliseconds.
288pub mod parse_opt_milliseconds {
289    use std::time::Duration;
290
291    use serde::Deserialize;
292    use serde::Deserializer;
293    use serde::Serializer;
294
295    pub fn serialize<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
296    where
297        S: Serializer,
298    {
299        match value {
300            Some(duration) => serializer.serialize_some(&(duration.as_millis() as u64)),
301            None => serializer.serialize_none(),
302        }
303    }
304
305    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
306    where
307        D: Deserializer<'de>,
308    {
309        Ok(Option::<u64>::deserialize(deserializer)?.map(Duration::from_millis))
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    use serde_json::json;
318
319    #[test]
320    fn can_parse_unknown_report_type() {
321        let report_json = json!({
322            "age": 500,
323            "url": "https://example.com/about/",
324            "user_agent": "Mozilla/5.0",
325            "type": "unknown",
326            "body": {},
327        });
328        let report: BareReport =
329            serde_json::from_value(report_json).expect("Should be able to parse JSON report");
330        assert_eq!(
331            report,
332            BareReport {
333                age: Duration::from_millis(500),
334                url: "https://example.com/about/".to_string(),
335                user_agent: "Mozilla/5.0".to_string(),
336                report_type: "unknown".to_string(),
337                body: json!({}),
338            }
339        );
340    }
341
342    #[test]
343    fn cannot_parse_missing_report_type() {
344        let report_json = json!({
345            "age": 500,
346            "url": "https://example.com/about/",
347            "user_agent": "Mozilla/5.0",
348            "body": {},
349        });
350        assert!(serde_json::from_value::<BareReport>(report_json).is_err());
351    }
352
353    #[test]
354    fn cannot_parse_missing_body() {
355        let report_json = json!({
356            "age": 500,
357            "url": "https://example.com/about/",
358            "user_agent": "Mozilla/5.0",
359            "type": "unknown",
360        });
361        assert!(serde_json::from_value::<BareReport>(report_json).is_err());
362    }
363
364    #[test]
365    fn can_parse_nel_report() {
366        let report_json = json!({
367            "age": 500,
368            "type": "network-error",
369            "url": "https://example.com/about/",
370            "user_agent": "Mozilla/5.0",
371            "body": {
372                "referrer": "https://example.com/",
373                "sampling_fraction": 0.5,
374                "server_ip": "203.0.113.75",
375                "protocol": "h2",
376                "method": "POST",
377                "status_code": 200,
378                "elapsed_time": 45,
379                "phase":"application",
380                "type": "ok"
381            }
382        });
383        let bare_report: BareReport =
384            serde_json::from_value(report_json).expect("Should be able to parse JSON report");
385        let report: Report<NEL> = bare_report
386            .parse()
387            .expect("Report should be a NEL report")
388            .expect("Should be able to parse NEL report body");
389        assert_eq!(
390            report,
391            Report {
392                age: Duration::from_millis(500),
393                url: "https://example.com/about/".to_string(),
394                user_agent: "Mozilla/5.0".to_string(),
395                body: NEL {
396                    referrer: "https://example.com/".to_string(),
397                    sampling_fraction: 0.5,
398                    server_ip: "203.0.113.75".to_string(),
399                    protocol: "h2".to_string(),
400                    method: "POST".to_string(),
401                    status_code: Some(200),
402                    elapsed_time: Some(Duration::from_millis(45)),
403                    phase: "application".to_string(),
404                    status: "ok".to_string(),
405                },
406            }
407        );
408    }
409}