/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Support for crash report creation and upload. //! //! Upload currently uses the system libcurl or curl binary rather than a rust network stack (as //! curl is more mature, albeit the code to interact with it must be a bit more careful). use super::http; use crate::std::path::Path; #[cfg(mock)] use crate::std::mock::{mock_key, MockKey}; #[cfg(mock)] mock_key! { /// The outer Result is for CrashReport::send(), the inner is for CrashReporterSender::finish(). pub struct MockReport => Box std::io::Result> + Send + Sync> } /// A crash report to upload. /// /// Post a multipart form payload to the report URL. /// /// The form data contains: /// | name | filename | content | mime | /// ==================================== /// | `extra` | `extra.json` | extra json object | `application/json`| /// | `upload_file_minidump` | dump file name | dump file contents | derived (probably application/binary) | /// if present: /// | `memory_report` | memory file name | memory file contents | derived (probably gzipped json) | pub struct CrashReport<'a> { pub extra: &'a serde_json::Value, pub dump_file: &'a Path, pub memory_file: Option<&'a Path>, pub url: &'a str, } impl CrashReport<'_> { /// Send the crash report. pub fn send(&self) -> anyhow::Result { let mut request = None; #[cfg(mock)] if let Some(r) = MockReport.try_get(|f| f(self)) { let response = r?; request = Some(http::Request::Mock { response: response.map(|s| s.into()), }); } let extra_json_data; if request.is_none() { let mut extra = self.extra.clone(); // Bug 2021130: If ProcessType is main (it always should be, because we only send // reports for main process crashes), remove the annotation. Socorro expects main // processes to not have the ProcessType annotation. // // This is mostly a workaround; ideally Socorro would change to accept a ProcessType of // "main" (see bug 2021204). if extra["ProcessType"].as_str() == Some("main") { extra.as_object_mut().unwrap().remove("ProcessType"); } extra_json_data = serde_json::to_string(&extra)?; let mut parts = vec![ http::MimePart { name: "extra", content: http::MimePartContent::String(&extra_json_data), filename: Some("extra.json"), mime_type: Some("application/json"), }, http::MimePart { name: "upload_file_minidump", content: http::MimePartContent::File(self.dump_file), filename: None, mime_type: None, }, ]; if let Some(path) = self.memory_file { parts.push(http::MimePart { name: "memory_report", content: http::MimePartContent::File(path), filename: None, mime_type: None, }) } request = Some(http::RequestBuilder::MimePost { parts }.build(self.url)?); } let response = request.unwrap().send()?; let response = String::from_utf8_lossy(&response).into_owned(); log::debug!("received response from sending report: {:?}", &*response); Ok(Response::parse(response)) } } /// A parsed response from submitting a crash report. #[derive(Default, Debug)] pub struct Response { pub crash_id: Option, pub stop_sending_reports_for: Option, pub view_url: Option, pub discarded: bool, } impl Response { /// Parse a server response. /// /// The response should be newline-separated `=` pairs. fn parse>(response: S) -> Self { let mut ret = Self::default(); // Fields may be omitted, and parsing is best-effort but will not produce any errors (just // a default Response struct). for line in response.as_ref().lines() { if let Some((key, value)) = line.split_once('=') { match key { "StopSendingReportsFor" => { ret.stop_sending_reports_for = Some(value.to_owned()) } "Discarded" => ret.discarded = true, "CrashID" => ret.crash_id = Some(value.to_owned()), "ViewURL" => ret.view_url = Some(value.to_owned()), _ => (), } } } ret } } #[cfg(test)] mod test { use super::*; use crate::std::mock; #[test] fn report_http() { for memory_file in [None, Some(Path::new("minidump.memory.gz"))] { let report = CrashReport { extra: &serde_json::json! {{ "Foo": "Bar" }}, dump_file: Path::new("minidump.dmp"), memory_file, url: "reports.example.com".as_ref(), }; let checked = crate::test::Counter::new(); mock::builder() .set( http::MockHttp, Box::new(cc!( (checked) move |request, url| { checked.inc(); assert_eq!(url, "reports.example.com"); let mut parts = vec![ http::MimePart { name: "extra", content: http::MimePartContent::String(r#"{"Foo":"Bar"}"#), filename: Some("extra.json"), mime_type: Some("application/json"), }, http::MimePart { name: "upload_file_minidump", content: http::MimePartContent::File(Path::new("minidump.dmp")), filename: None, mime_type: None, }, ]; if let Some(name) = memory_file { parts.push(http::MimePart { name: "memory_report", content: http::MimePartContent::File(name), filename: None, mime_type: None, }); } assert_eq!(request, &http::RequestBuilder::MimePost { parts }); Ok(Ok(vec![])) } )), ) .run(|| report.send().unwrap()); checked.assert_one(); } } #[test] fn report_response() { let report = CrashReport { extra: &serde_json::json! {{}}, dump_file: Path::new("minidump.dmp"), memory_file: None, url: "reports.example.com".as_ref(), }; mock::builder() .set( http::MockHttp, Box::new(|_request, _url| { Ok(Ok("CrashID=1234\nDiscarded=1\nStopSendingReportsFor=100\nViewURL=reports.example.com/foo".into())) }), ) .run(|| { let response = report.send().unwrap(); assert_eq!(response.crash_id.as_deref(), Some("1234")); assert!(response.discarded); assert_eq!(response.stop_sending_reports_for.as_deref(), Some("100")); assert_eq!(response.view_url.as_deref(), Some("reports.example.com/foo")); }); } }