diff --git a/dangerzone/cli.py b/dangerzone/cli.py index 8f68e62..1cf0bf6 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -42,6 +42,11 @@ def print_header(s: str) -> None: type=click.UNPROCESSED, callback=args.validate_input_filenames, ) +@click.option( + "--debug", + "debug", + flag_value=True, + help="Run Dangerzone in debug mode, and get more logs from the conversion process.") @click.version_option(version=get_version(), message="%(version)s") @errors.handle_document_errors def cli_main( @@ -50,6 +55,7 @@ def cli_main( filenames: List[str], archive: bool, dummy_conversion: bool, + debug: bool, ) -> None: setup_logging() @@ -58,7 +64,7 @@ def cli_main( elif is_qubes_native_conversion(): dangerzone = DangerzoneCore(Qubes()) else: - dangerzone = DangerzoneCore(Container()) + dangerzone = DangerzoneCore(Container(debug=debug)) display_banner() if len(filenames) == 1 and output_filename: diff --git a/dangerzone/conversion/errors.py b/dangerzone/conversion/errors.py index 007caea..354549d 100644 --- a/dangerzone/conversion/errors.py +++ b/dangerzone/conversion/errors.py @@ -18,11 +18,20 @@ class ConversionException(Exception): error_message = "Unspecified error" error_code = ERROR_SHIFT - def __init__(self, error_message: Optional[str] = None) -> None: + def __init__( + self, error_message: Optional[str] = None, logs: Optional[str] = None + ) -> None: if error_message: self.error_message = error_message + self.logs = logs super().__init__(self.error_message) + def __str__(self): + msg = f"{self.error_message}" + if self.logs: + msg += f" {self.logs}" + return msg + @classmethod def get_subclasses(cls) -> List[Type["ConversionException"]]: subclasses = [cls] @@ -100,9 +109,10 @@ class UnexpectedConversionError(ConversionException): def exception_from_error_code( error_code: int, + logs: Optional[str] = None, ) -> Union[ConversionException, ValueError]: """returns the conversion exception corresponding to the error code""" for cls in ConversionException.get_subclasses(): if cls.error_code == error_code: - return cls() - return UnexpectedConversionError(f"Unknown error code '{error_code}'") + return cls(logs=logs) + return UnexpectedConversionError(f"Unknown error code '{error_code}', logs= {logs}") diff --git a/dangerzone/isolation_provider/base.py b/dangerzone/isolation_provider/base.py index 9404cee..7eb8bc9 100644 --- a/dangerzone/isolation_provider/base.py +++ b/dangerzone/isolation_provider/base.py @@ -5,6 +5,7 @@ import platform import signal import subprocess import sys +import tempfile from abc import ABC, abstractmethod from pathlib import Path from typing import IO, Callable, Iterator, Optional @@ -87,12 +88,19 @@ class IsolationProvider(ABC): Abstracts an isolation provider """ - def __init__(self) -> None: - if getattr(sys, "dangerzone_dev", False) is True: + def __init__(self, debug) -> None: + self.debug = debug + if self.should_display_errors(): self.proc_stderr = subprocess.PIPE else: + # We do not trust STDERR from the inner container, + # which could be exploited to ask users to open the document with + # a default PDF reader for instance. self.proc_stderr = subprocess.DEVNULL + def should_display_errors(self) -> bool: + return self.debug or getattr(sys, "dangerzone_dev", False) + @abstractmethod def install(self) -> bool: pass @@ -220,6 +228,17 @@ class IsolationProvider(ABC): text = "Successfully converted document" self.print_progress(document, False, text, 100) + if self.should_display_errors(): + assert p.stderr + debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS) + p.stderr.close() + log.debug( + "Conversion output (doc to pixels)\n" + f"{DOC_TO_PIXELS_LOG_START}\n" + f"{debug_log}" # no need for an extra newline here + f"{DOC_TO_PIXELS_LOG_END}" + ) + def print_progress( self, document: Document, error: bool, text: str, percentage: float ) -> None: @@ -252,7 +271,10 @@ class IsolationProvider(ABC): "Encountered an I/O error during document to pixels conversion," f" but the status of the conversion process is unknown (PID: {p.pid})" ) - return errors.exception_from_error_code(error_code) + logs = None + if self.debug: + logs = "".join([line.decode() for line in p.stderr.readlines()]) + return errors.exception_from_error_code(error_code, logs=logs) @abstractmethod def get_max_parallel_conversions(self) -> int: diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index b0f4880..9316637 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -93,7 +93,7 @@ class Container(IsolationProvider): return runtime @staticmethod - def get_runtime_security_args() -> List[str]: + def get_runtime_security_args(debug: bool) -> List[str]: """Security options applicable to the outer Dangerzone container. Our security precautions for the outer Dangerzone container are the following: @@ -137,6 +137,9 @@ class Container(IsolationProvider): security_args += ["--network=none"] security_args += ["-u", "dangerzone"] + if debug: + security_args += ["-e", "RUNSC_DEBUG=1"] + return security_args @staticmethod @@ -250,7 +253,7 @@ class Container(IsolationProvider): extra_args: List[str] = [], ) -> subprocess.Popen: container_runtime = self.get_runtime() - security_args = self.get_runtime_security_args() + security_args = self.get_runtime_security_args(self.debug) enable_stdin = ["-i"] set_name = ["--name", name] prevent_leakage_args = ["--rm"]