2cdc8e33c0
Resolves: RHEL-70763
1987 lines
71 KiB
Diff
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);
|