From dbc110c0d66ede151a75ed128fa1ed4f85c714c2 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 1 Feb 2022 20:12:49 +0800 Subject: [PATCH 01/26] webextensions: add a portal for managing WebExtensions native messaging servers. This is intended to provide a way for a confined web browser to start native code helpers for their extensions. At present it can start the servers installed on the host system. But in future this could be extended to cover sandboxed native messaging servers too. --- data/meson.build | 1 + data/org.freedesktop.portal.WebExtensions.xml | 97 ++++ src/meson.build | 1 + src/web-extensions.c | 471 ++++++++++++++++++ src/web-extensions.h | 23 + src/xdg-desktop-portal.c | 2 + 6 files changed, 595 insertions(+) create mode 100644 data/org.freedesktop.portal.WebExtensions.xml create mode 100644 src/web-extensions.c create mode 100644 src/web-extensions.h diff --git a/data/meson.build b/data/meson.build index 66ad68d16..11c36a053 100644 --- a/data/meson.build +++ b/data/meson.build @@ -37,6 +37,7 @@ portal_sources = files( 'org.freedesktop.portal.Trash.xml', 'org.freedesktop.portal.Usb.xml', 'org.freedesktop.portal.Wallpaper.xml', + 'org.freedesktop.portal.WebExtensions.xml', ) portal_impl_sources = files( diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml new file mode 100644 index 000000000..02284791f --- /dev/null +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/meson.build b/src/meson.build index 5b6d553b6..e306b3eab 100644 --- a/src/meson.build +++ b/src/meson.build @@ -96,6 +96,7 @@ xdg_desktop_portal_sources = files( 'settings.c', 'trash.c', 'wallpaper.c', + 'web-extensions.c', 'xdg-desktop-portal.c', 'xdp-app-launch-context.c', 'xdp-background-monitor.c', diff --git a/src/web-extensions.c b/src/web-extensions.c new file mode 100644 index 000000000..313150ff9 --- /dev/null +++ b/src/web-extensions.c @@ -0,0 +1,471 @@ +/* + * Copyright © 2022 Canonical Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "xdp-session.h" +#include "web-extensions.h" +#include "xdp-request.h" +#include "xdp-permissions.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +typedef struct _WebExtensions WebExtensions; +typedef struct _WebExtensionsClass WebExtensionsClass; + +struct _WebExtensions +{ + XdpDbusWebExtensionsSkeleton parent_instance; +}; + +struct _WebExtensionsClass +{ + XdpDbusWebExtensionsSkeletonClass parent_class; +}; + +static WebExtensions *web_extensions; + +GType web_extensions_get_type (void); +static void web_extensions_iface_init (XdpDbusWebExtensionsIface *iface); + +G_DEFINE_TYPE_WITH_CODE (WebExtensions, web_extensions, XDP_DBUS_TYPE_WEB_EXTENSIONS_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_WEB_EXTENSIONS, + web_extensions_iface_init)); + +typedef enum _WebExtensionsSessionState +{ + WEB_EXTENSIONS_SESSION_STATE_INIT, + WEB_EXTENSIONS_SESSION_STATE_STARTED, + WEB_EXTENSIONS_SESSION_STATE_CLOSED, +} WebExtensionsSessionState; + +typedef struct _WebExtensionsSession +{ + XdpSession parent; + + WebExtensionsSessionState state; + + GPid child_pid; + guint child_watch_id; + + int standard_input; + int standard_output; +} WebExtensionsSession; + +typedef struct _WebExtensionsSessionClass +{ + XdpSessionClass parent_class; +} WebExtensionsSessionClass; + +GType web_extensions_session_get_type (void); + +G_DEFINE_TYPE (WebExtensionsSession, web_extensions_session, xdp_session_get_type ()); + +static void +web_extensions_session_init (WebExtensionsSession *session) +{ + session->child_pid = -1; + session->child_watch_id = 0; + + session->standard_input = -1; + session->standard_output = -1; +} + +static void +web_extensions_session_close (XdpSession *session) +{ + WebExtensionsSession *we_session = (WebExtensionsSession *)session; + + if (we_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; + + we_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; + if (we_session->child_watch_id != 0) + { + g_source_remove (we_session->child_watch_id); + we_session->child_watch_id = 0; + } + + if (we_session->child_pid > 0) + { + kill (we_session->child_pid, SIGTERM); + waitpid (we_session->child_pid, NULL, 0); + g_spawn_close_pid (we_session->child_pid); + we_session->child_pid = -1; + } + + if (we_session->standard_input >= 0) + { + close (we_session->standard_input); + we_session->standard_input = -1; + } + if (we_session->standard_output >= 0) + { + close (we_session->standard_output); + we_session->standard_output = -1; + } +} + +static void +web_extensions_session_finalize (GObject *object) +{ + XdpSession *session = (XdpSession *)object; + + web_extensions_session_close (session); + + G_OBJECT_CLASS (web_extensions_session_parent_class)->finalize (object); +} + +static void +web_extensions_session_class_init (WebExtensionsSessionClass *klass) +{ + GObjectClass *object_class; + XdpSessionClass *session_class; + + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = web_extensions_session_finalize; + + session_class = (XdpSessionClass *)klass; + session_class->close = web_extensions_session_close; +} + +static WebExtensionsSession * +web_extensions_session_new (GVariant *options, + XdpCall *call, + GDBusConnection *connection, + GError **error) +{ + XdpSession *session; + const char *session_token; + + session_token = lookup_session_token (options); + session = g_initable_new (web_extensions_session_get_type (), NULL, error, + "sender", call->sender, + "app-id", xdp_app_info_get_id (call->app_info), + "token", session_token, + "connection", connection, + NULL); + + if (session) + g_debug ("screen cast session owned by '%s' created", session->sender); + + return (WebExtensionsSession *)session; +} + +static gboolean +handle_create_session (XdpDbusWebExtensions *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + XdpCall *call = xdp_call_from_invocation (invocation); + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + g_autoptr(GError) error = NULL; + XdpSession *session; + + session = (XdpSession *)web_extensions_session_new (arg_options, call, connection, &error); + if (!session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + if (!xdp_session_export (session, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + xdp_session_close (session, FALSE); + return TRUE; + } + xdp_session_register (session); + + xdp_dbus_web_extensions_complete_create_session (object, invocation, session->id); + + return TRUE; +} + +static void +on_server_exited (GPid pid, + gint status, + gpointer user_data) +{ + WebExtensionsSession *we_session = user_data; + + we_session->child_pid = -1; + we_session->child_watch_id = 0; + xdp_session_close ((XdpSession *)we_session, TRUE); +} + +static gboolean +array_contains (JsonArray *array, + const char *value) +{ + guint length, i; + + if (array == NULL) + return FALSE; + + length = json_array_get_length (array); + for (i = 0; i < length; i++) + { + const char *element = json_array_get_string_element (array, i); + if (g_strcmp0 (element, value) == 0) + return TRUE; + } + return FALSE; +} + +static gboolean +is_valid_name (const char *name) +{ + return g_regex_match_simple ("^\\w+(\\.\\w+)*$", name, 0, 0); +} + +static char * +find_server (const char *server_name, + const char *extension_or_origin, + GError **error) +{ + const char *hosts_path_str; + g_auto(GStrv) hosts_path; + g_autoptr(JsonParser) parser = NULL; + g_autofree char *metadata_basename = NULL; + int i; + + /* Check that the we have a valid native messaging host name */ + if (!is_valid_name (server_name)) + { + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "invalid native messaging server name"); + return NULL; + } + + hosts_path_str = g_getenv ("XDG_DESKTOP_PORTAL_WEB_EXTENSIONS_PATH"); + if (hosts_path_str == NULL) + { + hosts_path_str = "/usr/lib/mozilla/native-messaging-hosts:/etc/opt/chrome/native-messaging-hosts:/etc/chromium/native-messaging-hosts"; + } + + hosts_path = g_strsplit (hosts_path_str, ":", -1); + parser = json_parser_new (); + metadata_basename = g_strconcat (server_name, ".json", NULL); + + for (i = 0; hosts_path[i] != NULL; i++) + { + g_autofree char *metadata_filename = NULL; + JsonObject *metadata_root; + + metadata_filename = g_build_filename (hosts_path[i], metadata_basename, NULL); + if (!g_file_test (metadata_filename, G_FILE_TEST_EXISTS)) + continue; + + if (!json_parser_load_from_file (parser, metadata_filename, error)) + return NULL; + + metadata_root = json_node_get_object (json_parser_get_root (parser)); + + /* Skip if metadata contains an unexpected name */ + if (g_strcmp0 (json_object_get_string_member (metadata_root, "name"), server_name) != 0) + continue; + + /* Skip if this is not a "stdio" type native messaging server */ + if (g_strcmp0 (json_object_get_string_member (metadata_root, "type"), "stdio") != 0) + continue; + + /* Skip if this server isn't available to the extension. Note + * that this ID is provided by the sandboxed browser, so this + * check is just to help implement its security policy. */ + if (!array_contains (json_object_get_array_member (metadata_root, "allowed_extensions"), extension_or_origin) && + !array_contains (json_object_get_array_member (metadata_root, "allowed_origins"), extension_or_origin)) + continue; + + /* Server matches: return its executable path */ + return g_strdup (json_object_get_string_member (metadata_root, "path")); + } + + g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, "cannot find native messaging server"); + return NULL; +} + +static gboolean +handle_start (XdpDbusWebExtensions *object, + GDBusMethodInvocation *invocation, + const char *arg_session_handle, + const char *arg_name, + const char *arg_extension_or_origin, + GVariant *arg_options) +{ + XdpCall *call = xdp_call_from_invocation (invocation); + XdpSession *session; + WebExtensionsSession *we_session; + g_autofree char *server_path = NULL; + char *argv[] = {NULL, NULL}; + g_autoptr(GError) error = NULL; + + session = xdp_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return TRUE; + } + + SESSION_AUTOLOCK_UNREF (session); + we_session = (WebExtensionsSession *)session; + + if (we_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session already started"); + return TRUE; + } + + server_path = find_server (arg_name, arg_extension_or_origin, &error); + if (server_path == NULL) + { + xdp_session_close(session, TRUE); + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + argv[0] = server_path; + if (!g_spawn_async_with_pipes (NULL, /* working_directory */ + argv, + NULL, /* envp */ + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, /* child_setup */ + NULL, /* user_data */ + &we_session->child_pid, + &we_session->standard_input, + &we_session->standard_output, + NULL, /* standard_error */ + &error)) + { + we_session->child_pid = -1; + xdp_session_close(session, TRUE); + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + we_session->child_watch_id = g_child_watch_add ( + we_session->child_pid, on_server_exited, we_session); + we_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTED; + + xdp_dbus_web_extensions_complete_start (object, invocation); + + return TRUE; +} + +static gboolean +handle_get_pipes (XdpDbusWebExtensions *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const char *arg_session_handle, + GVariant *arg_options) +{ + XdpCall *call = xdp_call_from_invocation (invocation); + XdpSession *session; + WebExtensionsSession *we_session; + g_autoptr(GUnixFDList) out_fd_list = NULL; + int stdin_id, stdout_id; + g_autoptr(GError) error = NULL; + + session = xdp_session_from_call (arg_session_handle, call); + if (!session) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid session"); + return TRUE; + } + + SESSION_AUTOLOCK_UNREF (session); + we_session = (WebExtensionsSession *)session; + + if (we_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTED) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Session not started"); + return TRUE; + } + + out_fd_list = g_unix_fd_list_new (); + stdin_id = g_unix_fd_list_append ( + out_fd_list, we_session->standard_input, &error); + if (stdin_id == -1) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Failed to append fd: %s", + error->message); + return TRUE; + } + + stdout_id = g_unix_fd_list_append ( + out_fd_list, we_session->standard_output, &error); + if (stdout_id == -1) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Failed to append fd: %s", + error->message); + return TRUE; + } + + xdp_dbus_web_extensions_complete_get_pipes (object, invocation, out_fd_list, + g_variant_new_handle (stdin_id), + g_variant_new_handle (stdout_id)); + return TRUE; +} + +static void +web_extensions_iface_init (XdpDbusWebExtensionsIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_start = handle_start; + iface->handle_get_pipes = handle_get_pipes; +} + +static void +web_extensions_init (WebExtensions *web_extensions) +{ + xdp_dbus_web_extensions_set_version (XDP_DBUS_WEB_EXTENSIONS (web_extensions), 1); +} + +static void +web_extensions_class_init (WebExtensionsClass *klass) +{ +} + +GDBusInterfaceSkeleton * +web_extensions_create (GDBusConnection *connection) +{ + web_extensions = g_object_new (web_extensions_get_type (), NULL); + return G_DBUS_INTERFACE_SKELETON (web_extensions); +} diff --git a/src/web-extensions.h b/src/web-extensions.h new file mode 100644 index 000000000..9dd9c5e3b --- /dev/null +++ b/src/web-extensions.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2022 Canonical Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * web_extensions_create (GDBusConnection *connection); diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index fc84030d1..5575ed769 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -69,6 +69,7 @@ #include "trash.h" #include "usb.h" #include "wallpaper.h" +#include "web-extensions.h" static int global_exit_status = 0; static GMainLoop *loop = NULL; @@ -251,6 +252,7 @@ on_bus_acquired (GDBusConnection *connection, export_portal_implementation (connection, trash_create (connection)); export_portal_implementation (connection, game_mode_create (connection)); export_portal_implementation (connection, realtime_create (connection)); + export_portal_implementation (connection, web_extensions_create (connection)); impls = find_all_portal_implementations ("org.freedesktop.impl.portal.Settings"); if (impls->len > 0) From c9e02e9ab4bb0d10d1489bf1268553d1bbaaa2e0 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Fri, 4 Mar 2022 18:24:19 +0800 Subject: [PATCH 02/26] webextensions: add a permission prompt when starting an extension --- data/org.freedesktop.portal.WebExtensions.xml | 14 ++ src/web-extensions.c | 203 ++++++++++++++---- src/web-extensions.h | 3 +- src/xdg-desktop-portal.c | 5 +- src/xdp-request.c | 12 ++ 5 files changed, 199 insertions(+), 38 deletions(-) diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index 02284791f..246f6304e 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -60,6 +60,7 @@ @name: name of the native messaging server @extension_or_origin: extension ID or origin URI identifying the extension @options: Vardict with optional further information + @handle: Object path for the #org.freedesktop.portal.Request object representing this call Start the named native messaging server. The caller must indicate the requesting web extension (either by extension ID @@ -68,12 +69,25 @@ If the server can't be started, or invalid data is provided, the session will be closed. + + Supported keys in the @options vardict include: + + + handle_token s + + A string that will be used as the last element of the @handle. Must be a valid + object path element. See the #org.freedesktop.portal.Request documentation for + more information about the @handle. + + + --> + + + + + + + @@ -114,7 +114,8 @@ @stderr: File descriptor representing the server's stderr. Retrieve file descriptors for the native messaging server - identified by the session. + identified by the session. This method should only be called + after the Start request recveives a successful response. --> diff --git a/src/web-extensions.c b/src/web-extensions.c index a0c07ff08..c7454b14a 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -103,39 +103,39 @@ web_extensions_session_init (WebExtensionsSession *session) static void web_extensions_session_close (XdpSession *session) { - WebExtensionsSession *we_session = (WebExtensionsSession *)session; + WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; - if (we_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; + if (web_extensions_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; - we_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; - if (we_session->child_watch_id != 0) + web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; + if (web_extensions_session->child_watch_id != 0) { - g_source_remove (we_session->child_watch_id); - we_session->child_watch_id = 0; + g_source_remove (web_extensions_session->child_watch_id); + web_extensions_session->child_watch_id = 0; } - if (we_session->child_pid > 0) + if (web_extensions_session->child_pid > 0) { - kill (we_session->child_pid, SIGTERM); - waitpid (we_session->child_pid, NULL, 0); - g_spawn_close_pid (we_session->child_pid); - we_session->child_pid = -1; + kill (web_extensions_session->child_pid, SIGTERM); + waitpid (web_extensions_session->child_pid, NULL, 0); + g_spawn_close_pid (web_extensions_session->child_pid); + web_extensions_session->child_pid = -1; } - if (we_session->standard_input >= 0) + if (web_extensions_session->standard_input >= 0) { - close (we_session->standard_input); - we_session->standard_input = -1; + close (web_extensions_session->standard_input); + web_extensions_session->standard_input = -1; } - if (we_session->standard_output >= 0) + if (web_extensions_session->standard_output >= 0) { - close (we_session->standard_output); - we_session->standard_output = -1; + close (web_extensions_session->standard_output); + web_extensions_session->standard_output = -1; } - if (we_session->standard_error >= 0) + if (web_extensions_session->standard_error >= 0) { - close (we_session->standard_error); - we_session->standard_error = -1; + close (web_extensions_session->standard_error); + web_extensions_session->standard_error = -1; } } @@ -219,11 +219,11 @@ on_server_exited (GPid pid, gpointer user_data) { XdpSession *session = user_data; - WebExtensionsSession *we_session = (WebExtensionsSession *)session; + WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; SESSION_AUTOLOCK (session); - we_session->child_pid = -1; - we_session->child_watch_id = 0; + web_extensions_session->child_pid = -1; + web_extensions_session->child_watch_id = 0; xdp_session_close (session, TRUE); } @@ -249,6 +249,14 @@ array_contains (JsonArray *array, static gboolean is_valid_name (const char *name) { + /* This regexp comes from the Mozilla documentation on valid native + messaging server names: + + https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests + + That is, one or more dot-separated groups composed of + alphanumeric characters and underscores. + */ return g_regex_match_simple ("^\\w+(\\.\\w+)*$", name, 0, 0); } @@ -293,11 +301,11 @@ get_manifest_search_path (void) static char * find_server (const char *server_name, const char *extension_or_origin, - char **server_description, - char **json_manifest, + char **out_server_description, + char **out_json_manifest, GError **error) { - g_auto(GStrv) search_path; + g_auto(GStrv) search_path = NULL; g_autoptr(JsonParser) parser = NULL; g_autofree char *metadata_basename = NULL; int i; @@ -305,7 +313,10 @@ find_server (const char *server_name, /* Check that the we have a valid native messaging host name */ if (!is_valid_name (server_name)) { - g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "invalid native messaging server name"); + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid native messaging server name"); return NULL; } @@ -316,14 +327,19 @@ find_server (const char *server_name, for (i = 0; search_path[i] != NULL; i++) { g_autofree char *metadata_filename = NULL; + g_autoptr(GError) load_error = NULL; JsonObject *metadata_root; metadata_filename = g_build_filename (search_path[i], metadata_basename, NULL); - if (!g_file_test (metadata_filename, G_FILE_TEST_EXISTS)) - continue; - - if (!json_parser_load_from_file (parser, metadata_filename, error)) - return NULL; + if (!json_parser_load_from_file (parser, metadata_filename, &load_error)) + { + /* If the file doesn't exist, continue searching. Error out + on anything else. */ + if (g_error_matches (load_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + continue; + g_propagate_error (error, g_steal_pointer (&load_error)); + return NULL; + } metadata_root = json_node_get_object (json_parser_get_root (parser)); @@ -343,14 +359,17 @@ find_server (const char *server_name, continue; /* Server matches: return its executable path and description */ - if (server_description != NULL) - *server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); - if (json_manifest != NULL) - *json_manifest = json_to_string (json_parser_get_root (parser), FALSE); + if (out_server_description != NULL) + *out_server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); + if (out_json_manifest != NULL) + *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); return g_strdup (json_object_get_string_member (metadata_root, "path")); } - g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, "cannot find native messaging server"); + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, + "Could not find native messaging server"); return NULL; } @@ -363,7 +382,7 @@ handle_get_manifest (XdpDbusWebExtensions *object, { XdpCall *call = xdp_call_from_invocation (invocation); XdpSession *session; - WebExtensionsSession *we_session; + WebExtensionsSession *web_extensions_session; g_autofree char *server_path = NULL; g_autofree char *json_manifest = NULL; g_autoptr(GError) error = NULL; @@ -379,9 +398,9 @@ handle_get_manifest (XdpDbusWebExtensions *object, } SESSION_AUTOLOCK_UNREF (session); - we_session = (WebExtensionsSession *)session; + web_extensions_session = (WebExtensionsSession *)session; - if (we_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) + if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, @@ -410,7 +429,7 @@ handle_start_in_thread (GTask *task, { XdpRequest *request = (XdpRequest *)task_data; XdpSession *session; - WebExtensionsSession *we_session; + WebExtensionsSession *web_extensions_session; const char *arg_name; const char *arg_extension_or_origin; const char *app_id; @@ -427,9 +446,9 @@ handle_start_in_thread (GTask *task, session = g_object_get_data (G_OBJECT (request), "session"); SESSION_AUTOLOCK_UNREF (g_object_ref (session)); g_object_set_data (G_OBJECT (request), "session", NULL); - we_session = (WebExtensionsSession *)session; + web_extensions_session = (WebExtensionsSession *)session; - if (!request->exported || we_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTING) + if (!request->exported || web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTING) goto out; arg_name = g_object_get_data (G_OBJECT (request), "name"); @@ -439,6 +458,8 @@ handle_start_in_thread (GTask *task, if (server_path == NULL) { g_warning ("Could not find WebExtensions backend: %s", error->message); + fflush(stderr); + fflush(stdout); goto out; } @@ -465,7 +486,6 @@ handle_start_in_thread (GTask *task, display_name = info ? g_app_info_get_display_name (info) : app_id; title = g_strdup_printf (_("Allow %s to start WebExtension backend?"), display_name); subtitle = g_strdup_printf (_("%s is requesting to launch \"%s\" (%s)."), display_name, server_description, arg_name); - /* This UI does not currently exist */ body = g_strdup (_("This permission can be changed at any time from the privacy settings.")); g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); @@ -493,7 +513,9 @@ handle_start_in_thread (GTask *task, xdp_set_permission_sync (app_id, PERMISSION_TABLE, arg_name, allowed ? XDP_PERMISSION_YES : XDP_PERMISSION_NO); } else - allowed = permission == XDP_PERMISSION_YES ? TRUE : FALSE; + { + allowed = permission == XDP_PERMISSION_YES ? TRUE : FALSE; + } if (!allowed) { @@ -508,20 +530,22 @@ handle_start_in_thread (GTask *task, G_SPAWN_DO_NOT_REAP_CHILD, NULL, /* child_setup */ NULL, /* user_data */ - &we_session->child_pid, - &we_session->standard_input, - &we_session->standard_output, - &we_session->standard_error, + &web_extensions_session->child_pid, + &web_extensions_session->standard_input, + &web_extensions_session->standard_output, + &web_extensions_session->standard_error, &error)) { - we_session->child_pid = -1; + web_extensions_session->child_pid = -1; goto out; } - we_session->child_watch_id = g_child_watch_add_full ( - G_PRIORITY_DEFAULT, we_session->child_pid, on_server_exited, - g_object_ref (we_session), g_object_unref); - we_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTED; + web_extensions_session->child_watch_id = g_child_watch_add_full (G_PRIORITY_DEFAULT, + web_extensions_session->child_pid, + on_server_exited, + g_object_ref (web_extensions_session), + g_object_unref); + web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTED; response = XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS; @@ -551,7 +575,7 @@ handle_start (XdpDbusWebExtensions *object, { XdpRequest *request = xdp_request_from_invocation (invocation); XdpSession *session; - WebExtensionsSession *we_session; + WebExtensionsSession *web_extensions_session; g_autoptr(GTask) task = NULL; REQUEST_AUTOLOCK (request); @@ -567,9 +591,9 @@ handle_start (XdpDbusWebExtensions *object, } SESSION_AUTOLOCK_UNREF (session); - we_session = (WebExtensionsSession *)session; + web_extensions_session = (WebExtensionsSession *)session; - if (we_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) + if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_INIT) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, @@ -578,7 +602,7 @@ handle_start (XdpDbusWebExtensions *object, return TRUE; } - we_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTING; + web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTING; g_object_set_data_full (G_OBJECT (request), "session", g_object_ref (session), g_object_unref); g_object_set_data_full (G_OBJECT (request), "name", g_strdup (arg_name), g_free); g_object_set_data_full (G_OBJECT (request), "extension-or-origin", g_strdup (arg_extension_or_origin), g_free); @@ -603,7 +627,8 @@ handle_get_pipes (XdpDbusWebExtensions *object, { XdpCall *call = xdp_call_from_invocation (invocation); XdpSession *session; - WebExtensionsSession *we_session; + WebExtensionsSession *web_extensions_session; + int fds[3]; g_autoptr(GUnixFDList) out_fd_list = NULL; session = xdp_session_from_call (arg_session_handle, call); @@ -617,9 +642,9 @@ handle_get_pipes (XdpDbusWebExtensions *object, } SESSION_AUTOLOCK_UNREF (session); - we_session = (WebExtensionsSession *)session; + web_extensions_session = (WebExtensionsSession *)session; - if (we_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTED) + if (web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTED) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, @@ -628,9 +653,9 @@ handle_get_pipes (XdpDbusWebExtensions *object, return TRUE; } - if (we_session->standard_input < 0 || - we_session->standard_output < 0 || - we_session->standard_error < 0) + if (web_extensions_session->standard_input < 0 || + web_extensions_session->standard_output < 0 || + web_extensions_session->standard_error < 0) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, @@ -639,16 +664,14 @@ handle_get_pipes (XdpDbusWebExtensions *object, return TRUE; } - int fds[3] = { - we_session->standard_input, - we_session->standard_output, - we_session->standard_error, - }; + fds[0] = web_extensions_session->standard_input; + fds[1] = web_extensions_session->standard_output; + fds[2] = web_extensions_session->standard_error; out_fd_list = g_unix_fd_list_new_from_array (fds, G_N_ELEMENTS (fds)); /* out_fd_list now owns the file descriptors */ - we_session->standard_input = -1; - we_session->standard_output = -1; - we_session->standard_error = -1; + web_extensions_session->standard_input = -1; + web_extensions_session->standard_output = -1; + web_extensions_session->standard_error = -1; xdp_dbus_web_extensions_complete_get_pipes (object, invocation, out_fd_list, g_variant_new_handle (0), diff --git a/tests/test-portals.c b/tests/test-portals.c index 3d59cea37..ac147b7ea 100644 --- a/tests/test-portals.c +++ b/tests/test-portals.c @@ -599,6 +599,7 @@ main (int argc, char **argv) g_test_add_func ("/portal/notification/supported-properties", test_notification_supported_properties); g_test_add_func ("/portal/webextensions/basic", test_web_extensions_basic); + g_test_add_func ("/portal/webextensions/bad-name", test_web_extensions_bad_name); #endif global_setup (); diff --git a/tests/web-extensions.c b/tests/web-extensions.c index 9e5ea919b..e46d17bfa 100644 --- a/tests/web-extensions.c +++ b/tests/web-extensions.c @@ -1,6 +1,7 @@ #include #include "web-extensions.h" +#include "xdp-utils.h" #include #include @@ -430,6 +431,7 @@ cancel_call (gpointer data) typedef struct { GCancellable *cancellable; char *session_handle; + const char *server_name; } TestData; static void @@ -473,7 +475,10 @@ get_pipes_cb (GObject *object, GAsyncResult *result, gpointer data) close (stdout_fileno); close (stderr_fileno); - close_session (test_data->session_handle, test_data->cancellable, close_session_cb, test_data); + close_session (test_data->session_handle, + test_data->cancellable, + close_session_cb, + test_data); } static void @@ -487,7 +492,10 @@ start_cb (GObject *object, GAsyncResult *result, gpointer data) g_assert_no_error (error); g_assert_true (ret); - get_pipes (test_data->session_handle, test_data->cancellable, get_pipes_cb, test_data); + get_pipes (test_data->session_handle, + test_data->cancellable, + get_pipes_cb, + test_data); } @@ -502,7 +510,12 @@ get_manifest_cb (GObject *object, GAsyncResult *result, gpointer data) g_assert_no_error (error); g_assert_cmpstr (json_manifest, ==, "{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"/bin/cat\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}"); - start (test_data->session_handle, "org.example.testing", "some-extension@example.org", test_data->cancellable, start_cb, test_data); + start (test_data->session_handle, + "org.example.testing", + "some-extension@example.org", + test_data->cancellable, + start_cb, + test_data); } static void @@ -515,7 +528,12 @@ create_session_cb (GObject *object, GAsyncResult *result, gpointer data) g_assert_no_error (error); g_assert_nonnull (test_data->session_handle); - get_manifest (test_data->session_handle, "org.example.testing", "some-extension@example.org", test_data->cancellable, get_manifest_cb, test_data); + get_manifest (test_data->session_handle, + "org.example.testing", + "some-extension@example.org", + test_data->cancellable, + get_manifest_cb, + test_data); } void @@ -546,3 +564,61 @@ test_web_extensions_basic (void) g_main_context_iteration (NULL, TRUE); g_free (test_data.session_handle); } + +static void +start_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data) +{ + g_autoptr(GError) error = NULL; + gboolean ret; + + ret = start_finish (result, &error); + g_assert_false (ret); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED); + + got_info++; + g_main_context_wakeup (NULL); +} + +static void +create_session_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data) +{ + TestData *test_data = data; + g_autoptr(GError) error = NULL; + + test_data->session_handle = create_session_finish (result, &error); + g_assert_no_error (error); + g_assert_nonnull (test_data->session_handle); + + start (test_data->session_handle, + test_data->server_name, + "some-extension@example.org", + test_data->cancellable, + start_bad_name_cb, + test_data); +} + +void +test_web_extensions_bad_name (void) +{ + const char *server_name[] = { + "no-dashes", + "../foo", + "no_trailing_dot.", + }; + int i; + + for (i = 0; i < G_N_ELEMENTS (server_name); i++) + { + g_autoptr(GCancellable) cancellable = NULL; + TestData test_data = { cancellable, NULL, server_name[i] }; + + got_info = 0; + set_web_extensions_permissions ("yes"); + create_session (cancellable, create_session_bad_name_cb, &test_data); + + g_timeout_add (100, cancel_call, cancellable); + while (!got_info) + g_main_context_iteration (NULL, TRUE); + g_free (test_data.session_handle); + } +} diff --git a/tests/web-extensions.h b/tests/web-extensions.h index 5b4a0a927..0090184b9 100644 --- a/tests/web-extensions.h +++ b/tests/web-extensions.h @@ -1 +1,2 @@ void test_web_extensions_basic (void); +void test_web_extensions_bad_name (void); From d2375d53059c2a7f2e0a9d19cc63dd5183791112 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Sat, 4 Nov 2023 14:48:17 +0200 Subject: [PATCH 15/26] web-extensions: send a SIGKILL rather than SIGTERM when closing the session. At this point, the browser has signalled the native messaging host to exit by closing its standard input. If the process is still running when the browser closes the session, then it is not responding. --- src/web-extensions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-extensions.c b/src/web-extensions.c index c7454b14a..a2af2bf9b 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -116,7 +116,7 @@ web_extensions_session_close (XdpSession *session) if (web_extensions_session->child_pid > 0) { - kill (web_extensions_session->child_pid, SIGTERM); + kill (web_extensions_session->child_pid, SIGKILL); waitpid (web_extensions_session->child_pid, NULL, 0); g_spawn_close_pid (web_extensions_session->child_pid); web_extensions_session->child_pid = -1; From e48d9eb58b631151fa3b59aed0edd1e5e67fb933 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 7 Nov 2023 09:07:14 +0200 Subject: [PATCH 16/26] webextensions: add a mode option to CreateSession that determines whether to provide Chromium-ish or Mozilla-ish behaviour This affects the following behaviour: 1. the search path for manifest files. 2. whether to match the "allowed_origins" or "allowed_extensions" key. 3. the command line arguments passed to the native messaging server. The tests have also been updated to use a shell script as the test native messaging server. Previously we just used /bin/cat, since it would stick around until its standard input was closed. This broke once we started passing arguments to the server. So now use a shell script that calls cat with no arguments. --- data/org.freedesktop.portal.WebExtensions.xml | 8 ++ src/web-extensions.c | 136 +++++++++++++----- tests/native-messaging-hosts/meson.build | 32 ++++- ...sting.json => org.example.testing.json.in} | 2 +- tests/native-messaging-hosts/server.sh | 3 + tests/test-portals.c | 2 +- tests/web-extensions.c | 8 +- 7 files changed, 147 insertions(+), 44 deletions(-) rename tests/native-messaging-hosts/{org.example.testing.json => org.example.testing.json.in} (86%) create mode 100755 tests/native-messaging-hosts/server.sh diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index e4ce12c16..8d1983609 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -40,6 +40,14 @@ Supported keys in the @options vardict include: + + mode s + + A string indicating which behaviour the portal should + use when locating and starting native messaging + servers. Valid values are "mozilla" and "chromium". + + session_handle_token s diff --git a/src/web-extensions.c b/src/web-extensions.c index a2af2bf9b..640e304b9 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -58,6 +58,12 @@ G_DEFINE_TYPE_WITH_CODE (WebExtensions, web_extensions, XDP_DBUS_TYPE_WEB_EXTENS G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_WEB_EXTENSIONS, web_extensions_iface_init)); +typedef enum _WebExtensionsSessionMode +{ + WEB_EXTENSIONS_SESSION_MODE_CHROMIUM, + WEB_EXTENSIONS_SESSION_MODE_MOZILLA, +} WebExtensionsSessionMode; + typedef enum _WebExtensionsSessionState { WEB_EXTENSIONS_SESSION_STATE_INIT, @@ -70,6 +76,7 @@ typedef struct _WebExtensionsSession { XdpSession parent; + WebExtensionsSessionMode mode; WebExtensionsSessionState state; GPid child_pid; @@ -168,8 +175,28 @@ web_extensions_session_new (GVariant *options, GError **error) { XdpSession *session; + WebExtensionsSession *web_extensions_session; + WebExtensionsSessionMode mode = WEB_EXTENSIONS_SESSION_MODE_CHROMIUM; + const char *mode_str = NULL; const char *session_token; + g_variant_lookup (options, "mode", "&s", &mode_str); + if (mode_str != NULL) + { + if (!strcmp(mode_str, "chromium")) + mode = WEB_EXTENSIONS_SESSION_MODE_CHROMIUM; + else if (!strcmp(mode_str, "mozilla")) + mode = WEB_EXTENSIONS_SESSION_MODE_MOZILLA; + else + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Invalid mode"); + return NULL; + } + } + session_token = lookup_session_token (options); session = g_initable_new (web_extensions_session_get_type (), NULL, error, "sender", call->sender, @@ -181,7 +208,9 @@ web_extensions_session_new (GVariant *options, if (session) g_debug ("webextensions session owned by '%s' created", session->sender); - return (WebExtensionsSession *)session; + web_extensions_session = (WebExtensionsSession *)session; + web_extensions_session->mode = mode; + return web_extensions_session; } static gboolean @@ -261,7 +290,7 @@ is_valid_name (const char *name) } static GStrv -get_manifest_search_path (void) +get_manifest_search_path (WebExtensionsSessionMode mode) { const char *hosts_path_str; g_autoptr(GPtrArray) search_path = NULL; @@ -270,38 +299,48 @@ get_manifest_search_path (void) if (hosts_path_str != NULL) return g_strsplit (hosts_path_str, ":", -1); - /* By default, use the native messaging search paths of Firefox, - * Chrome, and Chromium, as documented here: - * - * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location - * https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-location - */ - search_path = g_ptr_array_new_with_free_func (g_free); - /* Add per-user directories */ - g_ptr_array_add (search_path, g_build_filename (g_get_home_dir (), ".mozilla", "native-messaging-hosts", NULL)); - g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "google-chrome", "NativeMessagingHosts", NULL)); - g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "chromium", "NativeMessagingHosts", NULL)); - - /* Add system wide directories */ - g_ptr_array_add (search_path, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup ("/etc/opt/chrome/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup ("/etc/chromium/native-messaging-hosts")); - - /* And the same for xdg-desktop-portal's configured prefix */ - g_ptr_array_add (search_path, g_strdup (LIBDIR "mozilla/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "opt/chrome/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "chromium/native-messaging-hosts")); + switch (mode) + { + case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: + /* Chrome and Chromium search paths documented here: + * https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-location + */ + /* Add per-user directories */ + g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "google-chrome", "NativeMessagingHosts", NULL)); + g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "chromium", "NativeMessagingHosts", NULL)); + /* Add system wide directories */ + g_ptr_array_add (search_path, g_strdup ("/etc/opt/chrome/native-messaging-hosts")); + g_ptr_array_add (search_path, g_strdup ("/etc/chromium/native-messaging-hosts")); + /* And the same for xdg-desktop-portal's configured prefix */ + g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "opt/chrome/native-messaging-hosts")); + g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "chromium/native-messaging-hosts")); + break; + + case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: + /* Firefox search paths documented here: + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location + */ + /* Add per-user directories */ + g_ptr_array_add (search_path, g_build_filename (g_get_home_dir (), ".mozilla", "native-messaging-hosts", NULL)); + /* Add system wide directories */ + g_ptr_array_add (search_path, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); + g_ptr_array_add (search_path, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); + /* And the same for xdg-desktop-portal's configured prefix */ + g_ptr_array_add (search_path, g_strdup (LIBDIR "mozilla/native-messaging-hosts")); + break; + } g_ptr_array_add (search_path, NULL); return (GStrv)g_ptr_array_free (g_steal_pointer (&search_path), FALSE); } static char * -find_server (const char *server_name, +find_server (WebExtensionsSessionMode mode, + const char *server_name, const char *extension_or_origin, char **out_server_description, + char **out_manifest_filename, char **out_json_manifest, GError **error) { @@ -320,7 +359,7 @@ find_server (const char *server_name, return NULL; } - search_path = get_manifest_search_path (); + search_path = get_manifest_search_path (mode); parser = json_parser_new (); metadata_basename = g_strconcat (server_name, ".json", NULL); @@ -354,15 +393,25 @@ find_server (const char *server_name, /* Skip if this server isn't available to the extension. Note * that this ID is provided by the sandboxed browser, so this * check is just to help implement its security policy. */ - if (!array_contains (json_object_get_array_member (metadata_root, "allowed_extensions"), extension_or_origin) && - !array_contains (json_object_get_array_member (metadata_root, "allowed_origins"), extension_or_origin)) - continue; + switch (mode) + { + case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: + if (!array_contains (json_object_get_array_member (metadata_root, "allowed_origins"), extension_or_origin)) + continue; + break; + case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: + if (!array_contains (json_object_get_array_member (metadata_root, "allowed_extensions"), extension_or_origin)) + continue; + break; + } /* Server matches: return its executable path and description */ if (out_server_description != NULL) - *out_server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); + *out_server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); + if (out_manifest_filename != NULL) + *out_manifest_filename = g_strdup (metadata_filename); if (out_json_manifest != NULL) - *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); + *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); return g_strdup (json_object_get_string_member (metadata_root, "path")); } @@ -409,8 +458,9 @@ handle_get_manifest (XdpDbusWebExtensions *object, return TRUE; } - server_path = find_server (arg_name, arg_extension_or_origin, - NULL, &json_manifest, &error); + server_path = find_server (web_extensions_session->mode, + arg_name, arg_extension_or_origin, + NULL, NULL, &json_manifest, &error); if (!server_path) { g_dbus_method_invocation_return_gerror (invocation, error); @@ -431,15 +481,16 @@ handle_start_in_thread (GTask *task, XdpSession *session; WebExtensionsSession *web_extensions_session; const char *arg_name; - const char *arg_extension_or_origin; + char *arg_extension_or_origin; const char *app_id; g_autofree char *server_path = NULL; g_autofree char *server_description = NULL; + g_autofree char *manifest_filename = NULL; guint response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; gboolean should_close_session; XdpPermission permission; gboolean allowed; - char *argv[] = {NULL, NULL}; + char *argv[] = {NULL, NULL, NULL, NULL}; g_autoptr(GError) error = NULL; REQUEST_AUTOLOCK (request); @@ -454,7 +505,10 @@ handle_start_in_thread (GTask *task, arg_name = g_object_get_data (G_OBJECT (request), "name"); arg_extension_or_origin = g_object_get_data (G_OBJECT (request), "extension-or-origin"); - server_path = find_server (arg_name, arg_extension_or_origin, &server_description, NULL, &error); + server_path = find_server (web_extensions_session->mode, + arg_name, arg_extension_or_origin, + &server_description, &manifest_filename, NULL, + &error); if (server_path == NULL) { g_warning ("Could not find WebExtensions backend: %s", error->message); @@ -524,6 +578,16 @@ handle_start_in_thread (GTask *task, } argv[0] = server_path; + switch (web_extensions_session->mode) + { + case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: + argv[1] = arg_extension_or_origin; + break; + case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: + argv[1] = manifest_filename; + argv[2] = arg_extension_or_origin; + break; + } if (!g_spawn_async_with_pipes (NULL, /* working_directory */ argv, NULL, /* envp */ diff --git a/tests/native-messaging-hosts/meson.build b/tests/native-messaging-hosts/meson.build index 726f54fe2..5ee790531 100644 --- a/tests/native-messaging-hosts/meson.build +++ b/tests/native-messaging-hosts/meson.build @@ -1,6 +1,28 @@ -test_portal = configure_file(input: 'org.example.testing.json', - output: '@PLAINNAME@', - copy: true, - install: enable_installed_tests, - install_dir: installed_tests_dir / 'native-messaging-hosts', +configure_file(input: 'server.sh', + output: '@PLAINNAME@', + copy: true, + install_mode: 'rwxr-xr-x', + install: enable_installed_tests, + install_dir: installed_tests_dir / 'native-messaging-hosts', +) + +config = configuration_data() +config.set('server_path', meson.current_build_dir() / 'server.sh') +configure_file(input: 'org.example.testing.json.in', + output: '@BASENAME@', + configuration: config, ) + +# create a second version to be installed +if enable_installed_tests + config = configuration_data() + config.set('server_path', installed_tests_dir / 'native-messaging-hosts/server.sh') + configure_file(input: 'org.example.testing.json.in', + output: 'installed-org.example.testing.json', + configuration: config, + ) + install_data(meson.current_build_dir() / 'installed-org.example.testing.json', + rename: ['org.example.testing.json'], + install_dir: installed_tests_dir / 'native-messaging-hosts', + ) +endif diff --git a/tests/native-messaging-hosts/org.example.testing.json b/tests/native-messaging-hosts/org.example.testing.json.in similarity index 86% rename from tests/native-messaging-hosts/org.example.testing.json rename to tests/native-messaging-hosts/org.example.testing.json.in index 93b2f11e4..58cd31029 100644 --- a/tests/native-messaging-hosts/org.example.testing.json +++ b/tests/native-messaging-hosts/org.example.testing.json.in @@ -1,7 +1,7 @@ { "name": "org.example.testing", "description": "Test native messaging host", - "path": "/bin/cat", + "path": "@server_path@", "type": "stdio", "allowed_extensions": [ "some-extension@example.org" diff --git a/tests/native-messaging-hosts/server.sh b/tests/native-messaging-hosts/server.sh new file mode 100755 index 000000000..b0f3e402d --- /dev/null +++ b/tests/native-messaging-hosts/server.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec cat diff --git a/tests/test-portals.c b/tests/test-portals.c index ac147b7ea..7593c84b3 100644 --- a/tests/test-portals.c +++ b/tests/test-portals.c @@ -283,7 +283,7 @@ global_setup (void) NULL); portal_dir = g_test_build_filename (G_TEST_BUILT, "portals", "test", NULL); - web_extensions_dir = g_test_build_filename (G_TEST_DIST, "native-messaging-hosts", NULL); + web_extensions_dir = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", NULL); g_clear_object (&launcher); launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); diff --git a/tests/web-extensions.c b/tests/web-extensions.c index e46d17bfa..abfd3b12c 100644 --- a/tests/web-extensions.c +++ b/tests/web-extensions.c @@ -51,6 +51,7 @@ create_session (GCancellable *cancellable, session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT)); g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&options, "{sv}", "mode", g_variant_new_string ("mozilla")); g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token)); g_dbus_connection_call (session_bus, "org.freedesktop.portal.Desktop", @@ -505,10 +506,15 @@ get_manifest_cb (GObject *object, GAsyncResult *result, gpointer data) TestData *test_data = data; g_autoptr(GError) error = NULL; g_autofree char *json_manifest = NULL; + g_autofree char *server_path = NULL; + g_autofree char *expected = NULL; + + server_path = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", "server.sh", NULL); + expected = g_strdup_printf ("{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"%s\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}", server_path); json_manifest = get_manifest_finish (result, &error); g_assert_no_error (error); - g_assert_cmpstr (json_manifest, ==, "{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"/bin/cat\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}"); + g_assert_cmpstr (json_manifest, ==, expected); start (test_data->session_handle, "org.example.testing", From bec4a1b8c9b7e1ea69127fc14300f0065c00d637 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 7 Nov 2023 16:16:20 +0200 Subject: [PATCH 17/26] webextensions: default to mozilla behaviour While Chromium based browsers are more common, we only have an implementation for Firefox for now, which is not setting this option. --- data/org.freedesktop.portal.WebExtensions.xml | 3 ++- src/web-extensions.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index 8d1983609..ef832c020 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -45,7 +45,8 @@ A string indicating which behaviour the portal should use when locating and starting native messaging - servers. Valid values are "mozilla" and "chromium". + servers. Valid values are "mozilla" and "chromium". By + default, mozilla behaviour is used. diff --git a/src/web-extensions.c b/src/web-extensions.c index 640e304b9..a9752d8c3 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -176,7 +176,7 @@ web_extensions_session_new (GVariant *options, { XdpSession *session; WebExtensionsSession *web_extensions_session; - WebExtensionsSessionMode mode = WEB_EXTENSIONS_SESSION_MODE_CHROMIUM; + WebExtensionsSessionMode mode = WEB_EXTENSIONS_SESSION_MODE_MOZILLA; const char *mode_str = NULL; const char *session_token; From 7b14ac418feb31748c8e5930733698f2704a68b7 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 7 Nov 2023 16:37:34 +0200 Subject: [PATCH 18/26] webextensions: reject manifest files with non-absolute paths for the executable --- src/web-extensions.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/web-extensions.c b/src/web-extensions.c index a9752d8c3..dbf3aa18a 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -368,6 +368,7 @@ find_server (WebExtensionsSessionMode mode, g_autofree char *metadata_filename = NULL; g_autoptr(GError) load_error = NULL; JsonObject *metadata_root; + const char *server_path; metadata_filename = g_build_filename (search_path[i], metadata_basename, NULL); if (!json_parser_load_from_file (parser, metadata_filename, &load_error)) @@ -405,14 +406,24 @@ find_server (WebExtensionsSessionMode mode, break; } + server_path = json_object_get_string_member (metadata_root, "path"); + if (!g_path_is_absolute (server_path)) + { + g_set_error (error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Native messaging host path is not absolute"); + return NULL; + } + /* Server matches: return its executable path and description */ if (out_server_description != NULL) *out_server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); if (out_manifest_filename != NULL) - *out_manifest_filename = g_strdup (metadata_filename); + *out_manifest_filename = g_steal_pointer (&metadata_filename); if (out_json_manifest != NULL) *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); - return g_strdup (json_object_get_string_member (metadata_root, "path")); + return g_strdup (server_path); } g_set_error (error, From 5026bd95b51aca64ea2526ca8dc9bc131093622e Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Wed, 8 Nov 2023 11:33:21 +0200 Subject: [PATCH 19/26] webextensions: update terminology to consistently use "native messaging host" --- data/org.freedesktop.portal.WebExtensions.xml | 26 +++--- src/web-extensions.c | 80 +++++++++---------- tests/web-extensions.c | 16 ++-- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index ef832c020..80ccc75f6 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -22,7 +22,7 @@ @short_description: WebExtensions portal The WebExtensions portal allows sandboxed web browsers to start - native messaging servers installed on the host system. + native messaging hosts installed on the host system. This documentation describes version 1 of this interface. --> @@ -45,7 +45,7 @@ A string indicating which behaviour the portal should use when locating and starting native messaging - servers. Valid values are "mozilla" and "chromium". By + hosts. Valid values are "mozilla" and "chromium". By default, mozilla behaviour is used. @@ -66,11 +66,11 @@ @@ -82,17 +82,17 @@ diff --git a/src/web-extensions.c b/src/web-extensions.c index dbf3aa18a..e56a06e06 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -243,9 +243,9 @@ handle_create_session (XdpDbusWebExtensions *object, } static void -on_server_exited (GPid pid, - gint status, - gpointer user_data) +on_host_exited (GPid pid, + gint status, + gpointer user_data) { XdpSession *session = user_data; WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; @@ -279,7 +279,7 @@ static gboolean is_valid_name (const char *name) { /* This regexp comes from the Mozilla documentation on valid native - messaging server names: + messaging host names: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests @@ -336,13 +336,13 @@ get_manifest_search_path (WebExtensionsSessionMode mode) } static char * -find_server (WebExtensionsSessionMode mode, - const char *server_name, - const char *extension_or_origin, - char **out_server_description, - char **out_manifest_filename, - char **out_json_manifest, - GError **error) +find_messaging_host (WebExtensionsSessionMode mode, + const char *messaging_host_name, + const char *extension_or_origin, + char **out_description, + char **out_manifest_filename, + char **out_json_manifest, + GError **error) { g_auto(GStrv) search_path = NULL; g_autoptr(JsonParser) parser = NULL; @@ -350,25 +350,25 @@ find_server (WebExtensionsSessionMode mode, int i; /* Check that the we have a valid native messaging host name */ - if (!is_valid_name (server_name)) + if (!is_valid_name (messaging_host_name)) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, - "Invalid native messaging server name"); + "Invalid native messaging host name"); return NULL; } search_path = get_manifest_search_path (mode); parser = json_parser_new (); - metadata_basename = g_strconcat (server_name, ".json", NULL); + metadata_basename = g_strconcat (messaging_host_name, ".json", NULL); for (i = 0; search_path[i] != NULL; i++) { g_autofree char *metadata_filename = NULL; g_autoptr(GError) load_error = NULL; JsonObject *metadata_root; - const char *server_path; + const char *host_path; metadata_filename = g_build_filename (search_path[i], metadata_basename, NULL); if (!json_parser_load_from_file (parser, metadata_filename, &load_error)) @@ -384,14 +384,14 @@ find_server (WebExtensionsSessionMode mode, metadata_root = json_node_get_object (json_parser_get_root (parser)); /* Skip if metadata contains an unexpected name */ - if (g_strcmp0 (json_object_get_string_member (metadata_root, "name"), server_name) != 0) + if (g_strcmp0 (json_object_get_string_member (metadata_root, "name"), messaging_host_name) != 0) continue; - /* Skip if this is not a "stdio" type native messaging server */ + /* Skip if this is not a "stdio" type native messaging host */ if (g_strcmp0 (json_object_get_string_member (metadata_root, "type"), "stdio") != 0) continue; - /* Skip if this server isn't available to the extension. Note + /* Skip if this host isn't available to the extension. Note * that this ID is provided by the sandboxed browser, so this * check is just to help implement its security policy. */ switch (mode) @@ -406,8 +406,8 @@ find_server (WebExtensionsSessionMode mode, break; } - server_path = json_object_get_string_member (metadata_root, "path"); - if (!g_path_is_absolute (server_path)) + host_path = json_object_get_string_member (metadata_root, "path"); + if (!g_path_is_absolute (host_path)) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, @@ -416,20 +416,20 @@ find_server (WebExtensionsSessionMode mode, return NULL; } - /* Server matches: return its executable path and description */ - if (out_server_description != NULL) - *out_server_description = g_strdup (json_object_get_string_member (metadata_root, "description")); + /* Host matches: return its executable path and description */ + if (out_description != NULL) + *out_description = g_strdup (json_object_get_string_member (metadata_root, "description")); if (out_manifest_filename != NULL) *out_manifest_filename = g_steal_pointer (&metadata_filename); if (out_json_manifest != NULL) *out_json_manifest = json_to_string (json_parser_get_root (parser), FALSE); - return g_strdup (server_path); + return g_strdup (host_path); } g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND, - "Could not find native messaging server"); + "Could not find native messaging host"); return NULL; } @@ -443,7 +443,7 @@ handle_get_manifest (XdpDbusWebExtensions *object, XdpCall *call = xdp_call_from_invocation (invocation); XdpSession *session; WebExtensionsSession *web_extensions_session; - g_autofree char *server_path = NULL; + g_autofree char *host_path = NULL; g_autofree char *json_manifest = NULL; g_autoptr(GError) error = NULL; @@ -469,10 +469,10 @@ handle_get_manifest (XdpDbusWebExtensions *object, return TRUE; } - server_path = find_server (web_extensions_session->mode, - arg_name, arg_extension_or_origin, - NULL, NULL, &json_manifest, &error); - if (!server_path) + host_path = find_messaging_host (web_extensions_session->mode, + arg_name, arg_extension_or_origin, + NULL, NULL, &json_manifest, &error); + if (!host_path) { g_dbus_method_invocation_return_gerror (invocation, error); return TRUE; @@ -494,8 +494,8 @@ handle_start_in_thread (GTask *task, const char *arg_name; char *arg_extension_or_origin; const char *app_id; - g_autofree char *server_path = NULL; - g_autofree char *server_description = NULL; + g_autofree char *host_path = NULL; + g_autofree char *description = NULL; g_autofree char *manifest_filename = NULL; guint response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; gboolean should_close_session; @@ -516,11 +516,11 @@ handle_start_in_thread (GTask *task, arg_name = g_object_get_data (G_OBJECT (request), "name"); arg_extension_or_origin = g_object_get_data (G_OBJECT (request), "extension-or-origin"); - server_path = find_server (web_extensions_session->mode, - arg_name, arg_extension_or_origin, - &server_description, &manifest_filename, NULL, - &error); - if (server_path == NULL) + host_path = find_messaging_host (web_extensions_session->mode, + arg_name, arg_extension_or_origin, + &description, &manifest_filename, NULL, + &error); + if (host_path == NULL) { g_warning ("Could not find WebExtensions backend: %s", error->message); fflush(stderr); @@ -550,7 +550,7 @@ handle_start_in_thread (GTask *task, } display_name = info ? g_app_info_get_display_name (info) : app_id; title = g_strdup_printf (_("Allow %s to start WebExtension backend?"), display_name); - subtitle = g_strdup_printf (_("%s is requesting to launch \"%s\" (%s)."), display_name, server_description, arg_name); + subtitle = g_strdup_printf (_("%s is requesting to launch \"%s\" (%s)."), display_name, description, arg_name); body = g_strdup (_("This permission can be changed at any time from the privacy settings.")); g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); @@ -588,7 +588,7 @@ handle_start_in_thread (GTask *task, goto out; } - argv[0] = server_path; + argv[0] = host_path; switch (web_extensions_session->mode) { case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: @@ -617,7 +617,7 @@ handle_start_in_thread (GTask *task, web_extensions_session->child_watch_id = g_child_watch_add_full (G_PRIORITY_DEFAULT, web_extensions_session->child_pid, - on_server_exited, + on_host_exited, g_object_ref (web_extensions_session), g_object_unref); web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_STARTED; diff --git a/tests/web-extensions.c b/tests/web-extensions.c index abfd3b12c..f73090690 100644 --- a/tests/web-extensions.c +++ b/tests/web-extensions.c @@ -432,7 +432,7 @@ cancel_call (gpointer data) typedef struct { GCancellable *cancellable; char *session_handle; - const char *server_name; + const char *messaging_host_name; } TestData; static void @@ -506,11 +506,11 @@ get_manifest_cb (GObject *object, GAsyncResult *result, gpointer data) TestData *test_data = data; g_autoptr(GError) error = NULL; g_autofree char *json_manifest = NULL; - g_autofree char *server_path = NULL; + g_autofree char *host_path = NULL; g_autofree char *expected = NULL; - server_path = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", "server.sh", NULL); - expected = g_strdup_printf ("{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"%s\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}", server_path); + host_path = g_test_build_filename (G_TEST_BUILT, "native-messaging-hosts", "server.sh", NULL); + expected = g_strdup_printf ("{\"name\":\"org.example.testing\",\"description\":\"Test native messaging host\",\"path\":\"%s\",\"type\":\"stdio\",\"allowed_extensions\":[\"some-extension@example.org\"]}", host_path); json_manifest = get_manifest_finish (result, &error); g_assert_no_error (error); @@ -596,7 +596,7 @@ create_session_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data g_assert_nonnull (test_data->session_handle); start (test_data->session_handle, - test_data->server_name, + test_data->messaging_host_name, "some-extension@example.org", test_data->cancellable, start_bad_name_cb, @@ -606,17 +606,17 @@ create_session_bad_name_cb (GObject *object, GAsyncResult *result, gpointer data void test_web_extensions_bad_name (void) { - const char *server_name[] = { + const char *messaging_host_name[] = { "no-dashes", "../foo", "no_trailing_dot.", }; int i; - for (i = 0; i < G_N_ELEMENTS (server_name); i++) + for (i = 0; i < G_N_ELEMENTS (messaging_host_name); i++) { g_autoptr(GCancellable) cancellable = NULL; - TestData test_data = { cancellable, NULL, server_name[i] }; + TestData test_data = { cancellable, NULL, messaging_host_name[i] }; got_info = 0; set_web_extensions_permissions ("yes"); From bcef3c711e45bccc8bf86b498f298f59d46b83ec Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Tue, 5 Mar 2024 13:00:54 -0500 Subject: [PATCH 20/26] webextensions: small documentation updates and path typo fixes --- src/web-extensions.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/web-extensions.c b/src/web-extensions.c index e56a06e06..e7a0bc0f3 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -112,6 +112,10 @@ web_extensions_session_close (XdpSession *session) { WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session; + /* This function can be called repeatedly, e.g. by an explicit + "org.freedesktop.portal.Session::Close" message followed by + a call to finalize due to session's refcount reaching zero. + */ if (web_extensions_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; @@ -165,6 +169,7 @@ web_extensions_session_class_init (WebExtensionsSessionClass *klass) object_class->finalize = web_extensions_session_finalize; session_class = (XdpSessionClass *)klass; + /* Register handler for org.freedesktop.portal.Session::Close */ session_class->close = web_extensions_session_close; } @@ -304,7 +309,7 @@ get_manifest_search_path (WebExtensionsSessionMode mode) { case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: /* Chrome and Chromium search paths documented here: - * https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-location + * https://developer.chrome.com/docs/extensions/nativeMessaging/#native-messaging-host-location */ /* Add per-user directories */ g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "google-chrome", "NativeMessagingHosts", NULL)); @@ -313,8 +318,8 @@ get_manifest_search_path (WebExtensionsSessionMode mode) g_ptr_array_add (search_path, g_strdup ("/etc/opt/chrome/native-messaging-hosts")); g_ptr_array_add (search_path, g_strdup ("/etc/chromium/native-messaging-hosts")); /* And the same for xdg-desktop-portal's configured prefix */ - g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "opt/chrome/native-messaging-hosts")); - g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "chromium/native-messaging-hosts")); + g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "/opt/chrome/native-messaging-hosts")); + g_ptr_array_add (search_path, g_strdup (SYSCONFDIR "/chromium/native-messaging-hosts")); break; case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: @@ -326,8 +331,13 @@ get_manifest_search_path (WebExtensionsSessionMode mode) /* Add system wide directories */ g_ptr_array_add (search_path, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); g_ptr_array_add (search_path, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); - /* And the same for xdg-desktop-portal's configured prefix */ - g_ptr_array_add (search_path, g_strdup (LIBDIR "mozilla/native-messaging-hosts")); + /* And the same for xdg-desktop-portal's configured prefix. + This is helpful on Debian-based systems where LIBDIR is + suffixed with 'dpkg-architecture -qDEB_HOST_MULTIARCH', + e.g. '/usr/lib/x86_64-linux-gnu'. + https://salsa.debian.org/debian/debhelper/-/blob/5b96b19b456fe5e094f2870327a753b4b3ece0dc/lib/Debian/Debhelper/Buildsystem/meson.pm#L78 + */ + g_ptr_array_add (search_path, g_strdup (LIBDIR "/mozilla/native-messaging-hosts")); break; } From 20cd02f6ee8ea484dae3e7c462e1240251ab52d5 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 12 Mar 2024 15:59:03 +0800 Subject: [PATCH 21/26] web-extensions: pass the cancellable to xdp_..._call_access_dialog_sync() --- src/web-extensions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-extensions.c b/src/web-extensions.c index e7a0bc0f3..60b862974 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -576,7 +576,7 @@ handle_start_in_thread (GTask *task, g_variant_builder_end (&opt_builder), &access_response, &access_results, - NULL, + cancellable, &error)) { g_warning ("AccessDialog call failed: %s", error->message); From 916ae2abf4601df50eabfcbd3125e3cc484eeb45 Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Fri, 31 May 2024 08:31:45 -0400 Subject: [PATCH 22/26] webextensions: add some comments about flow of execution To help folks looking to work on the Web Extensions portal without prior familiarity with the XDG Desktop Portal and related code-bases like one of its main dependencies GLib and its object system. --- src/web-extensions.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/web-extensions.c b/src/web-extensions.c index 60b862974..32298c4a4 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -118,7 +118,19 @@ web_extensions_session_close (XdpSession *session) */ if (web_extensions_session->state == WEB_EXTENSIONS_SESSION_STATE_CLOSED) return; + /* We can assume that it is safe to transition to + WEB_EXTENSIONS_SESSION_STATE_CLOSED here, because we arrive + at web_extensions_session_close via one of two ways: + + 1. via the session_class->close function pointer from the + session_close function in src/session.c, which expects + its caller to lock the session's mutex (usually via the + SESSION_AUTOLOCK_UNREF macro); or + 2. from web_extensions_session_finalize, at which point + the last reference to the session has been released. + */ web_extensions_session->state = WEB_EXTENSIONS_SESSION_STATE_CLOSED; + if (web_extensions_session->child_watch_id != 0) { g_source_remove (web_extensions_session->child_watch_id); @@ -127,6 +139,10 @@ web_extensions_session_close (XdpSession *session) if (web_extensions_session->child_pid > 0) { + /* The responsibility of gracefully killing the process is + delegated to the browser, and the following SIGKILL is + a final attempt to clean up if necessary. + */ kill (web_extensions_session->child_pid, SIGKILL); waitpid (web_extensions_session->child_pid, NULL, 0); g_spawn_close_pid (web_extensions_session->child_pid); @@ -162,10 +178,19 @@ web_extensions_session_finalize (GObject *object) static void web_extensions_session_class_init (WebExtensionsSessionClass *klass) { + /* Called at the first instantiation of WebExtensionsSession, + i.e. in web_extensions_session_new with the call to + g_initable_new. + https://docs.gtk.org/gobject/concepts.html#object-instantiation + https://docs.gtk.org/gio/type_func.Initable.new.html + */ GObjectClass *object_class; XdpSessionClass *session_class; object_class = G_OBJECT_CLASS (klass); + /* finalize is called when the session refcount reaches zero. + https://docs.gtk.org/gobject/concepts.html#reference-count + */ object_class->finalize = web_extensions_session_finalize; session_class = (XdpSessionClass *)klass; @@ -783,6 +808,10 @@ web_extensions_init (WebExtensions *web_extensions) static void web_extensions_class_init (WebExtensionsClass *klass) { + /* Called at the first instantiation of WebExtensions, + i.e. in web_extensions_create with the call to g_object_new. + https://docs.gtk.org/gobject/concepts.html#object-instantiation + */ } GDBusInterfaceSkeleton * From f2cdd8ebf7cfc9e70282f0099b3f796637dbee46 Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Thu, 27 Jun 2024 22:58:58 -0400 Subject: [PATCH 23/26] webextensions: add references, explain session closure behaviour Explain the session closure behaviour from browsers' perspective and link to the document about Firefox's implementation. Also, add references about arguments passed to the native executable in the Mozilla/Firefox and Chromium modes. --- data/org.freedesktop.portal.WebExtensions.xml | 16 ++++++++++++++++ src/web-extensions.c | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index 80ccc75f6..36caaaf95 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -24,6 +24,10 @@ The WebExtensions portal allows sandboxed web browsers to start native messaging hosts installed on the host system. + Accompanying documentation for Firefox's implementation is + available: `Native messaging for a strictly-confined Firefox + `_. + This documentation describes version 1 of this interface. --> @@ -38,6 +42,18 @@ closed by the portal implementation, which will be signalled via org.freedesktop.portal.Session::Closed. + To close a session, the browser should: + + 1. close the process's stdin/stdout/stderr file descriptors + obtained from the portal; + 2. wait for a D-Bus Closed signal from the portal on the + org.freedesktop.portal.Session object (which will be + triggered on SIGCHLD via the g_child_watch_add_full + handler); and + 3. if the Closed signal from the portal doesn't come in time, + call the Close method on the org.freedesktop.portal.Session + object. + Supported keys in the @options vardict include: diff --git a/src/web-extensions.c b/src/web-extensions.c index 32298c4a4..df1a5d1f4 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -627,9 +627,16 @@ handle_start_in_thread (GTask *task, switch (web_extensions_session->mode) { case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM: + /* Pass the origin + https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging + */ argv[1] = arg_extension_or_origin; break; case WEB_EXTENSIONS_SESSION_MODE_MOZILLA: + /* Pass the manifest filename and extension ID + https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging + https://searchfox.org/mozilla-central/rev/9fcc11127fbfbdc88cbf37489dac90542e141c77/toolkit/components/extensions/NativeMessaging.sys.mjs#104-110 + */ argv[1] = manifest_filename; argv[2] = arg_extension_or_origin; break; From 838cadcecde76fa105f35d8936f35391512e444f Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Wed, 31 Jul 2024 22:18:39 -0400 Subject: [PATCH 24/26] webextensions: add $XDG_CONFIG_HOME/mozilla/native-messaging-hosts to manifest search path https://bugzilla.mozilla.org/show_bug.cgi?id=259356 Also add a comment in get_manifest_search_path about the importance and explanation of why the searched locations must be inaccessible to the sandboxed browsers. --- src/web-extensions.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/web-extensions.c b/src/web-extensions.c index df1a5d1f4..5dedd2664 100644 --- a/src/web-extensions.c +++ b/src/web-extensions.c @@ -322,6 +322,26 @@ is_valid_name (const char *name) static GStrv get_manifest_search_path (WebExtensionsSessionMode mode) { + /* IMPORTANT: + The safety model depends on the inability of the sandboxed + browser to write to the search locations specified below. + + As this portal allows browser extensions to run a native + messaging application outside of the sandbox through the portal, + the sandboxing mechanism must ensure that these locations are + inaccessible to the browser. If the locations are both readable + AND writable by the sandboxed browser, then a vulnerability + resulting in a file writing primitive within the sandbox could + result in arbitrary code execution outside of the sandbox through + the portal. + + For example, the Firefox Snap package meets this criterion, + because all strictly confined Snap packages (including Firefox) + are prohibited by AppArmor from accessing most directories and + files in the user's home directory, except where explicitly + specified, for instance using the 'personal-files' interface. + https://snapcraft.io/docs/personal-files-interface + */ const char *hosts_path_str; g_autoptr(GPtrArray) search_path = NULL; @@ -353,6 +373,7 @@ get_manifest_search_path (WebExtensionsSessionMode mode) */ /* Add per-user directories */ g_ptr_array_add (search_path, g_build_filename (g_get_home_dir (), ".mozilla", "native-messaging-hosts", NULL)); + g_ptr_array_add (search_path, g_build_filename (g_get_user_config_dir (), "mozilla", "native-messaging-hosts", NULL)); /* Add system wide directories */ g_ptr_array_add (search_path, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); g_ptr_array_add (search_path, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); From 21e3a9acde45584e1a3757c9a964ae44f4cc6ce4 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Mon, 13 Jan 2025 11:43:44 +0800 Subject: [PATCH 25/26] po: add src/webextensions.c to POTFILES.in --- po/POTFILES.in | 1 + 1 file changed, 1 insertion(+) diff --git a/po/POTFILES.in b/po/POTFILES.in index d514b2167..fd3232518 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -7,3 +7,4 @@ src/screenshot.c src/settings.c src/usb.c src/wallpaper.c +src/web-extensions.c From f2d0e704491d7ab30db01de06ac79c77b47d9cba Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Mon, 13 Jan 2025 13:29:39 +0800 Subject: [PATCH 26/26] data: add QtDBus annotations to interface --- data/org.freedesktop.portal.WebExtensions.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/org.freedesktop.portal.WebExtensions.xml b/data/org.freedesktop.portal.WebExtensions.xml index 36caaaf95..2725c0234 100644 --- a/data/org.freedesktop.portal.WebExtensions.xml +++ b/data/org.freedesktop.portal.WebExtensions.xml @@ -76,6 +76,7 @@ --> + @@ -127,6 +128,7 @@ + @@ -145,6 +147,7 @@ +