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:
@@ -17709,6 +17709,16 @@
|
|||||||
value: 2
|
value: 2
|
||||||
mirror: always
|
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.
|
# 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
|
# https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Settings
|
||||||
# - 0: never
|
# - 0: never
|
||||||
|
|||||||
@@ -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: */
|
/* vim: set sts=2 sw=2 et tw=80: */
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* 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
|
* 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;
|
return manifest ? { path, manifest } : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
async _tryPath(type, path, name, context, logIfNotFound) {
|
/**
|
||||||
let manifest;
|
* Parse a native manifest of the given type and name.
|
||||||
try {
|
*
|
||||||
manifest = await IOUtils.readJSON(path);
|
* @param {string} type The type, one of: "pkcs11", "stdio" or "storage".
|
||||||
} catch (ex) {
|
* @param {string} path The path to the manifest file.
|
||||||
if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) {
|
* @param {string} name The name of the application.
|
||||||
Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`);
|
* @param {object} context A context object as expected by Schemas.normalize.
|
||||||
return null;
|
* @param {object} data The JSON object of the manifest.
|
||||||
}
|
* @returns {object} The contents of the validated manifest, or null if
|
||||||
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
|
* the manifest is not valid.
|
||||||
if (logIfNotFound) {
|
*/
|
||||||
Cu.reportError(
|
async parseManifest(type, path, name, context, data) {
|
||||||
`Error reading native manifest file ${path}: file is referenced in the registry but does not exist`
|
await this.init();
|
||||||
);
|
let manifest = data;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Cu.reportError(ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let normalized = lazy.Schemas.normalize(
|
let normalized = lazy.Schemas.normalize(
|
||||||
manifest,
|
manifest,
|
||||||
"manifest.NativeManifest",
|
"manifest.NativeManifest",
|
||||||
@@ -158,6 +152,30 @@ export var NativeManifests = {
|
|||||||
return manifest;
|
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) {
|
async _tryPaths(type, name, dirs, context) {
|
||||||
for (let dir of dirs) {
|
for (let dir of dirs) {
|
||||||
let path = PathUtils.join(dir, TYPES[type], `${name}.json`);
|
let path = PathUtils.join(dir, TYPES[type], `${name}.json`);
|
||||||
|
|||||||
@@ -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: */
|
/* vim: set sts=2 sw=2 et tw=80: */
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* 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
|
* 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;
|
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
|
// 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
|
// 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)
|
// application to exit before we start trying to kill it. (in milliseconds)
|
||||||
@@ -49,6 +56,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export class NativeApp extends EventEmitter {
|
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 {BaseContext} context The context that initiated the native app.
|
||||||
* @param {string} application The identifier of the native app.
|
* @param {string} application The identifier of the native app.
|
||||||
@@ -67,6 +80,18 @@ export class NativeApp extends EventEmitter {
|
|||||||
this.sendQueue = [];
|
this.sendQueue = [];
|
||||||
this.writePromise = null;
|
this.writePromise = null;
|
||||||
this.cleanupStarted = false;
|
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(
|
this.startupPromise = lazy.NativeManifests.lookupManifest(
|
||||||
"stdio",
|
"stdio",
|
||||||
@@ -74,10 +99,8 @@ export class NativeApp extends EventEmitter {
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
.then(hostInfo => {
|
.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) {
|
if (!hostInfo) {
|
||||||
throw new ExtensionError(`No such native application ${application}`);
|
this._throwGenericError(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = hostInfo.manifest.path;
|
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.
|
* Open a connection to a native messaging host.
|
||||||
*
|
*
|
||||||
@@ -299,6 +383,22 @@ export class NativeApp extends EventEmitter {
|
|||||||
|
|
||||||
await this.startupPromise;
|
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) {
|
if (!this.proc) {
|
||||||
// Failed to initialize proc in the constructor.
|
// Failed to initialize proc in the constructor.
|
||||||
return;
|
return;
|
||||||
|
|||||||
696
toolkit/components/extensions/NativeMessagingPortal.cpp
Normal file
696
toolkit/components/extensions/NativeMessagingPortal.cpp
Normal 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
|
||||||
76
toolkit/components/extensions/NativeMessagingPortal.h
Normal file
76
toolkit/components/extensions/NativeMessagingPortal.h
Normal 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
|
||||||
@@ -14,3 +14,15 @@ Classes = [
|
|||||||
'categories': {'app-startup': 'ExtensionsChild'},
|
'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'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ XPIDL_SOURCES += [
|
|||||||
"extIWebNavigation.idl",
|
"extIWebNavigation.idl",
|
||||||
"mozIExtensionAPIRequestHandling.idl",
|
"mozIExtensionAPIRequestHandling.idl",
|
||||||
"mozIExtensionProcessScript.idl",
|
"mozIExtensionProcessScript.idl",
|
||||||
|
"nsINativeMessagingPortal.idl",
|
||||||
]
|
]
|
||||||
|
|
||||||
XPIDL_MODULE = "webextensions"
|
XPIDL_MODULE = "webextensions"
|
||||||
@@ -106,6 +107,13 @@ UNIFIED_SOURCES += [
|
|||||||
"WebExtensionPolicy.cpp",
|
"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 += [
|
XPCOM_MANIFESTS += [
|
||||||
"components.conf",
|
"components.conf",
|
||||||
]
|
]
|
||||||
|
|||||||
86
toolkit/components/extensions/nsINativeMessagingPortal.idl
Normal file
86
toolkit/components/extensions/nsINativeMessagingPortal.idl
Normal 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);
|
||||||
|
};
|
||||||
@@ -218,6 +218,8 @@ bool ShouldUsePortal(PortalKind aPortalKind) {
|
|||||||
// Mime portal breaks default browser handling, see bug 1516290.
|
// Mime portal breaks default browser handling, see bug 1516290.
|
||||||
autoBehavior = IsRunningUnderFlatpakOrSnap();
|
autoBehavior = IsRunningUnderFlatpakOrSnap();
|
||||||
return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler();
|
return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler();
|
||||||
|
case PortalKind::NativeMessaging:
|
||||||
|
return StaticPrefs::widget_use_xdg_desktop_portal_native_messaging();
|
||||||
case PortalKind::Settings:
|
case PortalKind::Settings:
|
||||||
autoBehavior = true;
|
autoBehavior = true;
|
||||||
return StaticPrefs::widget_use_xdg_desktop_portal_settings();
|
return StaticPrefs::widget_use_xdg_desktop_portal_settings();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ inline bool IsRunningUnderFlatpakOrSnap() {
|
|||||||
enum class PortalKind {
|
enum class PortalKind {
|
||||||
FilePicker,
|
FilePicker,
|
||||||
MimeHandler,
|
MimeHandler,
|
||||||
|
NativeMessaging,
|
||||||
Settings,
|
Settings,
|
||||||
Location,
|
Location,
|
||||||
OpenUri,
|
OpenUri,
|
||||||
|
|||||||
Reference in New Issue
Block a user