• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// chrome.runtime.messaging API implementation.
6
7  // TODO(kalman): factor requiring chrome out of here.
8  var chrome = requireNative('chrome').GetChrome();
9  var Event = require('event_bindings').Event;
10  var lastError = require('lastError');
11  var logActivity = requireNative('activityLogger');
12  var logging = requireNative('logging');
13  var messagingNatives = requireNative('messaging_natives');
14  var processNatives = requireNative('process');
15  var unloadEvent = require('unload_event');
16  var utils = require('utils');
17  var messagingUtils = require('messaging_utils');
18
19  // The reserved channel name for the sendRequest/send(Native)Message APIs.
20  // Note: sendRequest is deprecated.
21  var kRequestChannel = "chrome.extension.sendRequest";
22  var kMessageChannel = "chrome.runtime.sendMessage";
23  var kNativeMessageChannel = "chrome.runtime.sendNativeMessage";
24
25  // Map of port IDs to port object.
26  var ports = {};
27
28  // Map of port IDs to unloadEvent listeners. Keep track of these to free the
29  // unloadEvent listeners when ports are closed.
30  var portReleasers = {};
31
32  // Change even to odd and vice versa, to get the other side of a given
33  // channel.
34  function getOppositePortId(portId) { return portId ^ 1; }
35
36  // Port object.  Represents a connection to another script context through
37  // which messages can be passed.
38  function PortImpl(portId, opt_name) {
39    this.portId_ = portId;
40    this.name = opt_name;
41
42    var portSchema = {name: 'port', $ref: 'runtime.Port'};
43    var options = {unmanaged: true};
44    this.onDisconnect = new Event(null, [portSchema], options);
45    this.onMessage = new Event(
46        null,
47        [{name: 'message', type: 'any', optional: true}, portSchema],
48        options);
49    this.onDestroy_ = null;
50  }
51
52  // Sends a message asynchronously to the context on the other end of this
53  // port.
54  PortImpl.prototype.postMessage = function(msg) {
55    // JSON.stringify doesn't support a root object which is undefined.
56    if (msg === undefined)
57      msg = null;
58    msg = $JSON.stringify(msg);
59    if (msg === undefined) {
60      // JSON.stringify can fail with unserializable objects. Log an error and
61      // drop the message.
62      //
63      // TODO(kalman/mpcomplete): it would be better to do the same validation
64      // here that we do for runtime.sendMessage (and variants), i.e. throw an
65      // schema validation Error, but just maintain the old behaviour until
66      // there's a good reason not to (http://crbug.com/263077).
67      console.error('Illegal argument to Port.postMessage');
68      return;
69    }
70    messagingNatives.PostMessage(this.portId_, msg);
71  };
72
73  // Disconnects the port from the other end.
74  PortImpl.prototype.disconnect = function() {
75    messagingNatives.CloseChannel(this.portId_, true);
76    this.destroy_();
77  };
78
79  PortImpl.prototype.destroy_ = function() {
80    var portId = this.portId_;
81
82    if (this.onDestroy_)
83      this.onDestroy_();
84    privates(this.onDisconnect).impl.destroy_();
85    privates(this.onMessage).impl.destroy_();
86
87    messagingNatives.PortRelease(portId);
88    unloadEvent.removeListener(portReleasers[portId]);
89
90    delete ports[portId];
91    delete portReleasers[portId];
92  };
93
94  // Returns true if the specified port id is in this context. This is used by
95  // the C++ to avoid creating the javascript message for all the contexts that
96  // don't care about a particular message.
97  function hasPort(portId) {
98    return portId in ports;
99  };
100
101  // Hidden port creation function.  We don't want to expose an API that lets
102  // people add arbitrary port IDs to the port list.
103  function createPort(portId, opt_name) {
104    if (ports[portId])
105      throw new Error("Port '" + portId + "' already exists.");
106    var port = new Port(portId, opt_name);
107    ports[portId] = port;
108    portReleasers[portId] = $Function.bind(messagingNatives.PortRelease,
109                                           this,
110                                           portId);
111    unloadEvent.addListener(portReleasers[portId]);
112    messagingNatives.PortAddRef(portId);
113    return port;
114  };
115
116  // Helper function for dispatchOnRequest.
117  function handleSendRequestError(isSendMessage,
118                                  responseCallbackPreserved,
119                                  sourceExtensionId,
120                                  targetExtensionId,
121                                  sourceUrl) {
122    var errorMsg = [];
123    var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
124    if (isSendMessage && !responseCallbackPreserved) {
125      $Array.push(errorMsg,
126          "The chrome." + eventName + " listener must return true if you " +
127          "want to send a response after the listener returns");
128    } else {
129      $Array.push(errorMsg,
130          "Cannot send a response more than once per chrome." + eventName +
131          " listener per document");
132    }
133    $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId);
134    if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId)
135      $Array.push(errorMsg, "for extension " + targetExtensionId);
136    if (sourceUrl != "")
137      $Array.push(errorMsg, "for URL " + sourceUrl);
138    lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome);
139  }
140
141  // Helper function for dispatchOnConnect
142  function dispatchOnRequest(portId, channelName, sender,
143                             sourceExtensionId, targetExtensionId, sourceUrl,
144                             isExternal) {
145    var isSendMessage = channelName == kMessageChannel;
146    var requestEvent = null;
147    if (isSendMessage) {
148      if (chrome.runtime) {
149        requestEvent = isExternal ? chrome.runtime.onMessageExternal
150                                  : chrome.runtime.onMessage;
151      }
152    } else {
153      if (chrome.extension) {
154        requestEvent = isExternal ? chrome.extension.onRequestExternal
155                                  : chrome.extension.onRequest;
156      }
157    }
158    if (!requestEvent)
159      return false;
160    if (!requestEvent.hasListeners())
161      return false;
162    var port = createPort(portId, channelName);
163
164    function messageListener(request) {
165      var responseCallbackPreserved = false;
166      var responseCallback = function(response) {
167        if (port) {
168          port.postMessage(response);
169          privates(port).impl.destroy_();
170          port = null;
171        } else {
172          // We nulled out port when sending the response, and now the page
173          // is trying to send another response for the same request.
174          handleSendRequestError(isSendMessage, responseCallbackPreserved,
175                                 sourceExtensionId, targetExtensionId);
176        }
177      };
178      // In case the extension never invokes the responseCallback, and also
179      // doesn't keep a reference to it, we need to clean up the port. Do
180      // so by attaching to the garbage collection of the responseCallback
181      // using some native hackery.
182      messagingNatives.BindToGC(responseCallback, function() {
183        if (port) {
184          privates(port).impl.destroy_();
185          port = null;
186        }
187      });
188      var rv = requestEvent.dispatch(request, sender, responseCallback);
189      if (isSendMessage) {
190        responseCallbackPreserved =
191            rv && rv.results && $Array.indexOf(rv.results, true) > -1;
192        if (!responseCallbackPreserved && port) {
193          // If they didn't access the response callback, they're not
194          // going to send a response, so clean up the port immediately.
195          privates(port).impl.destroy_();
196          port = null;
197        }
198      }
199    }
200
201    privates(port).impl.onDestroy_ = function() {
202      port.onMessage.removeListener(messageListener);
203    };
204    port.onMessage.addListener(messageListener);
205
206    var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest";
207    if (isExternal)
208      eventName += "External";
209    logActivity.LogEvent(targetExtensionId,
210                         eventName,
211                         [sourceExtensionId, sourceUrl]);
212    return true;
213  }
214
215  // Called by native code when a channel has been opened to this context.
216  function dispatchOnConnect(portId,
217                             channelName,
218                             sourceTab,
219                             sourceExtensionId,
220                             targetExtensionId,
221                             sourceUrl,
222                             tlsChannelId) {
223    // Only create a new Port if someone is actually listening for a connection.
224    // In addition to being an optimization, this also fixes a bug where if 2
225    // channels were opened to and from the same process, closing one would
226    // close both.
227    var extensionId = processNatives.GetExtensionId();
228
229    // messaging_bindings.cc should ensure that this method only gets called for
230    // the right extension.
231    logging.CHECK(targetExtensionId == extensionId);
232
233    if (ports[getOppositePortId(portId)])
234      return false;  // this channel was opened by us, so ignore it
235
236    // Determine whether this is coming from another extension, so we can use
237    // the right event.
238    var isExternal = sourceExtensionId != extensionId;
239
240    var sender = {};
241    if (sourceExtensionId != '')
242      sender.id = sourceExtensionId;
243    if (sourceUrl)
244      sender.url = sourceUrl;
245    if (sourceTab)
246      sender.tab = sourceTab;
247    if (tlsChannelId !== undefined)
248      sender.tlsChannelId = tlsChannelId;
249
250    // Special case for sendRequest/onRequest and sendMessage/onMessage.
251    if (channelName == kRequestChannel || channelName == kMessageChannel) {
252      return dispatchOnRequest(portId, channelName, sender,
253                               sourceExtensionId, targetExtensionId, sourceUrl,
254                               isExternal);
255    }
256
257    var connectEvent = null;
258    if (chrome.runtime) {
259      connectEvent = isExternal ? chrome.runtime.onConnectExternal
260                                : chrome.runtime.onConnect;
261    }
262    if (!connectEvent)
263      return false;
264    if (!connectEvent.hasListeners())
265      return false;
266
267    var port = createPort(portId, channelName);
268    port.sender = sender;
269    if (processNatives.manifestVersion < 2)
270      port.tab = port.sender.tab;
271
272    var eventName = (isExternal ?
273        "runtime.onConnectExternal" : "runtime.onConnect");
274    connectEvent.dispatch(port);
275    logActivity.LogEvent(targetExtensionId,
276                         eventName,
277                         [sourceExtensionId]);
278    return true;
279  };
280
281  // Called by native code when a channel has been closed.
282  function dispatchOnDisconnect(portId, errorMessage) {
283    var port = ports[portId];
284    if (port) {
285      // Update the renderer's port bookkeeping, without notifying the browser.
286      messagingNatives.CloseChannel(portId, false);
287      if (errorMessage)
288        lastError.set('Port', errorMessage, null, chrome);
289      try {
290        port.onDisconnect.dispatch(port);
291      } finally {
292        privates(port).impl.destroy_();
293        lastError.clear(chrome);
294      }
295    }
296  };
297
298  // Called by native code when a message has been sent to the given port.
299  function dispatchOnMessage(msg, portId) {
300    var port = ports[portId];
301    if (port) {
302      if (msg)
303        msg = $JSON.parse(msg);
304      port.onMessage.dispatch(msg, port);
305    }
306  };
307
308  // Shared implementation used by tabs.sendMessage and runtime.sendMessage.
309  function sendMessageImpl(port, request, responseCallback) {
310    if (port.name != kNativeMessageChannel)
311      port.postMessage(request);
312
313    if (port.name == kMessageChannel && !responseCallback) {
314      // TODO(mpcomplete): Do this for the old sendRequest API too, after
315      // verifying it doesn't break anything.
316      // Go ahead and disconnect immediately if the sender is not expecting
317      // a response.
318      port.disconnect();
319      return;
320    }
321
322    // Ensure the callback exists for the older sendRequest API.
323    if (!responseCallback)
324      responseCallback = function() {};
325
326    // Note: make sure to manually remove the onMessage/onDisconnect listeners
327    // that we added before destroying the Port, a workaround to a bug in Port
328    // where any onMessage/onDisconnect listeners added but not removed will
329    // be leaked when the Port is destroyed.
330    // http://crbug.com/320723 tracks a sustainable fix.
331
332    function disconnectListener() {
333      // For onDisconnects, we only notify the callback if there was an error.
334      if (chrome.runtime && chrome.runtime.lastError)
335        responseCallback();
336    }
337
338    function messageListener(response) {
339      try {
340        responseCallback(response);
341      } finally {
342        port.disconnect();
343      }
344    }
345
346    privates(port).impl.onDestroy_ = function() {
347      port.onDisconnect.removeListener(disconnectListener);
348      port.onMessage.removeListener(messageListener);
349    };
350    port.onDisconnect.addListener(disconnectListener);
351    port.onMessage.addListener(messageListener);
352  };
353
354  function sendMessageUpdateArguments(functionName, hasOptionsArgument) {
355    // skip functionName and hasOptionsArgument
356    var args = $Array.slice(arguments, 2);
357    var alignedArgs = messagingUtils.alignSendMessageArguments(args,
358        hasOptionsArgument);
359    if (!alignedArgs)
360      throw new Error('Invalid arguments to ' + functionName + '.');
361    return alignedArgs;
362  }
363
364var Port = utils.expose('Port', PortImpl, { functions: [
365    'disconnect',
366    'postMessage'
367  ],
368  properties: [
369    'name',
370    'onDisconnect',
371    'onMessage'
372  ] });
373
374exports.kRequestChannel = kRequestChannel;
375exports.kMessageChannel = kMessageChannel;
376exports.kNativeMessageChannel = kNativeMessageChannel;
377exports.Port = Port;
378exports.createPort = createPort;
379exports.sendMessageImpl = sendMessageImpl;
380exports.sendMessageUpdateArguments = sendMessageUpdateArguments;
381
382// For C++ code to call.
383exports.hasPort = hasPort;
384exports.dispatchOnConnect = dispatchOnConnect;
385exports.dispatchOnDisconnect = dispatchOnDisconnect;
386exports.dispatchOnMessage = dispatchOnMessage;
387