/* 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/. */ /** * WebChannel is an abstraction that uses the Message Manager and Custom Events * to create a two-way communication channel between chrome and content code. */ this.EXPORTED_SYMBOLS = ["WebChannel", "WebChannelBroker"]; const ERRNO_UNKNOWN_ERROR = 999; const ERROR_UNKNOWN = "UNKNOWN_ERROR"; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); /** * WebChannelBroker is a global object that helps manage WebChannel objects. * This object handles channel registration, origin validation and message multiplexing. */ var WebChannelBroker = Object.create({ /** * Register a new channel that callbacks messages * based on proper origin and channel name * * @param channel {WebChannel} */ registerChannel: function (channel) { if (!this._channelMap.has(channel)) { this._channelMap.set(channel); } else { Cu.reportError("Failed to register the channel. Channel already exists."); } // attach the global message listener if needed if (!this._messageListenerAttached) { this._messageListenerAttached = true; this._manager.addMessageListener("WebChannelMessageToChrome", this._listener.bind(this)); } }, /** * Unregister a channel * * @param channelToRemove {WebChannel} * WebChannel to remove from the channel map * * Removes the specified channel from the channel map */ unregisterChannel: function (channelToRemove) { if (!this._channelMap.delete(channelToRemove)) { Cu.reportError("Failed to unregister the channel. Channel not found."); } }, /** * @param event {Event} * Message Manager event * @private */ _listener: function (event) { let data = event.data; let sendingContext = { browser: event.target, eventTarget: event.objects.eventTarget, principal: event.principal, }; // data must be a string except for a few legacy origins allowed by browser-content.js. if (typeof data == "string") { try { data = JSON.parse(data); } catch (e) { Cu.reportError("Failed to parse WebChannel data as a JSON object"); return; } } if (data && data.id) { if (!event.principal) { this._sendErrorEventToContent(data.id, sendingContext, "Message principal missing"); } else { let validChannelFound = false; data.message = data.message || {}; for (var channel of this._channelMap.keys()) { if (channel.id === data.id && channel._originCheckCallback(event.principal)) { validChannelFound = true; channel.deliver(data, sendingContext); } } // if no valid origins send an event that there is no such valid channel if (!validChannelFound) { this._sendErrorEventToContent(data.id, sendingContext, "No Such Channel"); } } } else { Cu.reportError("WebChannel channel id missing"); } }, /** * The global message manager operates on every <browser> */ _manager: Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager), /** * Boolean used to detect if the global message manager event is already attached */ _messageListenerAttached: false, /** * Object to store pairs of message origins and callback functions */ _channelMap: new Map(), /** * * @param id {String} * The WebChannel id to include in the message * @param sendingContext {Object} * Message sending context * @param [errorMsg] {String} * Error message * @private */ _sendErrorEventToContent: function (id, sendingContext, errorMsg) { let { browser: targetBrowser, eventTarget, principal: targetPrincipal } = sendingContext; errorMsg = errorMsg || "Web Channel Broker error"; if (targetBrowser && targetBrowser.messageManager) { targetBrowser.messageManager.sendAsyncMessage("WebChannelMessageToContent", { id: id, error: errorMsg, }, { eventTarget: eventTarget }, targetPrincipal); } else { Cu.reportError("Failed to send a WebChannel error. Target invalid."); } Cu.reportError(id.toString() + " error message. " + errorMsg); }, }); /** * Creates a new WebChannel that listens and sends messages over some channel id * * @param id {String} * WebChannel id * @param originOrPermission {nsIURI/string} * If an nsIURI, a valid origin that should be part of requests for * this channel. If a string, a permission for which the permission * manager will be checked to determine if the request is allowed. Note * that in addition to the permission manager check, the request must * be made over https:// * @constructor */ this.WebChannel = function(id, originOrPermission) { if (!id || !originOrPermission) { throw new Error("WebChannel id and originOrPermission are required."); } this.id = id; // originOrPermission can be either an nsIURI or a string representing a // permission name. if (typeof originOrPermission == "string") { this._originCheckCallback = requestPrincipal => { // The permission manager operates on domain names rather than true // origins (bug 1066517). To mitigate that, we explicitly check that // the scheme is https://. let uri = Services.io.newURI(requestPrincipal.originNoSuffix, null, null); if (uri.scheme != "https") { return false; } // OK - we have https - now we can check the permission. let perm = Services.perms.testExactPermissionFromPrincipal(requestPrincipal, originOrPermission); return perm == Ci.nsIPermissionManager.ALLOW_ACTION; } } else { // a simple URI, so just check for an exact match. this._originCheckCallback = requestPrincipal => { return originOrPermission.prePath === requestPrincipal.originNoSuffix; } } this._originOrPermission = originOrPermission; }; this.WebChannel.prototype = { /** * WebChannel id */ id: null, /** * The originOrPermission value passed to the constructor, mainly for * debugging and tests. */ _originOrPermission: null, /** * Callback that will be called with the principal of an incoming message * to check if the request should be dispatched to the listeners. */ _originCheckCallback: null, /** * WebChannelBroker that manages WebChannels */ _broker: WebChannelBroker, /** * Callback that will be called with the contents of an incoming message */ _deliverCallback: null, /** * Registers the callback for messages on this channel * Registers the channel itself with the WebChannelBroker * * @param callback {Function} * Callback that will be called when there is a message * @param {String} id * The WebChannel id that was used for this message * @param {Object} message * The message itself * @param sendingContext {Object} * The sending context of the source of the message. Can be passed to * `send` to respond to a message. * @param sendingContext.browser {browser} * The <browser> object that captured the * WebChannelMessageToChrome. * @param sendingContext.eventTarget {EventTarget} * The <EventTarget> where the message was sent. * @param sendingContext.principal {Principal} * The <Principal> of the EventTarget where the * message was sent. */ listen: function (callback) { if (this._deliverCallback) { throw new Error("Failed to listen. Listener already attached."); } else if (!callback) { throw new Error("Failed to listen. Callback argument missing."); } else { this._deliverCallback = callback; this._broker.registerChannel(this); } }, /** * Resets the callback for messages on this channel * Removes the channel from the WebChannelBroker */ stopListening: function () { this._broker.unregisterChannel(this); this._deliverCallback = null; }, /** * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event * * @param message {Object} * The message object that will be sent * @param target {Object} * A <target> with the information of where to send the message. * @param target.browser {browser} * The <browser> object with a "messageManager" that will * be used to send the message. * @param target.principal {Principal} * Principal of the target. Prevents messages from * being dispatched to unexpected origins. The system principal * can be specified to send to any target. * @param [target.eventTarget] {EventTarget} * Optional eventTarget within the browser, use to send to a * specific element, e.g., an iframe. */ send: function (message, target) { let { browser, principal, eventTarget } = target; if (message && browser && browser.messageManager && principal) { browser.messageManager.sendAsyncMessage("WebChannelMessageToContent", { id: this.id, message: message }, { eventTarget }, principal); } else if (!message) { Cu.reportError("Failed to send a WebChannel message. Message not set."); } else { Cu.reportError("Failed to send a WebChannel message. Target invalid."); } }, /** * Deliver WebChannel messages to the set "_channelCallback" * * @param data {Object} * Message data * @param sendingContext {Object} * Message sending context. * @param sendingContext.browser {browser} * The <browser> object that captured the * WebChannelMessageToChrome. * @param sendingContext.eventTarget {EventTarget} * The <EventTarget> where the message was sent. * @param sendingContext.principal {Principal} * The <Principal> of the EventTarget where the message was sent. * */ deliver: function(data, sendingContext) { if (this._deliverCallback) { try { this._deliverCallback(data.id, data.message, sendingContext); } catch (ex) { this.send({ errno: ERRNO_UNKNOWN_ERROR, error: ex.message ? ex.message : ERROR_UNKNOWN }, sendingContext); Cu.reportError("Failed to execute WebChannel callback:"); Cu.reportError(ex); } } else { Cu.reportError("No callback set for this channel."); } } };