mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
This creates a separate script dangerzone-container which is a wrapper for running the container. This lets us run dangerzone as unprivileged, but dangerzone-container as privileged, to avoid adding the user to the dangerzone group.
This commit is contained in:
parent
0b1cd9e9ef
commit
cf367adcfa
12 changed files with 424 additions and 215 deletions
|
@ -1,146 +1,13 @@
|
||||||
from PyQt5 import QtCore, QtWidgets
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import signal
|
from .container import container_main
|
||||||
import platform
|
|
||||||
import click
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .global_common import GlobalCommon
|
|
||||||
from .main_window import MainWindow
|
|
||||||
from .docker_installer import (
|
|
||||||
is_docker_installed,
|
|
||||||
is_docker_ready,
|
|
||||||
launch_docker_windows,
|
|
||||||
DockerInstaller,
|
|
||||||
)
|
|
||||||
|
|
||||||
dangerzone_version = "0.1"
|
dangerzone_version = "0.1"
|
||||||
|
|
||||||
|
# This is a hack for Windows and Mac to be able to run dangerzone-container, even though
|
||||||
class Application(QtWidgets.QApplication):
|
# PyInstaller builds a single binary
|
||||||
document_selected = QtCore.pyqtSignal(str)
|
if os.path.basename(sys.argv[0]) == "dangerzone-container":
|
||||||
application_activated = QtCore.pyqtSignal()
|
main = container_main
|
||||||
|
else:
|
||||||
def __init__(self):
|
# If the binary isn't "dangerzone-contatiner", then launch the GUI
|
||||||
QtWidgets.QApplication.__init__(self, sys.argv)
|
from .gui import gui_main as main
|
||||||
|
|
||||||
def event(self, event):
|
|
||||||
# In macOS, handle the file open event
|
|
||||||
if event.type() == QtCore.QEvent.FileOpen:
|
|
||||||
self.document_selected.emit(event.file())
|
|
||||||
return True
|
|
||||||
elif event.type() == QtCore.QEvent.ApplicationActivate:
|
|
||||||
self.application_activated.emit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
return QtWidgets.QApplication.event(self, event)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--custom-container") # Use this container instead of flmcode/dangerzone
|
|
||||||
@click.argument("filename", required=False)
|
|
||||||
def main(custom_container, filename):
|
|
||||||
click.echo(f"dangerzone {dangerzone_version}")
|
|
||||||
|
|
||||||
# Create the Qt app
|
|
||||||
app = Application()
|
|
||||||
app.setQuitOnLastWindowClosed(False)
|
|
||||||
|
|
||||||
# GlobalCommon object
|
|
||||||
global_common = GlobalCommon(app)
|
|
||||||
|
|
||||||
if custom_container:
|
|
||||||
# Do we have this container?
|
|
||||||
output = subprocess.check_output(
|
|
||||||
[global_common.container_runtime, "image", "ls", custom_container],
|
|
||||||
startupinfo=global_common.get_subprocess_startupinfo(),
|
|
||||||
)
|
|
||||||
if custom_container.encode() not in output:
|
|
||||||
click.echo(f"Container '{container}' not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
global_common.custom_container = custom_container
|
|
||||||
|
|
||||||
# Allow Ctrl-C to smoothly quit the program instead of throwing an exception
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
||||||
|
|
||||||
# If we're using Linux and docker, see if we need to add the user to the docker group
|
|
||||||
if (
|
|
||||||
platform.system() == "Linux"
|
|
||||||
and global_common.container_runtime == "/usr/bin/docker"
|
|
||||||
):
|
|
||||||
if not global_common.ensure_user_is_in_docker_group():
|
|
||||||
click.echo("Failed to add user to docker group")
|
|
||||||
return
|
|
||||||
if not global_common.ensure_docker_service_is_started():
|
|
||||||
click.echo("Failed to start docker service")
|
|
||||||
return
|
|
||||||
|
|
||||||
# See if we need to install Docker...
|
|
||||||
if (platform.system() == "Darwin" or platform.system() == "Windows") and (
|
|
||||||
not is_docker_installed(global_common) or not is_docker_ready(global_common)
|
|
||||||
):
|
|
||||||
click.echo("Docker is either not installed or not running")
|
|
||||||
docker_installer = DockerInstaller(global_common)
|
|
||||||
docker_installer.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
closed_windows = {}
|
|
||||||
windows = {}
|
|
||||||
|
|
||||||
def delete_window(window_id):
|
|
||||||
closed_windows[window_id] = windows[window_id]
|
|
||||||
del windows[window_id]
|
|
||||||
|
|
||||||
# Open a document in a window
|
|
||||||
def select_document(filename=None):
|
|
||||||
if (
|
|
||||||
len(windows) == 1
|
|
||||||
and windows[list(windows.keys())[0]].common.document_filename == None
|
|
||||||
):
|
|
||||||
window = windows[list(windows.keys())[0]]
|
|
||||||
else:
|
|
||||||
window_id = uuid.uuid4().hex
|
|
||||||
window = MainWindow(global_common, window_id)
|
|
||||||
window.delete_window.connect(delete_window)
|
|
||||||
windows[window_id] = window
|
|
||||||
|
|
||||||
if filename:
|
|
||||||
# Validate filename
|
|
||||||
filename = os.path.abspath(os.path.expanduser(filename))
|
|
||||||
try:
|
|
||||||
open(filename, "rb")
|
|
||||||
except FileNotFoundError:
|
|
||||||
click.echo("File not found")
|
|
||||||
return False
|
|
||||||
except PermissionError:
|
|
||||||
click.echo("Permission denied")
|
|
||||||
return False
|
|
||||||
window.common.document_filename = filename
|
|
||||||
window.doc_selection_widget.document_selected.emit()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Open a new window if not filename is passed
|
|
||||||
if filename is None:
|
|
||||||
select_document()
|
|
||||||
else:
|
|
||||||
# If filename is passed as an argument, open it
|
|
||||||
if not select_document(filename):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Open a new window, if all windows are closed
|
|
||||||
def application_activated():
|
|
||||||
if len(windows) == 0:
|
|
||||||
select_document()
|
|
||||||
|
|
||||||
# If we get a file open event, open it
|
|
||||||
app.document_selected.connect(select_document)
|
|
||||||
|
|
||||||
# If the application is activated and all windows are closed, open a new one
|
|
||||||
app.application_activated.connect(application_activated)
|
|
||||||
|
|
||||||
sys.exit(app.exec_())
|
|
||||||
|
|
115
dangerzone/container.py
Normal file
115
dangerzone/container.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import click
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import pipes
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
# What is the container runtime for this platform?
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
container_runtime = "/usr/local/bin/docker"
|
||||||
|
elif platform.system() == "Windows":
|
||||||
|
container_runtime = "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe"
|
||||||
|
else:
|
||||||
|
container_runtime = "/usr/bin/docker"
|
||||||
|
|
||||||
|
# Define startupinfo for subprocesses
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
else:
|
||||||
|
startupinfo = None
|
||||||
|
|
||||||
|
|
||||||
|
def exec_container(args):
|
||||||
|
args = [container_runtime] + args
|
||||||
|
|
||||||
|
args_str = " ".join(pipes.quote(s) for s in args)
|
||||||
|
sys.stdout.write(f"Executing: {args_str}\n\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdin=None,
|
||||||
|
stdout=sys.stdout,
|
||||||
|
stderr=sys.stderr,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=True,
|
||||||
|
startupinfo=startupinfo,
|
||||||
|
) as p:
|
||||||
|
p.communicate()
|
||||||
|
return p.returncode
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def container_main():
|
||||||
|
"""
|
||||||
|
Dangerzone container commands with elevated privileges.
|
||||||
|
Humans don't need to run this command by themselves.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@container_main.command()
|
||||||
|
@click.option("--container-name", default="flmcode/dangerzone")
|
||||||
|
def image_ls(container_name):
|
||||||
|
"""docker image ls [container_name]"""
|
||||||
|
sys.exit(exec_container(["image", "ls", container_name]))
|
||||||
|
|
||||||
|
|
||||||
|
@container_main.command()
|
||||||
|
def pull():
|
||||||
|
"""docker pull flmcode/dangerzone"""
|
||||||
|
sys.exit(exec_container(["pull", "flmcode/dangerzone"]))
|
||||||
|
|
||||||
|
|
||||||
|
@container_main.command()
|
||||||
|
@click.option("--document-filename", required=True)
|
||||||
|
@click.option("--pixel-dir", required=True)
|
||||||
|
@click.option("--container-name", default="flmcode/dangerzone")
|
||||||
|
def document_to_pixels(document_filename, pixel_dir, container_name):
|
||||||
|
"""docker run --network none -v [document_filename]:/tmp/input_file -v [pixel_dir]:/dangerzone [container_name] document-to-pixels"""
|
||||||
|
sys.exit(
|
||||||
|
exec_container(
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"--network",
|
||||||
|
"none",
|
||||||
|
"-v",
|
||||||
|
f"{document_filename}:/tmp/input_file",
|
||||||
|
"-v",
|
||||||
|
f"{pixel_dir}:/dangerzone",
|
||||||
|
container_name,
|
||||||
|
"document-to-pixels",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@container_main.command()
|
||||||
|
@click.option("--pixel-dir", required=True)
|
||||||
|
@click.option("--safe-dir", required=True)
|
||||||
|
@click.option("--container-name", default="flmcode/dangerzone")
|
||||||
|
@click.option("--ocr", required=True)
|
||||||
|
@click.option("--ocr-lang", required=True)
|
||||||
|
def pixels_to_pdf(pixel_dir, safe_dir, container_name, ocr, ocr_lang):
|
||||||
|
"""docker run --network none -v [pixel_dir]:/dangerzone -v [safe_dir]:/safezone [container_name] -e OCR=[ocr] -e OCR_LANGUAGE=[ocr_lang] pixels-to-pdf"""
|
||||||
|
sys.exit(
|
||||||
|
exec_container(
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"--network",
|
||||||
|
"none",
|
||||||
|
"-v",
|
||||||
|
f"{pixel_dir}:/dangerzone",
|
||||||
|
"-v",
|
||||||
|
f"{safe_dir}:/safezone",
|
||||||
|
"-e",
|
||||||
|
f"OCR={ocr}",
|
||||||
|
"-e",
|
||||||
|
f"OCR_LANGUAGE={ocr_lang}",
|
||||||
|
container_name,
|
||||||
|
"pixels-to-pdf",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
|
@ -8,29 +8,31 @@ import time
|
||||||
import platform
|
import platform
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
from .container import container_runtime
|
||||||
|
|
||||||
|
|
||||||
def is_docker_installed(global_common):
|
def is_docker_installed(global_common):
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
# Does the docker binary exist?
|
# Does the docker binary exist?
|
||||||
if os.path.isdir("/Applications/Docker.app") and os.path.exists(
|
if os.path.isdir("/Applications/Docker.app") and os.path.exists(
|
||||||
global_common.container_runtime
|
container_runtime
|
||||||
):
|
):
|
||||||
# Is it executable?
|
# Is it executable?
|
||||||
st = os.stat(global_common.container_runtime)
|
st = os.stat(container_runtime)
|
||||||
return bool(st.st_mode & stat.S_IXOTH)
|
return bool(st.st_mode & stat.S_IXOTH)
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
return os.path.exists(global_common.container_runtime)
|
return os.path.exists(container_runtime)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_docker_ready(global_common):
|
def is_docker_ready(global_common):
|
||||||
# Run `docker ps` without an error
|
# Run `docker image ls` without an error
|
||||||
try:
|
try:
|
||||||
|
print(global_common.get_dangerzone_container_args())
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[global_common.container_runtime, "ps"],
|
global_common.get_dangerzone_container_args() + ["image-ls"],
|
||||||
check=True,
|
|
||||||
startupinfo=global_common.get_subprocess_startupinfo(),
|
startupinfo=global_common.get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -57,26 +57,12 @@ class GlobalCommon(object):
|
||||||
# App data folder
|
# App data folder
|
||||||
self.appdata_path = appdirs.user_config_dir("dangerzone")
|
self.appdata_path = appdirs.user_config_dir("dangerzone")
|
||||||
|
|
||||||
# Container runtime
|
|
||||||
if platform.system() == "Darwin":
|
|
||||||
self.container_runtime = "/usr/local/bin/docker"
|
|
||||||
elif platform.system() == "Windows":
|
|
||||||
self.container_runtime = (
|
|
||||||
"C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Linux
|
|
||||||
|
|
||||||
# If this is fedora-like, use podman
|
|
||||||
if os.path.exists("/usr/bin/dnf"):
|
|
||||||
self.container_runtime = "/usr/bin/podman"
|
|
||||||
# Otherwise, use docker
|
|
||||||
else:
|
|
||||||
self.container_runtime = "/usr/bin/docker"
|
|
||||||
|
|
||||||
# In case we have a custom container
|
# In case we have a custom container
|
||||||
self.custom_container = None
|
self.custom_container = None
|
||||||
|
|
||||||
|
# dangerzone-container path
|
||||||
|
self.dz_container_path = self.get_dangerzone_container_path()
|
||||||
|
|
||||||
# Preload list of PDF viewers on computer
|
# Preload list of PDF viewers on computer
|
||||||
self.pdf_viewers = self._find_pdf_viewers()
|
self.pdf_viewers = self._find_pdf_viewers()
|
||||||
|
|
||||||
|
@ -278,6 +264,35 @@ class GlobalCommon(object):
|
||||||
resource_path = os.path.join(prefix, filename)
|
resource_path = os.path.join(prefix, filename)
|
||||||
return resource_path
|
return resource_path
|
||||||
|
|
||||||
|
def get_dangerzone_container_path(self):
|
||||||
|
if getattr(sys, "dangerzone_dev", False):
|
||||||
|
# Look for resources directory relative to python file
|
||||||
|
return os.path.join(
|
||||||
|
os.path.dirname(
|
||||||
|
os.path.dirname(
|
||||||
|
os.path.abspath(inspect.getfile(inspect.currentframe()))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"dev_scripts",
|
||||||
|
"dangerzone-container",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
return os.path.join(os.path.dirname(sys.executable), "dangerzone-container")
|
||||||
|
elif platform.system() == "Windows":
|
||||||
|
return os.path.join(os.path.dirname(sys.executable), "dangerzone-container.exe")
|
||||||
|
else:
|
||||||
|
return "/usr/bin/dangerzone-container"
|
||||||
|
|
||||||
|
def get_dangerzone_container_args(self):
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
if self.settings.get("linux_prefers_typing_password"):
|
||||||
|
return ["/usr/bin/pkexec", self.dz_container_path]
|
||||||
|
else:
|
||||||
|
return [self.dz_container_path]
|
||||||
|
else:
|
||||||
|
return [self.dz_container_path]
|
||||||
|
|
||||||
def get_window_icon(self):
|
def get_window_icon(self):
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
path = self.get_resource_path("dangerzone.ico")
|
path = self.get_resource_path("dangerzone.ico")
|
||||||
|
@ -372,18 +387,40 @@ class GlobalCommon(object):
|
||||||
|
|
||||||
return pdf_viewers
|
return pdf_viewers
|
||||||
|
|
||||||
def ensure_user_is_in_docker_group(self):
|
def ensure_docker_group_preference(self):
|
||||||
|
# If the user prefers typing their password
|
||||||
|
if self.settings.get("linux_prefers_typing_password") == True:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Get the docker group
|
||||||
try:
|
try:
|
||||||
groupinfo = grp.getgrnam("docker")
|
groupinfo = grp.getgrnam("docker")
|
||||||
except:
|
except:
|
||||||
# Ignore if group is not found
|
# Ignore if group is not found
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# See if the user is in the group
|
||||||
username = getpass.getuser()
|
username = getpass.getuser()
|
||||||
if username not in groupinfo.gr_mem:
|
if username not in groupinfo.gr_mem:
|
||||||
# User is not in docker group, so prompt about adding the user to the docker group
|
# User is not in the docker group, ask if they prefer typing their password
|
||||||
message = "<b>Dangerzone requires Docker.</b><br><br>Click Ok to add your user to the 'docker' group. You will have to type your login password."
|
message = "<b>Dangerzone requires Docker</b><br><br>In order to use Docker, your user must be in the 'docker' group or you'll need to type your password each time you run dangerzone.<br><br><b>Adding your user to the 'docker' group is more convenient but less secure</b>, and will require just typing your password once. Which do you prefer?"
|
||||||
if Alert(self, message).launch():
|
return_code = Alert(
|
||||||
|
self,
|
||||||
|
message,
|
||||||
|
ok_text="I'll type my password each time",
|
||||||
|
extra_button_text="Add my user to the 'docker' group",
|
||||||
|
).launch()
|
||||||
|
if return_code == QtWidgets.QDialog.Accepted:
|
||||||
|
# Prefers typing password
|
||||||
|
self.settings.set("linux_prefers_typing_password", True)
|
||||||
|
self.settings.save()
|
||||||
|
return True
|
||||||
|
elif return_code == 2:
|
||||||
|
# Prefers being in the docker group
|
||||||
|
self.settings.set("linux_prefers_typing_password", False)
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
|
# Add user to the docker group
|
||||||
p = subprocess.run(
|
p = subprocess.run(
|
||||||
[
|
[
|
||||||
"/usr/bin/pkexec",
|
"/usr/bin/pkexec",
|
||||||
|
@ -401,14 +438,17 @@ class GlobalCommon(object):
|
||||||
message = "Failed to add your user to the 'docker' group, quitting."
|
message = "Failed to add your user to the 'docker' group, quitting."
|
||||||
Alert(self, message).launch()
|
Alert(self, message).launch()
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
# Cancel
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def ensure_docker_service_is_started(self):
|
def ensure_docker_service_is_started(self):
|
||||||
if not is_docker_ready(self):
|
if not is_docker_ready(self):
|
||||||
message = "<b>Dangerzone requires Docker.</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>Click Ok to try starting the docker service. You will have to type your login password."
|
message = "<b>Dangerzone requires Docker</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>Click Ok to try starting the docker service. You will have to type your login password."
|
||||||
if Alert(self, message).launch():
|
if Alert(self, message).launch() == QtWidgets.QDialog.Accepted:
|
||||||
p = subprocess.run(
|
p = subprocess.run(
|
||||||
[
|
[
|
||||||
"/usr/bin/pkexec",
|
"/usr/bin/pkexec",
|
||||||
|
@ -440,7 +480,7 @@ class GlobalCommon(object):
|
||||||
|
|
||||||
|
|
||||||
class Alert(QtWidgets.QDialog):
|
class Alert(QtWidgets.QDialog):
|
||||||
def __init__(self, common, message):
|
def __init__(self, common, message, ok_text="Ok", extra_button_text=None):
|
||||||
super(Alert, self).__init__()
|
super(Alert, self).__init__()
|
||||||
self.common = common
|
self.common = common
|
||||||
|
|
||||||
|
@ -470,22 +510,38 @@ class Alert(QtWidgets.QDialog):
|
||||||
|
|
||||||
message_layout = QtWidgets.QHBoxLayout()
|
message_layout = QtWidgets.QHBoxLayout()
|
||||||
message_layout.addWidget(logo)
|
message_layout.addWidget(logo)
|
||||||
message_layout.addWidget(label)
|
message_layout.addSpacing(10)
|
||||||
|
message_layout.addWidget(label, stretch=1)
|
||||||
|
|
||||||
ok_button = QtWidgets.QPushButton("Ok")
|
ok_button = QtWidgets.QPushButton(ok_text)
|
||||||
ok_button.clicked.connect(self.accept)
|
ok_button.clicked.connect(self.clicked_ok)
|
||||||
|
if extra_button_text:
|
||||||
|
extra_button = QtWidgets.QPushButton(extra_button_text)
|
||||||
|
extra_button.clicked.connect(self.clicked_extra)
|
||||||
cancel_button = QtWidgets.QPushButton("Cancel")
|
cancel_button = QtWidgets.QPushButton("Cancel")
|
||||||
cancel_button.clicked.connect(self.reject)
|
cancel_button.clicked.connect(self.clicked_cancel)
|
||||||
|
|
||||||
buttons_layout = QtWidgets.QHBoxLayout()
|
buttons_layout = QtWidgets.QHBoxLayout()
|
||||||
buttons_layout.addStretch()
|
buttons_layout.addStretch()
|
||||||
buttons_layout.addWidget(ok_button)
|
buttons_layout.addWidget(ok_button)
|
||||||
|
if extra_button_text:
|
||||||
|
buttons_layout.addWidget(extra_button)
|
||||||
buttons_layout.addWidget(cancel_button)
|
buttons_layout.addWidget(cancel_button)
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
layout = QtWidgets.QVBoxLayout()
|
||||||
layout.addLayout(message_layout)
|
layout.addLayout(message_layout)
|
||||||
|
layout.addSpacing(10)
|
||||||
layout.addLayout(buttons_layout)
|
layout.addLayout(buttons_layout)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def clicked_ok(self):
|
||||||
|
self.done(QtWidgets.QDialog.Accepted)
|
||||||
|
|
||||||
|
def clicked_extra(self):
|
||||||
|
self.done(2)
|
||||||
|
|
||||||
|
def clicked_cancel(self):
|
||||||
|
self.done(QtWidgets.QDialog.Rejected)
|
||||||
|
|
||||||
def launch(self):
|
def launch(self):
|
||||||
return self.exec_() == QtWidgets.QDialog.Accepted
|
return self.exec_()
|
||||||
|
|
140
dangerzone/gui.py
Normal file
140
dangerzone/gui.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import platform
|
||||||
|
import click
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .global_common import GlobalCommon
|
||||||
|
from .main_window import MainWindow
|
||||||
|
from .docker_installer import (
|
||||||
|
is_docker_installed,
|
||||||
|
is_docker_ready,
|
||||||
|
launch_docker_windows,
|
||||||
|
DockerInstaller,
|
||||||
|
)
|
||||||
|
from .container import container_runtime
|
||||||
|
|
||||||
|
|
||||||
|
class Application(QtWidgets.QApplication):
|
||||||
|
document_selected = QtCore.pyqtSignal(str)
|
||||||
|
application_activated = QtCore.pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
QtWidgets.QApplication.__init__(self, sys.argv)
|
||||||
|
|
||||||
|
def event(self, event):
|
||||||
|
# In macOS, handle the file open event
|
||||||
|
if event.type() == QtCore.QEvent.FileOpen:
|
||||||
|
self.document_selected.emit(event.file())
|
||||||
|
return True
|
||||||
|
elif event.type() == QtCore.QEvent.ApplicationActivate:
|
||||||
|
self.application_activated.emit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return QtWidgets.QApplication.event(self, event)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--custom-container") # Use this container instead of flmcode/dangerzone
|
||||||
|
@click.argument("filename", required=False)
|
||||||
|
def gui_main(custom_container, filename):
|
||||||
|
# Create the Qt app
|
||||||
|
app = Application()
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
|
# GlobalCommon object
|
||||||
|
global_common = GlobalCommon(app)
|
||||||
|
|
||||||
|
if custom_container:
|
||||||
|
# Do we have this container?
|
||||||
|
output = subprocess.check_output(
|
||||||
|
global_common.get_dangerzone_container_args()
|
||||||
|
+ ["image-ls", custom_container],
|
||||||
|
startupinfo=global_common.get_subprocess_startupinfo(),
|
||||||
|
)
|
||||||
|
if custom_container.encode() not in output:
|
||||||
|
click.echo(f"Container '{container}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
global_common.custom_container = custom_container
|
||||||
|
|
||||||
|
# Allow Ctrl-C to smoothly quit the program instead of throwing an exception
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
|
||||||
|
# If we're using Linux and docker, see if we need to add the user to the docker group or if the user prefers typing their password
|
||||||
|
if platform.system() == "Linux" and container_runtime == "/usr/bin/docker":
|
||||||
|
if not global_common.ensure_docker_group_preference():
|
||||||
|
return
|
||||||
|
if not global_common.ensure_docker_service_is_started():
|
||||||
|
click.echo("Failed to start docker service")
|
||||||
|
return
|
||||||
|
|
||||||
|
# See if we need to install Docker...
|
||||||
|
if (platform.system() == "Darwin" or platform.system() == "Windows") and (
|
||||||
|
not is_docker_installed(global_common) or not is_docker_ready(global_common)
|
||||||
|
):
|
||||||
|
click.echo("Docker is either not installed or not running")
|
||||||
|
docker_installer = DockerInstaller(global_common)
|
||||||
|
docker_installer.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
closed_windows = {}
|
||||||
|
windows = {}
|
||||||
|
|
||||||
|
def delete_window(window_id):
|
||||||
|
closed_windows[window_id] = windows[window_id]
|
||||||
|
del windows[window_id]
|
||||||
|
|
||||||
|
# Open a document in a window
|
||||||
|
def select_document(filename=None):
|
||||||
|
if (
|
||||||
|
len(windows) == 1
|
||||||
|
and windows[list(windows.keys())[0]].common.document_filename == None
|
||||||
|
):
|
||||||
|
window = windows[list(windows.keys())[0]]
|
||||||
|
else:
|
||||||
|
window_id = uuid.uuid4().hex
|
||||||
|
window = MainWindow(global_common, window_id)
|
||||||
|
window.delete_window.connect(delete_window)
|
||||||
|
windows[window_id] = window
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
# Validate filename
|
||||||
|
filename = os.path.abspath(os.path.expanduser(filename))
|
||||||
|
try:
|
||||||
|
open(filename, "rb")
|
||||||
|
except FileNotFoundError:
|
||||||
|
click.echo("File not found")
|
||||||
|
return False
|
||||||
|
except PermissionError:
|
||||||
|
click.echo("Permission denied")
|
||||||
|
return False
|
||||||
|
window.common.document_filename = filename
|
||||||
|
window.doc_selection_widget.document_selected.emit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Open a new window if not filename is passed
|
||||||
|
if filename is None:
|
||||||
|
select_document()
|
||||||
|
else:
|
||||||
|
# If filename is passed as an argument, open it
|
||||||
|
if not select_document(filename):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Open a new window, if all windows are closed
|
||||||
|
def application_activated():
|
||||||
|
if len(windows) == 0:
|
||||||
|
select_document()
|
||||||
|
|
||||||
|
# If we get a file open event, open it
|
||||||
|
app.document_selected.connect(select_document)
|
||||||
|
|
||||||
|
# If the application is activated and all windows are closed, open a new one
|
||||||
|
app.application_activated.connect(application_activated)
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
|
@ -20,6 +20,7 @@ class Settings:
|
||||||
"open": True,
|
"open": True,
|
||||||
"open_app": default_pdf_viewer,
|
"open_app": default_pdf_viewer,
|
||||||
"update_container": True,
|
"update_container": True,
|
||||||
|
"linux_prefers_typing_password": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
|
@ -141,7 +141,12 @@ class SettingsWidget(QtWidgets.QWidget):
|
||||||
self.update_checkbox.hide()
|
self.update_checkbox.hide()
|
||||||
else:
|
else:
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
[self.global_common.container_runtime, "image", "ls", self.global_common.get_container_name()],
|
self.global_common.get_dangerzone_container_args()
|
||||||
|
+ [
|
||||||
|
"image-ls",
|
||||||
|
"--container-name",
|
||||||
|
self.global_common.get_container_name(),
|
||||||
|
],
|
||||||
startupinfo=self.global_common.get_subprocess_startupinfo(),
|
startupinfo=self.global_common.get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
if b"dangerzone" not in output:
|
if b"dangerzone" not in output:
|
||||||
|
|
|
@ -16,12 +16,8 @@ class TaskBase(QtCore.QThread):
|
||||||
super(TaskBase, self).__init__()
|
super(TaskBase, self).__init__()
|
||||||
|
|
||||||
def exec_container(self, args):
|
def exec_container(self, args):
|
||||||
args = [self.global_common.container_runtime] + args
|
args = self.global_common.get_dangerzone_container_args() + args
|
||||||
args_str = " ".join(pipes.quote(s) for s in args)
|
output = ""
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"Executing: {args_str}")
|
|
||||||
output = f"Executing: {args_str}\n\n"
|
|
||||||
self.update_details.emit(output)
|
self.update_details.emit(output)
|
||||||
|
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
|
@ -38,9 +34,12 @@ class TaskBase(QtCore.QThread):
|
||||||
print(line, end="")
|
print(line, end="")
|
||||||
self.update_details.emit(output)
|
self.update_details.emit(output)
|
||||||
|
|
||||||
output += p.stderr.read()
|
stderr = p.stderr.read()
|
||||||
|
output += stderr
|
||||||
|
print(stderr)
|
||||||
self.update_details.emit(output)
|
self.update_details.emit(output)
|
||||||
|
|
||||||
|
print("")
|
||||||
return p.returncode, output
|
return p.returncode, output
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +54,7 @@ class PullImageTask(TaskBase):
|
||||||
"Pulling container image (this might take a few minutes)"
|
"Pulling container image (this might take a few minutes)"
|
||||||
)
|
)
|
||||||
self.update_details.emit("")
|
self.update_details.emit("")
|
||||||
args = ["pull", "flmcode/dangerzone"]
|
args = ["pull"]
|
||||||
returncode, _ = self.exec_container(args)
|
returncode, _ = self.exec_container(args)
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
|
@ -78,15 +77,13 @@ class ConvertToPixels(TaskBase):
|
||||||
def run(self):
|
def run(self):
|
||||||
self.update_label.emit("Converting document to pixels")
|
self.update_label.emit("Converting document to pixels")
|
||||||
args = [
|
args = [
|
||||||
"run",
|
|
||||||
"--network",
|
|
||||||
"none",
|
|
||||||
"-v",
|
|
||||||
f"{self.common.document_filename}:/tmp/input_file",
|
|
||||||
"-v",
|
|
||||||
f"{self.common.pixel_dir.name}:/dangerzone",
|
|
||||||
self.global_common.get_container_name(),
|
|
||||||
"document-to-pixels",
|
"document-to-pixels",
|
||||||
|
"--document-filename",
|
||||||
|
self.common.document_filename,
|
||||||
|
"--pixel-dir",
|
||||||
|
self.common.pixel_dir.name,
|
||||||
|
"--container-name",
|
||||||
|
self.global_common.get_container_name(),
|
||||||
]
|
]
|
||||||
returncode, output = self.exec_container(args)
|
returncode, output = self.exec_container(args)
|
||||||
|
|
||||||
|
@ -173,29 +170,27 @@ class ConvertToPDF(TaskBase):
|
||||||
self.update_label.emit("Converting pixels to safe PDF")
|
self.update_label.emit("Converting pixels to safe PDF")
|
||||||
|
|
||||||
# Build environment variables list
|
# Build environment variables list
|
||||||
envs = []
|
|
||||||
if self.global_common.settings.get("ocr"):
|
if self.global_common.settings.get("ocr"):
|
||||||
envs += ["-e", "OCR=1"]
|
ocr = "1"
|
||||||
else:
|
else:
|
||||||
envs += ["-e", "OCR=0"]
|
ocr = "0"
|
||||||
envs += [
|
ocr_lang = self.global_common.ocr_languages[
|
||||||
"-e",
|
self.global_common.settings.get("ocr_language")
|
||||||
f"OCR_LANGUAGE={self.global_common.ocr_languages[self.global_common.settings.get('ocr_language')]}",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
args = (
|
args = [
|
||||||
[
|
"pixels-to-pdf",
|
||||||
"run",
|
"--pixel-dir",
|
||||||
"--network",
|
self.common.pixel_dir.name,
|
||||||
"none",
|
"--safe-dir",
|
||||||
"-v",
|
self.common.safe_dir.name,
|
||||||
f"{self.common.pixel_dir.name}:/dangerzone",
|
"--container-name",
|
||||||
"-v",
|
self.global_common.get_container_name(),
|
||||||
f"{self.common.safe_dir.name}:/safezone",
|
"--ocr",
|
||||||
]
|
ocr,
|
||||||
+ envs
|
"--ocr-lang",
|
||||||
+ [self.global_common.get_container_name(), "pixels-to-pdf",]
|
ocr_lang,
|
||||||
)
|
]
|
||||||
returncode, output = self.exec_container(args)
|
returncode, output = self.exec_container(args)
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
# Load dangerzone module and resources from the source code tree
|
# Load dangerzone module and resources from the source code tree
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
sys.dangerzone_dev = True
|
sys.dangerzone_dev = True
|
||||||
|
|
||||||
import dangerzone
|
import dangerzone
|
||||||
dangerzone.main()
|
|
||||||
|
dangerzone.main()
|
||||||
|
|
1
dev_scripts/dangerzone-container
Symbolic link
1
dev_scripts/dangerzone-container
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
dangerzone
|
16
install/linux/media.firstlook.dangerzone-container.policy
Normal file
16
install/linux/media.firstlook.dangerzone-container.policy
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||||
|
<policyconfig>
|
||||||
|
<action id="org.freedesktop.policykit.pkexec.dangerzone">
|
||||||
|
<description>Run Dangerzone Container</description>
|
||||||
|
<message>Dangerzone needs you to authenticate to run containers</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin_keep</allow_any>
|
||||||
|
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/dangerzone-container</annotate>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
11
setup.py
11
setup.py
|
@ -29,11 +29,20 @@ setuptools.setup(
|
||||||
["install/linux/media.firstlook.dangerzone.png"],
|
["install/linux/media.firstlook.dangerzone.png"],
|
||||||
),
|
),
|
||||||
("share/dangerzone", file_list("share")),
|
("share/dangerzone", file_list("share")),
|
||||||
|
(
|
||||||
|
"share/polkit-1/actions",
|
||||||
|
["install/linux/media.firstlook.dangerzone-container.policy"],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
],
|
],
|
||||||
entry_points={"console_scripts": ["dangerzone = dangerzone:main"]},
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"dangerzone = dangerzone:main",
|
||||||
|
"dangerzone-container = dangerzone:container_main",
|
||||||
|
]
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue