diff --git a/data/org.freedesktop.impl.portal.InputCapture.xml b/data/org.freedesktop.impl.portal.InputCapture.xml index c431a413e..fd41c7d4d 100644 --- a/data/org.freedesktop.impl.portal.InputCapture.xml +++ b/data/org.freedesktop.impl.portal.InputCapture.xml @@ -26,7 +26,7 @@ #org.freedesktop.portal.InputCapture portal, see that portal's documentation for details on methods, signals and arguments. - This documentation describes version 1 of this interface. + This documentation describes version 2 of this interface. --> diff --git a/data/org.freedesktop.portal.InputCapture.xml b/data/org.freedesktop.portal.InputCapture.xml index b12ce9b27..bc50f6252 100644 --- a/data/org.freedesktop.portal.InputCapture.xml +++ b/data/org.freedesktop.portal.InputCapture.xml @@ -51,7 +51,7 @@ captured. The transport of actual input events is delegated to a transport layer, specifically libei. See org.freedesktop.portal.InputCapture.ConnectToEIS(). - This documentation describes version 1 of this interface. + This documentation describes version 2 of this interface. --> diff --git a/src/input-capture.c b/src/input-capture.c index 05f1cc710..c1b05c6e7 100644 --- a/src/input-capture.c +++ b/src/input-capture.c @@ -25,11 +25,15 @@ #include "session.h" #include "input-capture.h" #include "request.h" +#include "restore-token.h" #include "xdp-dbus.h" #include "xdp-impl-dbus.h" #include "xdp-utils.h" #define VERSION_1 1 /* Makes grep easier */ +#define VERSION_2 2 + +#define INPUT_CAPTURE_TABLE "input-capture" typedef struct _InputCapture InputCapture; typedef struct _InputCaptureClass InputCaptureClass; @@ -71,6 +75,10 @@ typedef struct _InputCaptureSession Session parent; InputCaptureSessionState state; + + char *restore_token; + PersistMode persist_mode; + GVariant *restore_data; } InputCaptureSession; typedef struct _InputCaptureSessionClass @@ -119,6 +127,23 @@ input_capture_session_new (GVariant *options, return (InputCaptureSession*)session; } +static gboolean +process_results (Session *session, + GVariant **in_out_results, + GError **error) +{ + InputCaptureSession *input_capture_session = (InputCaptureSession*)session; + + xdp_session_persistence_replace_restore_data_with_token (session, + INPUT_CAPTURE_TABLE, + in_out_results, + &input_capture_session->persist_mode, + &input_capture_session->restore_token, + &input_capture_session->restore_data); + + return TRUE; +} + static void create_session_done (GObject *source_object, GAsyncResult *res, @@ -129,9 +154,10 @@ create_session_done (GObject *source_object, GVariantBuilder results_builder; GVariant *results; Session *session; - gboolean should_close_session; + gboolean should_close_session = FALSE; uint32_t capabilities = 0; uint32_t response = 2; + const char *restore_token; REQUEST_AUTOLOCK (request); @@ -155,6 +181,16 @@ create_session_done (GObject *source_object, if (request->exported && response == 0) { + if (!process_results (session, &results, &error)) + { + g_warning ("Could not start input-capture session: %s", + error->message); + g_clear_error (&error); + g_clear_pointer (&results, g_variant_unref); + response = 2; + goto out; + } + if (!session_export (session, &error)) { g_warning ("Failed to export session: %s", error->message); @@ -178,6 +214,10 @@ create_session_done (GObject *source_object, "capabilities", g_variant_new_uint32 (capabilities)); g_variant_builder_add (&results_builder, "{sv}", "session_handle", g_variant_new ("o", session->id)); + + if (g_variant_lookup (results, "restore_token", "s", &restore_token)) + g_variant_builder_add (&results_builder, "{sv}", + "restore_token", g_variant_new_string (restore_token)); } else { @@ -221,8 +261,33 @@ validate_capabilities (const char *key, static XdpOptionKey input_capture_create_session_options[] = { { "capabilities", G_VARIANT_TYPE_UINT32, validate_capabilities }, + { "restore_token", G_VARIANT_TYPE_STRING, xdp_session_persistence_validate_restore_token }, + { "persist_mode", G_VARIANT_TYPE_UINT32, xdp_session_persistence_validate_persist_mode }, }; +static gboolean +replace_input_capture_restore_token_with_data (Session *session, + GVariant **in_out_options, + GError **error) +{ + InputCaptureSession *input_capture_session = (InputCaptureSession *) session; + g_autoptr(GVariant) options = NULL; + PersistMode persist_mode; + + options = *in_out_options; + + if (!g_variant_lookup (options, "persist_mode", "u", &persist_mode)) + persist_mode = PERSIST_MODE_NONE; + + input_capture_session->persist_mode = persist_mode; + xdp_session_persistence_replace_restore_token_with_data (session, + INPUT_CAPTURE_TABLE, + in_out_options, + &input_capture_session->restore_token); + + return TRUE; +} + static gboolean handle_create_session (XdpDbusInputCapture *object, GDBusMethodInvocation *invocation, @@ -269,8 +334,19 @@ handle_create_session (XdpDbusInputCapture *object, g_dbus_method_invocation_return_gerror (invocation, error); return G_DBUS_METHOD_INVOCATION_HANDLED; } + options = g_variant_builder_end (&options_builder); + /* If 'restore_token' is passed, lookup the corresponding data in the + * permission store and / or the GHashTable with transient permissions. + * Portal implementations do not have access to the restore token. + */ + if (!replace_input_capture_restore_token_with_data (session, &options, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + g_object_set_qdata_full (G_OBJECT (request), quark_request_session, g_object_ref (session), @@ -1118,7 +1194,7 @@ input_capture_init (InputCapture *input_capture) { unsigned int supported_capabilities; - xdp_dbus_input_capture_set_version (XDP_DBUS_INPUT_CAPTURE (input_capture), VERSION_1); + xdp_dbus_input_capture_set_version (XDP_DBUS_INPUT_CAPTURE (input_capture), VERSION_2); supported_capabilities = xdp_dbus_impl_input_capture_get_supported_capabilities (impl); diff --git a/src/remote-desktop.c b/src/remote-desktop.c index 360db514f..49ddfd54d 100644 --- a/src/remote-desktop.c +++ b/src/remote-desktop.c @@ -430,50 +430,10 @@ validate_device_types (const char *key, return TRUE; } -static gboolean -validate_restore_token (const char *key, - GVariant *value, - GVariant *options, - GError **error) -{ - const char *restore_token = g_variant_get_string (value, NULL); - - if (!g_uuid_string_is_valid (restore_token)) - { - g_set_error (error, - XDG_DESKTOP_PORTAL_ERROR, - XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, - "Restore token is not a valid UUID string"); - return FALSE; - } - - return TRUE; -} - -static gboolean -validate_persist_mode (const char *key, - GVariant *value, - GVariant *options, - GError **error) -{ - uint32_t mode = g_variant_get_uint32 (value); - - if (mode > PERSIST_MODE_PERSISTENT) - { - g_set_error (error, - XDG_DESKTOP_PORTAL_ERROR, - XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, - "Invalid persist mode %x", mode); - return FALSE; - } - - return TRUE; -} - static XdpOptionKey remote_desktop_select_devices_options[] = { { "types", G_VARIANT_TYPE_UINT32, validate_device_types }, - { "restore_token", G_VARIANT_TYPE_STRING, validate_restore_token }, - { "persist_mode", G_VARIANT_TYPE_UINT32, validate_persist_mode }, + { "restore_token", G_VARIANT_TYPE_STRING, xdp_session_persistence_validate_restore_token }, + { "persist_mode", G_VARIANT_TYPE_UINT32, xdp_session_persistence_validate_persist_mode }, }; static gboolean diff --git a/src/restore-token.c b/src/restore-token.c index b084d2528..67b3de4a4 100644 --- a/src/restore-token.c +++ b/src/restore-token.c @@ -19,6 +19,8 @@ #include "config.h" +#include + #include "permissions.h" #include "restore-token.h" @@ -360,8 +362,9 @@ xdp_session_persistence_replace_restore_data_with_token (Session *session, *in_out_persist_mode, in_out_restore_token, in_out_restore_data); - g_variant_builder_add (&results_builder, "{sv}", "restore_token", - g_variant_new_string (*in_out_restore_token)); + if (*in_out_restore_token) + g_variant_builder_add (&results_builder, "{sv}", "restore_token", + g_variant_new_string (*in_out_restore_token)); } else { @@ -370,3 +373,39 @@ xdp_session_persistence_replace_restore_data_with_token (Session *session, *in_out_results = g_variant_builder_end (&results_builder); } + +gboolean +xdp_session_persistence_validate_restore_token (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + const char *restore_token = g_variant_get_string (value, NULL); + + if (!g_uuid_string_is_valid (restore_token)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Restore token is not a valid UUID string"); + return FALSE; + } + + return TRUE; +} + +gboolean +xdp_session_persistence_validate_persist_mode (const char *key, + GVariant *value, + GVariant *options, + GError **error) +{ + uint32_t mode = g_variant_get_uint32 (value); + + if (mode > PERSIST_MODE_PERSISTENT) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid persist mode %x", mode); + return FALSE; + } + + return TRUE; +} diff --git a/src/restore-token.h b/src/restore-token.h index fdd3389e6..49e5ac17a 100644 --- a/src/restore-token.h +++ b/src/restore-token.h @@ -69,3 +69,13 @@ void xdp_session_persistence_generate_and_save_restore_token (Session *session, PersistMode persist_mode, char **in_out_restore_token, GVariant **in_out_restore_data); + +gboolean xdp_session_persistence_validate_restore_token (const char *key, + GVariant *value, + GVariant *options, + GError **error); + +gboolean xdp_session_persistence_validate_persist_mode (const char *key, + GVariant *value, + GVariant *options, + GError **error); diff --git a/src/screen-cast.c b/src/screen-cast.c index 1d10845a1..30c3cd135 100644 --- a/src/screen-cast.c +++ b/src/screen-cast.c @@ -360,7 +360,7 @@ validate_source_types (const char *key, return TRUE; } -static gboolean +gboolean validate_cursor_mode (const char *key, GVariant *value, GVariant *options, @@ -385,48 +385,12 @@ validate_cursor_mode (const char *key, return TRUE; } -static gboolean -validate_restore_token (const char *key, - GVariant *value, - GVariant *options, - GError **error) -{ - const char *restore_token = g_variant_get_string (value, NULL); - - if (!g_uuid_string_is_valid (restore_token)) - { - g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, - "Restore token is not a valid UUID string"); - return FALSE; - } - - return TRUE; -} - -static gboolean -validate_persist_mode (const char *key, - GVariant *value, - GVariant *options, - GError **error) -{ - uint32_t mode = g_variant_get_uint32 (value); - - if (mode > PERSIST_MODE_PERSISTENT) - { - g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, - "Invalid persist mode %x", mode); - return FALSE; - } - - return TRUE; -} - static XdpOptionKey screen_cast_select_sources_options[] = { { "types", G_VARIANT_TYPE_UINT32, validate_source_types }, { "multiple", G_VARIANT_TYPE_BOOLEAN, NULL }, { "cursor_mode", G_VARIANT_TYPE_UINT32, validate_cursor_mode }, - { "restore_token", G_VARIANT_TYPE_STRING, validate_restore_token }, - { "persist_mode", G_VARIANT_TYPE_UINT32, validate_persist_mode }, + { "restore_token", G_VARIANT_TYPE_STRING, xdp_session_persistence_validate_restore_token }, + { "persist_mode", G_VARIANT_TYPE_UINT32, xdp_session_persistence_validate_persist_mode }, }; static gboolean diff --git a/tests/templates/inputcapture.py b/tests/templates/inputcapture.py index e9c2eccaa..17c89a2fe 100644 --- a/tests/templates/inputcapture.py +++ b/tests/templates/inputcapture.py @@ -2,6 +2,7 @@ # # This file is formatted with Python Black +from tests.templates import Response, ImplSession from collections import namedtuple from itertools import count from gi.repository import GLib @@ -15,7 +16,7 @@ MAIN_OBJ = "/org/freedesktop/portal/desktop" SYSTEM_BUS = False MAIN_IFACE = "org.freedesktop.impl.portal.InputCapture" -VERSION = 1 +VERSION = 2 logger = logging.getLogger(f"templates.{__name__}") logger.setLevel(logging.DEBUG) @@ -53,7 +54,7 @@ def load(mock, parameters=None): ), ) - mock.active_session_handles = [] + mock.sessions: dict[str, ImplSession] = {} @dbus.service.method( @@ -67,6 +68,9 @@ def CreateSession(self, handle, session_handle, app_id, parent_window, options): assert "capabilities" in options + session = ImplSession(self, BUS_NAME, session_handle).export() + self.sessions[session_handle] = session + # Filter to the subset of supported capabilities if self.capabilities is None: capabilities = options["capabilities"] @@ -74,10 +78,31 @@ def CreateSession(self, handle, session_handle, app_id, parent_window, options): capabilities = self.capabilities capabilities &= self.supported_capabilities - response = Response(0, {}) + response = Response(0, {"session_handle": session.handle}) response.results["capabilities"] = dbus.UInt32(capabilities) - self.active_session_handles.append(session_handle) + + if options.get("persist_mode") != 0: + restore_data = options.get("restore_data") + if not restore_data: + # The restore data isn't actually visible to the app but oh well + data = dbus.String("some restore token", variant_level=1) + self.restore_data = dbus.Struct( + list( + [ + dbus.String("TEST", variant_level=0), + dbus.UInt32(1, variant_level=0), + data, + ] + ), + signature="suv", + variant_level=0, + ) + else: + if restore_data != self.restore_data: + logger.error(f"Invalid restore_data passed") + return (2, {}) + response.results["restore_data"] = self.restore_data logger.debug(f"CreateSession with response {response}") @@ -96,7 +121,7 @@ def GetZones(self, handle, session_handle, app_id, options): try: logger.debug(f"GetZones({session_handle}, {options})") - assert session_handle in self.active_session_handles + assert session_handle in self.sessions response = Response(0, {}) response.results["zones"] = self.default_zone @@ -127,7 +152,7 @@ def SetPointerBarriers( f"SetPointerBarriers({session_handle}, {options}, {barriers}, {zone_set})" ) - assert session_handle in self.active_session_handles + assert session_handle in self.sessions assert zone_set == self.current_zone_set self.current_barriers = [] @@ -192,7 +217,7 @@ def Enable(self, session_handle, app_id, options): try: logger.debug(f"Enable({session_handle}, {options})") - assert session_handle in self.active_session_handles + assert session_handle in self.sessions # for use in the signals activation_id = next(serials) @@ -250,7 +275,7 @@ def Disable(self, session_handle, app_id, options): try: logger.debug(f"Disable({session_handle}, {options})") - assert session_handle in self.active_session_handles + assert session_handle in self.sessions except Exception as e: logger.critical(e) return (2, {}) @@ -265,7 +290,7 @@ def Release(self, session_handle, app_id, options): try: logger.debug(f"Release({session_handle}, {options})") - assert session_handle in self.active_session_handles + assert session_handle in self.sessions except Exception as e: logger.critical(e) return (2, {}) @@ -280,7 +305,7 @@ def ConnectToEIS(self, session_handle, app_id, options): try: logger.debug(f"ConnectToEIS({session_handle}, {options})") - assert session_handle in self.active_session_handles + assert session_handle in self.sessions sockets = socket.socketpair() self.eis_socket = sockets[0] diff --git a/tests/test_inputcapture.py b/tests/test_inputcapture.py index 07c130d63..258d7377f 100644 --- a/tests/test_inputcapture.py +++ b/tests/test_inputcapture.py @@ -3,7 +3,8 @@ # This file is formatted with Python Black from gi.repository import GLib - +from . import Response, Session +from enum import IntEnum from itertools import count import dbus @@ -27,8 +28,20 @@ def zones(): return default_zones() +class PersistMode(IntEnum): + NONE = 0 + TRANSIENT = 1 + PERSISTENT = 2 + + class TestInputCapture: - def create_session(self, portal_mock, capabilities=0xF): + def create_session( + self, + portal_mock, + capabilities=0xF, + restore_token=None, + persist_mode=PersistMode.NONE, + ) -> Response: """ Call CreateSession for the given capabilities and return the (response, results) tuple. @@ -38,25 +51,27 @@ def create_session(self, portal_mock, capabilities=0xF): capabilities = dbus.UInt32(capabilities, variant_level=1) session_handle_token = dbus.String(f"session{next(counter)}", variant_level=1) - options = dbus.Dictionary( - { - "capabilities": capabilities, - "session_handle_token": session_handle_token, - }, - signature="sv", - ) + options = { + "capabilities": capabilities, + "session_handle_token": session_handle_token, + } + if restore_token: + options["restore_token"] = dbus.String(restore_token) - response, results = request.call( - "CreateSession", parent_window="", options=options - ) - assert response == 0 - assert "session_handle" in results - assert "capabilities" in results - caps = results["capabilities"] + if persist_mode: + options["persist_mode"] = dbus.UInt32(persist_mode) + + options = dbus.Dictionary(options, signature="sv") + + response = request.call("CreateSession", parent_window="", options=options) + assert response.response == 0 + assert "session_handle" in response.results + assert "capabilities" in response.results + caps = response.results["capabilities"] # Returned capabilities must be a subset of the requested ones assert caps & ~capabilities == 0 - self.current_session_handle = results["session_handle"] + self.current_session_handle = response.results["session_handle"] # Check the impl portal was called with the right args method_calls = portal_mock.mock_interface.GetMethodCalls("CreateSession") @@ -64,8 +79,14 @@ def create_session(self, portal_mock, capabilities=0xF): _, args = method_calls[-1] assert args[3] == "" # parent window assert args[4]["capabilities"] == capabilities + if persist_mode: + assert args[4]["persist_mode"] == persist_mode - return response, results + # if restore_token: + # # The portal converts from token to data, we don't know the exact data + # assert args[4]["restore_data"] is not None + + return response def get_zones(self, portal_mock): """ @@ -180,7 +201,7 @@ def release(self, portal_mock, activation_id: int, cursor_position=None): assert pos == cursor_position def test_version(self, portal_mock): - portal_mock.check_version(1) + portal_mock.check_version(2) @pytest.mark.parametrize( "params", @@ -614,3 +635,56 @@ def cb_deactivated(session_handle, options): # Release() implies deactivated assert not deactivated_signal_received assert not disabled_signal_received + + def test_restore_session(self, portal_mock): + import uuid + + # This should initialize a session with restore_data + response = self.create_session(portal_mock, persist_mode=PersistMode.TRANSIENT) + assert response.response == 0 + restore_token = response.results["restore_token"] + assert restore_token is not None + try: + # We cannot see the actual data but we can verify our token is a valid UUID + uuid.UUID(restore_token) + except ValueError as e: + pytest.fail(f"Invalid UUID: {e}") + + session = Session.from_response(portal_mock.dbus_con, response) + session.close() + + mainloop = GLib.MainLoop() + GLib.timeout_add(500, mainloop.quit) + mainloop.run() + + assert session.closed + + # Second session, try to restore it with the token + response, results = self.create_session( + portal_mock, persist_mode=PersistMode.TRANSIENT, restore_token=restore_token + ) + assert response == 0 + assert results["restore_token"] is not None + # An implementation detail, the spec does not require it + assert results["restore_token"] == restore_token + + # Third session, try to restore with an invalid token which is silently ignored + # but should give us a new restore token + response, results = self.create_session( + portal_mock, + persist_mode=PersistMode.TRANSIENT, + restore_token=str(uuid.uuid4()), + ) + assert response == 0 + assert results["restore_token"] is not None + assert results["restore_token"] != restore_token + + # Fourth session, try to restore with a non-uuid value + with pytest.raises(dbus.exceptions.DBusException) as excinfo: + self.create_session( + portal_mock, persist_mode=PersistMode.TRANSIENT, restore_token="blah" + ) + assert ( + "Restore token is not a valid UUID string" + in excinfo.value.get_dbus_message() + )