summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/WebChannel.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/WebChannel.jsm')
-rw-r--r--toolkit/modules/WebChannel.jsm328
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.");
+ }
+ }
+};