summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-06-17 21:15:10 +0000
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-06-19 12:17:13 +0000
commit20218e532558ccedae793cfe823a396f06c194a7 (patch)
tree2abbd7df93cf494acbd460392dbcd035d82201ff
parent0d58496ed00837c380e1397e05fbe4f37a98bbd3 (diff)
downloadmullvadvpn-20218e532558ccedae793cfe823a396f06c194a7.tar.xz
mullvadvpn-20218e532558ccedae793cfe823a396f06c194a7.zip
Allow `mullvad-problem-report` to be used as a lib
-rw-r--r--mullvad-problem-report/src/lib.rs552
-rw-r--r--mullvad-problem-report/src/main.rs545
2 files changed, 554 insertions, 543 deletions
diff --git a/mullvad-problem-report/src/lib.rs b/mullvad-problem-report/src/lib.rs
new file mode 100644
index 0000000000..4fa42413fa
--- /dev/null
+++ b/mullvad-problem-report/src/lib.rs
@@ -0,0 +1,552 @@
+//! # 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.
+
+#![deny(rust_2018_idioms)]
+
+use lazy_static::lazy_static;
+use regex::Regex;
+use std::{
+ borrow::Cow,
+ cmp::min,
+ collections::{HashMap, HashSet},
+ ffi::OsStr,
+ fs::{self, File},
+ io::{self, BufWriter, Read, Seek, SeekFrom, Write},
+ path::{Path, PathBuf},
+};
+use talpid_types::ErrorExt;
+use tokio_core::reactor::Core;
+
+
+pub mod metadata;
+
+/// Maximum number of bytes to read from each log file
+const LOG_MAX_READ_BYTES: usize = 128 * 1024;
+const EXTRA_BYTES: usize = 32 * 1024;
+/// Fit five logs plus some system information in the report.
+const REPORT_MAX_SIZE: usize = (5 * LOG_MAX_READ_BYTES) + EXTRA_BYTES;
+
+
+/// Field delimeter in generated problem report
+const LOG_DELIMITER: &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))
+ };
+}
+
+/// These are critical errors that can happen when using the tool, that stops
+/// it from working. Meaning it will print the error and exit.
+#[derive(err_derive::Error, Debug)]
+pub enum Error {
+ #[error(display = "Failed to write the problem report to {}", path)]
+ WriteReportError {
+ path: String,
+ #[error(cause)]
+ source: io::Error,
+ },
+
+ #[error(display = "Failed to read the problem report at {}", path)]
+ ReadProblemReportError {
+ path: String,
+ #[error(cause)]
+ source: io::Error,
+ },
+
+ #[error(display = "Unable to create JSON-RPC 2.0 client")]
+ CreateRpcClientError(#[error(cause)] mullvad_rpc::HttpError),
+
+ #[error(display = "Error during RPC call")]
+ SendRpcError(#[error(cause)] mullvad_rpc::Error),
+}
+
+/// These are errors that can happen during problem report collection.
+/// They are not critical, but they will be added inside the problem report,
+/// instead of whatever content was supposed to be there.
+#[derive(err_derive::Error, Debug)]
+pub enum LogError {
+ #[error(display = "Unable to get log directory")]
+ GetLogDir(#[error(source)] mullvad_paths::Error),
+
+ #[error(display = "Failed to list the files in the log directory: {}", path)]
+ ListLogDir {
+ path: String,
+ #[error(cause)]
+ source: io::Error,
+ },
+
+ #[error(display = "Error reading the contents of log file: {}", path)]
+ ReadLogError { path: String },
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[error(display = "No home directory for current user")]
+ NoHomeDir,
+
+ #[cfg(target_os = "windows")]
+ #[error(display = "Missing %LOCALAPPDATA% environment variable")]
+ NoLocalAppDataDir,
+}
+
+pub fn collect_report(
+ extra_logs: &[&Path],
+ output_path: &Path,
+ redact_custom_strings: Vec<String>,
+) -> Result<(), Error> {
+ let mut problem_report = ProblemReport::new(redact_custom_strings);
+
+ let daemon_logs = mullvad_paths::get_log_dir()
+ .map_err(LogError::GetLogDir)
+ .and_then(list_logs);
+ match daemon_logs {
+ Ok(daemon_logs) => {
+ let mut other_logs = Vec::new();
+ for log in daemon_logs {
+ match log {
+ Ok(path) => {
+ if is_tunnel_log(&path) {
+ problem_report.add_log(&path);
+ } else {
+ other_logs.push(path);
+ }
+ }
+ Err(error) => problem_report.add_error("Unable to get log path", &error),
+ }
+ }
+ for other_log in other_logs {
+ problem_report.add_log(&other_log);
+ }
+ }
+ Err(error) => {
+ problem_report.add_error("Failed to list logs in daemon log directory", &error)
+ }
+ };
+ match frontend_log_dir().map(|dir| dir.and_then(list_logs)) {
+ Some(Ok(frontend_logs)) => {
+ for log in frontend_logs {
+ match log {
+ Ok(path) => problem_report.add_log(&path),
+ Err(error) => problem_report.add_error("Unable to get log path", &error),
+ }
+ }
+ }
+ Some(Err(error)) => {
+ problem_report.add_error("Failed to list logs in frontend log directory", &error)
+ }
+ None => {}
+ }
+
+ problem_report.add_logs(extra_logs);
+
+ write_problem_report(&output_path, &problem_report).map_err(|source| Error::WriteReportError {
+ path: output_path.display().to_string(),
+ source,
+ })
+}
+
+/// Returns an iterator over all files in the given directory that has the `.log` extension.
+fn list_logs(
+ log_dir: PathBuf,
+) -> Result<impl Iterator<Item = Result<PathBuf, LogError>>, LogError> {
+ fs::read_dir(&log_dir)
+ .map_err(|source| LogError::ListLogDir {
+ path: log_dir.display().to_string(),
+ source,
+ })
+ .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(source) => Some(Err(LogError::ListLogDir {
+ path: log_dir.display().to_string(),
+ source,
+ })),
+ })
+ })
+}
+
+/// Returns the directory where the Mullvad GUI frontend stores its logs.
+/// If the current platform has a separate directory for frontend logs.
+fn frontend_log_dir() -> Option<Result<PathBuf, LogError>> {
+ #[cfg(target_os = "linux")]
+ {
+ Some(
+ dirs::home_dir()
+ .ok_or(LogError::NoHomeDir)
+ .map(|home_dir| home_dir.join(".config/Mullvad VPN/logs")),
+ )
+ }
+ #[cfg(target_os = "macos")]
+ {
+ Some(
+ dirs::home_dir()
+ .ok_or(LogError::NoHomeDir)
+ .map(|home_dir| home_dir.join("Library/Logs/Mullvad VPN")),
+ )
+ }
+ #[cfg(target_os = "windows")]
+ {
+ Some(match std::env::var_os("LOCALAPPDATA") {
+ Some(dir) => Ok(Path::new(&dir).join("Mullvad VPN/logs")),
+ None => Err(LogError::NoLocalAppDataDir),
+ })
+ }
+ #[cfg(target_os = "android")]
+ {
+ None
+ }
+}
+
+fn is_tunnel_log(path: &Path) -> bool {
+ match path.file_name() {
+ Some(file_name) => file_name.to_string_lossy().contains("openvpn"),
+ None => false,
+ }
+}
+
+pub fn send_problem_report(
+ user_email: &str,
+ user_message: &str,
+ report_path: &Path,
+) -> Result<(), Error> {
+ let report_content = normalize_newlines(
+ read_file_lossy(report_path, REPORT_MAX_SIZE).map_err(|source| {
+ Error::ReadProblemReportError {
+ path: report_path.display().to_string(),
+ source,
+ }
+ })?,
+ );
+ let metadata = metadata::collect();
+
+ let ca_path = mullvad_paths::resources::get_api_ca_path();
+
+ let mut core = Core::new().unwrap();
+ let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::new(ca_path);
+ let rpc_http_handle = rpc_manager
+ .new_connection_on_event_loop(&core.handle())
+ .map_err(Error::CreateRpcClientError)?;
+ let mut rpc_client = mullvad_rpc::ProblemReportProxy::new(rpc_http_handle);
+
+ core.run(rpc_client.problem_report(user_email, user_message, &report_content, &metadata))
+ .map_err(Error::SendRpcError)
+}
+
+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(mut redact_custom_strings: Vec<String>) -> Self {
+ redact_custom_strings.retain(|redact| !redact.is_empty());
+
+ ProblemReport {
+ metadata: metadata::collect(),
+ 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());
+ }
+ }
+
+ /// 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) {
+ let expanded_path = path.canonicalize().unwrap_or_else(|_| path.to_owned());
+ if self.log_paths.insert(expanded_path.clone()) {
+ let redacted_path = self.redact(&expanded_path.to_string_lossy());
+ let content = self.redact(&read_file_lossy(path, LOG_MAX_READ_BYTES).unwrap_or_else(
+ |error| {
+ error.display_chain_with_msg(&format!(
+ "Error reading the contents of log file: {}",
+ expanded_path.display()
+ ))
+ },
+ ));
+ self.logs.push((redacted_path, content));
+ println!("Adding {}", expanded_path.display());
+ }
+ }
+
+ /// Attach an error to the report.
+ pub fn add_error(&mut self, message: &'static str, error: &impl ErrorExt) {
+ let redacted_error = self.redact(&error.display_chain());
+ 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 dirs::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 boundary = "[^0-9a-zA-Z.:]";
+ let combined_pattern = format!(
+ "(?P<start>^|{})(?:{}|{}|{})",
+ boundary,
+ build_ipv4_regex(),
+ build_ipv6_regex(),
+ build_mac_regex(),
+ );
+ Regex::new(&combined_pattern).unwrap()
+ };
+ }
+ RE.replace_all(input, "$start[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 {
+ // Regular expression obtained from:
+ // https://stackoverflow.com/a/17871737
+ let ipv4_segment = "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])";
+ let ipv4_address = format!("({0}\\.){{3,3}}{0}", ipv4_segment);
+
+ let ipv6_segment = "[0-9a-fA-F]{1,4}";
+
+ let long = format!("({0}:){{7,7}}{0}", ipv6_segment);
+ let compressed_1 = format!("({0}:){{1,7}}:", ipv6_segment);
+ let compressed_2 = format!("({0}:){{1,6}}:{0}", ipv6_segment);
+ let compressed_3 = format!("({0}:){{1,5}}(:{0}){{1,2}}", ipv6_segment);
+ let compressed_4 = format!("({0}:){{1,4}}(:{0}){{1,3}}", ipv6_segment);
+ let compressed_5 = format!("({0}:){{1,3}}(:{0}){{1,4}}", ipv6_segment);
+ let compressed_6 = format!("({0}:){{1,2}}(:{0}){{1,5}}", ipv6_segment);
+ let compressed_7 = format!("{0}:((:{0}){{1,6}})", ipv6_segment);
+ let compressed_8 = format!(":((:{0}){{1,7}}|:)", ipv6_segment);
+ let link_local = "[Ff][Ee]80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}";
+ let ipv4_mapped = format!("::([fF]{{4}}(:0{{1,4}}){{0,1}}:){{0,1}}{}", ipv4_address);
+ let ipv4_embedded = format!("({0}:){{1,4}}:{1}", ipv6_segment, ipv4_address);
+
+ format!(
+ "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
+ long,
+ link_local,
+ ipv4_mapped,
+ ipv4_embedded,
+ compressed_8,
+ compressed_7,
+ compressed_6,
+ compressed_5,
+ compressed_4,
+ compressed_3,
+ compressed_2,
+ compressed_1,
+ )
+}
+
+/// 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())
+}
+
+#[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() {
+ assert_does_not_redact("127.0.0.1");
+ }
+
+ #[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("abcd:dead:beef::");
+ assert_redacts_ipv6("abcd:dead:beef:1234::");
+ assert_redacts_ipv6("::dead:beef:1234");
+ assert_redacts_ipv6("0::0");
+ assert_redacts_ipv6("0:0:0:0::1");
+ }
+
+ #[test]
+ fn doesnt_redact_not_ipv6() {
+ assert_does_not_redact("[talpid_core::firewall]");
+ }
+
+ 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_time() {
+ assert_does_not_redact("09:47:59");
+ }
+
+ fn assert_does_not_redact(input: &str) {
+ let report = ProblemReport::new(vec![]);
+ let res = report.redact(input);
+ assert_eq!(input, res);
+ }
+}
diff --git a/mullvad-problem-report/src/main.rs b/mullvad-problem-report/src/main.rs
index 868aadd2ef..4ca52967bf 100644
--- a/mullvad-problem-report/src/main.rs
+++ b/mullvad-problem-report/src/main.rs
@@ -9,104 +9,11 @@
#![deny(rust_2018_idioms)]
use clap::crate_authors;
-use lazy_static::lazy_static;
-use regex::Regex;
-use std::{
- borrow::Cow,
- cmp::min,
- collections::{HashMap, HashSet},
- env,
- ffi::OsStr,
- fs::{self, File},
- io::{self, BufWriter, Read, Seek, SeekFrom, Write},
- path::{Path, PathBuf},
- process,
-};
+use mullvad_problem_report::{collect_report, metadata, send_problem_report, Error};
+use std::{env, path::Path, process};
use talpid_types::ErrorExt;
-use tokio_core::reactor::Core;
-mod metadata;
-
-/// Maximum number of bytes to read from each log file
-const LOG_MAX_READ_BYTES: usize = 128 * 1024;
-const EXTRA_BYTES: usize = 32 * 1024;
-/// Fit five logs plus some system information in the report.
-const REPORT_MAX_SIZE: usize = (5 * LOG_MAX_READ_BYTES) + EXTRA_BYTES;
-
-
-/// Field delimeter in generated problem report
-const LOG_DELIMITER: &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))
- };
-}
-
-/// These are critical errors that can happen when using the tool, that stops
-/// it from working. Meaning it will print the error and exit.
-#[derive(err_derive::Error, Debug)]
-pub enum Error {
- #[error(display = "Failed to write the problem report to {}", path)]
- WriteReportError {
- path: String,
- #[error(cause)]
- source: io::Error,
- },
-
- #[error(display = "Failed to read the problem report at {}", path)]
- ReadProblemReportError {
- path: String,
- #[error(cause)]
- source: io::Error,
- },
-
- #[error(display = "Unable to create JSON-RPC 2.0 client")]
- CreateRpcClientError(#[error(cause)] mullvad_rpc::HttpError),
-
- #[error(display = "Error during RPC call")]
- SendRpcError(#[error(cause)] mullvad_rpc::Error),
-}
-
-/// These are errors that can happen during problem report collection.
-/// They are not critical, but they will be added inside the problem report,
-/// instead of whatever content was supposed to be there.
-#[derive(err_derive::Error, Debug)]
-pub enum LogError {
- #[error(display = "Unable to get log directory")]
- GetLogDir(#[error(source)] mullvad_paths::Error),
-
- #[error(display = "Failed to list the files in the log directory: {}", path)]
- ListLogDir {
- path: String,
- #[error(cause)]
- source: io::Error,
- },
-
- #[error(display = "Error reading the contents of log file: {}", path)]
- ReadLogError { path: String },
-
- #[cfg(any(target_os = "linux", target_os = "macos"))]
- #[error(display = "No home directory for current user")]
- NoHomeDir,
-
- #[cfg(target_os = "windows")]
- #[error(display = "Missing %LOCALAPPDATA% environment variable")]
- NoLocalAppDataDir,
-}
-
fn main() {
process::exit(match run() {
Ok(()) => 0,
@@ -219,451 +126,3 @@ fn run() -> Result<(), Error> {
unreachable!("No sub command given");
}
}
-
-fn collect_report(
- extra_logs: &[&Path],
- output_path: &Path,
- redact_custom_strings: Vec<String>,
-) -> Result<(), Error> {
- let mut problem_report = ProblemReport::new(redact_custom_strings);
-
- let daemon_logs = mullvad_paths::get_log_dir()
- .map_err(LogError::GetLogDir)
- .and_then(list_logs);
- match daemon_logs {
- Ok(daemon_logs) => {
- let mut other_logs = Vec::new();
- for log in daemon_logs {
- match log {
- Ok(path) => {
- if is_tunnel_log(&path) {
- problem_report.add_log(&path);
- } else {
- other_logs.push(path);
- }
- }
- Err(error) => problem_report.add_error("Unable to get log path", &error),
- }
- }
- for other_log in other_logs {
- problem_report.add_log(&other_log);
- }
- }
- Err(error) => {
- problem_report.add_error("Failed to list logs in daemon log directory", &error)
- }
- };
- match frontend_log_dir().map(|dir| dir.and_then(list_logs)) {
- Some(Ok(frontend_logs)) => {
- for log in frontend_logs {
- match log {
- Ok(path) => problem_report.add_log(&path),
- Err(error) => problem_report.add_error("Unable to get log path", &error),
- }
- }
- }
- Some(Err(error)) => {
- problem_report.add_error("Failed to list logs in frontend log directory", &error)
- }
- None => {}
- }
-
- problem_report.add_logs(extra_logs);
-
- write_problem_report(&output_path, &problem_report).map_err(|source| Error::WriteReportError {
- path: output_path.display().to_string(),
- source,
- })
-}
-
-/// Returns an iterator over all files in the given directory that has the `.log` extension.
-fn list_logs(
- log_dir: PathBuf,
-) -> Result<impl Iterator<Item = Result<PathBuf, LogError>>, LogError> {
- fs::read_dir(&log_dir)
- .map_err(|source| LogError::ListLogDir {
- path: log_dir.display().to_string(),
- source,
- })
- .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(source) => Some(Err(LogError::ListLogDir {
- path: log_dir.display().to_string(),
- source,
- })),
- })
- })
-}
-
-/// Returns the directory where the Mullvad GUI frontend stores its logs.
-/// If the current platform has a separate directory for frontend logs.
-fn frontend_log_dir() -> Option<Result<PathBuf, LogError>> {
- #[cfg(target_os = "linux")]
- {
- Some(
- dirs::home_dir()
- .ok_or(LogError::NoHomeDir)
- .map(|home_dir| home_dir.join(".config/Mullvad VPN/logs")),
- )
- }
- #[cfg(target_os = "macos")]
- {
- Some(
- dirs::home_dir()
- .ok_or(LogError::NoHomeDir)
- .map(|home_dir| home_dir.join("Library/Logs/Mullvad VPN")),
- )
- }
- #[cfg(target_os = "windows")]
- {
- Some(match std::env::var_os("LOCALAPPDATA") {
- Some(dir) => Ok(Path::new(&dir).join("Mullvad VPN/logs")),
- None => Err(LogError::NoLocalAppDataDir),
- })
- }
- #[cfg(target_os = "android")]
- {
- None
- }
-}
-
-fn is_tunnel_log(path: &Path) -> bool {
- match path.file_name() {
- Some(file_name) => file_name.to_string_lossy().contains("openvpn"),
- None => false,
- }
-}
-
-fn send_problem_report(
- user_email: &str,
- user_message: &str,
- report_path: &Path,
-) -> Result<(), Error> {
- let report_content = normalize_newlines(
- read_file_lossy(report_path, REPORT_MAX_SIZE).map_err(|source| {
- Error::ReadProblemReportError {
- path: report_path.display().to_string(),
- source,
- }
- })?,
- );
- let metadata = metadata::collect();
-
- let ca_path = mullvad_paths::resources::get_api_ca_path();
-
- let mut core = Core::new().unwrap();
- let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::new(ca_path);
- let rpc_http_handle = rpc_manager
- .new_connection_on_event_loop(&core.handle())
- .map_err(Error::CreateRpcClientError)?;
- let mut rpc_client = mullvad_rpc::ProblemReportProxy::new(rpc_http_handle);
-
- core.run(rpc_client.problem_report(user_email, user_message, &report_content, &metadata))
- .map_err(Error::SendRpcError)
-}
-
-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(mut redact_custom_strings: Vec<String>) -> Self {
- redact_custom_strings.retain(|redact| !redact.is_empty());
-
- ProblemReport {
- metadata: metadata::collect(),
- 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());
- }
- }
-
- /// 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) {
- let expanded_path = path.canonicalize().unwrap_or_else(|_| path.to_owned());
- if self.log_paths.insert(expanded_path.clone()) {
- let redacted_path = self.redact(&expanded_path.to_string_lossy());
- let content = self.redact(&read_file_lossy(path, LOG_MAX_READ_BYTES).unwrap_or_else(
- |error| {
- error.display_chain_with_msg(&format!(
- "Error reading the contents of log file: {}",
- expanded_path.display()
- ))
- },
- ));
- self.logs.push((redacted_path, content));
- println!("Adding {}", expanded_path.display());
- }
- }
-
- /// Attach an error to the report.
- pub fn add_error(&mut self, message: &'static str, error: &impl ErrorExt) {
- let redacted_error = self.redact(&error.display_chain());
- 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 dirs::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 boundary = "[^0-9a-zA-Z.:]";
- let combined_pattern = format!(
- "(?P<start>^|{})(?:{}|{}|{})",
- boundary,
- build_ipv4_regex(),
- build_ipv6_regex(),
- build_mac_regex(),
- );
- Regex::new(&combined_pattern).unwrap()
- };
- }
- RE.replace_all(input, "$start[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 {
- // Regular expression obtained from:
- // https://stackoverflow.com/a/17871737
- let ipv4_segment = "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])";
- let ipv4_address = format!("({0}\\.){{3,3}}{0}", ipv4_segment);
-
- let ipv6_segment = "[0-9a-fA-F]{1,4}";
-
- let long = format!("({0}:){{7,7}}{0}", ipv6_segment);
- let compressed_1 = format!("({0}:){{1,7}}:", ipv6_segment);
- let compressed_2 = format!("({0}:){{1,6}}:{0}", ipv6_segment);
- let compressed_3 = format!("({0}:){{1,5}}(:{0}){{1,2}}", ipv6_segment);
- let compressed_4 = format!("({0}:){{1,4}}(:{0}){{1,3}}", ipv6_segment);
- let compressed_5 = format!("({0}:){{1,3}}(:{0}){{1,4}}", ipv6_segment);
- let compressed_6 = format!("({0}:){{1,2}}(:{0}){{1,5}}", ipv6_segment);
- let compressed_7 = format!("{0}:((:{0}){{1,6}})", ipv6_segment);
- let compressed_8 = format!(":((:{0}){{1,7}}|:)", ipv6_segment);
- let link_local = "[Ff][Ee]80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}";
- let ipv4_mapped = format!("::([fF]{{4}}(:0{{1,4}}){{0,1}}:){{0,1}}{}", ipv4_address);
- let ipv4_embedded = format!("({0}:){{1,4}}:{1}", ipv6_segment, ipv4_address);
-
- format!(
- "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}",
- long,
- link_local,
- ipv4_mapped,
- ipv4_embedded,
- compressed_8,
- compressed_7,
- compressed_6,
- compressed_5,
- compressed_4,
- compressed_3,
- compressed_2,
- compressed_1,
- )
-}
-
-/// 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())
-}
-
-#[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() {
- assert_does_not_redact("127.0.0.1");
- }
-
- #[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("abcd:dead:beef::");
- assert_redacts_ipv6("abcd:dead:beef:1234::");
- assert_redacts_ipv6("::dead:beef:1234");
- assert_redacts_ipv6("0::0");
- assert_redacts_ipv6("0:0:0:0::1");
- }
-
- #[test]
- fn doesnt_redact_not_ipv6() {
- assert_does_not_redact("[talpid_core::firewall]");
- }
-
- 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_time() {
- assert_does_not_redact("09:47:59");
- }
-
- fn assert_does_not_redact(input: &str) {
- let report = ProblemReport::new(vec![]);
- let res = report.redact(input);
- assert_eq!(input, res);
- }
-}