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 364 var Port = utils.expose('Port', PortImpl, { functions: [ 365 'disconnect', 366 'postMessage' 367 ], 368 properties: [ 369 'name', 370 'onDisconnect', 371 'onMessage' 372 ] }); 373 374 exports.kRequestChannel = kRequestChannel; 375 exports.kMessageChannel = kMessageChannel; 376 exports.kNativeMessageChannel = kNativeMessageChannel; 377 exports.Port = Port; 378 exports.createPort = createPort; 379 exports.sendMessageImpl = sendMessageImpl; 380 exports.sendMessageUpdateArguments = sendMessageUpdateArguments; 381 382 // For C++ code to call. 383 exports.hasPort = hasPort; 384 exports.dispatchOnConnect = dispatchOnConnect; 385 exports.dispatchOnDisconnect = dispatchOnDisconnect; 386 exports.dispatchOnMessage = dispatchOnMessage; 387