Bug 1602639 - Switch native messaging from MessageManagers to Conduits r=robwu

Differential Revision: https://phabricator.services.mozilla.com/D58356

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Tomislav Jovanovic 2020-01-16 22:41:27 +00:00
Родитель 3f8e528138
Коммит 48d05a45cb
13 изменённых файлов: 233 добавлений и 230 удалений

Просмотреть файл

@ -40,7 +40,6 @@ class ChildDevToolsPanel extends ExtensionCommon.EventEmitter {
this._panelContext = null;
this.conduit = context.openConduit(this, {
id: this.id,
recv: ["PanelHidden", "PanelShown"],
});
}
@ -147,7 +146,6 @@ class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter {
this.id = id;
this.conduit = context.openConduit(this, {
id: this.id,
recv: ["InspectorSidebarHidden", "InspectorSidebarShown"],
});
}

Просмотреть файл

@ -22,7 +22,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
EventDispatcher: "resource://gre/modules/Messaging.jsm",
Extension: "resource://gre/modules/Extension.jsm",
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm",
});
@ -100,26 +99,21 @@ class ExtensionActionHelper {
}
}
class EmbedderPort extends ExtensionChild.Port {
constructor(...args) {
super(...args);
class EmbedderPort {
constructor(portId, messenger) {
this.id = portId;
this.messenger = messenger;
EventDispatcher.instance.registerListener(this, [
"GeckoView:WebExtension:PortMessageFromApp",
"GeckoView:WebExtension:PortDisconnect",
]);
}
handleDisconnection() {
super.handleDisconnection();
close() {
EventDispatcher.instance.unregisterListener(this, [
"GeckoView:WebExtension:PortMessageFromApp",
"GeckoView:WebExtension:PortDisconnect",
]);
}
close() {
// Notify listeners that this port is being closed because the context is
// gone.
this.disconnectByOtherEnd();
}
onEvent(aEvent, aData, aCallback) {
debug`onEvent ${aEvent} ${aData}`;
@ -129,12 +123,14 @@ class EmbedderPort extends ExtensionChild.Port {
switch (aEvent) {
case "GeckoView:WebExtension:PortMessageFromApp": {
this.postMessage(aData.message);
const holder = new StructuredCloneHolder(aData.message);
this.messenger.sendPortMessage(this.id, holder);
break;
}
case "GeckoView:WebExtension:PortDisconnect": {
this.disconnect();
this.messenger.sendPortDisconnect(this.id);
this.close();
break;
}
}
@ -142,29 +138,24 @@ class EmbedderPort extends ExtensionChild.Port {
}
class GeckoViewConnection {
constructor(context, sender, target, nativeApp) {
this.context = context;
constructor(sender, nativeApp) {
this.sender = sender;
this.target = target;
this.nativeApp = nativeApp;
this.allowContentMessaging = GeckoViewWebExtension.extensionScopes.get(
sender.extensionId
).allowContentMessaging;
}
_getMessageManager(aTarget) {
if (aTarget.frameLoader) {
return aTarget.frameLoader.messageManager;
const scope = GeckoViewWebExtension.extensionScopes.get(sender.extensionId);
this.allowContentMessaging = scope.allowContentMessaging;
if (!this.allowContentMessaging && !sender.verified) {
throw new Error(`Unexpected messaging sender: ${JSON.stringify(sender)}`);
}
return aTarget;
}
get dispatcher() {
const target = this.sender.actor.browsingContext.top.embedderElement;
if (this.sender.envType === "addon_child") {
// If this is a WebExtension Page we will have a GeckoSession associated
// to it and thus a dispatcher.
const dispatcher = GeckoViewUtils.getDispatcherForWindow(
this.target.ownerGlobal
target.ownerGlobal
);
if (dispatcher) {
return dispatcher;
@ -180,7 +171,7 @@ class GeckoViewConnection {
// If this message came from a content script, send the message to
// the corresponding tab messenger so that GeckoSession can pick it
// up.
return GeckoViewUtils.getDispatcherForWindow(this.target.ownerGlobal);
return GeckoViewUtils.getDispatcherForWindow(target.ownerGlobal);
}
throw new Error(`Uknown sender envType: ${this.sender.envType}`);
@ -205,37 +196,32 @@ class GeckoViewConnection {
});
}
onConnect(portId) {
const port = new EmbedderPort(
this.context,
this.target.messageManager,
[Services.mm],
"",
portId,
this.sender,
this.sender
);
port.registerOnMessage(holder =>
onConnect(portId, messenger) {
const port = new EmbedderPort(portId, messenger);
port.onPortMessage = holder =>
this._sendMessage({
type: "GeckoView:WebExtension:PortMessage",
portId: port.id,
data: holder.deserialize({}),
})
);
});
port.registerOnDisconnect(msg =>
port.onPortDisconnect = () => {
EventDispatcher.instance.sendRequest({
type: "GeckoView:WebExtension:Disconnect",
sender: this.sender,
portId: port.id,
})
);
});
port.close();
};
return this._sendMessage({
this._sendMessage({
type: "GeckoView:WebExtension:Connect",
data: {},
portId: port.id,
});
return port;
}
}

Просмотреть файл

@ -62,6 +62,7 @@ const {
} = ExtensionUtils;
const {
defineLazyGetter,
EventEmitter,
EventManager,
LocalAPIImplementation,
@ -431,11 +432,92 @@ class Port {
}
}
class NativePort extends Port {
postMessage(data) {
data = NativeApp.encodeMessage(this.context, data);
// Simple single-event emitter-like helper, exposes the EventManager api.
class SimpleEventAPI extends EventManager {
constructor(context, name) {
super({ context, name });
this.fires = new Set();
this.register = fire => {
this.fires.add(fire);
return () => this.fires.delete(fire);
};
}
emit(...args) {
return [...this.fires].map(fire => fire.asyncWithoutClone(...args));
}
}
return super.postMessage(data);
function holdMessage(sender, data) {
if (AppConstants.platform !== "android") {
data = NativeApp.encodeMessage(sender.context, data);
}
return new StructuredCloneHolder(data);
}
class NativePort {
constructor(context, name, portId) {
this.id = portId;
this.context = context;
this.conduit = context.openConduit(this, {
recv: ["PortMessage", "PortDisconnect"],
send: ["PortMessage"],
});
this.onMessage = new SimpleEventAPI(context, "Port.onMessage");
this.onDisconnect = new SimpleEventAPI(context, "Port.onDisconnect");
let api = {
name,
error: null,
onMessage: this.onMessage.api(),
onDisconnect: this.onDisconnect.api(),
postMessage: this.sendPortMessage.bind(this),
disconnect: () => this.conduit.close(),
};
this.api = Cu.cloneInto(api, context.cloneScope, { cloneFunctions: true });
}
recvPortMessage({ holder }) {
this.onMessage.emit(holder.deserialize(this.api), this.api);
}
recvPortDisconnect({ error }) {
this.api.error = error && this.context.normalizeError(error);
this.onDisconnect.emit(this.api);
this.conduit.close();
}
sendPortMessage(json) {
if (this.conduit.actor) {
return this.conduit.sendPortMessage({ holder: holdMessage(this, json) });
}
throw new this.context.Error("Attempt to postMessage on disconnected port");
}
}
// Handles native messaging for a context, similar to the Messenger below.
class NativeMessenger {
constructor(context, sender) {
this.context = context;
this.conduit = context.openConduit(this, {
url: sender.url,
frameId: sender.frameId,
childId: context.childManager.id,
query: ["NativeMessage", "NativeConnect"],
});
}
sendNativeMessage(nativeApp, json) {
let holder = holdMessage(this, json);
return this.conduit.queryNativeMessage({ nativeApp, holder });
}
connectNative(nativeApp) {
let port = new NativePort(this.context, nativeApp, getUniqueId());
this.conduit
.queryNativeConnect({ nativeApp, portId: port.id })
.catch(error => port.recvPortDisconnect({ error }));
return port.api;
}
}
@ -511,16 +593,6 @@ class Messenger {
return this.context.wrapPromise(promise, responseCallback);
}
sendNativeMessage(messageManager, msg, recipient, responseCallback) {
if (
AppConstants.platform !== "android" ||
!this.context.extension.hasPermission("geckoViewAddons")
) {
msg = NativeApp.encodeMessage(this.context, msg);
}
return this.sendMessage(messageManager, msg, recipient, responseCallback);
}
_onMessage(name, filter) {
return new EventManager({
context: this.context,
@ -674,38 +746,6 @@ class Messenger {
return this._connect(messageManager, port, recipient);
}
connectNative(messageManager, name, recipient) {
let portId = getUniqueId();
let port;
if (
AppConstants.platform === "android" &&
this.context.extension.hasPermission("geckoViewAddons")
) {
port = new Port(
this.context,
messageManager,
this.messageManagers,
name,
portId,
null,
recipient
);
} else {
port = new NativePort(
this.context,
messageManager,
this.messageManagers,
name,
portId,
null,
recipient
);
}
return this._connect(messageManager, port, recipient);
}
_onConnect(name, filter) {
return new EventManager({
context: this.context,
@ -795,6 +835,10 @@ class Messenger {
}
}
defineLazyGetter(Messenger.prototype, "nm", function() {
return new NativeMessenger(this.context, this.sender);
});
// For test use only.
var ExtensionManager = {
extensions: new Map(),
@ -974,7 +1018,6 @@ class BrowserExtensionContent extends EventEmitter {
emit(event, ...args) {
Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, { event, args });
super.emit(event, ...args);
}
@ -1174,7 +1217,6 @@ class ChildAPIManager {
this.id = `${context.extension.id}.${context.contextId}`;
this.conduit = context.openConduit(this, {
id: this.id,
send: ["CreateProxyContext", "APICall", "AddListener", "RemoveListener"],
recv: ["CallResult", "RunListener"],
});

Просмотреть файл

@ -499,6 +499,7 @@ class BaseContext {
openConduit(subject, address) {
let wgc = this.contentWindow.getWindowGlobalChild();
let conduit = wgc.getActor("Conduits").openConduit(subject, {
id: subject.id || getUniqueId(),
extensionId: this.extension.id,
envType: this.envType,
...address,

Просмотреть файл

@ -328,6 +328,66 @@ class ExtensionPortProxy {
}
}
// Handles NativeMessaging and GeckoView, similar to ProxyMessenger below.
const NativeMessenger = {
/**
* @typedef {object} ParentPort
* @prop {function(StructuredCloneHolder)} onPortMessage
* @prop {function()} onPortDisconnect
*/
/** @type Map<number, ParentPort> */
ports: new Map(),
init() {
this.conduit = new BroadcastConduit(NativeMessenger, {
id: "NativeMessenger",
recv: ["NativeMessage", "NativeConnect", "PortMessage"],
send: ["PortMessage", "PortDisconnect"],
});
},
openNative(nativeApp, sender) {
let context = ParentAPIManager.getContextById(sender.childId);
if (context.extension.hasPermission("geckoViewAddons")) {
return new GeckoViewConnection(sender, nativeApp);
} else if (sender.verified) {
return new NativeApp(context, nativeApp);
}
throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
},
recvNativeMessage({ nativeApp, holder }, { sender }) {
return this.openNative(nativeApp, sender).sendMessage(holder);
},
recvNativeConnect({ nativeApp, portId }, { sender }) {
let port = this.openNative(nativeApp, sender).onConnect(portId, this);
this.conduit.reportOnClosed(portId);
this.ports.set(portId, port);
},
recvConduitClosed(sender) {
let app = this.ports.get(sender.id);
if (this.ports.delete(sender.id)) {
app.onPortDisconnect();
}
},
recvPortMessage({ holder }, { sender }) {
this.ports.get(sender.id).onPortMessage(holder);
},
sendPortMessage(portId, holder) {
this.conduit.sendPortMessage(portId, { holder });
},
sendPortDisconnect(portId, error) {
this.conduit.sendPortDisconnect(portId, { error });
this.ports.delete(portId);
},
};
NativeMessenger.init();
// Subscribes to messages related to the extension messaging API and forwards it
// to the relevant message manager. The "sender" field for the `onMessage` and
// `onConnect` events are updated if needed.
@ -431,57 +491,6 @@ ProxyMessenger = {
data,
responseType,
}) {
if (recipient.toNativeApp) {
let { childId, toNativeApp } = recipient;
let context = ParentAPIManager.getContextById(childId);
if (
context.parentMessageManager !== target.messageManager ||
(sender.envType === "addon_child" &&
context.envType !== "addon_parent") ||
(sender.envType === "content_child" &&
context.envType !== "content_parent") ||
context.extension.id !== sender.extensionId
) {
throw new Error("Got message for an unexpected messageManager.");
}
if (
AppConstants.platform === "android" &&
context.extension.hasPermission("geckoViewAddons")
) {
let connection = new GeckoViewConnection(
context,
sender,
target,
toNativeApp
);
if (messageName == "Extension:Message") {
return connection.sendMessage(data);
} else if (messageName == "Extension:Connect") {
return connection.onConnect(data.portId);
}
return;
}
if (messageName == "Extension:Message") {
return new NativeApp(context, toNativeApp).sendMessage(data);
}
if (messageName == "Extension:Connect") {
NativeApp.onConnectNative(
context,
target.messageManager,
data.portId,
sender,
toNativeApp
);
return true;
}
// "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
// native messages are handled by NativeApp or GeckoViewConnection.
return;
}
const noHandlerError = {
result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler for the given recipient.",

Просмотреть файл

@ -51,7 +51,15 @@ function promiseTimeout(delay) {
* An Error subclass for which complete error messages are always passed
* to extensions, rather than being interpreted as an unknown error.
*/
class ExtensionError extends Error {}
class ExtensionError extends DOMException {
constructor(message) {
super(message, "ExtensionError");
}
// Custom JS classes can't survive IPC, so need to check error name.
static [Symbol.hasInstance](e) {
return e instanceof DOMException && e.name === "ExtensionError";
}
}
function filterStack(error) {
return String(error.stack).replace(

Просмотреть файл

@ -142,12 +142,6 @@ var NativeManifests = {
);
return null;
}
if (type !== "storage" && context.envType !== "addon_parent") {
Cu.reportError(
`Native manifest at ${path} is not available to non-extension contexts`
);
return;
}
return manifest;
})

Просмотреть файл

@ -15,10 +15,13 @@ const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const {
ExtensionUtils: { ExtensionError },
} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
NativeManifests: "resource://gre/modules/NativeManifests.jsm",
OS: "resource://gre/modules/osfile.jsm",
Services: "resource://gre/modules/Services.jsm",
@ -79,9 +82,7 @@ var NativeApp = class extends EventEmitter {
// 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 context.cloneScope.Error(
`No such native application ${application}`
);
throw new ExtensionError(`No such native application ${application}`);
}
let command = hostInfo.manifest.path;
@ -118,34 +119,22 @@ var NativeApp = class extends EventEmitter {
/**
* Open a connection to a native messaging host.
*
* @param {BaseContext} context The context associated with the port.
* @param {nsIMessageSender} messageManager The message manager used to send
* and receive messages from the port's creator.
* @param {number} portId A unique internal ID that identifies the port.
* @param {object} sender The object describing the creator of the connection
* request.
* @param {string} application The name of the native messaging host.
* @param {NativeMessenger} port Parent NativeMessenger used to send messages.
* @returns {ParentPort}
*/
static onConnectNative(context, messageManager, portId, sender, application) {
let app = new NativeApp(context, application);
let port = new ExtensionChild.Port(
context,
messageManager,
[Services.mm],
"",
portId,
sender,
sender
);
app.once("disconnect", (what, err) => port.disconnect(err));
/* eslint-disable mozilla/balanced-listeners */
app.on("message", (what, msg) => port.postMessage(msg));
/* eslint-enable mozilla/balanced-listeners */
port.registerOnMessage(holder => app.send(holder));
port.registerOnDisconnect(msg => app.close());
onConnect(portId, port) {
// eslint-disable-next-line
this.on("message", (_, message) => {
port.sendPortMessage(portId, new StructuredCloneHolder(message));
});
this.once("disconnect", (_, error) => {
port.sendPortDisconnect(portId, error && new ClonedErrorHolder(error));
});
return {
onPortMessage: holder => this.send(holder),
onPortDisconnect: () => this.close(),
};
}
/**
@ -157,7 +146,7 @@ var NativeApp = class extends EventEmitter {
message = context.jsonStringify(message);
let buffer = new TextEncoder().encode(message).buffer;
if (buffer.byteLength > NativeApp.maxWrite) {
throw new context.cloneScope.Error("Write too big");
throw new context.Error("Write too big");
}
return buffer;
}
@ -178,7 +167,7 @@ var NativeApp = class extends EventEmitter {
.readUint32()
.then(len => {
if (len > NativeApp.maxRead) {
throw new this.context.cloneScope.Error(
throw new ExtensionError(
`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${
NativeApp.maxRead
} bytes.`
@ -257,9 +246,7 @@ var NativeApp = class extends EventEmitter {
send(holder) {
if (this._isDisconnected) {
throw new this.context.cloneScope.Error(
"Attempt to postMessage on disconnected port"
);
throw new ExtensionError("Attempt to postMessage on disconnected port");
}
let msg = holder.deserialize(global);
if (Cu.getClassName(msg, true) != "ArrayBuffer") {
@ -273,7 +260,7 @@ var NativeApp = class extends EventEmitter {
let buffer = msg;
if (buffer.byteLength > NativeApp.maxWrite) {
throw new this.context.cloneScope.Error("Write too big");
throw new ExtensionError("Write too big");
}
this.sendQueue.push(buffer);
@ -282,9 +269,9 @@ var NativeApp = class extends EventEmitter {
}
}
// Shut down the native application and also signal to the extension
// Shut down the native application and (by default) signal to the extension
// that the connect has been disconnected.
_cleanup(err) {
_cleanup(err, fromExtension = false) {
this.context.forgetOnClose(this);
let doCleanup = () => {
@ -326,18 +313,18 @@ var NativeApp = class extends EventEmitter {
this.startupPromise.then(doCleanup);
}
if (!this.sentDisconnect) {
this.sentDisconnect = true;
if (!this.sentDisconnect && !fromExtension) {
if (err && err.errorCode == Subprocess.ERROR_END_OF_FILE) {
err = null;
}
this.emit("disconnect", err);
}
this.sentDisconnect = true;
}
// Called from Context when the extension is shut down.
// Called when the Context or Port is closed.
close() {
this._cleanup();
this._cleanup(null, true);
}
sendMessage(holder) {

Просмотреть файл

@ -115,29 +115,12 @@ this.runtime = class extends ExtensionAPI {
);
},
connectNative(application) {
let recipient = {
childId: context.childManager.id,
toNativeApp: application,
};
return context.messenger.connectNative(
context.messageManager,
"",
recipient
);
connectNative(nativeApp) {
return context.messenger.nm.connectNative(nativeApp);
},
sendNativeMessage(application, message) {
let recipient = {
childId: context.childManager.id,
toNativeApp: application,
};
return context.messenger.sendNativeMessage(
context.messageManager,
message,
recipient
);
sendNativeMessage(nativeApp, message) {
return context.messenger.nm.sendNativeMessage(nativeApp, message);
},
get lastError() {
@ -150,7 +133,7 @@ this.runtime = class extends ExtensionAPI {
id: extension.id,
getURL: function(url) {
getURL(url) {
return extension.baseURI.resolve(url);
},
},

Просмотреть файл

@ -1,5 +1,8 @@
"use strict";
// Disable extra warnings "Reference to undefined property".
options("strict"); // eslint-disable-line
/* exported createHttpServer, cleanupDir, clearCache, promiseConsoleOutput,
promiseQuotaManagerServiceReset, promiseQuotaManagerServiceClear,
runWithPrefs, testEnv, withHandlingUserInput */

Просмотреть файл

@ -269,6 +269,10 @@ add_task(async function test_disconnect() {
} else if (what == "disconnect") {
try {
port.disconnect();
browser.test.assertThrows(
() => port.postMessage("void"),
"Attempt to postMessage on disconnected port"
);
browser.test.sendMessage("disconnect-result", { success: true });
} catch (err) {
browser.test.sendMessage("disconnect-result", {
@ -630,7 +634,7 @@ add_task(async function test_connect_native_from_content_script() {
"onDisconnect handler should receive the port as the first argument"
);
browser.test.assertEq(
"No such native application echo",
"An unexpected error occurred",
port.error && port.error.message
);
browser.test.sendMessage("result", "disconnected");

Просмотреть файл

@ -282,19 +282,6 @@ add_task(async function test_no_allowed_extensions() {
);
});
add_task(async function test_invalid_context_envType() {
let manifest = Object.assign({}, templateManifest);
await writeManifest(USER_TEST_JSON, manifest);
for (let type of ["stdio", "pkcs11"]) {
let badContext = { ...context, type, envType: "content_parent" };
let result = await lookupApplication("test", badContext);
equal(result, null, `lookupApplication ignores bad envType for "${type}"`);
}
// type=storage is allowed to be accessed from content_parent, which is
// covered by the test task "test_storage_managed_from_content_script"
// in ./test_ext_storage_managed.js.
});
const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, TYPE_SLUG, "test.json");
let globalManifest = Object.assign({}, templateManifest);
globalManifest.description = "This manifest is from the systemwide directory";

Просмотреть файл

@ -104,6 +104,7 @@ module.exports = {
ChromeWorker: false,
Clipboard: false,
ClipboardEvent: false,
ClonedErrorHolder: false,
CloseEvent: false,
CommandEvent: false,
Comment: false,