From 7b8d99b84940f1b6647b62a7d1db11cd17386421 Mon Sep 17 00:00:00 2001 From: Alexandre Date: Tue, 29 Oct 2024 09:50:10 +0100 Subject: [PATCH] Bug 1661935 - Flip Native Messaging portal from to enable use on Snap --- patches/native-messaging-portal.patch | 2223 +------------------------ 1 file changed, 8 insertions(+), 2215 deletions(-) diff --git a/patches/native-messaging-portal.patch b/patches/native-messaging-portal.patch index d66c598..c8737f6 100644 --- a/patches/native-messaging-portal.patch +++ b/patches/native-messaging-portal.patch @@ -1,2220 +1,13 @@ -From 9ed3f1acb16c64d93eabfa7d361bdea41a89216c Mon Sep 17 00:00:00 2001 -From: Amin Bandali -Date: Fri, 4 Oct 2024 17:31:15 +0200 -Subject: [PATCH] Bug 1661935 - Integration with a new WebExtensions XDG - desktop portal for native messaging on Linux r=robwu - -Differential Revision: https://phabricator.services.mozilla.com/D140803 ---- - modules/libpref/init/StaticPrefList.yaml | 10 + - python/mozbuild/mozbuild/mozinfo.py | 3 + - python/sites/xpcshell-test.txt | 4 + - .../configs/unittests/linux_unittest.py | 7 +- - testing/xpcshell/mach_commands.py | 1 + - .../extensions/NativeManifests.sys.mjs | 60 +- - .../extensions/NativeMessaging.sys.mjs | 107 ++- - .../extensions/NativeMessagingPortal.cpp | 696 ++++++++++++++++++ - .../extensions/NativeMessagingPortal.h | 76 ++ - toolkit/components/extensions/components.conf | 12 + - .../docs/native-messaging-portal-design.rst | 46 ++ - toolkit/components/extensions/moz.build | 8 + - .../extensions/nsINativeMessagingPortal.idl | 86 +++ - .../test/xpcshell/native_messaging.toml | 4 + - .../test_ext_native_messaging_portal.js | 397 ++++++++++ - .../test/xpcshell/test_native_manifests.js | 94 +++ - toolkit/modules/subprocess/Subprocess.sys.mjs | 13 + - .../subprocess/subprocess_common.sys.mjs | 32 +- - .../subprocess/subprocess_unix.sys.mjs | 4 + - .../subprocess/subprocess_unix.worker.js | 24 +- - .../modules/subprocess/subprocess_win.sys.mjs | 6 + - .../subprocess/subprocess_win.worker.js | 6 + - .../subprocess/subprocess_worker_common.js | 31 +- - .../test/xpcshell/test_subprocess.js | 77 ++ - widget/gtk/WidgetUtilsGtk.cpp | 2 + - widget/gtk/WidgetUtilsGtk.h | 1 + - 26 files changed, 1774 insertions(+), 33 deletions(-) - create mode 100644 python/sites/xpcshell-test.txt - create mode 100644 toolkit/components/extensions/NativeMessagingPortal.cpp - create mode 100644 toolkit/components/extensions/NativeMessagingPortal.h - create mode 100644 toolkit/components/extensions/docs/native-messaging-portal-design.rst - create mode 100644 toolkit/components/extensions/nsINativeMessagingPortal.idl - create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js - diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml -index b38571bb86536..fc6ca98f05dec 100644 +index 022bbb64125ce..e90ad2e1f15d1 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml -@@ -17386,6 +17386,16 @@ - value: 2 - mirror: always - -+# Whether to use XDG portal for native messaging. -+# https://github.com/flatpak/xdg-desktop-portal/issues/655 -+# - 0: never -+# - 1: always -+# - 2: auto (true for snap and flatpak or GTK_USE_PORTAL=1, false otherwise) -+- name: widget.use-xdg-desktop-portal.native-messaging -+ type: int32_t +@@ -17586,7 +17586,7 @@ + # - 2: auto (true for snap and flatpak or GTK_USE_PORTAL=1, false otherwise) + - name: widget.use-xdg-desktop-portal.native-messaging + type: int32_t +- value: 0 + value: 2 -+ mirror: always -+ - # Whether to try to use XDG portal for settings / look-and-feel information. - # https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Settings - # - 0: never -diff --git a/python/mozbuild/mozbuild/mozinfo.py b/python/mozbuild/mozbuild/mozinfo.py -index 8f58eec0620f2..d84348105c068 100644 ---- a/python/mozbuild/mozbuild/mozinfo.py -+++ b/python/mozbuild/mozbuild/mozinfo.py -@@ -154,6 +154,9 @@ def build_dict(config, env=os.environ): - ): - d["android_min_sdk"] = substs["MOZ_ANDROID_MIN_SDK_VERSION"] - -+ if "MOZ_ENABLE_DBUS" in substs: -+ d["dbus_enabled"] = bool(substs.get("MOZ_ENABLE_DBUS")) -+ - return d - - -diff --git a/python/sites/xpcshell-test.txt b/python/sites/xpcshell-test.txt -new file mode 100644 -index 0000000000000..54244d52f3397 ---- /dev/null -+++ b/python/sites/xpcshell-test.txt -@@ -0,0 +1,4 @@ -+# dbus and dbusmock are needed for some xpcshell tests on linux -+# e.g. toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js -+pypi-optional:dbus-python:some xpcshell tests requiring DBus mocking will not run -+pypi-optional:python-dbusmock==0.28.4:some xpcshell tests requiring DBus mocking will not run -diff --git a/testing/mozharness/configs/unittests/linux_unittest.py b/testing/mozharness/configs/unittests/linux_unittest.py -index 103b06b462c8a..e6fc0cea7dfb6 100644 ---- a/testing/mozharness/configs/unittests/linux_unittest.py -+++ b/testing/mozharness/configs/unittests/linux_unittest.py -@@ -34,7 +34,12 @@ else: - ##### - config = { - ### -- "virtualenv_modules": ["six==1.16.0", "vcversioner==2.16.0.0"], -+ "virtualenv_modules": [ -+ "six==1.16.0", -+ "vcversioner==2.16.0.0", -+ "dbus-python==1.2.18", -+ "python-dbusmock==0.28.4", -+ ], - "installer_path": INSTALLER_PATH, - "binary_path": BINARY_PATH, - "xpcshell_name": XPCSHELL_NAME, -diff --git a/testing/xpcshell/mach_commands.py b/testing/xpcshell/mach_commands.py -index 0e8abda5e6093..f25f149397d8f 100644 ---- a/testing/xpcshell/mach_commands.py -+++ b/testing/xpcshell/mach_commands.py -@@ -210,6 +210,7 @@ def get_parser(): - description="Run XPCOM Shell tests (API direct unit testing)", - conditions=[lambda *args: True], - parser=get_parser, -+ virtualenv_name="xpcshell-test", - ) - def run_xpcshell_test(command_context, test_objects=None, **params): - from mozbuild.controller.building import BuildDriver -diff --git a/toolkit/components/extensions/NativeManifests.sys.mjs b/toolkit/components/extensions/NativeManifests.sys.mjs -index 6d9836b8cde6a..282597c2db80a 100644 ---- a/toolkit/components/extensions/NativeManifests.sys.mjs -+++ b/toolkit/components/extensions/NativeManifests.sys.mjs -@@ -1,4 +1,4 @@ --/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -+/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ - /* vim: set sts=2 sw=2 et tw=80: */ - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this -@@ -89,26 +89,20 @@ export var NativeManifests = { - return manifest ? { path, manifest } : null; - }, - -- async _tryPath(type, path, name, context, logIfNotFound) { -- let manifest; -- try { -- manifest = await IOUtils.readJSON(path); -- } catch (ex) { -- if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) { -- Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`); -- return null; -- } -- if (DOMException.isInstance(ex) && ex.name == "NotFoundError") { -- if (logIfNotFound) { -- Cu.reportError( -- `Error reading native manifest file ${path}: file is referenced in the registry but does not exist` -- ); -- } -- return null; -- } -- Cu.reportError(ex); -- return null; -- } -+ /** -+ * Parse a native manifest of the given type and name. -+ * -+ * @param {string} type The type, one of: "pkcs11", "stdio" or "storage". -+ * @param {string} path The path to the manifest file. -+ * @param {string} name The name of the application. -+ * @param {object} context A context object as expected by Schemas.normalize. -+ * @param {object} data The JSON object of the manifest. -+ * @returns {object} The contents of the validated manifest, or null if -+ * the manifest is not valid. -+ */ -+ async parseManifest(type, path, name, context, data) { -+ await this.init(); -+ let manifest = data; - let normalized = lazy.Schemas.normalize( - manifest, - "manifest.NativeManifest", -@@ -158,6 +152,30 @@ export var NativeManifests = { - return manifest; - }, - -+ async _tryPath(type, path, name, context, logIfNotFound) { -+ let manifest; -+ try { -+ manifest = await IOUtils.readJSON(path); -+ } catch (ex) { -+ if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) { -+ Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`); -+ return null; -+ } -+ if (DOMException.isInstance(ex) && ex.name == "NotFoundError") { -+ if (logIfNotFound) { -+ Cu.reportError( -+ `Error reading native manifest file ${path}: file is referenced in the registry but does not exist` -+ ); -+ } -+ return null; -+ } -+ Cu.reportError(ex); -+ return null; -+ } -+ manifest = await this.parseManifest(type, path, name, context, manifest); -+ return manifest; -+ }, -+ - async _tryPaths(type, name, dirs, context) { - for (let dir of dirs) { - let path = PathUtils.join(dir, TYPES[type], `${name}.json`); -diff --git a/toolkit/components/extensions/NativeMessaging.sys.mjs b/toolkit/components/extensions/NativeMessaging.sys.mjs -index aecca5c438ed1..4f621ed857966 100644 ---- a/toolkit/components/extensions/NativeMessaging.sys.mjs -+++ b/toolkit/components/extensions/NativeMessaging.sys.mjs -@@ -1,4 +1,4 @@ --/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -+/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ - /* vim: set sts=2 sw=2 et tw=80: */ - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this -@@ -19,6 +19,13 @@ ChromeUtils.defineESModuleGetters(lazy, { - - const { ExtensionError, promiseTimeout } = ExtensionUtils; - -+XPCOMUtils.defineLazyServiceGetter( -+ lazy, -+ "portal", -+ "@mozilla.org/extensions/native-messaging-portal;1", -+ "nsINativeMessagingPortal" -+); -+ - // For a graceful shutdown (i.e., when the extension is unloaded or when it - // explicitly calls disconnect() on a native port), how long we give the native - // application to exit before we start trying to kill it. (in milliseconds) -@@ -49,6 +56,12 @@ XPCOMUtils.defineLazyPreferenceGetter( - ); - - export class NativeApp extends EventEmitter { -+ _throwGenericError(application) { -+ // Report a generic error to not leak information about whether a native -+ // application is installed to addons that do not have the right permission. -+ throw new ExtensionError(`No such native application ${application}`); -+ } -+ - /** - * @param {BaseContext} context The context that initiated the native app. - * @param {string} application The identifier of the native app. -@@ -67,6 +80,18 @@ export class NativeApp extends EventEmitter { - this.sendQueue = []; - this.writePromise = null; - this.cleanupStarted = false; -+ this.portalSessionHandle = null; -+ -+ if ("@mozilla.org/extensions/native-messaging-portal;1" in Cc) { -+ if (lazy.portal.shouldUse()) { -+ this.startupPromise = this._doInitPortal().catch(err => { -+ this.startupPromise = null; -+ Cu.reportError(err instanceof Error ? err : err.message); -+ this._cleanup(err); -+ }); -+ return; -+ } -+ } - - this.startupPromise = lazy.NativeManifests.lookupManifest( - "stdio", -@@ -74,10 +99,8 @@ export class NativeApp extends EventEmitter { - context - ) - .then(hostInfo => { -- // Report a generic error to not leak information about whether a native -- // application is installed to addons that do not have the right permission. - if (!hostInfo) { -- throw new ExtensionError(`No such native application ${application}`); -+ this._throwGenericError(application); - } - - let command = hostInfo.manifest.path; -@@ -123,6 +146,67 @@ export class NativeApp extends EventEmitter { - }); - } - -+ async _doInitPortal() { -+ let available = await lazy.portal.available; -+ if (!available) { -+ Cu.reportError("Native messaging portal is not available"); -+ this._throwGenericError(this.name); -+ } -+ -+ let handle = await lazy.portal.createSession(this.name); -+ this.portalSessionHandle = handle; -+ -+ let hostInfo = null; -+ let path; -+ try { -+ let manifest = await lazy.portal.getManifest( -+ handle, -+ this.name, -+ this.context.extension.id -+ ); -+ path = manifest.substring(0, 30) + "..."; -+ hostInfo = await lazy.NativeManifests.parseManifest( -+ "stdio", -+ path, -+ this.name, -+ this.context, -+ JSON.parse(manifest) -+ ); -+ } catch (ex) { -+ if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) { -+ Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`); -+ this._throwGenericError(this.name); -+ } -+ } -+ if (!hostInfo) { -+ this._throwGenericError(this.name); -+ } -+ -+ let pipes; -+ try { -+ pipes = await lazy.portal.start( -+ handle, -+ this.name, -+ this.context.extension.id -+ ); -+ } catch (err) { -+ if (err.name == "NotFoundError") { -+ this._throwGenericError(this.name); -+ } else { -+ throw err; -+ } -+ } -+ this.proc = await lazy.Subprocess.connectRunning([ -+ pipes.stdin, -+ pipes.stdout, -+ pipes.stderr, -+ ]); -+ this.startupPromise = null; -+ this._startRead(); -+ this._startWrite(); -+ this._startStderrRead(); -+ } -+ - /** - * Open a connection to a native messaging host. - * -@@ -299,6 +383,21 @@ export class NativeApp extends EventEmitter { - - await this.startupPromise; - -+ if (this.portalSessionHandle) { -+ if (this.writePromise) { -+ await this.writePromise.catch(Cu.reportError); -+ } -+ // When using the WebExtensions portal, we don't control the external -+ // process, the portal does. So let the portal handle waiting/killing the -+ // external process as it sees fit. -+ await lazy.portal -+ .closeSession(this.portalSessionHandle) -+ .catch(Cu.reportError); -+ this.portalSessionHandle = null; -+ this.proc = null; -+ return; -+ } -+ - if (!this.proc) { - // Failed to initialize proc in the constructor. - return; -diff --git a/toolkit/components/extensions/NativeMessagingPortal.cpp b/toolkit/components/extensions/NativeMessagingPortal.cpp -new file mode 100644 -index 0000000000000..32b3576a6ef6b ---- /dev/null -+++ b/toolkit/components/extensions/NativeMessagingPortal.cpp -@@ -0,0 +1,696 @@ -+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -+/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -+/* This Source Code Form is subject to the terms of the Mozilla Public -+ * License, v. 2.0. If a copy of the MPL was not distributed with this -+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -+ -+#include "NativeMessagingPortal.h" -+ -+#include -+#include -+ -+#include "mozilla/ClearOnShutdown.h" -+#include "mozilla/GUniquePtr.h" -+#include "mozilla/Logging.h" -+#include "mozilla/UniquePtrExtensions.h" -+#include "mozilla/WidgetUtilsGtk.h" -+#include "mozilla/dom/Promise.h" -+ -+#include -+ -+static mozilla::LazyLogModule gNativeMessagingPortalLog( -+ "NativeMessagingPortal"); -+ -+#ifdef MOZ_LOGGING -+# define LOG_NMP(...) \ -+ MOZ_LOG(gNativeMessagingPortalLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) -+#else -+# define LOG_NMP(args) -+#endif -+ -+namespace mozilla::extensions { -+ -+NS_IMPL_ISUPPORTS(NativeMessagingPortal, nsINativeMessagingPortal) -+ -+/* static */ -+already_AddRefed NativeMessagingPortal::GetSingleton() { -+ static StaticRefPtr sInstance; -+ -+ if (MOZ_UNLIKELY(!sInstance)) { -+ sInstance = new NativeMessagingPortal(); -+ ClearOnShutdown(&sInstance); -+ } -+ -+ return do_AddRef(sInstance); -+} -+ -+static void LogError(const char* aMethod, const GError& aError) { -+ g_warning("%s error: %s", aMethod, aError.message); -+} -+ -+static void RejectPromiseWithErrorMessage(dom::Promise& aPromise, -+ const GError& aError) { -+ aPromise.MaybeRejectWithOperationError(nsDependentCString(aError.message)); -+} -+ -+static nsresult GetPromise(JSContext* aCx, RefPtr& aPromise) { -+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); -+ if (NS_WARN_IF(!globalObject)) { -+ return NS_ERROR_UNEXPECTED; -+ } -+ ErrorResult result; -+ aPromise = dom::Promise::Create(globalObject, result); -+ if (NS_WARN_IF(result.Failed())) { -+ return result.StealNSResult(); -+ } -+ return NS_OK; -+} -+ -+struct CallbackData { -+ explicit CallbackData(dom::Promise& aPromise, -+ const gchar* aSessionHandle = nullptr) -+ : promise(&aPromise), sessionHandle(g_strdup(aSessionHandle)) {} -+ RefPtr promise; -+ GUniquePtr sessionHandle; -+ guint subscription_id = 0; -+}; -+ -+NativeMessagingPortal::NativeMessagingPortal() { -+ LOG_NMP("NativeMessagingPortal::NativeMessagingPortal()"); -+ mCancellable = dont_AddRef(g_cancellable_new()); -+ g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, -+ "org.freedesktop.portal.Desktop", -+ "/org/freedesktop/portal/desktop", -+ "org.freedesktop.portal.WebExtensions", mCancellable, -+ &NativeMessagingPortal::OnProxyReady, this); -+} -+ -+NativeMessagingPortal::~NativeMessagingPortal() { -+ LOG_NMP("NativeMessagingPortal::~NativeMessagingPortal()"); -+ -+ g_cancellable_cancel(mCancellable); -+ -+ // Close all active sessions -+ for (const auto& it : mSessions) { -+ if (it.second != SessionState::Active) { -+ continue; -+ } -+ GUniquePtr error; -+ RefPtr proxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync( -+ G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, -+ "org.freedesktop.portal.Desktop", it.first.c_str(), -+ "org.freedesktop.portal.Session", nullptr, getter_Transfers(error))); -+ if (!proxy) { -+ LOG_NMP("failed to get a D-Bus proxy: %s", error->message); -+ LogError(__func__, *error); -+ continue; -+ } -+ RefPtr res = dont_AddRef( -+ g_dbus_proxy_call_sync(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE, -+ -1, nullptr, getter_Transfers(error))); -+ if (!res) { -+ LOG_NMP("failed to close session: %s", error->message); -+ LogError(__func__, *error); -+ } -+ } -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::ShouldUse(bool* aResult) { -+ *aResult = widget::ShouldUsePortal(widget::PortalKind::NativeMessaging); -+ LOG_NMP("will %sbe used", *aResult ? "" : "not "); -+ return NS_OK; -+} -+ -+struct NativeMessagingPortal::DelayedCall { -+ using DelayedMethodCall = void (NativeMessagingPortal::*)(dom::Promise&, -+ GVariant*); -+ -+ DelayedCall(DelayedMethodCall aCallback, dom::Promise& aPromise, -+ GVariant* aArgs = nullptr) -+ : callback(aCallback), promise(&aPromise), args(aArgs) { -+ LOG_NMP("NativeMessagingPortal::DelayedCall::DelayedCall()"); -+ } -+ ~DelayedCall() { -+ LOG_NMP("NativeMessagingPortal::DelayedCall::~DelayedCall()"); -+ } -+ -+ DelayedMethodCall callback; -+ RefPtr promise; -+ RefPtr args; -+}; -+ -+/* static */ -+void NativeMessagingPortal::OnProxyReady(GObject* source, GAsyncResult* result, -+ gpointer user_data) { -+ NativeMessagingPortal* self = static_cast(user_data); -+ GUniquePtr error; -+ self->mProxy = dont_AddRef( -+ g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error))); -+ if (self->mProxy) { -+ LOG_NMP("D-Bus proxy ready for name %s, path %s, interface %s", -+ g_dbus_proxy_get_name(self->mProxy), -+ g_dbus_proxy_get_object_path(self->mProxy), -+ g_dbus_proxy_get_interface_name(self->mProxy)); -+ } else { -+ LOG_NMP("failed to get a D-Bus proxy: %s", error->message); -+ LogError(__func__, *error); -+ } -+ self->mInitialized = true; -+ while (!self->mPending.empty()) { -+ auto pending = std::move(self->mPending.front()); -+ self->mPending.pop_front(); -+ (self->*pending->callback)(*pending->promise, pending->args.get()); -+ } -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::GetAvailable(JSContext* aCx, dom::Promise** aPromise) { -+ RefPtr promise; -+ MOZ_TRY(GetPromise(aCx, promise)); -+ -+ if (mInitialized) { -+ MaybeDelayedIsAvailable(*promise, nullptr); -+ } else { -+ auto delayed = MakeUnique( -+ &NativeMessagingPortal::MaybeDelayedIsAvailable, *promise); -+ mPending.push_back(std::move(delayed)); -+ } -+ -+ promise.forget(aPromise); -+ return NS_OK; -+} -+ -+void NativeMessagingPortal::MaybeDelayedIsAvailable(dom::Promise& aPromise, -+ GVariant* aArgs) { -+ MOZ_ASSERT(!aArgs); -+ -+ bool available = false; -+ if (mProxy) { -+ RefPtr version = -+ dont_AddRef(g_dbus_proxy_get_cached_property(mProxy, "version")); -+ if (version) { -+ if (g_variant_get_uint32(version) >= 1) { -+ available = true; -+ } -+ } -+ } -+ -+ LOG_NMP("is %savailable", available ? "" : "not "); -+ aPromise.MaybeResolve(available); -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::CreateSession(const nsACString& aApplication, -+ JSContext* aCx, dom::Promise** aPromise) { -+ RefPtr promise; -+ MOZ_TRY(GetPromise(aCx, promise)); -+ -+ // Creating a session requires passing a unique token that will be used as the -+ // suffix for the session handle, and it should be a valid D-Bus object path -+ // component (i.e. it contains only the characters "[A-Z][a-z][0-9]_", see -+ // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path -+ // and -+ // https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Session). -+ // The token should be unique and not guessable. To avoid clashes with calls -+ // made from unrelated libraries, it is a good idea to use a per-library -+ // prefix combined with a random number. -+ // Here, we build the token by concatenating MOZ_APP_NAME (e.g. "firefox"), -+ // with the name of the native application (sanitized to remove invalid -+ // characters, see -+ // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests), -+ // and a random number. -+ const nsCString& application = PromiseFlatCString(aApplication); -+ GUniquePtr sanitizedApplicationName(g_strdup(application.get())); -+ g_strdelimit(sanitizedApplicationName.get(), ".", '_'); -+ GUniquePtr token(g_strdup_printf("%s_%s_%u", MOZ_APP_NAME, -+ sanitizedApplicationName.get(), -+ g_random_int())); -+ RefPtr args = dont_AddRef(g_variant_new_string(token.get())); -+ -+ if (mInitialized) { -+ MaybeDelayedCreateSession(*promise, args); -+ } else { -+ auto delayed = MakeUnique( -+ &NativeMessagingPortal::MaybeDelayedCreateSession, *promise, args); -+ mPending.push_back(std::move(delayed)); -+ } -+ -+ promise.forget(aPromise); -+ return NS_OK; -+} -+ -+void NativeMessagingPortal::MaybeDelayedCreateSession(dom::Promise& aPromise, -+ GVariant* aArgs) { -+ MOZ_ASSERT(g_variant_is_of_type(aArgs, G_VARIANT_TYPE_STRING)); -+ -+ if (!mProxy) { -+ return aPromise.MaybeRejectWithOperationError( -+ "No D-Bus proxy for the native messaging portal"); -+ } -+ -+ LOG_NMP("creating session with handle suffix %s", -+ g_variant_get_string(aArgs, nullptr)); -+ -+ GVariantBuilder options; -+ g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); -+ g_variant_builder_add(&options, "{sv}", "session_handle_token", -+ g_variant_ref_sink(aArgs)); -+ auto callbackData = MakeUnique(aPromise); -+ g_dbus_proxy_call(mProxy, "CreateSession", g_variant_new("(a{sv})", &options), -+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, -+ &NativeMessagingPortal::OnCreateSessionDone, -+ callbackData.release()); -+} -+ -+/* static */ -+void NativeMessagingPortal::OnCreateSessionDone(GObject* source, -+ GAsyncResult* result, -+ gpointer user_data) { -+ GDBusProxy* proxy = G_DBUS_PROXY(source); -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ GUniquePtr error; -+ RefPtr res = dont_AddRef( -+ g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error))); -+ if (res) { -+ RefPtr sessionHandle = -+ dont_AddRef(g_variant_get_child_value(res, 0)); -+ gsize length; -+ const char* value = g_variant_get_string(sessionHandle, &length); -+ LOG_NMP("session created with handle %s", value); -+ RefPtr portal = GetSingleton(); -+ portal->mSessions[value] = SessionState::Active; -+ -+ GDBusConnection* connection = g_dbus_proxy_get_connection(proxy); -+ // The "Closed" signal is emitted e.g. when the user denies access to the -+ // native application when the shell prompts them. -+ auto subscription_id_ptr = MakeUnique(0); -+ *subscription_id_ptr = g_dbus_connection_signal_subscribe( -+ connection, "org.freedesktop.portal.Desktop", -+ "org.freedesktop.portal.Session", "Closed", value, nullptr, -+ G_DBUS_SIGNAL_FLAGS_NONE, &NativeMessagingPortal::OnSessionClosedSignal, -+ subscription_id_ptr.get(), [](gpointer aUserData) { -+ UniquePtr release(reinterpret_cast(aUserData)); -+ }); -+ Unused << subscription_id_ptr.release(); // Ownership transferred above. -+ -+ callbackData->promise->MaybeResolve(nsDependentCString(value, length)); -+ } else { -+ LOG_NMP("failed to create session: %s", error->message); -+ LogError(__func__, *error); -+ RejectPromiseWithErrorMessage(*callbackData->promise, *error); -+ } -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::CloseSession(const nsACString& aHandle, JSContext* aCx, -+ dom::Promise** aPromise) { -+ const nsCString& sessionHandle = PromiseFlatCString(aHandle); -+ -+ if (!g_variant_is_object_path(sessionHandle.get())) { -+ LOG_NMP("cannot close session %s, invalid handle", sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ auto sessionIterator = mSessions.find(sessionHandle.get()); -+ if (sessionIterator == mSessions.end()) { -+ LOG_NMP("cannot close session %s, unknown handle", sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ if (sessionIterator->second != SessionState::Active) { -+ LOG_NMP("cannot close session %s, not active", sessionHandle.get()); -+ return NS_ERROR_FAILURE; -+ } -+ -+ RefPtr promise; -+ MOZ_TRY(GetPromise(aCx, promise)); -+ -+ sessionIterator->second = SessionState::Closing; -+ LOG_NMP("closing session %s", sessionHandle.get()); -+ auto callbackData = MakeUnique(*promise, sessionHandle.get()); -+ g_dbus_proxy_new_for_bus( -+ G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, -+ "org.freedesktop.portal.Desktop", sessionHandle.get(), -+ "org.freedesktop.portal.Session", nullptr, -+ &NativeMessagingPortal::OnCloseSessionProxyReady, callbackData.release()); -+ -+ promise.forget(aPromise); -+ return NS_OK; -+} -+ -+/* static */ -+void NativeMessagingPortal::OnCloseSessionProxyReady(GObject* source, -+ GAsyncResult* result, -+ gpointer user_data) { -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ GUniquePtr error; -+ RefPtr proxy = dont_AddRef( -+ g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error))); -+ if (!proxy) { -+ LOG_NMP("failed to close session: %s", error->message); -+ LogError(__func__, *error); -+ return RejectPromiseWithErrorMessage(*callbackData->promise, *error); -+ } -+ -+ g_dbus_proxy_call(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, -+ nullptr, &NativeMessagingPortal::OnCloseSessionDone, -+ callbackData.release()); -+} -+ -+/* static */ -+void NativeMessagingPortal::OnCloseSessionDone(GObject* source, -+ GAsyncResult* result, -+ gpointer user_data) { -+ GDBusProxy* proxy = G_DBUS_PROXY(source); -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ RefPtr portal = GetSingleton(); -+ GUniquePtr error; -+ RefPtr res = dont_AddRef( -+ g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error))); -+ if (res) { -+ LOG_NMP("session %s closed", callbackData->sessionHandle.get()); -+ portal->mSessions.erase(callbackData->sessionHandle.get()); -+ callbackData->promise->MaybeResolve(NS_OK); -+ } else { -+ LOG_NMP("failed to close session %s: %s", callbackData->sessionHandle.get(), -+ error->message); -+ LogError(__func__, *error); -+ portal->mSessions[callbackData->sessionHandle.get()] = SessionState::Error; -+ RejectPromiseWithErrorMessage(*callbackData->promise, *error); -+ } -+} -+ -+/* static */ -+void NativeMessagingPortal::OnSessionClosedSignal( -+ GDBusConnection* bus, const gchar* sender_name, const gchar* object_path, -+ const gchar* interface_name, const gchar* signal_name, GVariant* parameters, -+ gpointer user_data) { -+ guint subscription_id = *reinterpret_cast(user_data); -+ LOG_NMP("session %s was closed by the portal", object_path); -+ g_dbus_connection_signal_unsubscribe(bus, subscription_id); -+ RefPtr portal = GetSingleton(); -+ portal->mSessions.erase(object_path); -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::GetManifest(const nsACString& aHandle, -+ const nsACString& aName, -+ const nsACString& aExtension, JSContext* aCx, -+ dom::Promise** aPromise) { -+ const nsCString& sessionHandle = PromiseFlatCString(aHandle); -+ const nsCString& name = PromiseFlatCString(aName); -+ const nsCString& extension = PromiseFlatCString(aExtension); -+ -+ if (!g_variant_is_object_path(sessionHandle.get())) { -+ LOG_NMP("cannot find manifest for %s, invalid session handle %s", -+ name.get(), sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ auto sessionIterator = mSessions.find(sessionHandle.get()); -+ if (sessionIterator == mSessions.end()) { -+ LOG_NMP("cannot find manifest for %s, unknown session handle %s", -+ name.get(), sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ if (sessionIterator->second != SessionState::Active) { -+ LOG_NMP("cannot find manifest for %s, inactive session %s", name.get(), -+ sessionHandle.get()); -+ return NS_ERROR_FAILURE; -+ } -+ -+ if (!mProxy) { -+ LOG_NMP("cannot find manifest for %s, missing D-Bus proxy", name.get()); -+ return NS_ERROR_FAILURE; -+ } -+ -+ RefPtr promise; -+ MOZ_TRY(GetPromise(aCx, promise)); -+ -+ auto callbackData = MakeUnique(*promise, sessionHandle.get()); -+ g_dbus_proxy_call( -+ mProxy, "GetManifest", -+ g_variant_new("(oss)", sessionHandle.get(), name.get(), extension.get()), -+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, -+ &NativeMessagingPortal::OnGetManifestDone, callbackData.release()); -+ -+ promise.forget(aPromise); -+ return NS_OK; -+} -+ -+/* static */ -+void NativeMessagingPortal::OnGetManifestDone(GObject* source, -+ GAsyncResult* result, -+ gpointer user_data) { -+ GDBusProxy* proxy = G_DBUS_PROXY(source); -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ GUniquePtr error; -+ RefPtr jsonManifest = dont_AddRef( -+ g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error))); -+ if (jsonManifest) { -+ jsonManifest = dont_AddRef(g_variant_get_child_value(jsonManifest, 0)); -+ gsize length; -+ const char* value = g_variant_get_string(jsonManifest, &length); -+ LOG_NMP("manifest found in session %s: %s", -+ callbackData->sessionHandle.get(), value); -+ callbackData->promise->MaybeResolve(nsDependentCString(value, length)); -+ } else { -+ LOG_NMP("failed to find a manifest in session %s: %s", -+ callbackData->sessionHandle.get(), error->message); -+ LogError(__func__, *error); -+ RejectPromiseWithErrorMessage(*callbackData->promise, *error); -+ } -+} -+ -+NS_IMETHODIMP -+NativeMessagingPortal::Start(const nsACString& aHandle, const nsACString& aName, -+ const nsACString& aExtension, JSContext* aCx, -+ dom::Promise** aPromise) { -+ const nsCString& sessionHandle = PromiseFlatCString(aHandle); -+ const nsCString& name = PromiseFlatCString(aName); -+ const nsCString& extension = PromiseFlatCString(aExtension); -+ -+ if (!g_variant_is_object_path(sessionHandle.get())) { -+ LOG_NMP("cannot start %s, invalid session handle %s", name.get(), -+ sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ auto sessionIterator = mSessions.find(sessionHandle.get()); -+ if (sessionIterator == mSessions.end()) { -+ LOG_NMP("cannot start %s, unknown session handle %s", name.get(), -+ sessionHandle.get()); -+ return NS_ERROR_INVALID_ARG; -+ } -+ -+ if (sessionIterator->second != SessionState::Active) { -+ LOG_NMP("cannot start %s, inactive session %s", name.get(), -+ sessionHandle.get()); -+ return NS_ERROR_FAILURE; -+ } -+ -+ if (!mProxy) { -+ LOG_NMP("cannot start %s, missing D-Bus proxy", name.get()); -+ return NS_ERROR_FAILURE; -+ } -+ -+ RefPtr promise; -+ MOZ_TRY(GetPromise(aCx, promise)); -+ -+ auto callbackData = MakeUnique(*promise, sessionHandle.get()); -+ auto* releasedCallbackData = callbackData.release(); -+ -+ LOG_NMP("starting %s, requested by %s in session %s", name.get(), -+ extension.get(), sessionHandle.get()); -+ -+ GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy); -+ GUniquePtr senderName( -+ g_strdup(g_dbus_connection_get_unique_name(connection))); -+ g_strdelimit(senderName.get(), ".", '_'); -+ GUniquePtr handleToken( -+ g_strdup_printf("%s/%d", MOZ_APP_NAME, g_random_int_range(0, G_MAXINT))); -+ GUniquePtr requestPath( -+ g_strdup_printf("/org/freedesktop/portal/desktop/request/%s/%s", -+ senderName.get() + 1, handleToken.get())); -+ releasedCallbackData->subscription_id = g_dbus_connection_signal_subscribe( -+ connection, "org.freedesktop.portal.Desktop", -+ "org.freedesktop.portal.Request", "Response", requestPath.get(), nullptr, -+ G_DBUS_SIGNAL_FLAGS_NONE, -+ &NativeMessagingPortal::OnStartRequestResponseSignal, -+ releasedCallbackData, nullptr); -+ -+ auto callbackDataCopy = -+ MakeUnique(*promise, sessionHandle.get()); -+ GVariantBuilder options; -+ g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); -+ g_variant_builder_add(&options, "{sv}", "handle_token", -+ g_variant_new_string(handleToken.get())); -+ g_dbus_proxy_call(mProxy, "Start", -+ g_variant_new("(ossa{sv})", sessionHandle.get(), name.get(), -+ extension.get(), &options), -+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, -+ &NativeMessagingPortal::OnStartDone, -+ callbackDataCopy.release()); -+ -+ promise.forget(aPromise); -+ return NS_OK; -+} -+ -+/* static */ -+void NativeMessagingPortal::OnStartDone(GObject* source, GAsyncResult* result, -+ gpointer user_data) { -+ GDBusProxy* proxy = G_DBUS_PROXY(source); -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ GUniquePtr error; -+ RefPtr handle = dont_AddRef( -+ g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error))); -+ if (handle) { -+ handle = dont_AddRef(g_variant_get_child_value(handle, 0)); -+ LOG_NMP( -+ "native application start requested in session %s, pending response " -+ "for %s", -+ callbackData->sessionHandle.get(), -+ g_variant_get_string(handle, nullptr)); -+ } else { -+ LOG_NMP("failed to start native application in session %s: %s", -+ callbackData->sessionHandle.get(), error->message); -+ LogError(__func__, *error); -+ RejectPromiseWithErrorMessage(*callbackData->promise, *error); -+ } -+} -+ -+/* static */ -+void NativeMessagingPortal::OnStartRequestResponseSignal( -+ GDBusConnection* bus, const gchar* sender_name, const gchar* object_path, -+ const gchar* interface_name, const gchar* signal_name, GVariant* parameters, -+ gpointer user_data) { -+ UniquePtr callbackData(static_cast(user_data)); -+ -+ LOG_NMP("got response signal for %s in session %s", object_path, -+ callbackData->sessionHandle.get()); -+ g_dbus_connection_signal_unsubscribe(bus, callbackData->subscription_id); -+ -+ RefPtr result = -+ dont_AddRef(g_variant_get_child_value(parameters, 0)); -+ guint32 response = g_variant_get_uint32(result); -+ // Possible values for response -+ // (https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response): -+ // 0: Success, the request is carried out -+ // 1: The user cancelled the interaction -+ // 2: The user interaction was ended in some other way -+ if (response == 0) { -+ LOG_NMP( -+ "native application start successful in session %s, requesting file " -+ "descriptors", -+ callbackData->sessionHandle.get()); -+ RefPtr portal = GetSingleton(); -+ GVariantBuilder options; -+ g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); -+ g_dbus_proxy_call_with_unix_fd_list( -+ portal->mProxy.get(), "GetPipes", -+ g_variant_new("(oa{sv})", callbackData->sessionHandle.get(), &options), -+ G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, -+ &NativeMessagingPortal::OnGetPipesDone, callbackData.release()); -+ } else if (response == 1) { -+ LOG_NMP("native application start canceled by user in session %s", -+ callbackData->sessionHandle.get()); -+ callbackData->promise->MaybeRejectWithAbortError( -+ "Native application start canceled by user"); -+ } else { -+ LOG_NMP("native application start failed in session %s", -+ callbackData->sessionHandle.get()); -+ callbackData->promise->MaybeRejectWithNotFoundError( -+ "Native application start failed"); -+ } -+} -+ -+static gint GetFD(const RefPtr& result, GUnixFDList* fds, -+ gint index) { -+ RefPtr value = -+ dont_AddRef(g_variant_get_child_value(result, index)); -+ GUniquePtr error; -+ gint fd = g_unix_fd_list_get(fds, g_variant_get_handle(value), -+ getter_Transfers(error)); -+ if (fd == -1) { -+ LOG_NMP("failed to get file descriptor at index %d: %s", index, -+ error->message); -+ LogError("GetFD", *error); -+ } -+ return fd; -+} -+ -+/* static */ -+void NativeMessagingPortal::OnGetPipesDone(GObject* source, -+ GAsyncResult* result, -+ gpointer user_data) { -+ GDBusProxy* proxy = G_DBUS_PROXY(source); -+ UniquePtr callbackData(static_cast(user_data)); -+ auto promise = callbackData->promise; -+ -+ RefPtr fds; -+ GUniquePtr error; -+ RefPtr pipes = -+ dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish( -+ proxy, getter_AddRefs(fds), result, getter_Transfers(error))); -+ -+ if (!pipes) { -+ LOG_NMP( -+ "failed to get file descriptors for native application in session %s: " -+ "%s", -+ callbackData->sessionHandle.get(), error->message); -+ LogError(__func__, *error); -+ return RejectPromiseWithErrorMessage(*promise, *error); -+ } -+ -+ gint32 _stdin = GetFD(pipes, fds, 0); -+ gint32 _stdout = GetFD(pipes, fds, 1); -+ gint32 _stderr = GetFD(pipes, fds, 2); -+ LOG_NMP( -+ "got file descriptors for native application in session %s: (%d, %d, %d)", -+ callbackData->sessionHandle.get(), _stdin, _stdout, _stderr); -+ -+ if (_stdin == -1 || _stdout == -1 || _stderr == -1) { -+ return promise->MaybeRejectWithOperationError("Invalid file descriptor"); -+ } -+ -+ dom::AutoJSAPI jsapi; -+ if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { -+ return promise->MaybeRejectWithUnknownError( -+ "Failed to initialize JS context"); -+ } -+ JSContext* cx = jsapi.cx(); -+ -+ JS::Rooted jsPipes(cx, JS_NewPlainObject(cx)); -+ if (!jsPipes) { -+ return promise->MaybeRejectWithOperationError( -+ "Failed to create a JS object to hold the file descriptors"); -+ } -+ -+ auto setPipeProperty = [&](const char* name, int32_t value) { -+ JS::Rooted jsValue(cx, JS::Value::fromInt32(value)); -+ return JS_SetProperty(cx, jsPipes, name, jsValue); -+ }; -+ if (!setPipeProperty("stdin", _stdin)) { -+ return promise->MaybeRejectWithOperationError( -+ "Failed to set the 'stdin' property on the JS object"); -+ } -+ if (!setPipeProperty("stdout", _stdout)) { -+ return promise->MaybeRejectWithOperationError( -+ "Failed to set the 'stdout' property on the JS object"); -+ } -+ if (!setPipeProperty("stderr", _stderr)) { -+ return promise->MaybeRejectWithOperationError( -+ "Failed to set the 'stderr' property on the JS object"); -+ } -+ -+ promise->MaybeResolve(jsPipes); -+} -+ -+} // namespace mozilla::extensions -diff --git a/toolkit/components/extensions/NativeMessagingPortal.h b/toolkit/components/extensions/NativeMessagingPortal.h -new file mode 100644 -index 0000000000000..2a9998137a0d6 ---- /dev/null -+++ b/toolkit/components/extensions/NativeMessagingPortal.h -@@ -0,0 +1,76 @@ -+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -+/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -+/* This Source Code Form is subject to the terms of the Mozilla Public -+ * License, v. 2.0. If a copy of the MPL was not distributed with this -+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -+ -+#ifndef mozilla_extensions_NativeMessagingPortal_h -+#define mozilla_extensions_NativeMessagingPortal_h -+ -+#include "nsINativeMessagingPortal.h" -+ -+#include -+ -+#include "mozilla/GRefPtr.h" -+#include "mozilla/UniquePtr.h" -+ -+#include -+#include -+ -+namespace mozilla::extensions { -+ -+enum class SessionState { Active, Closing, Error }; -+ -+class NativeMessagingPortal : public nsINativeMessagingPortal { -+ public: -+ NS_DECL_NSINATIVEMESSAGINGPORTAL -+ NS_DECL_ISUPPORTS -+ -+ static already_AddRefed GetSingleton(); -+ -+ private: -+ NativeMessagingPortal(); -+ virtual ~NativeMessagingPortal(); -+ -+ RefPtr mProxy; -+ bool mInitialized = false; -+ RefPtr mCancellable; -+ -+ struct DelayedCall; -+ std::deque> mPending; -+ -+ using SessionsMap = std::unordered_map; -+ SessionsMap mSessions; -+ -+ // Callbacks -+ static void OnProxyReady(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ void MaybeDelayedIsAvailable(dom::Promise&, GVariant*); -+ void MaybeDelayedCreateSession(dom::Promise&, GVariant*); -+ static void OnCreateSessionDone(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ static void OnCloseSessionProxyReady(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ static void OnCloseSessionDone(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ static void OnSessionClosedSignal(GDBusConnection* bus, -+ const gchar* sender_name, -+ const gchar* object_path, -+ const gchar* interface_name, -+ const gchar* signal_name, -+ GVariant* parameters, gpointer user_data); -+ static void OnGetManifestDone(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ static void OnStartDone(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+ static void OnStartRequestResponseSignal( -+ GDBusConnection* bus, const gchar* sender_name, const gchar* object_path, -+ const gchar* interface_name, const gchar* signal_name, -+ GVariant* parameters, gpointer user_data); -+ static void OnGetPipesDone(GObject* source, GAsyncResult* result, -+ gpointer user_data); -+}; -+ -+} // namespace mozilla::extensions -+ -+#endif // mozilla_extensions_NativeMessagingPortal_h -diff --git a/toolkit/components/extensions/components.conf b/toolkit/components/extensions/components.conf -index 0b6461f13dd62..628adc096cc75 100644 ---- a/toolkit/components/extensions/components.conf -+++ b/toolkit/components/extensions/components.conf -@@ -14,3 +14,15 @@ Classes = [ - 'categories': {'app-startup': 'ExtensionsChild'}, - }, - ] -+ -+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'gtk' and defined('MOZ_ENABLE_DBUS'): -+ Classes += [ -+ { -+ 'cid': '{8a9a1406-d700-4221-8615-1d84b0d213fb}', -+ 'contract_ids': ['@mozilla.org/extensions/native-messaging-portal;1'], -+ 'singleton': True, -+ 'type': 'mozilla::extensions::NativeMessagingPortal', -+ 'constructor': 'mozilla::extensions::NativeMessagingPortal::GetSingleton', -+ 'headers': ['mozilla/extensions/NativeMessagingPortal.h'], -+ }, -+ ] -diff --git a/toolkit/components/extensions/docs/native-messaging-portal-design.rst b/toolkit/components/extensions/docs/native-messaging-portal-design.rst -new file mode 100644 -index 0000000000000..10305aff931a2 ---- /dev/null -+++ b/toolkit/components/extensions/docs/native-messaging-portal-design.rst -@@ -0,0 +1,46 @@ -+Native messaging for a strictly-confined Firefox -+================================================ -+ -+Rationale -+--------- -+ -+Firefox, when packaged as a snap or flatpak, is confined in a way that the browser only has a very partial view of the host filesystem and limited capabilities. -+Because of this, when an extension requests talking to a native application, the browser cannot locate the corresponding manifest and launch the application directly. -+Instead, it can use the `WebExtensions XDG desktop portal `_ (work in progress). The portal is responsible for mediating accesses to otherwise unavailable files on the host filesystem, prompting the user whether they want to allow a given extension to launch a given native application (and remembering the user's choice), and spawning the native application on behalf of the browser. -+The portal is browser-agnostic, although currently its only known use is in Firefox. -+ -+Workflow -+-------- -+ -+When Firefox detects that it is running strictly confined, and if the value of the ``widget.use-xdg-desktop-portal.native-messaging`` preference is ≠ ``0``, it queries the existence of the WebExtensions portal on the session bus. If the portal is not available, native messaging will not work (a generic error is reported). -+ -+If the portal is available, Firefox starts by creating a session (`CreateSession method `_). The resulting Session object will be used to communicate with the portal until it is closed (`Close method `_). -+ -+Firefox then calls `the GetManifest method `_ on the portal, and the portal looks up a host manifest matching the name of the native application and the extension ID, and returns the JSON manifest, which Firefox can use to do its own validation before pursuing. -+ -+Firefox then calls `the Start method `_ on the Session object, which creates and returns `a Request object `_. The portal asynchronously spawns the native application and emits `the Response signal `_ on the Request object. -+ -+Firefox then calls `the GetPipes method `_ on the portal, which returns open file descriptors for stdin, stdout and stderr of the spawned process. -+ -+From that point on, Firefox can talk to the native process exactly as it does when running unconfined (i.e. when it is responsible for launching the process itself). -+ -+Closing the session will have the portal terminate the native process cleanly. -+ -+From a end user's perspective, assuming the portal is present and in use, the only visible difference is going to be a one-time prompt for each extension requesting to launch a given native application. There is currently no GUI tool to edit the saved authorizations, but there is a CLI tool (``flatpak permissions webextensions``, whose name is confusing because it's not flatpak-specific). -+ -+Implementation details -+---------------------- -+ -+Some complexity that is specific to XDG desktop portals architecture is hidden away in the XPCOM interface used by Firefox to talk to the portal: the Request and Response objects aren't exposed (instead the relevant methods are asynchronous and return a Promise that resolves when the response has arrived), and the GetPipes method has been folded into the Start method. -+ -+A ``connectRunning()`` method was added to the ``Subprocess`` javascript module to wrap a process spawned externally. Interaction with a ``Process`` object created this way is limited to communication through its open file descriptors, the caller cannot kill or wait on the process. -+ -+Extensions with the "nativeMessaging" permission should know nothing about the underlying mechanism used to talk to native applications, so it is important that the errors thrown in this separate code path aren't distinguishable from the generic errors thrown in the "legacy" code path where the browser is responsible for managing the lifecycle of the native applications itself. -+ -+Future work -+----------- -+ -+The WebExtensions portal isn't widely available yet in a release of the XDG desktop portals project, however an agreement in principle was reached with its maintainers, pending minor changes to the current implementation, and the goal is to land it with the next stable release, 1.18. -+In the meantime, the portal has been available in Ubuntu `as a distro patch `_ starting with release 22.04. -+ -+The functionality is exercised with XPCShell tests that mock the portal's DBus interface. There are currently no integration tests that exercise the real portal. -diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build -index cf426e7283b99..d34ed784f22d3 100644 ---- a/toolkit/components/extensions/moz.build -+++ b/toolkit/components/extensions/moz.build -@@ -77,6 +77,7 @@ XPIDL_SOURCES += [ - "extIWebNavigation.idl", - "mozIExtensionAPIRequestHandling.idl", - "mozIExtensionProcessScript.idl", -+ "nsINativeMessagingPortal.idl", - ] - - XPIDL_MODULE = "webextensions" -@@ -103,6 +104,13 @@ UNIFIED_SOURCES += [ - "WebExtensionPolicy.cpp", - ] - -+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk" and CONFIG["MOZ_ENABLE_DBUS"]: -+ EXPORTS.mozilla.extensions += ["NativeMessagingPortal.h"] -+ UNIFIED_SOURCES += ["NativeMessagingPortal.cpp"] -+ CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] -+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] -+ DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] -+ - XPCOM_MANIFESTS += [ - "components.conf", - ] -diff --git a/toolkit/components/extensions/nsINativeMessagingPortal.idl b/toolkit/components/extensions/nsINativeMessagingPortal.idl -new file mode 100644 -index 0000000000000..42dc7e96bd983 ---- /dev/null -+++ b/toolkit/components/extensions/nsINativeMessagingPortal.idl -@@ -0,0 +1,86 @@ -+/* This Source Code Form is subject to the terms of the Mozilla Public -+ * License, v. 2.0. If a copy of the MPL was not distributed with this -+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -+ -+#include "nsISupports.idl" -+ -+/** -+ * An interface to talk to the WebExtensions XDG desktop portal, -+ * for sandboxed browsers (e.g. packaged as a snap or a flatpak). -+ * See https://github.com/flatpak/xdg-desktop-portal/issues/655. -+ */ -+[scriptable, builtinclass, uuid(7c3003e8-6d10-46cc-b754-70cd889871e7)] -+interface nsINativeMessagingPortal : nsISupports -+{ -+ /** -+ * Whether client code should use the portal, or fall back to the "legacy" -+ * implementation that spawns and communicates directly with native -+ * applications. -+ */ -+ boolean shouldUse(); -+ -+ /** -+ * Whether the portal is available and can be talked to. It is an error to -+ * call other methods in this interface if the portal isn't available. -+ * -+ * @returns Promise that resolves with a boolean that reflects -+ the availability of the portal. -+ */ -+ [implicit_jscontext] -+ readonly attribute Promise available; -+ -+ /** -+ * Create a native messaging session. -+ * -+ * @param aApplication The name of the native application which the portal is -+ * being requested to talk to. See -+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests. -+ * -+ * @returns Promise that resolves with a string that represents the -+ session handle (a D-Bus object path of the form -+ /org/freedesktop/portal/desktop/session/SENDER/TOKEN). -+ */ -+ [implicit_jscontext] -+ Promise createSession(in ACString aApplication); -+ -+ /** -+ * Close a previously open session. -+ * -+ * @param aHandle The handle of a valid session. -+ * -+ * @returns Promise that resolves when the session is successfully closed. -+ */ -+ [implicit_jscontext] -+ Promise closeSession(in ACString aHandle); -+ -+ /** -+ * Find and return the JSON manifest for the named native messaging server -+ * as a string. This allows the browser to validate the manifest before -+ * deciding to start the server. -+ * -+ * @param aHandle The handle of a valid session. -+ * @param aName The name of the native messaging server to start. -+ * @param aExtension The ID of the extension that issues the request. -+ * -+ * @returns Promise that resolves with an UTF8-encoded string containing -+ the raw JSON manifest. -+ */ -+ [implicit_jscontext] -+ Promise getManifest(in ACString aHandle, in ACString aName, in ACString aExtension); -+ -+ /** -+ * Start the named native messaging server, in a previously open session. -+ * The caller must indicate the requesting web extension (by extension ID). -+ * -+ * @param aHandle The handle of a valid session. -+ * @param aName The name of the native messaging server to start. -+ * @param aExtension The ID of the extension that issues the request. -+ * -+ * @returns Promise that resolves with an object that has 'stdin', 'stdout' -+ and 'stderr' attributes for the open file descriptors that the -+ caller can use to communicate with the native application once -+ successfully started. -+ */ -+ [implicit_jscontext] -+ Promise start(in ACString aHandle, in ACString aName, in ACString aExtension); -+}; -diff --git a/toolkit/components/extensions/test/xpcshell/native_messaging.toml b/toolkit/components/extensions/test/xpcshell/native_messaging.toml -index 2b6eabe5c9491..15d1aa6a1fb55 100644 ---- a/toolkit/components/extensions/test/xpcshell/native_messaging.toml -+++ b/toolkit/components/extensions/test/xpcshell/native_messaging.toml -@@ -16,4 +16,8 @@ run-sequentially = "very high failure rate in parallel" - ["test_ext_native_messaging_perf.js"] - skip-if = ["tsan"] # Unreasonably slow, bug 1612707 - -+["test_ext_native_messaging_portal.js"] -+run-if = ["os == 'linux' && toolkit == 'gtk' && dbus_enabled"] -+tags = "portal" -+ - ["test_ext_native_messaging_unresponsive.js"] -diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js -new file mode 100644 -index 0000000000000..610a83f2aeb71 ---- /dev/null -+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js -@@ -0,0 +1,397 @@ -+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -+/* vim: set sts=2 sw=2 et tw=80: */ -+"use strict"; -+ -+const lazy = {}; -+ -+ChromeUtils.defineESModuleGetters(lazy, { -+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs", -+}); -+ -+AddonTestUtils.init(this); -+AddonTestUtils.overrideCertDB(); -+AddonTestUtils.createAppInfo( -+ "xpcshell@tests.mozilla.org", -+ "XPCShell", -+ "1", -+ "42" -+); -+ -+// Helpful documentation on the WebExtensions portal that is being tested here: -+// - feature request: https://github.com/flatpak/xdg-desktop-portal/issues/655 -+// - pull request: https://github.com/flatpak/xdg-desktop-portal/pull/705 -+// - D-Bus API: https://github.com/jhenstridge/xdg-desktop-portal/blob/native-messaging-portal/data/org.freedesktop.portal.WebExtensions.xml -+ -+const SESSION_HANDLE = -+ "/org/freedesktop/portal/desktop/session/foobar/firefox_xpcshell_tests_mozilla_org_42"; -+ -+const portalBusName = "org.freedesktop.portal.Desktop"; -+const portalObjectPath = "/org/freedesktop/portal/desktop"; -+const portalInterfaceName = "org.freedesktop.portal.WebExtensions"; -+const sessionInterfaceName = "org.freedesktop.portal.Session"; -+const dbusMockInterface = "org.freedesktop.DBus.Mock"; -+const addObjectMethod = `${dbusMockInterface}.AddObject`; -+const addMethodMethod = `${dbusMockInterface}.AddMethod`; -+const addPropertyMethod = `${dbusMockInterface}.AddProperty`; -+const updatePropertiesMethod = `${dbusMockInterface}.UpdateProperties`; -+const emitSignalDetailedMethod = `${dbusMockInterface}.EmitSignalDetailed`; -+const getCallsMethod = `${dbusMockInterface}.GetCalls`; -+const clearCallsMethod = `${dbusMockInterface}.ClearCalls`; -+const resetMethod = `${dbusMockInterface}.Reset`; -+const mockRequestObjectPath = "/org/freedesktop/portal/desktop/request"; -+const mockManifest = -+ '{"name":"echo","description":"a native connector","type":"stdio","path":"/usr/bin/echo","allowed_extensions":["native@tests.mozilla.org"]}'; -+const nativeMessagingPref = "widget.use-xdg-desktop-portal.native-messaging"; -+ -+var DBUS_SESSION_BUS_ADDRESS = ""; -+var DBUS_SESSION_BUS_PID = 0; // eslint-disable-line no-unused-vars -+var DBUS_MOCK = null; -+var FDS_MOCK = null; -+ -+async function background() { -+ let port; -+ browser.test.onMessage.addListener(async (what, payload) => { -+ if (what == "request") { -+ await browser.permissions.request({ permissions: ["nativeMessaging"] }); -+ // connectNative requires permission -+ port = browser.runtime.connectNative("echo"); -+ port.onMessage.addListener(msg => { -+ browser.test.sendMessage("message", msg); -+ }); -+ browser.test.sendMessage("ready"); -+ } else if (what == "send") { -+ if (payload._json) { -+ let json = payload._json; -+ payload.toJSON = () => json; -+ delete payload._json; -+ } -+ port.postMessage(payload); -+ } -+ }); -+} -+ -+async function mockSetup(objectPath, methodName, args) { -+ let mockProcess = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("gdbus"), -+ arguments: [ -+ "call", -+ "--session", -+ "-d", -+ portalBusName, -+ "-o", -+ objectPath, -+ "-m", -+ methodName, -+ ...args, -+ ], -+ }); -+ return mockProcess.wait(); -+} -+ -+add_setup(async function () { -+ // Start and use a separate message bus for the tests, to not interfere with -+ // the current's session message bus. -+ let dbus = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("dbus-launch"), -+ }); -+ let stdout = await dbus.stdout.readString(); -+ let lines = stdout.split("\n"); -+ for (let i in lines) { -+ let tokens = lines[i].split("="); -+ switch (tokens.shift()) { -+ case "DBUS_SESSION_BUS_ADDRESS": -+ DBUS_SESSION_BUS_ADDRESS = tokens.join("="); -+ break; -+ case "DBUS_SESSION_BUS_PID": -+ DBUS_SESSION_BUS_PID = tokens.join(); -+ break; -+ default: -+ } -+ } -+ -+ let prefValue = Services.prefs.getIntPref(nativeMessagingPref, 0); -+ Services.prefs.setIntPref(nativeMessagingPref, 2); -+ -+ Services.env.set("DBUS_SESSION_BUS_ADDRESS", DBUS_SESSION_BUS_ADDRESS); -+ Services.env.set("GTK_USE_PORTAL", "1"); -+ -+ // dbusmock is used to mock the native messaging portal's D-Bus API. -+ DBUS_MOCK = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("python3"), -+ arguments: [ -+ "-m", -+ "dbusmock", -+ portalBusName, -+ portalObjectPath, -+ portalInterfaceName, -+ ], -+ }); -+ -+ // When talking to the native messaging portal over D-Bus, it returns a tuple -+ // of file descriptors. For the mock to work correctly, the file descriptors -+ // must exist, so create a dummy process in order to use its stdin, stdout -+ // and stderr file descriptors. -+ FDS_MOCK = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("tail"), -+ arguments: ["-f", "/dev/null"], -+ stderr: "pipe", -+ }); -+ -+ registerCleanupFunction(async function () { -+ await FDS_MOCK.kill(); -+ await mockSetup(portalObjectPath, resetMethod, []); -+ await DBUS_MOCK.kill(); -+ // XXX: While this works locally, it consistently fails when tests are run -+ // in CI, with "xpcshell return code: -15". This needs to be investigated -+ // further. This leaves a stray dbus-daemon process behind, -+ // which isn't ideal, but is harmless. -+ /*await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("kill"), -+ arguments: ["-SIGQUIT", DBUS_SESSION_BUS_PID], -+ });*/ -+ Services.prefs.setIntPref(nativeMessagingPref, prefValue); -+ }); -+ -+ // Set up the mock objects and methods. -+ await mockSetup(portalObjectPath, addPropertyMethod, [ -+ portalInterfaceName, -+ "version", -+ "", -+ ]); -+ await mockSetup(portalObjectPath, addMethodMethod, [ -+ portalInterfaceName, -+ "CreateSession", -+ "a{sv}", -+ "o", -+ `ret = "${SESSION_HANDLE}"`, -+ ]); -+ await mockSetup(portalObjectPath, addObjectMethod, [ -+ SESSION_HANDLE, -+ sessionInterfaceName, -+ "@a{sv} {}", -+ "@a(ssss) [('Close', '', '', '')]", -+ ]); -+ await mockSetup(portalObjectPath, addMethodMethod, [ -+ portalInterfaceName, -+ "GetManifest", -+ "oss", -+ "s", -+ `ret = '${mockManifest}'`, -+ ]); -+ await mockSetup(portalObjectPath, addMethodMethod, [ -+ portalInterfaceName, -+ "Start", -+ "ossa{sv}", -+ "o", -+ `ret = "${mockRequestObjectPath}/foobar"`, -+ ]); -+ await mockSetup(portalObjectPath, addMethodMethod, [ -+ portalInterfaceName, -+ "GetPipes", -+ "oa{sv}", -+ "hhh", -+ `ret = (dbus.types.UnixFd(${FDS_MOCK.stdin.fd}), dbus.types.UnixFd(${FDS_MOCK.stdout.fd}), dbus.types.UnixFd(${FDS_MOCK.stderr.fd}))`, -+ ]); -+ -+ optionalPermissionsPromptHandler.init(); -+ optionalPermissionsPromptHandler.acceptPrompt = true; -+ await AddonTestUtils.promiseStartupManager(); -+ await setupHosts([]); // these tests don't use any native app script -+}); -+ -+async function verifyDbusMockCall(objectPath, method, offset) { -+ let getCalls = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("gdbus"), -+ arguments: [ -+ "call", -+ "--session", -+ "-d", -+ portalBusName, -+ "-o", -+ objectPath, -+ "-m", -+ getCallsMethod, -+ ], -+ }); -+ let out = await getCalls.stdout.readString(); -+ out = out.match(/\((@a\(tsav\) )?\[(.*)\],\)/)[2]; -+ let calls = out.matchAll(/\(.*?\),?/g); -+ let methodCalled = false; -+ let params = {}; -+ let i = 0; -+ for (let call of calls) { -+ if (i++ < offset) { -+ continue; -+ } -+ let matches = call[0].match( -+ /\((uint64 )?(?\d+), '(?\w+)', (@av )?\[(?.*)\]\),?/ -+ ); -+ ok(parseFloat(matches.groups.timestamp), "timestamp is valid"); -+ if (matches.groups.method == method) { -+ methodCalled = true; -+ params = matches.groups.params; -+ break; -+ } -+ } -+ if (method) { -+ ok(methodCalled, `The ${method} mock was called`); -+ } else { -+ equal(i, 0, "No method mock was called"); -+ } -+ return { offset: i, params: params }; -+} -+ -+add_task(async function test_talk_to_portal() { -+ await mockSetup(portalObjectPath, clearCallsMethod, []); -+ -+ // Make sure the portal is considered available -+ await mockSetup(portalObjectPath, updatePropertiesMethod, [ -+ portalInterfaceName, -+ "{'version': }", -+ ]); -+ -+ // dbusmock's logging output doesn't reveal the sender name, -+ // so run dbus-monitor in parallel. The sender name is needed to build the -+ // object path of the Request that is returned by the portal's Start method. -+ let dbusMonitor = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("dbus-monitor"), -+ arguments: [ -+ "--session", -+ `interface='${portalInterfaceName}', member='CreateSession'`, -+ ], -+ }); -+ -+ let extension = ExtensionTestUtils.loadExtension({ -+ background, -+ manifest: { -+ applications: { gecko: { id: ID } }, -+ optional_permissions: ["nativeMessaging"], -+ }, -+ useAddonManager: "temporary", -+ }); -+ -+ await extension.startup(); -+ await withHandlingUserInput(extension, async () => { -+ extension.sendMessage("request"); -+ await extension.awaitMessage("ready"); -+ }); -+ -+ let handleToken = ""; -+ let senderName = ""; -+ -+ // Verify that starting the extension talks to the mock native messaging -+ // portal (i.e. CreateSession and Start are called with the expected -+ // arguments). -+ let result = await verifyDbusMockCall(portalObjectPath, "CreateSession", 0); -+ result = await verifyDbusMockCall( -+ portalObjectPath, -+ "GetManifest", -+ result.offset -+ ); -+ result = await verifyDbusMockCall(portalObjectPath, "Start", result.offset); -+ let match = result.params.match(/{'handle_token': <'(?.*)'>}/); -+ ok(match, "Start arguments contain a handle token"); -+ handleToken = match.groups.token; -+ -+ // Extract the sender name from the dbus-monitor process's output. -+ let dbusMonitorOutput = await dbusMonitor.stdout.readString(); -+ let lines = dbusMonitorOutput.split("\n"); -+ for (let i in lines) { -+ let line = lines[i]; -+ if (!line) { -+ continue; -+ } -+ if (line.startsWith("method call")) { -+ let match = line.match(/sender=(\S*)/); -+ ok(match, "dbus-monitor output informs us of the sender"); -+ senderName = match[1]; -+ } -+ } -+ ok(senderName, "Got the sender name"); -+ await dbusMonitor.kill(); -+ -+ // Mock the Request object that is expected to be created in response to -+ // calling the Start method on the native messaging portal, wait for it to be -+ // available, and emit its Response signal. -+ let requestPath = `${mockRequestObjectPath}/${senderName -+ .slice(1) -+ .replace(".", "_")}/${handleToken}`; -+ await mockSetup(portalObjectPath, addObjectMethod, [ -+ requestPath, -+ "org.freedesktop.portal.Request", -+ "@a{sv} {}", -+ "@a(ssss) []", -+ ]); -+ let waitForRequestObject = await lazy.Subprocess.call({ -+ command: await lazy.Subprocess.pathSearch("gdbus"), -+ arguments: [ -+ "introspect", -+ "--session", -+ "-d", -+ portalBusName, -+ "-o", -+ requestPath, -+ "-p", -+ ], -+ }); -+ await waitForRequestObject.wait(); -+ await mockSetup(requestPath, emitSignalDetailedMethod, [ -+ "org.freedesktop.portal.Request", -+ "Response", -+ "ua{sv}", -+ "[, <@a{sv} {}>]", -+ `{'destination': <'${senderName}'>}`, -+ ]); -+ -+ // Verify that the GetPipes method of the native messaging portal mock was -+ // called as expected after the Start request completed. -+ await verifyDbusMockCall(portalObjectPath, "GetPipes", result.offset); -+ -+ await extension.unload(); -+ -+ // Verify that the native messaging portal session is properly closed when -+ // the extension is unloaded. -+ await verifyDbusMockCall(SESSION_HANDLE, "Close", 0); -+}); -+ -+add_task(async function test_portal_unavailable() { -+ await mockSetup(portalObjectPath, clearCallsMethod, []); -+ -+ // Make sure the portal is NOT considered available -+ await mockSetup(portalObjectPath, updatePropertiesMethod, [ -+ portalInterfaceName, -+ "{'version': }", -+ ]); -+ -+ let extension = ExtensionTestUtils.loadExtension({ -+ background, -+ manifest: { -+ applications: { gecko: { id: ID } }, -+ optional_permissions: ["nativeMessaging"], -+ }, -+ useAddonManager: "temporary", -+ }); -+ -+ let logged = false; -+ function listener(msg) { -+ logged ||= /Native messaging portal is not available/.test(msg.message); -+ } -+ Services.console.registerListener(listener); -+ registerCleanupFunction(() => { -+ Services.console.unregisterListener(listener); -+ }); -+ -+ await extension.startup(); -+ await withHandlingUserInput(extension, async () => { -+ extension.sendMessage("request"); -+ await extension.awaitMessage("ready"); -+ }); -+ -+ ok(logged, "Non availability of the portal was logged"); -+ -+ // Verify that the native messaging portal wasn't talked to, -+ // because it advertised itself as not available. -+ await verifyDbusMockCall(portalObjectPath, null, 0); -+ -+ await extension.unload(); -+}); -diff --git a/toolkit/components/extensions/test/xpcshell/test_native_manifests.js b/toolkit/components/extensions/test/xpcshell/test_native_manifests.js -index d4f3ae7243f24..8b5c11a39fca9 100644 ---- a/toolkit/components/extensions/test/xpcshell/test_native_manifests.js -+++ b/toolkit/components/extensions/test/xpcshell/test_native_manifests.js -@@ -150,6 +150,100 @@ function lookupApplication(app, ctx) { - return NativeManifests.lookupManifest("stdio", app, ctx); - } - -+add_task(async function test_parse_good_manifest() { -+ let manifest = await NativeManifests.parseManifest( -+ "stdio", -+ "/some/path", -+ "test", -+ context, -+ templateManifest -+ ); -+ deepEqual( -+ manifest, -+ templateManifest, -+ "parseManifest returns the manifest contents" -+ ); -+}); -+ -+add_task(async function test_parse_invalid_manifest() { -+ function matchLastConsoleMessage(regex) { -+ ok(Services.console.getMessageArray().pop().message.match(regex)); -+ } -+ -+ equal( -+ null, -+ await NativeManifests.parseManifest( -+ "pkcs11", -+ "/some/path", -+ "test", -+ context, -+ templateManifest -+ ) -+ ); -+ matchLastConsoleMessage( -+ /Native manifest \/some\/path has type property stdio \(expected pkcs11\)/ -+ ); -+ -+ equal( -+ null, -+ await NativeManifests.parseManifest( -+ "stdio", -+ "/some/path", -+ "foobar", -+ context, -+ templateManifest -+ ) -+ ); -+ matchLastConsoleMessage( -+ /Native manifest \/some\/path has name property test \(expected foobar\)/ -+ ); -+ -+ const incompleteManifest = { ...templateManifest }; -+ delete incompleteManifest.description; -+ equal( -+ null, -+ await NativeManifests.parseManifest( -+ "stdio", -+ "/some/path", -+ "test", -+ context, -+ incompleteManifest -+ ) -+ ); -+ matchLastConsoleMessage(/Value must either: match the pattern/); -+ -+ const unauthorizedManifest = { ...templateManifest }; -+ unauthorizedManifest.allowed_extensions = []; -+ equal( -+ null, -+ await NativeManifests.parseManifest( -+ "stdio", -+ "/some/path", -+ "test", -+ context, -+ unauthorizedManifest -+ ) -+ ); -+ matchLastConsoleMessage( -+ /Value must either: .allowed_extensions must have at least 1 items/ -+ ); -+ -+ unauthorizedManifest.allowed_extensions = ["unauthorized@tests.mozilla.org"]; -+ equal( -+ null, -+ await NativeManifests.parseManifest( -+ "stdio", -+ "/some/path", -+ "test", -+ context, -+ unauthorizedManifest -+ ) -+ ); -+ matchLastConsoleMessage( -+ /This extension does not have permission to use native manifest \/some\/path/ -+ ); -+}); -+ - add_task(async function test_nonexistent_manifest() { - let result = await lookupApplication("test", context); - equal( -diff --git a/toolkit/modules/subprocess/Subprocess.sys.mjs b/toolkit/modules/subprocess/Subprocess.sys.mjs -index ffbeb0acbb56f..07a1da12aac7a 100644 ---- a/toolkit/modules/subprocess/Subprocess.sys.mjs -+++ b/toolkit/modules/subprocess/Subprocess.sys.mjs -@@ -188,6 +188,19 @@ export var Subprocess = { - let path = lazy.SubprocessImpl.pathSearch(command, environment); - return Promise.resolve(path); - }, -+ -+ /** -+ * Connect to an already-running subprocess -+ * given the file descriptors for its stdin, stdout and stderr. -+ * -+ * @param {int[]} [fds] -+ * A list of three file descriptors [stdin, stdout, stderr]. -+ * -+ * @returns {Process} -+ */ -+ connectRunning(fds) { -+ return lazy.SubprocessImpl.connectRunning(fds); -+ }, - }; - - Object.assign(Subprocess, SubprocessConstants); -diff --git a/toolkit/modules/subprocess/subprocess_common.sys.mjs b/toolkit/modules/subprocess/subprocess_common.sys.mjs -index aa3a28fb7b1fd..1a863697c62a4 100644 ---- a/toolkit/modules/subprocess/subprocess_common.sys.mjs -+++ b/toolkit/modules/subprocess/subprocess_common.sys.mjs -@@ -581,13 +581,22 @@ export class BaseProcess { - */ - this.pid = pid; - -+ /** -+ * @property {boolean} managed -+ * Whether the process is externally managed, or spawned by us. -+ * @readonly -+ */ -+ this.managed = pid == 0; -+ - this.exitCode = null; - - this.exitPromise = new Promise(resolve => { -- this.worker.call("wait", [this.id]).then(({ exitCode }) => { -- resolve(Object.freeze({ exitCode })); -- this.exitCode = exitCode; -- }); -+ if (!this.managed) { -+ this.worker.call("wait", [this.id]).then(({ exitCode }) => { -+ resolve(Object.freeze({ exitCode })); -+ this.exitCode = exitCode; -+ }); -+ } - }); - - if (fds[0] !== undefined) { -@@ -635,6 +644,14 @@ export class BaseProcess { - }); - } - -+ static fromRunning(fds) { -+ let worker = this.getWorker(); -+ -+ return worker.call("connectRunning", [fds]).then(({ processId, fds }) => { -+ return new this(worker, processId, fds, 0); -+ }); -+ } -+ - static get WORKER_URL() { - throw new Error("Not implemented"); - } -@@ -672,6 +689,10 @@ export class BaseProcess { - * has exited. - */ - kill(timeout = 300) { -+ if (this.managed) { -+ throw new Error("Cannot kill a process managed externally"); -+ } -+ - // If the process has already exited, don't bother sending a signal. - if (this.exitCode != null) { - return this.wait(); -@@ -706,6 +727,9 @@ export class BaseProcess { - * method. - */ - wait() { -+ if (this.managed) { -+ throw new Error("Cannot wait on a process managed externally"); -+ } - return this.exitPromise; - } - } -diff --git a/toolkit/modules/subprocess/subprocess_unix.sys.mjs b/toolkit/modules/subprocess/subprocess_unix.sys.mjs -index 59b5873af2c80..9496dc795366f 100644 ---- a/toolkit/modules/subprocess/subprocess_unix.sys.mjs -+++ b/toolkit/modules/subprocess/subprocess_unix.sys.mjs -@@ -198,6 +198,10 @@ var SubprocessUnix = { - error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE; - throw error; - }, -+ -+ connectRunning(fds) { -+ return Process.fromRunning(fds); -+ }, - }; - - export var SubprocessImpl = SubprocessUnix; -diff --git a/toolkit/modules/subprocess/subprocess_unix.worker.js b/toolkit/modules/subprocess/subprocess_unix.worker.js -index 85632d239824b..cf2de29a4cfc9 100644 ---- a/toolkit/modules/subprocess/subprocess_unix.worker.js -+++ b/toolkit/modules/subprocess/subprocess_unix.worker.js -@@ -405,6 +405,22 @@ class Process extends BaseProcess { - } - } - -+ /** -+ * Connect to an already running process that was spawned externally. -+ * -+ * @param {object} options -+ An object with a 'fds' attribute that's an array -+ of file descriptors (stdin, stdout and stderr). -+ */ -+ connectRunning(options) { -+ this.pid = 0; -+ this.pipes = []; -+ this.pipes.push(new OutputPipe(this, unix.Fd(options.fds[0]))); -+ this.pipes.push(new InputPipe(this, unix.Fd(options.fds[1]))); -+ this.pipes.push(new InputPipe(this, unix.Fd(options.fds[2]))); -+ // Not creating a poll fd here, because this process is managed externally. -+ } -+ - /** - * Called when input is available on our sentinel file descriptor. - * -@@ -457,7 +473,9 @@ class Process extends BaseProcess { - this.exitCode = unix.WEXITSTATUS(status.value); - } - -- this.fd.dispose(); -+ if (this.fd !== undefined) { -+ this.fd.dispose(); -+ } - io.updatePollFds(); - this.resolveExit(this.exitCode); - return this.exitCode; -@@ -521,7 +539,9 @@ io = { - let handlers = [ - this.signal, - ...this.pipes.values(), -- ...this.processes.values(), -+ // Filter out processes without a poll fd, because those are managed -+ // externally, not spawned by us. -+ ...Array.from(this.processes.values()).filter(p => p.fd !== undefined), - ]; - - handlers = handlers.filter(handler => handler.pollEvents); -diff --git a/toolkit/modules/subprocess/subprocess_win.sys.mjs b/toolkit/modules/subprocess/subprocess_win.sys.mjs -index baf86402357d5..7a3338274b571 100644 ---- a/toolkit/modules/subprocess/subprocess_win.sys.mjs -+++ b/toolkit/modules/subprocess/subprocess_win.sys.mjs -@@ -168,6 +168,12 @@ var SubprocessWin = { - error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE; - throw error; - }, -+ -+ connectRunning(_fds) { -+ // Not relevant (yet?) on Windows. This is currently used only on Unix -+ // for native messaging through the WebExtensions portal. -+ throw new Error("Not implemented"); -+ }, - }; - - export var SubprocessImpl = SubprocessWin; -diff --git a/toolkit/modules/subprocess/subprocess_win.worker.js b/toolkit/modules/subprocess/subprocess_win.worker.js -index 22d3857f8cf39..b4e431b7722df 100644 ---- a/toolkit/modules/subprocess/subprocess_win.worker.js -+++ b/toolkit/modules/subprocess/subprocess_win.worker.js -@@ -601,6 +601,12 @@ class Process extends BaseProcess { - libc.CloseHandle(procInfo.hThread); - } - -+ connectRunning(_options) { -+ // Not relevant (yet?) on Windows. This is currently used only on Unix -+ // for native messaging through the WebExtensions portal. -+ throw new Error("Not implemented"); -+ } -+ - /** - * Called when our process handle is signaled as active, meaning the process - * has exited. -diff --git a/toolkit/modules/subprocess/subprocess_worker_common.js b/toolkit/modules/subprocess/subprocess_worker_common.js -index b22480c0dd304..f8197773bc1a7 100644 ---- a/toolkit/modules/subprocess/subprocess_worker_common.js -+++ b/toolkit/modules/subprocess/subprocess_worker_common.js -@@ -59,7 +59,11 @@ class BaseProcess { - this.pid = null; - this.pipes = []; - -- this.spawn(options); -+ if (options.managed) { -+ this.connectRunning(options); -+ } else { -+ this.spawn(options); -+ } - } - - /** -@@ -96,6 +100,7 @@ let requests = { - }, - - spawn(options) { -+ options.managed = false; - let process = new Process(options); - let processId = process.id; - -@@ -106,6 +111,18 @@ let requests = { - return { data: { processId, fds, pid: process.pid } }; - }, - -+ connectRunning(fds) { -+ let options = {}; -+ options.managed = true; -+ options.fds = fds; -+ let process = new Process(options); -+ let processId = process.id; -+ -+ io.addProcess(process); -+ -+ return { data: { processId, fds: process.pipes.map(pipe => pipe.id) } }; -+ }, -+ - kill(processId, force = false) { - let process = io.getProcess(processId); - -@@ -162,6 +179,18 @@ let requests = { - Array.from(io.processes.values(), proc => proc.awaitFinished()) - ); - }, -+ -+ getFds(processId) { -+ let process = io.getProcess(processId); -+ let pipes = process.pipes; -+ return { -+ data: [ -+ pipes[0].fd.toString(), -+ pipes[1].fd.toString(), -+ pipes[2].fd.toString(), -+ ], -+ }; -+ }, - }; - - onmessage = event => { -diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js -index 51c9956d0d914..1a55cd9eec7c4 100644 ---- a/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js -+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js -@@ -855,6 +855,83 @@ add_task(async function test_bad_executable() { - ); - }); - -+add_task(async function test_subprocess_connectRunning() { -+ if (AppConstants.platform != "win") { -+ let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); -+ tempFile.append("test-subprocess-connectRunning.txt"); -+ if (tempFile.exists()) { -+ tempFile.remove(true); -+ } -+ registerCleanupFunction(async function () { -+ tempFile.remove(true); -+ }); -+ -+ let running = await Subprocess.call({ -+ command: await Subprocess.pathSearch("tee"), -+ arguments: [tempFile.path], -+ environment: {}, -+ stderr: "pipe", -+ }); -+ equal( -+ running.managed, -+ false, -+ "A process spawned by us is not externally managed" -+ ); -+ let { getSubprocessImplForTest } = ChromeUtils.importESModule( -+ "resource://gre/modules/Subprocess.sys.mjs" -+ ); -+ let worker = getSubprocessImplForTest().Process.getWorker(); -+ let fds = await worker.call("getFds", [running.id]); -+ let proc = await Subprocess.connectRunning(fds); -+ greater(proc.id, 0, "Already running process id is valid"); -+ equal(proc.pid, 0, "Already running process pid is 0"); -+ equal( -+ proc.managed, -+ true, -+ "A process not spawned by us is externally managed" -+ ); -+ Assert.throws( -+ () => proc.wait(), -+ /Cannot wait/, -+ "A process externally managed cannot be waited on" -+ ); -+ Assert.throws( -+ () => proc.kill(), -+ /Cannot kill/, -+ "A process externally managed cannot be killed" -+ ); -+ [proc.stdin, proc.stdout, proc.stderr].forEach((pipe, _i) => -+ greater( -+ pipe.id, -+ 0, -+ "File descriptor (stdin) for already running process is valid" -+ ) -+ ); -+ let contents = "lorem ipsum"; -+ let writeOp = proc.stdin.write(contents); -+ equal( -+ (await writeOp).bytesWritten, -+ contents.length, -+ "Contents correctly written to stdin" -+ ); -+ let readOp = running.stdout.readString(contents.length); -+ equal(await readOp, contents, "Pipes communication is functional"); -+ await running.kill(); -+ -+ ok(tempFile.exists(), "temp file was written to"); -+ equal( -+ await IOUtils.readUTF8(tempFile.path), -+ contents, -+ "Contents correctly written to temp file" -+ ); -+ } else { -+ Assert.throws( -+ () => Subprocess.connectRunning([42, 58, 63]), -+ /Not implemented/ -+ ); -+ } -+}); -+ - add_task(async function test_cleanup() { - let { getSubprocessImplForTest } = ChromeUtils.importESModule( - "resource://gre/modules/Subprocess.sys.mjs" -diff --git a/widget/gtk/WidgetUtilsGtk.cpp b/widget/gtk/WidgetUtilsGtk.cpp -index 0d2425b3d0d9a..52b6ce899c15b 100644 ---- a/widget/gtk/WidgetUtilsGtk.cpp -+++ b/widget/gtk/WidgetUtilsGtk.cpp -@@ -214,6 +214,8 @@ bool ShouldUsePortal(PortalKind aPortalKind) { - // Mime portal breaks default browser handling, see bug 1516290. - autoBehavior = IsRunningUnderFlatpakOrSnap(); - return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler(); -+ case PortalKind::NativeMessaging: -+ return StaticPrefs::widget_use_xdg_desktop_portal_native_messaging(); - case PortalKind::Settings: - autoBehavior = true; - return StaticPrefs::widget_use_xdg_desktop_portal_settings(); -diff --git a/widget/gtk/WidgetUtilsGtk.h b/widget/gtk/WidgetUtilsGtk.h -index 5cf6604b3c11d..8d6f1d67279e5 100644 ---- a/widget/gtk/WidgetUtilsGtk.h -+++ b/widget/gtk/WidgetUtilsGtk.h -@@ -53,6 +53,7 @@ inline bool IsRunningUnderFlatpakOrSnap() { - enum class PortalKind { - FilePicker, - MimeHandler, -+ NativeMessaging, - Settings, - Location, - OpenUri, --- -2.45.2 + mirror: always + # Whether to try to use XDG portal for settings / look-and-feel information.