Bug 1661935 - Integration with a new WebExtensions XDG desktop portal for native messaging on Linux r=emilio,robwu,rpl,andi,mach-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D140803
This commit is contained in:
Amin Bandali
2024-12-07 06:57:29 +00:00
parent fb7afe8d4a
commit e90ed03226
10 changed files with 1034 additions and 25 deletions

View File

@@ -17709,6 +17709,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
value: 0
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

View File

@@ -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`);

View File

@@ -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,22 @@ 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?.kill();
this.proc = null;
return;
}
if (!this.proc) {
// Failed to initialize proc in the constructor.
return;

View File

@@ -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 <gio/gunixfdlist.h>
#include <glib.h>
#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 <string.h>
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> NativeMessagingPortal::GetSingleton() {
static StaticRefPtr<NativeMessagingPortal> 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<dom::Promise>& 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<dom::Promise> promise;
GUniquePtr<gchar> 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<GError> error;
RefPtr<GDBusProxy> 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<GVariant> 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<dom::Promise> promise;
RefPtr<GVariant> args;
};
/* static */
void NativeMessagingPortal::OnProxyReady(GObject* source, GAsyncResult* result,
gpointer user_data) {
NativeMessagingPortal* self = static_cast<NativeMessagingPortal*>(user_data);
GUniquePtr<GError> 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<dom::Promise> promise;
MOZ_TRY(GetPromise(aCx, promise));
if (mInitialized) {
MaybeDelayedIsAvailable(*promise, nullptr);
} else {
auto delayed = MakeUnique<DelayedCall>(
&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<GVariant> 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<dom::Promise> 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<gchar> sanitizedApplicationName(g_strdup(application.get()));
g_strdelimit(sanitizedApplicationName.get(), ".", '_');
GUniquePtr<gchar> token(g_strdup_printf("%s_%s_%u", MOZ_APP_NAME,
sanitizedApplicationName.get(),
g_random_int()));
RefPtr<GVariant> args = dont_AddRef(g_variant_new_string(token.get()));
if (mInitialized) {
MaybeDelayedCreateSession(*promise, args);
} else {
auto delayed = MakeUnique<DelayedCall>(
&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<CallbackData>(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> callbackData(static_cast<CallbackData*>(user_data));
GUniquePtr<GError> error;
RefPtr<GVariant> res = dont_AddRef(
g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
if (res) {
RefPtr<GVariant> 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<NativeMessagingPortal> 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<guint>(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<guint> release(reinterpret_cast<guint*>(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<dom::Promise> promise;
MOZ_TRY(GetPromise(aCx, promise));
sessionIterator->second = SessionState::Closing;
LOG_NMP("closing session %s", sessionHandle.get());
auto callbackData = MakeUnique<CallbackData>(*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> callbackData(static_cast<CallbackData*>(user_data));
GUniquePtr<GError> error;
RefPtr<GDBusProxy> 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> callbackData(static_cast<CallbackData*>(user_data));
RefPtr<NativeMessagingPortal> portal = GetSingleton();
GUniquePtr<GError> error;
RefPtr<GVariant> 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<guint*>(user_data);
LOG_NMP("session %s was closed by the portal", object_path);
g_dbus_connection_signal_unsubscribe(bus, subscription_id);
RefPtr<NativeMessagingPortal> 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<dom::Promise> promise;
MOZ_TRY(GetPromise(aCx, promise));
auto callbackData = MakeUnique<CallbackData>(*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> callbackData(static_cast<CallbackData*>(user_data));
GUniquePtr<GError> error;
RefPtr<GVariant> 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<dom::Promise> promise;
MOZ_TRY(GetPromise(aCx, promise));
auto callbackData = MakeUnique<CallbackData>(*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<gchar> senderName(
g_strdup(g_dbus_connection_get_unique_name(connection)));
g_strdelimit(senderName.get(), ".", '_');
GUniquePtr<gchar> handleToken(
g_strdup_printf("%s/%d", MOZ_APP_NAME, g_random_int_range(0, G_MAXINT)));
GUniquePtr<gchar> 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<CallbackData>(*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> callbackData(static_cast<CallbackData*>(user_data));
GUniquePtr<GError> error;
RefPtr<GVariant> 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> callbackData(static_cast<CallbackData*>(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<GVariant> 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<NativeMessagingPortal> 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<GVariant>& result, GUnixFDList* fds,
gint index) {
RefPtr<GVariant> value =
dont_AddRef(g_variant_get_child_value(result, index));
GUniquePtr<GError> 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> callbackData(static_cast<CallbackData*>(user_data));
auto promise = callbackData->promise;
RefPtr<GUnixFDList> fds;
GUniquePtr<GError> error;
RefPtr<GVariant> 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<JSObject*> 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<JS::Value> 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

View File

@@ -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 <gio/gio.h>
#include "mozilla/GRefPtr.h"
#include "mozilla/UniquePtr.h"
#include <deque>
#include <unordered_map>
namespace mozilla::extensions {
enum class SessionState { Active, Closing, Error };
class NativeMessagingPortal : public nsINativeMessagingPortal {
public:
NS_DECL_NSINATIVEMESSAGINGPORTAL
NS_DECL_ISUPPORTS
static already_AddRefed<NativeMessagingPortal> GetSingleton();
private:
NativeMessagingPortal();
virtual ~NativeMessagingPortal();
RefPtr<GDBusProxy> mProxy;
bool mInitialized = false;
RefPtr<GCancellable> mCancellable;
struct DelayedCall;
std::deque<UniquePtr<DelayedCall>> mPending;
using SessionsMap = std::unordered_map<std::string, SessionState>;
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

View File

@@ -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'],
},
]

View File

@@ -80,6 +80,7 @@ XPIDL_SOURCES += [
"extIWebNavigation.idl",
"mozIExtensionAPIRequestHandling.idl",
"mozIExtensionProcessScript.idl",
"nsINativeMessagingPortal.idl",
]
XPIDL_MODULE = "webextensions"
@@ -106,6 +107,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",
]

View File

@@ -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);
};

View File

@@ -218,6 +218,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();

View File

@@ -53,6 +53,7 @@ inline bool IsRunningUnderFlatpakOrSnap() {
enum class PortalKind {
FilePicker,
MimeHandler,
NativeMessaging,
Settings,
Location,
OpenUri,