summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src
diff options
context:
space:
mode:
authorLinus Färnstrand <linus@mullvad.net>2018-07-02 16:10:37 +0200
committerLinus Färnstrand <linus@mullvad.net>2018-07-03 11:33:30 +0200
commit212ab525ed69e3ba47ade75dcc04a3d528ca7ece (patch)
tree154673d53e58a5c4d514279a732a3cd02aea8344 /mullvad-daemon/src
parent56922a2b644a1dc6f4b872e2dfcd84571ca354a3 (diff)
downloadmullvadvpn-212ab525ed69e3ba47ade75dcc04a3d528ca7ece.tar.xz
mullvadvpn-212ab525ed69e3ba47ade75dcc04a3d528ca7ece.zip
Move problem-report out to own crate
Diffstat (limited to 'mullvad-daemon/src')
-rw-r--r--mullvad-daemon/src/bin/problem-report.rs594
1 files changed, 0 insertions, 594 deletions
diff --git a/mullvad-daemon/src/bin/problem-report.rs b/mullvad-daemon/src/bin/problem-report.rs
deleted file mode 100644
index c4db962ab5..0000000000
--- a/mullvad-daemon/src/bin/problem-report.rs
+++ /dev/null
@@ -1,594 +0,0 @@
-//! # License
-//!
-//! Copyright (C) 2017 Amagicom AB
-//!
-//! This program is free software: you can redistribute it and/or modify it under the terms of the
-//! GNU General Public License as published by the Free Software Foundation, either version 3 of
-//! the License, or (at your option) any later version.
-
-#[macro_use]
-extern crate clap;
-#[macro_use]
-extern crate error_chain;
-#[macro_use]
-extern crate lazy_static;
-extern crate regex;
-
-extern crate mullvad_paths;
-extern crate mullvad_rpc;
-
-use error_chain::ChainedError;
-use regex::Regex;
-
-use std::borrow::Cow;
-use std::cmp::min;
-use std::collections::{HashMap, HashSet};
-use std::env;
-use std::ffi::OsStr;
-use std::fs::{self, File};
-use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
-use std::path::{Path, PathBuf};
-use std::process::Command;
-
-/// Maximum number of bytes to read from each log file
-const LOG_MAX_READ_BYTES: usize = 1 * 1024 * 1024;
-/// Fit two logs plus some system information in the report.
-const REPORT_MAX_SIZE: usize = 2 * LOG_MAX_READ_BYTES + 16 * 1024;
-
-
-/// Field delimeter in generated problem report
-const LOG_DELIMITER: &'static str = "====================";
-
-/// Line separator character sequence
-#[cfg(not(windows))]
-const LINE_SEPARATOR: &str = "\n";
-
-#[cfg(windows)]
-const LINE_SEPARATOR: &str = "\r\n";
-
-/// Custom macro to write a line to an output formatter that uses platform-specific newline
-/// character sequences.
-macro_rules! write_line {
- ($fmt:expr $(,)*) => { write!($fmt, "{}", LINE_SEPARATOR) };
- ($fmt:expr, $pattern:expr $(, $arg:expr)* $(,)*) => {
- write!($fmt, $pattern, $( $arg ),*)
- .and_then(|_| write!($fmt, "{}", LINE_SEPARATOR))
- };
-}
-
-error_chain!{
- errors {
- LogDirError(path: PathBuf) {
- description("Error listing the files in the mullvad-daemon log directory")
- display(
- "Error listing the files in the mullvad-daemon log directory: {}",
- path.to_string_lossy()
- )
- }
- WriteReportError(path: PathBuf) {
- description("Error writing the problem report file")
- display("Error writing the problem report file: {}", path.display())
- }
- ReadLogError(path: PathBuf) {
- description("Error reading the contents of log file")
- display("Error reading the contents of log file: {}", path.display())
- }
- RpcError {
- description("Error during RPC call")
- }
- }
-}
-
-quick_main!(run);
-
-fn run() -> Result<()> {
- let app = clap::App::new("problem-report")
- .version(daemon_version())
- .author(crate_authors!())
- .about("Mullvad VPN problem report tool. Collects logs and sends them to Mullvad support.")
- .setting(clap::AppSettings::SubcommandRequired)
- .subcommand(
- clap::SubCommand::with_name("collect")
- .about("Collect problem report")
- .arg(
- clap::Arg::with_name("output")
- .help("The destination path for saving the collected report.")
- .long("output")
- .short("o")
- .value_name("PATH")
- .takes_value(true)
- .required(true),
- )
- .arg(
- clap::Arg::with_name("extra_logs")
- .help("Paths to additional log files to be included.")
- .multiple(true)
- .value_name("EXTRA LOGS")
- .takes_value(true)
- .required(false),
- )
- .arg(
- clap::Arg::with_name("redact")
- .help("List of words and expressions to remove from the report")
- .long("redact")
- .value_name("PHRASE")
- .multiple(true)
- .takes_value(true),
- ),
- )
- .subcommand(
- clap::SubCommand::with_name("send")
- .about("Send collected problem report")
- .arg(
- clap::Arg::with_name("report")
- .long("report")
- .short("r")
- .help("The path to previously collected report file.")
- .takes_value(true)
- .required(true),
- )
- .arg(
- clap::Arg::with_name("email")
- .long("email")
- .short("e")
- .help("Reporter's email")
- .takes_value(true)
- .required(false),
- )
- .arg(
- clap::Arg::with_name("message")
- .long("message")
- .short("m")
- .help("Reporter's message")
- .takes_value(true)
- .required(false),
- ),
- );
-
- let matches = app.get_matches();
-
- if let Some(collect_matches) = matches.subcommand_matches("collect") {
- let redact_custom_strings = collect_matches
- .values_of_lossy("redact")
- .unwrap_or(Vec::new());
- let extra_logs = collect_matches
- .values_of_os("extra_logs")
- .map(|os_values| os_values.map(Path::new).collect())
- .unwrap_or(Vec::new());
- let output_path = Path::new(collect_matches.value_of_os("output").unwrap());
- collect_report(&extra_logs, output_path, redact_custom_strings)
- } else if let Some(send_matches) = matches.subcommand_matches("send") {
- let report_path = Path::new(send_matches.value_of_os("report").unwrap());
- let user_email = send_matches.value_of("email").unwrap_or("");
- let user_message = send_matches.value_of("message").unwrap_or("");
- send_problem_report(user_email, user_message, report_path)
- } else {
- unreachable!("No sub command given");
- }
-}
-
-fn collect_report(
- extra_logs: &[&Path],
- output_path: &Path,
- redact_custom_strings: Vec<String>,
-) -> Result<()> {
- let mut problem_report = ProblemReport::new(redact_custom_strings);
-
- match logs_from_log_directory() {
- Ok(logs) => problem_report.try_add_logs(logs),
- Err(error) => problem_report.add_error("Failed to list logs in log directory", error),
- }
-
- problem_report.add_logs(extra_logs);
-
- write_problem_report(&output_path, problem_report)
- .chain_err(|| ErrorKind::WriteReportError(output_path.to_path_buf()))
-}
-
-fn logs_from_log_directory() -> Result<impl Iterator<Item = Result<PathBuf>>> {
- let log_dir = mullvad_paths::get_log_dir().chain_err(|| "Unable to get log directory")?;
-
- fs::read_dir(&log_dir)
- .chain_err(|| ErrorKind::LogDirError(log_dir.clone()))
- .map(|dir_entries| {
- let log_extension = Some(OsStr::new("log"));
-
- dir_entries.filter_map(move |dir_entry_result| match dir_entry_result {
- Ok(dir_entry) => {
- let path = dir_entry.path();
-
- if path.extension() == log_extension {
- Some(Ok(path))
- } else {
- None
- }
- }
- Err(cause) => Some(Err(Error::with_chain(
- cause,
- ErrorKind::LogDirError(log_dir.clone()),
- ))),
- })
- })
-}
-
-fn send_problem_report(user_email: &str, user_message: &str, report_path: &Path) -> Result<()> {
- let report_content = normalize_newlines(
- read_file_lossy(report_path, REPORT_MAX_SIZE)
- .chain_err(|| ErrorKind::ReadLogError(report_path.to_path_buf()))?,
- );
- let metadata = collect_metadata();
- let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::new();
- let mut rpc_client = mullvad_rpc::ProblemReportProxy::connect(&mut rpc_manager)
- .chain_err(|| ErrorKind::RpcError)?;
- rpc_client
- .problem_report(user_email, user_message, &report_content, &metadata)
- .call()
- .chain_err(|| ErrorKind::RpcError)
-}
-
-fn write_problem_report(path: &Path, problem_report: ProblemReport) -> io::Result<()> {
- let file = File::create(path)?;
- let mut permissions = file.metadata()?.permissions();
- permissions.set_readonly(true);
- file.set_permissions(permissions)?;
- problem_report.write_to(BufWriter::new(file))?;
- Ok(())
-}
-
-
-#[derive(Debug)]
-struct ProblemReport {
- metadata: HashMap<String, String>,
- logs: Vec<(String, String)>,
- log_paths: HashSet<PathBuf>,
- redact_custom_strings: Vec<String>,
-}
-
-impl ProblemReport {
- /// Creates a new problem report with system information. Logs can be added with `add_log`.
- /// Logs will have all strings in `redact_custom_strings` removed from them.
- pub fn new(redact_custom_strings: Vec<String>) -> Self {
- ProblemReport {
- metadata: collect_metadata(),
- logs: Vec::new(),
- log_paths: HashSet::new(),
- redact_custom_strings,
- }
- }
-
- /// Attach some file logs to this report. This method adds the error chain instead of the log
- /// contents if an error occurs while reading one of the log files.
- pub fn add_logs<I>(&mut self, paths: I)
- where
- I: IntoIterator,
- I::Item: AsRef<Path>,
- {
- for path in paths {
- self.add_log(path.as_ref());
- }
- }
-
- /// Tries to attach some file logs to this report.
- ///
- /// This method receives a result with an iterator of results of file paths. If any of the
- /// results are errors, they are collected and displayed in the final report.
- pub fn try_add_logs<I, P>(&mut self, paths: I)
- where
- I: IntoIterator<Item = Result<P>>,
- P: AsRef<Path>,
- {
- for path_result in paths {
- match path_result {
- Ok(path) => self.add_log(path.as_ref()),
- Err(error) => self.add_error("Error getting next log file", error),
- }
- }
- }
-
- /// Attach a file log to this report. This method adds the error chain instead of the log
- /// contents if an error occurs while reading the log file.
- pub fn add_log(&mut self, path: &Path) {
- if self.log_paths.insert(path.to_owned()) {
- let content = self.redact(
- &read_file_lossy(path, LOG_MAX_READ_BYTES)
- .chain_err(|| ErrorKind::ReadLogError(path.to_path_buf()))
- .unwrap_or_else(|e| e.display_chain().to_string()),
- );
- let path = self.redact(&path.to_string_lossy());
- self.logs.push((path, content));
- }
- }
-
- /// Attach an error to the report.
- pub fn add_error<S, E>(&mut self, message: S, error: E)
- where
- S: ToString,
- E: ChainedError,
- {
- let redacted_error = self.redact(&error.display_chain().to_string());
-
- self.logs.push((message.to_string(), redacted_error));
- }
-
- fn redact(&self, input: &str) -> String {
- let out1 = Self::redact_account_number(input);
- let out2 = Self::redact_home_dir(&out1);
- let out3 = Self::redact_network_info(&out2);
- self.redact_custom_strings(&out3).to_string()
- }
-
- fn redact_account_number(input: &str) -> Cow<str> {
- lazy_static! {
- static ref RE: Regex = Regex::new("\\d{16}").unwrap();
- }
- RE.replace_all(input, "[REDACTED ACCOUNT NUMBER]")
- }
-
- fn redact_home_dir(input: &str) -> Cow<str> {
- match env::home_dir() {
- Some(home) => Cow::from(input.replace(home.to_string_lossy().as_ref(), "~")),
- None => Cow::from(input),
- }
- }
-
- fn redact_network_info(input: &str) -> Cow<str> {
- lazy_static! {
- static ref RE: Regex = {
- let combined_pattern = format!(
- "\\b({}|{}|{})\\b",
- build_ipv4_regex(),
- build_ipv6_regex(),
- build_mac_regex()
- );
- Regex::new(&combined_pattern).unwrap()
- };
- }
- RE.replace_all(input, "[REDACTED]")
- }
-
- fn redact_custom_strings<'a>(&self, input: &'a str) -> Cow<'a, str> {
- // Can probably me made a lot faster with aho-corasick if optimization is ever needed.
- let mut out = Cow::from(input);
- for redact in &self.redact_custom_strings {
- out = out.replace(redact, "[REDACTED]").into()
- }
- out
- }
-
- fn write_to<W: Write>(&self, mut output: W) -> io::Result<()> {
- write_line!(output, "System information:")?;
- for (key, value) in &self.metadata {
- write_line!(output, "{}: {}", key, value)?;
- }
- write_line!(output)?;
- for &(ref label, ref content) in &self.logs {
- write_line!(output, "{}", LOG_DELIMITER)?;
- write_line!(output, "Log: {}", label)?;
- write_line!(output, "{}", LOG_DELIMITER)?;
- output.write_all(content.as_bytes())?;
- write_line!(output)?;
- }
- Ok(())
- }
-}
-
-fn build_mac_regex() -> String {
- let octet = "[[:xdigit:]]{2}"; // 0 - ff
-
- // five pairs of two hexadecimal chars followed by colon or dash
- // followed by a pair of hexadecimal chars
- format!("(?:{0}[:-]){{5}}({0})", octet)
-}
-
-fn build_ipv4_regex() -> String {
- // regex adapted from https://www.regular-expressions.info/ip.html
-
- let above_250 = "25[0-5]";
- let above_200 = "2[0-4][0-9]";
- let above_100 = "1[0-9][0-9]";
-
- // 100-119 | 120-126 | 128-129 | 130 - 199
- let above_100_not_127 = "1(?:[01][0-9]|2[0-6]|2[89]|[3-9][0-9])";
-
- let above_0 = "0?[0-9][0-9]?";
-
- // matches 0-255, except 127
- let first_octet = format!(
- "(?:{}|{}|{}|{})",
- above_250, above_200, above_100_not_127, above_0
- );
-
- // matches 0-255
- let ip_octet = format!("(?:{}|{}|{}|{})", above_250, above_200, above_100, above_0);
-
- format!("(?:{0}\\.{1}\\.{1}\\.{1})", first_octet, ip_octet)
-}
-
-fn build_ipv6_regex() -> String {
- let hextet = "[[:xdigit:]]{1,4}"; // 0 - ffff
-
- // Matches 1-7 hextets followed by one or two colons
- // and one last hextet.
- //
- // This means that there are many
- // invalid IPv6 addresses that matches this. E.g.
- // all that has more than one instance of '::', but we
- // don't really care.
- let short = format!("({0}::?){{1,6}}(:{0}){{1,6}}", hextet);
-
- // Matches addresses without double colon. This is
- // a separate regex to make it easier to not match
- // on time
- let long = format!("({0}:){{7}}{0}", hextet);
-
- format!("(?:{})|(?:{})", short, long)
-}
-
-/// Helper to lossily read a file to a `String`. If the file size exceeds the given `max_bytes`,
-/// only the last `max_bytes` bytes of the file are read.
-fn read_file_lossy(path: &Path, max_bytes: usize) -> io::Result<String> {
- let mut file = File::open(path)?;
- let file_size = file.metadata()?.len();
-
- if file_size > max_bytes as u64 {
- file.seek(SeekFrom::Start(file_size - max_bytes as u64))?;
- }
-
- let capacity = min(file_size, max_bytes as u64) as usize;
- let mut buffer = Vec::with_capacity(capacity);
- file.take(max_bytes as u64).read_to_end(&mut buffer)?;
- Ok(String::from_utf8_lossy(&buffer).into_owned())
-}
-
-fn collect_metadata() -> HashMap<String, String> {
- let mut metadata = HashMap::new();
- metadata.insert(
- String::from("mullvad-daemon-version"),
- daemon_version().to_owned(),
- );
- metadata.insert(String::from("os"), os_version());
- metadata
-}
-
-fn daemon_version() -> &'static str {
- concat!(
- include_str!(concat!(env!("OUT_DIR"), "/product-version.txt")),
- " ",
- include_str!(concat!(env!("OUT_DIR"), "/git-commit-date.txt"))
- )
-}
-
-#[cfg(target_os = "linux")]
-fn os_version() -> String {
- format!(
- "Linux, {}",
- command_stdout_lossy("lsb_release", &["-ds"])
- .unwrap_or(String::from("[Failed to get LSB release]"))
- )
-}
-
-#[cfg(target_os = "macos")]
-fn os_version() -> String {
- format!(
- "macOS {}",
- command_stdout_lossy("sw_vers", &["-productVersion"])
- .unwrap_or(String::from("[Failed to detect version]"))
- )
-}
-
-#[cfg(windows)]
-fn os_version() -> String {
- let system_info =
- command_stdout_lossy("systeminfo", &["/FO", "LIST"]).unwrap_or_else(String::new);
-
- let mut os_name = None;
- let mut os_version = None;
-
- for info_line in system_info.lines() {
- let mut info_parts = info_line.split(":");
-
- match info_parts.next() {
- Some("OS Name") => os_name = info_parts.next(),
- Some("OS Version") => os_version = info_parts.next(),
- _ => {}
- }
- }
-
- match (os_name, os_version) {
- (None, None) => String::from("Windows [Failed to detect version]"),
- (Some(os_name), None) => os_name.trim().to_owned(),
- (None, Some(os_version)) => format!("Windows version {}", os_version.trim()),
- (Some(os_name), Some(os_version)) => {
- format!("{} version {}", os_name.trim(), os_version.trim())
- }
- }
-}
-
-/// Helper for getting stdout of some command as a String. Ignores the exit code of the command.
-fn command_stdout_lossy(cmd: &str, args: &[&str]) -> Option<String> {
- Command::new(cmd)
- .args(args)
- .output()
- .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
- .ok()
-}
-
-#[cfg(not(windows))]
-fn normalize_newlines(text: String) -> String {
- text
-}
-
-#[cfg(windows)]
-fn normalize_newlines(text: String) -> String {
- text.replace(LINE_SEPARATOR, "\n")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn redacts_ipv4() {
- assert_redacts_ipv4("1.2.3.4");
- assert_redacts_ipv4("10.127.0.1");
- assert_redacts_ipv4("192.168.1.1");
- assert_redacts_ipv4("10.0.16.1");
- assert_redacts_ipv4("173.54.12.32");
- assert_redacts_ipv4("68.4.4.1");
- }
-
- fn assert_redacts_ipv4(input: &str) {
- let report = ProblemReport::new(vec![]);
- let actual = report.redact(&format!("pre {} post", input));
- assert_eq!("pre [REDACTED] post", actual);
- }
-
- #[test]
- fn does_not_redact_localhost_ipv4() {
- let report = ProblemReport::new(vec![]);
- let res = report.redact("127.0.0.1");
- assert_eq!("127.0.0.1", res);
- }
-
- #[test]
- fn redacts_ipv6() {
- assert_redacts_ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
- assert_redacts_ipv6("2001:db8:85a3:0:0:8a2e:370:7334");
- assert_redacts_ipv6("2001:db8:85a3::8a2e:370:7334");
- assert_redacts_ipv6("2001:db8:0:0:0:0:2:1");
- assert_redacts_ipv6("2001:db8::2:1");
- assert_redacts_ipv6("2001:db8:0000:1:1:1:1:1");
- assert_redacts_ipv6("2001:db8:0:1:1:1:1:1");
- assert_redacts_ipv6("2001:db8:0:0:1:0:0:1");
- assert_redacts_ipv6("2001:db8::1:0:0:1");
- assert_redacts_ipv6("0::0");
- assert_redacts_ipv6("0:0:0:0::1");
- }
-
- #[test]
- fn doesnt_redact_not_ipv6() {
- let report = ProblemReport::new(vec![]);
- let actual = report.redact("[talpid_core::firewall]");
- assert_eq!("[talpid_core::firewall]", actual);
- }
-
- fn assert_redacts_ipv6(input: &str) {
- let report = ProblemReport::new(vec![]);
- let actual = report.redact(&format!("pre {} post", input));
- assert_eq!("pre [REDACTED] post", actual);
- }
-
- #[test]
- fn test_does_not_redact_localhost_ipv6() {
- let report = ProblemReport::new(vec![]);
- let res = report.redact("::1");
- assert_eq!("::1", res);
- }
-
- #[test]
- fn test_does_not_redact_time() {
- let report = ProblemReport::new(vec![]);
- let res = report.redact("09:47:59");
- assert_eq!("09:47:59", res);
- }
-}