xdg-desktop-portal/xdg-desktop-portal-webextensions.patch
Jan Grulich 2cdc8e33c0 Update to 1.19.1
Resolves: RHEL-70763
2025-01-06 12:40:35 +01:00

1987 lines
71 KiB
Diff

From b59c90fbd7bfd23bca1ce866e3c88e93fc678381 Mon Sep 17 00:00:00 2001
From: Jan Horak <jhorak@redhat.com>
Date: Tue, 17 Dec 2024 17:18:33 +0100
Subject: [PATCH] webextensions: add a portal for managing WebExtensions native
messaging servers.
Rebase, fix and continue work on webextensions: add a portal for managing WebExtensions
native messaging servers:
https://github.com/flatpak/xdg-desktop-portal/pull/705
This commit builds on the work done in the original MR authored by @jhenstridge
but resolves pending items and brings it closer to completion.
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 | 158 ++++
meson.build | 1 +
po/POTFILES.in | 1 +
src/meson.build | 1 +
src/web-extensions.c | 875 ++++++++++++++++++
src/web-extensions.h | 24 +
src/xdg-desktop-portal.c | 4 +
src/xdp-request.c | 12 +
tests/meson.build | 3 +
tests/native-messaging-hosts/meson.build | 28 +
.../org.example.testing.json.in | 9 +
tests/native-messaging-hosts/server.sh | 3 +
tests/test-portals.c | 7 +
tests/web-extensions.c | 632 +++++++++++++
tests/web-extensions.h | 2 +
16 files changed, 1761 insertions(+)
create mode 100644 data/org.freedesktop.portal.WebExtensions.xml
create mode 100644 src/web-extensions.c
create mode 100644 src/web-extensions.h
create mode 100644 tests/native-messaging-hosts/meson.build
create mode 100644 tests/native-messaging-hosts/org.example.testing.json.in
create mode 100755 tests/native-messaging-hosts/server.sh
create mode 100644 tests/web-extensions.c
create mode 100644 tests/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..2725c0234
--- /dev/null
+++ b/data/org.freedesktop.portal.WebExtensions.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+<!--
+ Copyright (C) 2022 Canonical Ltd
+
+ This library 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 <http://www.gnu.org/licenses/>.
+-->
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <!--
+ org.freedesktop.portal.WebExtensions:
+ @short_description: WebExtensions portal
+
+ 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
+ <https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/native-messaging-portal-design.html>`_.
+
+ This documentation describes version 1 of this interface.
+ -->
+ <interface name="org.freedesktop.portal.WebExtensions">
+ <!--
+ CreateSession:
+ @options: Vardict with optional further information
+ @session_handle: Object path for the #org.freedesktop.portal.Session created by this call.
+
+ Create a web extensions session. A successfully created
+ session can at any time be closed using
+ org.freedesktop.portal.Session::Close, or may at any time be
+ 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:
+ <variablelist>
+ <varlistentry>
+ <term>mode s</term>
+ <listitem><para>
+ A string indicating which behaviour the portal should
+ use when locating and starting native messaging
+ hosts. Valid values are "mozilla" and "chromium". By
+ default, mozilla behaviour is used.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>session_handle_token s</term>
+ <listitem><para>
+ A string that will be used as the last element of the session handle. Must be a valid
+ object path element. See the #org.freedesktop.portal.Session documentation for
+ more information about the session handle.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+ -->
+ <method name="CreateSession">
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
+ <arg type="a{sv}" name="options" direction="in"/>
+ <arg type="o" name="session_handle" direction="out"/>
+ </method>
+ <!--
+ GetManifest:
+ @session_handle: Object path for the #org.freedesktop.portal.Session object
+ @name: name of the native messaging host
+ @extension_or_origin: extension ID or origin URI identifying the extension
+ @json_manifest: the JSON manifest for the native messaging host
+
+ Return the JSON manifest of the native messaging host that
+ Start would invoke.
+ -->
+ <method name="GetManifest">
+ <arg type="o" name="session_handle" direction="in"/>
+ <arg type="s" name="name" direction="in"/>
+ <arg type="s" name="extension_or_origin" direction="in"/>
+ <arg type="s" name="json_manifest" direction="out"/>
+ </method>
+ <!--
+ Start:
+ @session_handle: Object path for the #org.freedesktop.portal.Session object
+ @name: name of the native messaging host
+ @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 host. The caller must
+ indicate the requesting web extension (either by extension ID
+ for Firefox, or origin URI for Chrome), which will be matched
+ against the host's access control list.
+
+ If the host can't be started, or invalid data is provided,
+ the session will be closed.
+
+ Supported keys in the @options vardict include:
+ <variablelist>
+ <varlistentry>
+ <term>handle_token s</term>
+ <listitem><para>
+ 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.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+ -->
+ <method name="Start">
+ <arg type="o" name="session_handle" direction="in"/>
+ <arg type="s" name="name" direction="in"/>
+ <arg type="s" name="extension_or_origin" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In3" value="QVariantMap"/>
+ <arg type="a{sv}" name="options" direction="in"/>
+ <arg type="o" name="handle" direction="out"/>
+ </method>
+ <!--
+ GetPipes:
+ @session_handle: Object path for the #org.freedesktop.portal.Session object
+ @options: Vardict with optional further information
+ @stdin: File descriptor representing the hosts's stdin.
+ @stdout: File descriptor representing the host's stdout.
+ @stderr: File descriptor representing the host's stderr.
+
+ Retrieve file descriptors for the native messaging host
+ identified by the session. This method should only be called
+ after the Start request recveives a successful response.
+ -->
+ <method name="GetPipes">
+ <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
+ <arg type="o" name="session_handle" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
+ <arg type="a{sv}" name="options" direction="in"/>
+ <arg type="h" name="stdin" direction="out"/>
+ <arg type="h" name="stdout" direction="out"/>
+ <arg type="h" name="stderr" direction="out"/>
+ </method>
+ <property name="version" type="u" access="read"/>
+ </interface>
+</node>
diff --git a/meson.build b/meson.build
index 876a531a1..55342e2a6 100644
--- a/meson.build
+++ b/meson.build
@@ -82,6 +82,7 @@ config_h = configuration_data()
config_h.set('_GNU_SOURCE', 1)
config_h.set_quoted('G_LOG_DOMAIN', 'xdg-desktop-portal')
config_h.set_quoted('DATADIR', datadir)
+config_h.set_quoted('LIBDIR', libdir)
config_h.set_quoted('LIBEXECDIR', libexecdir)
config_h.set_quoted('LOCALEDIR', localedir)
config_h.set_quoted('SYSCONFDIR', sysconfdir)
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
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..a33fee3e1
--- /dev/null
+++ b/src/web-extensions.c
@@ -0,0 +1,875 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <stdint.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <glib/gi18n.h>
+#include <gio/gunixfdlist.h>
+#include <json-glib/json-glib.h>
+
+#include "gio/gdesktopappinfo.h"
+#include "gio/gio.h"
+#include "glib.h"
+#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"
+#include "xdp-app-info.h"
+
+#define PERMISSION_TABLE "webextensions"
+
+typedef struct _WebExtensions WebExtensions;
+typedef struct _WebExtensionsClass WebExtensionsClass;
+
+struct _WebExtensions
+{
+ XdpDbusWebExtensionsSkeleton parent_instance;
+};
+
+struct _WebExtensionsClass
+{
+ XdpDbusWebExtensionsSkeletonClass parent_class;
+};
+
+static XdpDbusImplAccess *access_impl;
+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 _WebExtensionsSessionMode
+{
+ WEB_EXTENSIONS_SESSION_MODE_CHROMIUM,
+ WEB_EXTENSIONS_SESSION_MODE_MOZILLA,
+} WebExtensionsSessionMode;
+
+typedef enum _WebExtensionsSessionState
+{
+ WEB_EXTENSIONS_SESSION_STATE_INIT,
+ WEB_EXTENSIONS_SESSION_STATE_STARTING,
+ WEB_EXTENSIONS_SESSION_STATE_STARTED,
+ WEB_EXTENSIONS_SESSION_STATE_CLOSED,
+} WebExtensionsSessionState;
+
+typedef struct _WebExtensionsSession
+{
+ XdpSession parent;
+
+ WebExtensionsSessionMode mode;
+ WebExtensionsSessionState state;
+
+ GPid child_pid;
+ guint child_watch_id;
+
+ int standard_input;
+ int standard_output;
+ int standard_error;
+} 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;
+ session->standard_error = -1;
+}
+
+static void
+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;
+
+ /* 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);
+ web_extensions_session->child_watch_id = 0;
+ }
+
+ 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);
+ web_extensions_session->child_pid = -1;
+ }
+
+ if (web_extensions_session->standard_input >= 0)
+ {
+ close (web_extensions_session->standard_input);
+ web_extensions_session->standard_input = -1;
+ }
+ if (web_extensions_session->standard_output >= 0)
+ {
+ close (web_extensions_session->standard_output);
+ web_extensions_session->standard_output = -1;
+ }
+ if (web_extensions_session->standard_error >= 0)
+ {
+ close (web_extensions_session->standard_error);
+ web_extensions_session->standard_error = -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)
+{
+ /* 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;
+ /* Register handler for org.freedesktop.portal.Session::Close */
+ session_class->close = web_extensions_session_close;
+}
+
+static WebExtensionsSession *
+web_extensions_session_new (GVariant *options,
+ XdpCall *call,
+ GDBusConnection *connection,
+ GError **error)
+{
+ XdpSession *session;
+ WebExtensionsSession *web_extensions_session;
+ WebExtensionsSessionMode mode = WEB_EXTENSIONS_SESSION_MODE_MOZILLA;
+ 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,
+ "app-id", xdp_app_info_get_id (call->app_info),
+ "token", session_token,
+ "connection", connection,
+ NULL);
+
+ if (session)
+ g_debug ("webextensions session owned by '%s' created", session->sender);
+
+ if (!session)
+ {
+ g_warning ("Could not create WebExtensions session: %s", (*error)->message);
+ return NULL;
+ }
+ web_extensions_session = (WebExtensionsSession *)session;
+ web_extensions_session->mode = mode;
+ return web_extensions_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_host_exited (GPid pid,
+ gint status,
+ gpointer user_data)
+{
+ XdpSession *session = user_data;
+ WebExtensionsSession *web_extensions_session = (WebExtensionsSession *)session;
+
+ SESSION_AUTOLOCK (session);
+ web_extensions_session->child_pid = -1;
+ web_extensions_session->child_watch_id = 0;
+ xdp_session_close (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)
+{
+ /* This regexp comes from the Mozilla documentation on valid native
+ messaging host 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);
+}
+
+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;
+
+ hosts_path_str = g_getenv ("XDG_DESKTOP_PORTAL_WEB_EXTENSIONS_PATH");
+ if (hosts_path_str != NULL)
+ return g_strsplit (hosts_path_str, ":", -1);
+
+ search_path = g_ptr_array_new_with_free_func (g_free);
+ switch (mode)
+ {
+ case WEB_EXTENSIONS_SESSION_MODE_CHROMIUM:
+ /* Chrome and Chromium search paths documented here:
+ * 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));
+ 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));
+ 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"));
+ /* 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;
+ }
+
+ g_ptr_array_add (search_path, NULL);
+ return (GStrv)g_ptr_array_free (g_steal_pointer (&search_path), FALSE);
+}
+
+static char *
+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;
+ g_autofree char *metadata_basename = NULL;
+ int i;
+
+ /* Check that the we have a valid native messaging host 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 host name");
+ return NULL;
+ }
+
+ search_path = get_manifest_search_path (mode);
+ parser = json_parser_new ();
+ 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 *host_path;
+
+ metadata_filename = g_build_filename (search_path[i], metadata_basename, 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));
+
+ /* Skip if metadata contains an unexpected name */
+ 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 host */
+ if (g_strcmp0 (json_object_get_string_member (metadata_root, "type"), "stdio") != 0)
+ continue;
+
+ /* 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)
+ {
+ 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;
+ }
+
+ 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,
+ XDG_DESKTOP_PORTAL_ERROR_FAILED,
+ "Native messaging host path is not absolute");
+ return NULL;
+ }
+
+ /* 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 (host_path);
+ }
+
+ g_set_error (error,
+ XDG_DESKTOP_PORTAL_ERROR,
+ XDG_DESKTOP_PORTAL_ERROR_NOT_FOUND,
+ "Could not find native messaging host");
+ return NULL;
+}
+
+static gboolean
+handle_get_manifest (XdpDbusWebExtensions *object,
+ GDBusMethodInvocation *invocation,
+ const char *arg_session_handle,
+ const char *arg_name,
+ const char *arg_extension_or_origin)
+{
+ XdpCall *call = xdp_call_from_invocation (invocation);
+ XdpSession *session;
+ WebExtensionsSession *web_extensions_session;
+ g_autofree char *host_path = NULL;
+ g_autofree char *json_manifest = 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);
+ web_extensions_session = (WebExtensionsSession *)session;
+
+ if (web_extensions_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;
+ }
+
+ 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;
+ }
+
+ xdp_dbus_web_extensions_complete_get_manifest (object, invocation, json_manifest);
+ return TRUE;
+}
+
+static void
+handle_start_in_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ XdpRequest *request = (XdpRequest *)task_data;
+ XdpSession *session;
+ WebExtensionsSession *web_extensions_session;
+ const char *arg_name;
+ char *arg_extension_or_origin;
+ const char *app_id;
+ 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;
+ XdpPermission permission;
+ gboolean allowed;
+ char *argv[] = {NULL, NULL, NULL, NULL};
+ g_autoptr(GError) error = NULL;
+
+ REQUEST_AUTOLOCK (request);
+ 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);
+ web_extensions_session = (WebExtensionsSession *)session;
+
+ if (!request->exported || web_extensions_session->state != WEB_EXTENSIONS_SESSION_STATE_STARTING)
+ goto out;
+
+ arg_name = g_object_get_data (G_OBJECT (request), "name");
+ arg_extension_or_origin = g_object_get_data (G_OBJECT (request), "extension-or-origin");
+
+ 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);
+ fflush(stdout);
+ goto out;
+ }
+
+ app_id = xdp_app_info_get_id (request->app_info);
+ permission = xdp_get_permission_sync (app_id, PERMISSION_TABLE, arg_name);
+ if (permission == XDP_PERMISSION_ASK || permission == XDP_PERMISSION_UNSET)
+ {
+ guint access_response = 2;
+ g_autoptr(GVariant) access_results = NULL;
+ GVariantBuilder opt_builder;
+ const char *display_name;
+ g_autofree gchar *app_info_id = NULL;
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *subtitle = NULL;
+ g_autofree gchar *body = NULL;
+ GAppInfo* info = xdp_app_info_get_gappinfo (request->app_info);
+
+ if (info)
+ {
+ g_auto(GStrv) app_id_components = g_strsplit (g_app_info_get_id (info), ".desktop", 2);
+ app_info_id = g_strdup (app_id_components[0]);
+ }
+ 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, 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);
+ g_variant_builder_add (&opt_builder, "{sv}", "deny_label", g_variant_new_string (_("Don't allow")));
+ g_variant_builder_add (&opt_builder, "{sv}", "grant_label", g_variant_new_string (_("Allow")));
+ if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl,
+ request->id,
+ app_info_id ? app_info_id : app_id,
+ "",
+ title,
+ subtitle,
+ body,
+ g_variant_builder_end (&opt_builder),
+ &access_response,
+ &access_results,
+ cancellable,
+ &error))
+ {
+ g_warning ("AccessDialog call failed: %s", error->message);
+ g_clear_error (&error);
+ }
+ allowed = access_response == 0;
+
+ // access_response == 2 means dialog has been closed
+ if (permission == XDP_PERMISSION_UNSET && access_response != 2)
+ 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;
+ }
+
+ if (!allowed)
+ {
+ response = XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED;
+ goto out;
+ }
+
+ argv[0] = host_path;
+ 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;
+ }
+ if (!g_spawn_async_with_pipes (NULL, /* working_directory */
+ argv,
+ NULL, /* envp */
+ G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL, /* child_setup */
+ NULL, /* user_data */
+ &web_extensions_session->child_pid,
+ &web_extensions_session->standard_input,
+ &web_extensions_session->standard_output,
+ &web_extensions_session->standard_error,
+ &error))
+ {
+ web_extensions_session->child_pid = -1;
+ goto out;
+ }
+
+ web_extensions_session->child_watch_id = g_child_watch_add_full (G_PRIORITY_DEFAULT,
+ web_extensions_session->child_pid,
+ on_host_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;
+
+out:
+ should_close_session = !request->exported || response != XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS;
+
+ if (request->exported)
+ {
+ GVariantBuilder results;
+
+ g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT);
+ xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, g_variant_builder_end (&results));
+ xdp_request_unexport (request);
+ }
+
+ if (should_close_session)
+ xdp_session_close (session, TRUE);
+}
+
+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)
+{
+ XdpRequest *request = xdp_request_from_invocation (invocation);
+ XdpSession *session;
+ WebExtensionsSession *web_extensions_session;
+ g_autoptr(GTask) task = NULL;
+
+ REQUEST_AUTOLOCK (request);
+
+ session = xdp_session_from_request (arg_session_handle, request);
+ 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);
+ web_extensions_session = (WebExtensionsSession *)session;
+
+ if (web_extensions_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;
+ }
+
+ 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);
+
+ xdp_request_export (request, g_dbus_method_invocation_get_connection (invocation));
+ xdp_dbus_web_extensions_complete_start (object, invocation, request->id);
+
+ task = g_task_new (object, NULL, NULL, NULL);
+ g_task_set_task_data (task, g_object_ref (request), g_object_unref);
+ g_task_run_in_thread (task, handle_start_in_thread);
+
+ 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 *web_extensions_session;
+ int fds[3];
+ g_autoptr(GUnixFDList) out_fd_list = 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);
+ web_extensions_session = (WebExtensionsSession *)session;
+
+ if (web_extensions_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;
+ }
+
+ 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,
+ G_DBUS_ERROR_FAILED,
+ "GetPipes already called");
+ return TRUE;
+ }
+
+ 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 */
+ 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),
+ g_variant_new_handle (1),
+ g_variant_new_handle (2));
+ return TRUE;
+}
+
+static void
+web_extensions_iface_init (XdpDbusWebExtensionsIface *iface)
+{
+ iface->handle_create_session = handle_create_session;
+ iface->handle_get_manifest = handle_get_manifest;
+ 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)
+{
+ /* 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 *
+web_extensions_create (GDBusConnection *connection,
+ const char *dbus_name_access)
+{
+ g_autoptr(GError) error = NULL;
+
+ web_extensions = g_object_new (web_extensions_get_type (), NULL);
+
+ access_impl = xdp_dbus_impl_access_proxy_new_sync (connection,
+ G_DBUS_PROXY_FLAGS_NONE,
+ dbus_name_access,
+ DESKTOP_PORTAL_OBJECT_PATH,
+ NULL,
+ &error);
+ if (access_impl == NULL)
+ {
+ g_warning ("Failed to create access proxy: %s", error->message);
+ return 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..72b947a31
--- /dev/null
+++ b/src/web-extensions.h
@@ -0,0 +1,24 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+GDBusInterfaceSkeleton *web_extensions_create (GDBusConnection *connection,
+ const char *access_dbus_name);
diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c
index 55d44f9d0..e1559179f 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;
@@ -285,6 +286,9 @@ on_bus_acquired (GDBusConnection *connection,
{
XdpPortalImplementation *tmp;
+ export_portal_implementation (connection,
+ web_extensions_create (connection,
+ access_impl->dbus_name));
#ifdef HAVE_GEOCLUE
export_portal_implementation (connection,
location_create (connection,
diff --git a/src/xdp-request.c b/src/xdp-request.c
index 6aaa7ed23..46ceca861 100644
--- a/src/xdp-request.c
+++ b/src/xdp-request.c
@@ -188,6 +188,18 @@ get_token (GDBusMethodInvocation *invocation)
if (method_info->option_arg >= 0)
options = g_variant_get_child_value (parameters, method_info->option_arg);
}
+ else if (strcmp (interface, "org.freedesktop.portal.WebExtensions") == 0)
+ {
+ if (strcmp (method, "Start") == 0)
+ {
+ options = g_variant_get_child_value (parameters, 3);
+ }
+ else
+ {
+ g_warning ("Support for %s::%s missing in %s",
+ interface, method, G_STRLOC);
+ }
+ }
else
{
g_warning ("Support for %s::%s missing in %s",
diff --git a/tests/meson.build b/tests/meson.build
index d3a46dc5f..df1ec61c4 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -18,6 +18,7 @@ subdir('dbs')
subdir('portals')
subdir('services')
subdir('share')
+subdir('native-messaging-hosts')
test_db = executable(
'testdb',
@@ -106,6 +107,7 @@ if have_libportal
'screenshot.c',
'trash.c',
'wallpaper.c',
+ 'web-extensions.c',
'glib-backports.c',
)
endif
@@ -174,6 +176,7 @@ portal_tests = [
'settings',
'trash',
'wallpaper',
+ 'webextensions',
]
test_env = env_tests
diff --git a/tests/native-messaging-hosts/meson.build b/tests/native-messaging-hosts/meson.build
new file mode 100644
index 000000000..5ee790531
--- /dev/null
+++ b/tests/native-messaging-hosts/meson.build
@@ -0,0 +1,28 @@
+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.in b/tests/native-messaging-hosts/org.example.testing.json.in
new file mode 100644
index 000000000..58cd31029
--- /dev/null
+++ b/tests/native-messaging-hosts/org.example.testing.json.in
@@ -0,0 +1,9 @@
+{
+ "name": "org.example.testing",
+ "description": "Test native messaging host",
+ "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 bf56ea977..7593c84b3 100644
--- a/tests/test-portals.c
+++ b/tests/test-portals.c
@@ -22,6 +22,7 @@
#include "screenshot.h"
#include "trash.h"
#include "wallpaper.h"
+#include "web-extensions.h"
#endif
#include "utils.h"
@@ -133,6 +134,7 @@ global_setup (void)
g_autofree gchar *backends_executable = NULL;
g_autofree gchar *services = NULL;
g_autofree gchar *portal_dir = NULL;
+ g_autofree gchar *web_extensions_dir = NULL;
g_autofree gchar *argv0 = NULL;
g_autoptr(GSubprocessLauncher) launcher = NULL;
g_autoptr(GSubprocess) subprocess = NULL;
@@ -281,12 +283,14 @@ 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_BUILT, "native-messaging-hosts", NULL);
g_clear_object (&launcher);
launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
g_subprocess_launcher_setenv (launcher, "G_DEBUG", "fatal-criticals", TRUE);
g_subprocess_launcher_setenv (launcher, "DBUS_SESSION_BUS_ADDRESS", g_test_dbus_get_bus_address (dbus), TRUE);
g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_DIR", portal_dir, TRUE);
+ g_subprocess_launcher_setenv (launcher, "XDG_DESKTOP_PORTAL_WEB_EXTENSIONS_PATH", web_extensions_dir, TRUE);
g_subprocess_launcher_setenv (launcher, "XDG_DATA_HOME", outdir, TRUE);
g_subprocess_launcher_setenv (launcher, "PATH", g_getenv ("PATH"), TRUE);
g_subprocess_launcher_take_stdout_fd (launcher, xdup (STDERR_FILENO));
@@ -593,6 +597,9 @@ main (int argc, char **argv)
g_test_add_func ("/portal/notification/display-hint", test_notification_display_hint);
g_test_add_func ("/portal/notification/category", test_notification_category);
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
new file mode 100644
index 000000000..044d536a4
--- /dev/null
+++ b/tests/web-extensions.c
@@ -0,0 +1,632 @@
+#include <config.h>
+
+#include "web-extensions.h"
+#include "xdp-utils.h"
+
+#include <gio/gio.h>
+#include <gio/gunixfdlist.h>
+#include "xdp-impl-dbus.h"
+
+extern char outdir[];
+
+// TODO: convert these simple client wrappers to proper libportal APIs
+
+static void
+create_session_returned (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GTask) task = data;
+ g_autoptr(GVariant) ret = NULL;
+ GError *error = NULL;
+ g_autofree char *session_handle = NULL;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
+ if (!ret)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+ g_variant_get (ret, "(o)", &session_handle);
+ g_task_return_pointer (task, g_steal_pointer (&session_handle), g_free);
+}
+
+static void
+create_session (GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GDBusConnection) session_bus = NULL;
+ GError *error = NULL;
+ g_autofree char *session_token = NULL;
+ GVariantBuilder options;
+
+ task = g_task_new (NULL, cancellable, callback, data);
+
+ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (session_bus == NULL)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ 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",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.WebExtensions",
+ "CreateSession",
+ g_variant_new ("(a{sv})", &options),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cancellable,
+ create_session_returned,
+ g_steal_pointer (&task));
+}
+
+static char *
+create_session_finish (GAsyncResult *result, GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+get_manifest_returned (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GTask) task = data;
+ g_autoptr(GVariant) ret = NULL;
+ GError *error = NULL;
+ g_autofree char *json_manifest = NULL;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
+ if (!ret)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+ g_variant_get (ret, "(s)", &json_manifest);
+ g_task_return_pointer (task, g_steal_pointer (&json_manifest), g_free);
+}
+
+static void
+get_manifest (const char *session_handle,
+ const char *name,
+ const char *extension_or_origin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GDBusConnection) session_bus = NULL;
+ GError *error = NULL;
+
+ task = g_task_new (NULL, cancellable, callback, data);
+
+ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (session_bus == NULL)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ g_dbus_connection_call (session_bus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.WebExtensions",
+ "GetManifest",
+ g_variant_new ("(oss)", session_handle, name, extension_or_origin),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cancellable,
+ get_manifest_returned,
+ g_steal_pointer (&task));
+}
+
+static char *
+get_manifest_finish (GAsyncResult *result, GError **error)
+{
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+start_returned (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GTask) task = data;
+ g_autoptr(GVariant) ret = NULL;
+ GError *error = NULL;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
+ if (!ret)
+ {
+ guint signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "response-signal-id"));
+ g_dbus_connection_signal_unsubscribe (G_DBUS_CONNECTION (object), signal_id);
+ g_task_return_error (task, error);
+ return;
+ }
+}
+
+static void
+start_completed (GDBusConnection *session_bus,
+ const char *sender_name,
+ const char *object_path,
+ const char *interface_name,
+ const char *signal_name,
+ GVariant *parameters,
+ gpointer data)
+{
+ g_autoptr(GTask) task = g_object_ref (data);
+ guint signal_id;
+ guint32 response;
+ g_autoptr(GVariant) ret = NULL;
+
+ signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "response-signal-id"));
+ g_dbus_connection_signal_unsubscribe (session_bus, signal_id);
+
+ g_variant_get (parameters, "(u@a{sv})", &response, &ret);
+ switch (response)
+ {
+ case 0:
+ g_task_return_boolean (task, TRUE);
+ break;
+ case 1:
+ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Start cancelled");
+ break;
+ case 2:
+ default:
+ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed");
+ break;
+ }
+}
+
+static void
+start (const char *session_handle,
+ const char *name,
+ const char *extension_or_origin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GDBusConnection) session_bus = NULL;
+ GError *error = NULL;
+ g_autofree char *token = NULL;
+ g_autofree char *sender = NULL;
+ g_autofree char *request_path = NULL;
+ int i;
+ guint signal_id;
+ GVariantBuilder options;
+
+ task = g_task_new (NULL, cancellable, callback, data);
+
+ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (session_bus == NULL)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
+ sender = g_strdup (g_dbus_connection_get_unique_name (session_bus) + 1);
+ for (i = 0; sender[i]; i++)
+ if (sender[i] == '.')
+ sender[i] = '_';
+ request_path = g_strconcat ("/org/freedesktop/portal/desktop/request/", sender, "/", token, NULL);
+ signal_id = g_dbus_connection_signal_subscribe (session_bus,
+ "org.freedesktop.portal.Desktop",
+ "org.freedesktop.portal.Request",
+ "Response",
+ request_path,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+ start_completed,
+ g_object_ref (task),
+ g_object_unref);
+ g_object_set_data (G_OBJECT (task), "response-signal-id", GUINT_TO_POINTER (signal_id));
+
+ g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+ g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
+ g_dbus_connection_call (session_bus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.WebExtensions",
+ "Start",
+ g_variant_new ("(ossa{sv})", session_handle, name, extension_or_origin, &options),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cancellable,
+ start_returned,
+ g_steal_pointer (&task));
+}
+
+static gboolean
+start_finish (GAsyncResult *result, GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+get_pipes_returned (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GTask) task = data;
+ g_autoptr(GVariant) ret = NULL;
+ g_autoptr(GUnixFDList) fd_list = NULL;
+ GError *error = NULL;
+
+ ret = g_dbus_connection_call_with_unix_fd_list_finish (G_DBUS_CONNECTION (object), &fd_list, result, &error);
+ if (!ret)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+ g_task_return_pointer (task, g_steal_pointer (&fd_list), g_object_unref);
+}
+
+static void
+get_pipes (const char *session_handle,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GDBusConnection) session_bus = NULL;
+ GError *error = NULL;
+
+ task = g_task_new (NULL, cancellable, callback, data);
+
+ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (session_bus == NULL)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ g_dbus_connection_call_with_unix_fd_list (session_bus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.WebExtensions",
+ "GetPipes",
+ g_variant_new ("(oa{sv})", session_handle, NULL),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ cancellable,
+ get_pipes_returned,
+ g_steal_pointer (&task));
+}
+
+static gboolean
+get_pipes_finish (int *stdin_fileno, int *stdout_fileno, int *stderr_fileno, GAsyncResult *result, GError **error)
+{
+ g_autoptr(GUnixFDList) fd_list = NULL;
+
+ fd_list = g_task_propagate_pointer (G_TASK (result), error);
+ if (fd_list == NULL)
+ return FALSE;
+
+ if (stdin_fileno != NULL)
+ {
+ *stdin_fileno = g_unix_fd_list_get (fd_list, 0, error);
+ if (*stdin_fileno < 0)
+ return FALSE;
+ }
+ if (stdout_fileno != NULL)
+ {
+ *stdout_fileno = g_unix_fd_list_get (fd_list, 1, error);
+ if (*stdout_fileno < 0)
+ return FALSE;
+ }
+ if (stderr_fileno != NULL)
+ {
+ *stderr_fileno = g_unix_fd_list_get (fd_list, 2, error);
+ if (*stderr_fileno < 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+close_session_returned (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GTask) task = data;
+ g_autoptr(GVariant) ret = NULL;
+ GError *error = NULL;
+
+ ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
+ if (!ret)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+close_session (const char *session_handle,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GDBusConnection) session_bus = NULL;
+ GError *error = NULL;
+
+ task = g_task_new (NULL, cancellable, callback, data);
+
+ session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (session_bus == NULL)
+ {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ g_dbus_connection_call (session_bus,
+ "org.freedesktop.portal.Desktop",
+ session_handle,
+ "org.freedesktop.portal.Session",
+ "Close",
+ NULL,
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cancellable,
+ close_session_returned,
+ g_steal_pointer (&task));
+}
+
+static gboolean
+close_session_finish (GAsyncResult *result, GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+
+static int got_info = 0;
+
+extern XdpDbusImplPermissionStore *permission_store;
+
+static void
+set_web_extensions_permissions (const char *permission)
+{
+ const char *permissions[2] = { NULL, NULL };
+ g_autoptr(GError) error = NULL;
+
+ permissions[0] = permission;
+ xdp_dbus_impl_permission_store_call_set_permission_sync (permission_store,
+ "webextensions",
+ TRUE,
+ "org.example.testing",
+ "",
+ permissions,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+}
+
+static gboolean
+cancel_call (gpointer data)
+{
+ GCancellable *cancellable = data;
+
+ g_debug ("cancel call");
+ g_cancellable_cancel (cancellable);
+
+ return G_SOURCE_REMOVE;
+}
+
+typedef struct {
+ GCancellable *cancellable;
+ char *session_handle;
+ const char *messaging_host_name;
+} TestData;
+
+static void
+close_session_cb (GObject *object, GAsyncResult *result, gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ gboolean ret;
+
+ ret = close_session_finish (result, &error);
+ if (ret)
+ {
+ g_assert_no_error (error);
+ }
+ else
+ {
+ /* The native messaging host may have closed before we tried to
+ close it. */
+ g_assert_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD);
+ }
+
+ got_info++;
+ g_main_context_wakeup (NULL);
+}
+
+static void
+get_pipes_cb (GObject *object, GAsyncResult *result, gpointer data)
+{
+ TestData *test_data = data;
+ g_autoptr(GError) error = NULL;
+ gboolean ret;
+ int stdin_fileno = -1, stdout_fileno = -1, stderr_fileno = -1;
+
+ ret = get_pipes_finish (&stdin_fileno, &stdout_fileno, &stderr_fileno, result, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+ g_assert_cmpint (stdin_fileno, >, 0);
+ g_assert_cmpint (stdout_fileno, >, 0);
+ g_assert_cmpint (stderr_fileno, >, 0);
+
+ close (stdin_fileno);
+ close (stdout_fileno);
+ close (stderr_fileno);
+
+ close_session (test_data->session_handle,
+ test_data->cancellable,
+ close_session_cb,
+ test_data);
+}
+
+static void
+start_cb (GObject *object, GAsyncResult *result, gpointer data)
+{
+ TestData *test_data = data;
+ g_autoptr(GError) error = NULL;
+ gboolean ret;
+
+ ret = start_finish (result, &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ get_pipes (test_data->session_handle,
+ test_data->cancellable,
+ get_pipes_cb,
+ test_data);
+}
+
+
+static void
+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 *host_path = NULL;
+ g_autofree char *expected = NULL;
+
+ 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);
+ g_assert_cmpstr (json_manifest, ==, expected);
+
+ start (test_data->session_handle,
+ "org.example.testing",
+ "some-extension@example.org",
+ test_data->cancellable,
+ start_cb,
+ test_data);
+}
+
+static void
+create_session_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);
+
+ get_manifest (test_data->session_handle,
+ "org.example.testing",
+ "some-extension@example.org",
+ test_data->cancellable,
+ get_manifest_cb,
+ test_data);
+}
+
+void
+test_web_extensions_basic (void)
+{
+ g_autoptr(GCancellable) cancellable = NULL;
+ TestData test_data = { cancellable, NULL };
+ g_autoptr(GKeyFile) keyfile = NULL;
+ g_autofree char *path = NULL;
+ g_autoptr(GError) error = NULL;
+
+ got_info = 0;
+ keyfile = g_key_file_new ();
+
+ g_key_file_set_integer (keyfile, "backend", "delay", 0);
+ g_key_file_set_integer (keyfile, "backend", "response", 0);
+ g_key_file_set_integer (keyfile, "result", "response", 0);
+
+ path = g_build_filename (outdir, "access", NULL);
+ g_key_file_save_to_file (keyfile, path, &error);
+ g_assert_no_error (error);
+
+ g_key_file_unref (keyfile);
+
+ set_web_extensions_permissions ("yes");
+ create_session (cancellable, create_session_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);
+}
+
+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->messaging_host_name,
+ "some-extension@example.org",
+ test_data->cancellable,
+ start_bad_name_cb,
+ test_data);
+}
+
+void
+test_web_extensions_bad_name (void)
+{
+ const char *messaging_host_name[] = {
+ "no-dashes",
+ "../foo",
+ "no_trailing_dot.",
+ };
+ int i;
+
+ for (i = 0; i < G_N_ELEMENTS (messaging_host_name); i++)
+ {
+ g_autoptr(GCancellable) cancellable = NULL;
+ TestData test_data = { cancellable, NULL, messaging_host_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
new file mode 100644
index 000000000..0090184b9
--- /dev/null
+++ b/tests/web-extensions.h
@@ -0,0 +1,2 @@
+void test_web_extensions_basic (void);
+void test_web_extensions_bad_name (void);