diff options
Diffstat (limited to 'toolkit/modules/WebChannel.jsm')
-rw-r--r-- | toolkit/modules/WebChannel.jsm | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/toolkit/modules/WebChannel.jsm b/toolkit/modules/WebChannel.jsm new file mode 100644 index 000000000..a32bea0d4 --- /dev/null +++ b/toolkit/modules/WebChannel.jsm @@ -0,0 +1,328 @@ +/* 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."); + } + } +}; |