From 8cfab6ea3000e713285fd2513c36bce7f07bc7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Val=C3=A9rio?= Date: Sun, 26 Jun 2022 21:12:28 +0100 Subject: [PATCH] Added permission request dialog functionality --- README.rst | 13 +++--- clipboard_watcher/app.py | 10 ++++- clipboard_watcher/dialog.py | 6 +++ clipboard_watcher/notifications.py | 12 ++--- clipboard_watcher/xoperations.py | 16 ++++++- poetry.lock | 72 ++++++++++++++++++++++++++---- pyproject.toml | 2 +- 7 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 clipboard_watcher/dialog.py diff --git a/README.rst b/README.rst index 5d1c297..981e67c 100644 --- a/README.rst +++ b/README.rst @@ -5,6 +5,9 @@ This repository contains the code of an experiment, in order to understand how hard would it be (if possible) to have an application that can monitor the access of other apps to the clipboard on Linux machines. +This app can also ask the user for permission before providing the clipboard +contents. + At the moment it only supports desktop environments that use X. To learn more please read the @@ -13,19 +16,13 @@ To learn more please read the Installation ------------ -To build and run this demo app, you will need to have the following libraries -on your system: - -* ``libdbus-1-dev`` -* ``libglib2.0-dev`` - -You will also need to have `Poetry `_ in order to be +To build and run this demo app, you will need to have `Poetry `_ in order to be able to execute the following commands: .. code-block:: $ poetry install - $ poetry run watcher + $ poetry run watcher --help Contributions ------------- diff --git a/clipboard_watcher/app.py b/clipboard_watcher/app.py index 234a6b5..b3a17aa 100644 --- a/clipboard_watcher/app.py +++ b/clipboard_watcher/app.py @@ -43,6 +43,12 @@ def main() -> None: "Monitors the access of other processes to the clipboard contents." ) parser.add_argument("-l", "--loglevel", help="Choose the log level") + parser.add_argument( + "-p", + "--permission", + action="store_true", + help="Ask for permission before sending clipboard data", + ) args = parser.parse_args() if args.loglevel and args.loglevel in ["DEBUG", "INFO", "WARNING", "ERROR"]: set_logger_settings(args.loglevel) @@ -63,7 +69,9 @@ def main() -> None: job_queue = Queue() # Thread 1 event_worker = Thread( - target=process_event_loop, args=(disp, window, job_queue, cb_data), daemon=True + target=process_event_loop, + args=(disp, window, job_queue, cb_data, args.permission), + daemon=True, ) # Thread 2 notif_worker = Thread( diff --git a/clipboard_watcher/dialog.py b/clipboard_watcher/dialog.py new file mode 100644 index 0000000..8a4aaa3 --- /dev/null +++ b/clipboard_watcher/dialog.py @@ -0,0 +1,6 @@ +from tkinter.messagebox import askokcancel + + +def ask_for_permission(window_name, pid, path) -> bool: + details = f"The process {pid} ({path}) with the window named '{window_name}' wants to access your clipboard data." + return askokcancel("Clipboard-Watcher", details) diff --git a/clipboard_watcher/notifications.py b/clipboard_watcher/notifications.py index 8024ab6..8457a9a 100644 --- a/clipboard_watcher/notifications.py +++ b/clipboard_watcher/notifications.py @@ -1,16 +1,12 @@ -import dbus +from desktop_notifier import DesktopNotifier import logging logger = logging.getLogger("ClipboardWatcher") +notifier = DesktopNotifier(app_name="Clipboard-Watcher", app_icon=None) -def display_desktop_notification(title: str, details: str = "", icon: str = "") -> None: - interface = "org.freedesktop.Notifications" - path = "/org/freedesktop/Notifications" - notification = dbus.Interface( - dbus.SessionBus().get_object(interface, path), interface - ) +def display_desktop_notification(title: str, details: str = "") -> None: try: - notification.Notify("Clipboard-Watcher", 0, icon, title, details, [], [], 7000) + notifier.send_sync(title=title, message=details) except Exception: logger.error("Unable to publish notification due to dbus error") diff --git a/clipboard_watcher/xoperations.py b/clipboard_watcher/xoperations.py index 0480c79..4675229 100644 --- a/clipboard_watcher/xoperations.py +++ b/clipboard_watcher/xoperations.py @@ -13,14 +13,18 @@ The current functionality contains: import logging from typing import List, Optional, Tuple from queue import Queue +from collections import namedtuple from Xlib import X, Xatom from Xlib.ext.res import query_client_ids, LocalClientPIDMask from Xlib.protocol import event +from clipboard_watcher.dialog import ask_for_permission + from .process_info import ProcessInfo logger = logging.getLogger("ClipboardWatcher") +FakeData = namedtuple("FakeData", ["primary", "clipboard"]) def get_selection_targets(disp, win, selection: str) -> List[str]: @@ -214,7 +218,7 @@ def process_selection_clear_event(d, cb_data, e) -> None: logger.warning("Owner again") -def process_event_loop(d, w, q: Queue, cb_data) -> None: +def process_event_loop(d, w, q: Queue, cb_data, permission=False) -> None: while True: e = d.next_event() if ( @@ -237,7 +241,15 @@ def process_event_loop(d, w, q: Queue, cb_data) -> None: # due to the risk of the requestor no longer be running afterwards proc_info = ProcessInfo.collect(req_pid) if req_pid else None - process_selection_request_event(d, cb_data, e) + if permission and d.get_atom_name(e.target) != "TARGETS": + if ask_for_permission(req_name, proc_info.pid, proc_info.path): + data = cb_data + else: + data = FakeData({}, {}) + else: + data = cb_data + + process_selection_request_event(d, data, e) if d.get_atom_name(e.target) != "TARGETS": q.put( { diff --git a/poetry.lock b/poetry.lock index 7b3a0dc..64f1854 100644 --- a/poetry.lock +++ b/poetry.lock @@ -66,12 +66,30 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] -name = "dbus-python" -version = "1.2.18" -description = "Python bindings for libdbus" +name = "dbus-next" +version = "0.2.3" +description = "A zero-dependency DBus library for Python with asyncio support" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6.0" + +[[package]] +name = "desktop-notifier" +version = "3.4.0" +description = "Python library for cross-platform desktop notifications" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +dbus-next = {version = "*", markers = "sys_platform == \"linux\""} +packaging = "*" +rubicon-objc = {version = "*", markers = "sys_platform == \"darwin\""} +winsdk = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["black", "bump2version", "flake8", "mypy", "pre-commit", "pytest", "pytest-cov"] +docs = ["sphinx", "m2r2", "sphinx-autoapi", "sphinx-rtd-theme"] [[package]] name = "more-itertools" @@ -93,7 +111,7 @@ python-versions = "*" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -154,7 +172,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "pyparsing" version = "3.0.6" description = "Python parsing module" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -194,6 +212,14 @@ python-versions = "*" [package.dependencies] six = ">=1.10.0" +[[package]] +name = "rubicon-objc" +version = "0.4.2" +description = "A bridge between an Objective C runtime environment and Python." +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "six" version = "1.16.0" @@ -226,10 +252,18 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "winsdk" +version = "1.0.0b6" +description = "Python bindings for the Windows SDK" +category = "main" +optional = false +python-versions = "*" + [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "5de6bab639719a04f7cf4b791199fabcec0f9c75e6bb12ce6039a299313a9f3c" +content-hash = "fc4b2112388189c48f90187d3601f965cb6c402781eed13c0ccc4614cefc0bdd" [metadata.files] atomicwrites = [ @@ -252,8 +286,13 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] -dbus-python = [ - {file = "dbus-python-1.2.18.tar.gz", hash = "sha256:92bdd1e68b45596c833307a5ff4b217ee6929a1502f5341bae28fd120acf7260"}, +dbus-next = [ + {file = "dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b"}, + {file = "dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5"}, +] +desktop-notifier = [ + {file = "desktop-notifier-3.4.0.tar.gz", hash = "sha256:92b10dfe97ea5599adbe2c03520cae5a4343017c3bb1f1dc2256c17b224608a9"}, + {file = "desktop_notifier-3.4.0-py3-none-any.whl", hash = "sha256:f7151171ef78b9c46bb3509eb80c95b5108d7b482e4c0215cbc44fe35a61a4f3"}, ] more-itertools = [ {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, @@ -329,6 +368,10 @@ python-xlib = [ {file = "python-xlib-0.31.tar.gz", hash = "sha256:74d83a081f532bc07f6d7afcd6416ec38403d68f68b9b9dc9e1f28fbf2d799e9"}, {file = "python_xlib-0.31-py2.py3-none-any.whl", hash = "sha256:1ec6ce0de73d9e6592ead666779a5732b384e5b8fb1f1886bd0a81cafa477759"}, ] +rubicon-objc = [ + {file = "rubicon-objc-0.4.2.tar.gz", hash = "sha256:6fbc8e12bd66c84427cfb95634c4bd10ade356ae2b2ae0d2b51dcbf5810d2602"}, + {file = "rubicon_objc-0.4.2-py3-none-any.whl", hash = "sha256:c8780b9d6c3c906642080a9f8b710f5823a498c17e9bbea7f70781ea6ede7962"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -345,3 +388,14 @@ wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] +winsdk = [ + {file = "winsdk-1.0.0b6-cp310-cp310-win32.whl", hash = "sha256:3589f0535d159b6e64f25e9688adc7896acc01bfa0f0f1e3d08dfb3ed5809104"}, + {file = "winsdk-1.0.0b6-cp310-cp310-win_amd64.whl", hash = "sha256:c0352706fa68cd28064f82b934ec709494e8f32efda23bf8f92e452ec2543df8"}, + {file = "winsdk-1.0.0b6-cp37-cp37m-win32.whl", hash = "sha256:daef8d49c653a516430ac681f92ff28f96b2ff2f11fa805f8b2073b1a5864f2f"}, + {file = "winsdk-1.0.0b6-cp37-cp37m-win_amd64.whl", hash = "sha256:20e384b50dbc2bd360dbfd33804be78679337ed4a31afb376abb9ddfae453010"}, + {file = "winsdk-1.0.0b6-cp38-cp38-win32.whl", hash = "sha256:410c2323af51c8e11c41bc88e431b9dc22b6aa9a1f36d45ad758170cc9fed098"}, + {file = "winsdk-1.0.0b6-cp38-cp38-win_amd64.whl", hash = "sha256:493eeb3807d2a50c4c203420a4cea55a83a675b53ab1309f3be3fda09ff143fe"}, + {file = "winsdk-1.0.0b6-cp39-cp39-win32.whl", hash = "sha256:01886275aae8842135c0029e4c249b88356f6df95b1ff522ba2ac01647c01e4b"}, + {file = "winsdk-1.0.0b6-cp39-cp39-win_amd64.whl", hash = "sha256:22048f379d46232961b1d98fed467a8603f6f9c138750a1e310f12063d340207"}, + {file = "winsdk-1.0.0b6.tar.gz", hash = "sha256:c72248967311145d6544744d98aaf413161f24ec6e0849c3bfa86565ab2100cd"}, +] diff --git a/pyproject.toml b/pyproject.toml index 0d4c038..af2ccb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ authors = ["Gonçalo Valério "] [tool.poetry.dependencies] python = ">=3.9,<3.11" python-xlib = "^0.31" -dbus-python = "^1.2.18" psutil = "^5.9.0" +desktop-notifier = "^3.4.0" [tool.poetry.dev-dependencies] pytest = "^5.2"