summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/WebChannel.sys.mjs
blob: 1b5d725cbd15e581b174fb6724d6c7ed5428a839 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/* 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.
 */

const ERRNO_UNKNOWN_ERROR = 999;
const ERROR_UNKNOWN = "UNKNOWN_ERROR";

/**
 * WebChannelBroker is a global object that helps manage WebChannel objects.
 * This object handles channel registration, origin validation and message multiplexing.
 */

export var WebChannelBroker = Object.create({
  /**
   * Register a new channel that callbacks messages
   * based on proper origin and channel name
   *
   * @param channel {WebChannel}
   */
  registerChannel(channel) {
    if (!this._channelMap.has(channel)) {
      this._channelMap.set(channel);
    } else {
      console.error("Failed to register the channel. Channel already exists.");
    }
  },

  /**
   * Unregister a channel
   *
   * @param channelToRemove {WebChannel}
   *        WebChannel to remove from the channel map
   *
   * Removes the specified channel from the channel map
   */
  unregisterChannel(channelToRemove) {
    if (!this._channelMap.delete(channelToRemove)) {
      console.error("Failed to unregister the channel. Channel not found.");
    }
  },

  /**
   * Object to store pairs of message origins and callback functions
   */
  _channelMap: new Map(),

  /**
   * Deliver a message to a registered channel.
   *
   * @returns bool whether we managed to find a registered channel.
   */
  tryToDeliver(data, sendingContext) {
    let validChannelFound = false;
    data.message = data.message || {};

    for (var channel of this._channelMap.keys()) {
      if (
        channel.id === data.id &&
        channel._originCheckCallback(sendingContext.principal)
      ) {
        validChannelFound = true;
        channel.deliver(data, sendingContext);
      }
    }
    return validChannelFound;
  },
});

/**
 * 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, incoming events will be accepted from any origin matching
 *        that URI's origin.
 *        If a string, it names a permission, and incoming events will be accepted
 *        from any https:// origin that has been granted that permission by the
 *        permission manager.
 * @constructor
 */
export var 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 => {
      // Accept events from any secure origin having the named permission.
      // 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);
      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 {
    // Accept events from any origin matching the given URI.
    // We deliberately use `originNoSuffix` here because we only want to
    // restrict based on the site's origin, not on other origin attributes
    // such as containers or private browsing.
    this._originCheckCallback = requestPrincipal => {
      return originOrPermission.prePath === requestPrincipal.originNoSuffix;
    };
  }
  this._originOrPermission = originOrPermission;
};

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(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() {
    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.browsingContext {BrowsingContext}
   *               The browsingContext we should send the message to.
   *        @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. Can be null; if not null, should be
   *               a ContentDOMReference.
   */
  send(message, target) {
    let { browsingContext, principal, eventTarget } = target;

    if (message && browsingContext && principal) {
      let { currentWindowGlobal } = browsingContext;
      if (!currentWindowGlobal) {
        console.error(
          "Failed to send a WebChannel message. No currentWindowGlobal."
        );
        return;
      }
      currentWindowGlobal
        .getActor("WebChannel")
        .sendAsyncMessage("WebChannelMessageToContent", {
          id: this.id,
          message,
          eventTarget,
          principal,
        });
    } else if (!message) {
      console.error("Failed to send a WebChannel message. Message not set.");
    } else {
      console.error("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.browsingContext {BrowsingContext}
   *               The browsingcontext from which the
   *               WebChannelMessageToChrome was sent.
   *        @param sendingContext.eventTarget {EventTarget}
   *               The <EventTarget> where the message was sent.
   *               Can be null; if not null, should be a ContentDOMReference.
   *        @param sendingContext.principal {Principal}
   *               The <Principal> of the EventTarget where the message was sent.
   *
   */
  deliver(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
        );
        console.error("Failed to execute WebChannel callback:");
        console.error(ex);
      }
    } else {
      console.error("No callback set for this channel.");
    }
  },
};