); } }, /** * Cleanup. * * @param {BroadcastConduit} conduit */ closeConduit({ id, address }) { this.conduits.delete(id); for (let name of address.recv || []) { this.byMethod.delete(name); } }, /** * Confirm that a remote conduit comes from an extension background * service worker. * * @see ExtensionPolicyService::CheckParentFrames * @param {ConduitAddress} remote * @returns {boolean} */ verifyWorkerEnv({ actor, extensionId, workerScriptURL }) { const addonPolicy = WebExtensionPolicy.getByID(extensionId); if (!addonPolicy) { throw new Error(`No WebExtensionPolicy found for ${extensionId}`); } if (actor.manager.remoteType !== addonPolicy.extension.remoteType) { throw new Error( `Bad ${extensionId} process: ${actor.manager.remoteType}` ); } if (!addonPolicy.isManifestBackgroundWorker(workerScriptURL)) { throw new Error( `Bad ${extensionId} background service worker script url: ${workerScriptURL}` ); } return true; }, /** * Confirm that a remote conduit comes from an extension page or * an extension background service worker. * * @see ExtensionPolicyService::CheckParentFrames * @param {ConduitAddress} remote * @returns {boolean} */ verifyEnv({ actor, envType, extensionId, ...rest }) { if (!extensionId || !ADDON_ENV.has(envType)) { return false; } // ProcessConduit related to a background service worker context. if (actor.manager && actor.manager instanceof Ci.nsIDOMProcessParent) { return this.verifyWorkerEnv({ actor, envType, extensionId, ...rest }); } let windowGlobal = actor.manager; while (windowGlobal) { let { browsingContext: bc, documentPrincipal: prin } = windowGlobal; if (prin.addonId !== extensionId) { throw new Error(`Bad ${extensionId} principal: ${prin.URI.spec}`); } if (bc.currentRemoteType !== prin.addonPolicy.extension.remoteType) { throw new Error(`Bad ${extensionId} process: ${bc.currentRemoteType}`); } if (!bc.parent) { return true; } windowGlobal = bc.embedderWindowGlobal; } throw new Error(`Missing WindowGlobalParent for ${extensionId}`); }, /** * Fill in common address fields knowable from the parent process. * * @param {ConduitAddress} address * @param {ConduitsParent} actor */ fillInAddress(address, actor) { address.actor = actor; address.verified = this.verifyEnv(address); if (JSWindowActorParent.isInstance(actor)) { address.frameId = WebNavigationFrames.getFrameId(actor.browsingContext); address.url = actor.manager.documentURI?.spec; } else { // Background service worker contexts do not have an associated frame // and there is no browsingContext to retrieve the expected url from. // // WorkerContextChild sent in the address part of the ConduitOpened request // the worker script URL as address.workerScriptURL, and so we can use that // as the address.url too. address.frameId = -1; address.url = address.workerScriptURL; } }, /** * Save info about a new remote conduit. * * @param {ConduitAddress} address * @param {ConduitsParent} actor */ recvConduitOpened(address, actor) { this.fillInAddress(address, actor); this.remotes.set(address.id, address); this.byActor.get(actor).add(address); }, /** * Notifies listeners and cleans up after the remote conduit is closed. * * @param {ConduitAddress} remote */ recvConduitClosed(remote) { this.remotes.delete(remote.id); this.byActor.get(remote.actor).delete(remote); remote.actor = null; for (let [key, conduit] of Hub.reportOnClosed.entries()) { if (remote[key]) { conduit.subject.recvConduitClosed(remote); } } }, /** * Close all remote conduits when the actor goes away. * * @param {ConduitsParent} actor */ actorClosed(actor) { for (let remote of this.byActor.get(actor)) { // When a Port is closed, we notify the other side, but it might share // an actor, so we shouldn't sendQeury() in that case (see bug 1623976). this.remotes.delete(remote.id); } for (let remote of this.byActor.get(actor)) { this.recvConduitClosed(remote); } this.byActor.delete(actor); }, }; /** * Parent side conduit, registers as a global listeners for certain messages, * and can target specific child conduits when sending. */ export class BroadcastConduit extends BaseConduit { /** * @param {object} subject * @param {ConduitAddress} address */ constructor(subject, address) { super(subject, address); // Create conduit.castX() bidings. for (let name of address.cast || []) { this[`cast${name}`] = this._cast.bind(this, name); } // Wants to know when conduits with a specific attribute are closed. // `subject.recvConduitClosed(address)` method will be called. if (address.reportOnClosed) { Hub.reportOnClosed.set(address.reportOnClosed, this); } this.open = true; Hub.openConduit(this); } /** * Internal, sends a message to a specific conduit, used by sendX stubs. * * @param {string} method * @param {boolean} query * @param {ConduitID} target * @param {object?} arg * @returns {Promise} */ _send(method, query, target, arg = {}) { if (!this.open) { throw new Error(`send${method} on closed conduit ${this.id}`); } let sender = this.id; let { actor } = Hub.remotes.get(target); if (method === "RunListener" && arg.path.startsWith("webRequest.")) { return actor.batch(method, { target, arg, query, sender }); } return super._doSend(method, query, actor, { target, arg, query, sender }); } /** * Broadcasts a method call to all conduits of kind that satisfy filtering by * kind-specific properties from arg. If arg.query is true, these broadcasts * are all queries and this returns an array of response promises. Otherwise, * they are not, and undefined is returned. * * @param {string} method * @param {BroadcastKind} kind * @param {object} arg * @returns {undefined | Promise | Promise} */ _cast(method, kind, arg) { let filters = { // Target Ports by portId and side (connect caller/onConnect receiver). port: remote => remote.portId === arg.portId && (arg.source == null || remote.source === arg.source), // Target Messengers in extension pages by extensionId and envType. messenger: r => r.verified && r.id !== arg.sender.contextId && r.extensionId === arg.extensionId && r.recv.includes(method) && // TODO: Bug 1453343 - get rid of this: (r.envType === "addon_child" || arg.sender.envType !== "content_child"), // Target Messengers by extensionId, tabId (topBC) and frameId. tab: remote => remote.extensionId === arg.extensionId && remote.actor.manager.browsingContext?.top.id === arg.topBC && (arg.frameId == null || remote.frameId === arg.frameId) && remote.recv.includes(method), // Target Messengers by extensionId. extension: remote => remote.instanceId === arg.instanceId, }; let targets = Array.from(Hub.remotes.values()).filter(filters[kind]); let promises = targets.map(c => this._send(method, !!arg.query, c.id, arg)); if (arg.query) { return arg.firstResponse ? this._raceResponses(promises) : Promise.allSettled(promises); } return undefined; } /** * Custom Promise.race() function that ignores certain resolutions and errors. * * @typedef {{response?: any, received?: boolean}} Response * * @param {Promise[]} promises * @returns {Promise} */ _raceResponses(promises) { return new Promise((resolve, reject) => { let result; promises.map(p => p .then(value => { if (value.response) { // We have an explicit response, resolve immediately. resolve(value); } else if (value.received) { // Message was received, but no response. // Resolve with this only if there is no other explicit response. result = value; } }) .catch(err => { // Forward errors that are exposed to extension, but ignore // internal errors such as actor destruction and DataCloneError. if (err instanceof ExtensionError || err?.mozWebExtLocation) { reject(err); } else { Cu.reportError(err); } }) ); // Ensure resolving when there are no responses. Promise.allSettled(promises).then(() => resolve(result)); }); } async close() { this.open = false; Hub.closeConduit(this); } } /** * Implements the parent side of the Conduits actor. */ export class ConduitsParent extends JSWindowActorParent { constructor() { super(); this.batchData = []; this.batchPromise = null; this.batchResolve = null; this.timerActive = false; } /** * Group webRequest events to send them as a batch, reducing IPC overhead. * * @param {string} name * @param {import("ConduitsChild.sys.mjs").MessageData} data * @returns {Promise} */ batch(name, data) { let pos = this.batchData.length; this.batchData.push(data); let sendNow = idleDispatch => { if (this.batchData.length && this.manager) { this.batchResolve(this.sendQuery(name, this.batchData)); } else { this.batchResolve([]); } this.batchData = []; this.timerActive = !idleDispatch; }; if (!pos) { this.batchPromise = new Promise(r => (this.batchResolve = r)); if (!this.timerActive) { ChromeUtils.idleDispatch(sendNow, { timeout: BATCH_TIMEOUT_MS }); this.timerActive = true; } } if (data.arg.urgentSend) { // If this is an urgent blocking event, run this batch right away. sendNow(false); } return this.batchPromise.then(results => results[pos]); } /** * JSWindowActor method, routes the message to the target subject. * * @param {object} options * @param {string} options.name * @param {import("ConduitsChild.sys.mjs").MessageData} options.data * @returns {Promise?} */ async receiveMessage({ name, data: { arg, query, sender } }) { if (name === "ConduitOpened") { return Hub.recvConduitOpened(arg, this); } let remote = Hub.remotes.get(sender); if (!remote || remote.actor !== this) { throw new Error(`Unknown sender or wrong actor for recv${name}`); } if (name === "ConduitClosed") { return Hub.recvConduitClosed(remote); } let conduit = Hub.byMethod.get(name); if (!conduit) { throw new Error(`Parent conduit for recv${name} not found`); } return conduit._recv(name, arg, { actor: this, query, sender: remote }); } /** * JSWindowActor method, ensure cleanup. */ didDestroy() { Hub.actorClosed(this); } } /** * Parent side of the Conduits process actor. Same code as JSWindowActor. */ export class ProcessConduitsParent extends JSProcessActorParent { receiveMessage = ConduitsParent.prototype.receiveMessage; didDestroy = ConduitsParent.prototype.didDestroy; } PK