// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Contains the class which uses native messaging * facilities for cross domain communication. * */ goog.provide('goog.net.xpc.NativeMessagingTransport'); goog.require('goog.Timer'); goog.require('goog.asserts'); goog.require('goog.async.Deferred'); goog.require('goog.events'); goog.require('goog.events.EventHandler'); goog.require('goog.log'); goog.require('goog.net.xpc'); goog.require('goog.net.xpc.CrossPageChannelRole'); goog.require('goog.net.xpc.Transport'); goog.require('goog.net.xpc.TransportTypes'); /** * The native messaging transport * * Uses document.postMessage() to send messages to other documents. * Receiving is done by listening on 'message'-events on the document. * * @param {goog.net.xpc.CrossPageChannel} channel The channel this * transport belongs to. * @param {string} peerHostname The hostname (protocol, domain, and port) of the * peer. * @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for * finding the correct window/document. * @param {boolean=} opt_oneSidedHandshake If this is true, only the outer * transport sends a SETUP message and expects a SETUP_ACK. The inner * transport goes connected when it receives the SETUP. * @param {number=} opt_protocolVersion Which version of its setup protocol the * transport should use. The default is '2'. * @constructor * @extends {goog.net.xpc.Transport} * @final */ goog.net.xpc.NativeMessagingTransport = function( channel, peerHostname, opt_domHelper, opt_oneSidedHandshake, opt_protocolVersion) { goog.net.xpc.NativeMessagingTransport.base( this, 'constructor', opt_domHelper); /** * The channel this transport belongs to. * @type {goog.net.xpc.CrossPageChannel} * @private */ this.channel_ = channel; /** * Which version of the transport's protocol should be used. * @type {number} * @private */ this.protocolVersion_ = opt_protocolVersion || 2; goog.asserts.assert(this.protocolVersion_ >= 1); goog.asserts.assert(this.protocolVersion_ <= 2); /** * The hostname of the peer. This parameterizes all calls to postMessage, and * should contain the precise protocol, domain, and port of the peer window. * @type {string} * @private */ this.peerHostname_ = peerHostname || '*'; /** * The event handler. * @type {!goog.events.EventHandler} * @private */ this.eventHandler_ = new goog.events.EventHandler(this); /** * Timer for connection reattempts. * @type {!goog.Timer} * @private */ this.maybeAttemptToConnectTimer_ = new goog.Timer(100, this.getWindow()); /** * Whether one-sided handshakes are enabled. * @type {boolean} * @private */ this.oneSidedHandshake_ = !!opt_oneSidedHandshake; /** * Fires once we've received our SETUP_ACK message. * @type {!goog.async.Deferred} * @private */ this.setupAckReceived_ = new goog.async.Deferred(); /** * Fires once we've sent our SETUP_ACK message. * @type {!goog.async.Deferred} * @private */ this.setupAckSent_ = new goog.async.Deferred(); /** * Fires once we're marked connected. * @type {!goog.async.Deferred} * @private */ this.connected_ = new goog.async.Deferred(); /** * The unique ID of this side of the connection. Used to determine when a peer * is reloaded. * @type {string} * @private */ this.endpointId_ = goog.net.xpc.getRandomString(10); /** * The unique ID of the peer. If we get a message from a peer with an ID we * don't expect, we reset the connection. * @type {?string} * @private */ this.peerEndpointId_ = null; // We don't want to mark ourselves connected until we have sent whatever // message will cause our counterpart in the other frame to also declare // itself connected, if there is such a message. Otherwise we risk a user // message being sent in advance of that message, and it being discarded. if (this.oneSidedHandshake_) { if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) { // One sided handshake, inner frame: // SETUP_ACK must be received. this.connected_.awaitDeferred(this.setupAckReceived_); } else { // One sided handshake, outer frame: // SETUP_ACK must be sent. this.connected_.awaitDeferred(this.setupAckSent_); } } else { // Two sided handshake: // SETUP_ACK has to have been received, and sent. this.connected_.awaitDeferred(this.setupAckReceived_); if (this.protocolVersion_ == 2) { this.connected_.awaitDeferred(this.setupAckSent_); } } this.connected_.addCallback(this.notifyConnected_, this); this.connected_.callback(true); this.eventHandler_.listen( this.maybeAttemptToConnectTimer_, goog.Timer.TICK, this.maybeAttemptToConnect_); goog.log.info( goog.net.xpc.logger, 'NativeMessagingTransport created. ' + 'protocolVersion=' + this.protocolVersion_ + ', oneSidedHandshake=' + this.oneSidedHandshake_ + ', role=' + this.channel_.getRole()); }; goog.inherits(goog.net.xpc.NativeMessagingTransport, goog.net.xpc.Transport); /** * Length of the delay in milliseconds between the channel being connected and * the connection callback being called, in cases where coverage of timing flaws * is required. * @type {number} * @private */ goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ = 200; /** * Current determination of peer's protocol version, or null for unknown. * @type {?number} * @private */ goog.net.xpc.NativeMessagingTransport.prototype.peerProtocolVersion_ = null; /** * Flag indicating if this instance of the transport has been initialized. * @type {boolean} * @private */ goog.net.xpc.NativeMessagingTransport.prototype.initialized_ = false; /** * The transport type. * @type {number} * @override */ goog.net.xpc.NativeMessagingTransport.prototype.transportType = goog.net.xpc.TransportTypes.NATIVE_MESSAGING; /** * The delimiter used for transport service messages. * @type {string} * @private */ goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_ = ','; /** * Tracks the number of NativeMessagingTransport channels that have been * initialized but not disposed yet in a map keyed by the UID of the window * object. This allows for multiple windows to be initiallized and listening * for messages. * @type {Object} * @private */ goog.net.xpc.NativeMessagingTransport.activeCount_ = {}; /** * Id of a timer user during postMessage sends. * @type {number} * @private */ goog.net.xpc.NativeMessagingTransport.prototype.sendTimerId_ = 0; /** * Checks whether the peer transport protocol version could be as indicated. * @param {number} version The version to check for. * @return {boolean} Whether the peer transport protocol version is as * indicated, or null. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.couldPeerVersionBe_ = function( version) { return this.peerProtocolVersion_ == null || this.peerProtocolVersion_ == version; }; /** * Initializes this transport. Registers a listener for 'message'-events * on the document. * @param {Window} listenWindow The window to listen to events on. * @private */ goog.net.xpc.NativeMessagingTransport.initialize_ = function(listenWindow) { var uid = goog.getUid(listenWindow); var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid]; if (!goog.isNumber(value)) { value = 0; } if (value == 0) { // Listen for message-events. These are fired on window in FF3 and on // document in Opera. goog.events.listen( listenWindow.postMessage ? listenWindow : listenWindow.document, 'message', goog.net.xpc.NativeMessagingTransport.messageReceived_, false, goog.net.xpc.NativeMessagingTransport); } goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value + 1; }; /** * Processes an incoming message-event. * @param {goog.events.BrowserEvent} msgEvt The message event. * @return {boolean} True if message was successfully delivered to a channel. * @private */ goog.net.xpc.NativeMessagingTransport.messageReceived_ = function(msgEvt) { var data = msgEvt.getBrowserEvent().data; if (!goog.isString(data)) { return false; } var headDelim = data.indexOf('|'); var serviceDelim = data.indexOf(':'); // make sure we got something reasonable if (headDelim == -1 || serviceDelim == -1) { return false; } var channelName = data.substring(0, headDelim); var service = data.substring(headDelim + 1, serviceDelim); var payload = data.substring(serviceDelim + 1); goog.log.fine( goog.net.xpc.logger, 'messageReceived: channel=' + channelName + ', service=' + service + ', payload=' + payload); // Attempt to deliver message to the channel. Keep in mind that it may not // exist for several reasons, including but not limited to: // - a malformed message // - the channel simply has not been created // - channel was created in a different namespace // - message was sent to the wrong window // - channel has become stale (e.g. caching iframes and back clicks) var channel = goog.net.xpc.channels[channelName]; if (channel) { channel.xpcDeliver( service, payload, /** @type {!MessageEvent} */ (msgEvt.getBrowserEvent()).origin); return true; } var transportMessageType = goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload)[0]; // Check if there are any stale channel names that can be updated. for (var staleChannelName in goog.net.xpc.channels) { var staleChannel = goog.net.xpc.channels[staleChannelName]; if (staleChannel.getRole() == goog.net.xpc.CrossPageChannelRole.INNER && !staleChannel.isConnected() && service == goog.net.xpc.TRANSPORT_SERVICE_ && (transportMessageType == goog.net.xpc.SETUP || transportMessageType == goog.net.xpc.SETUP_NTPV2) && staleChannel.isMessageOriginAcceptable( msgEvt.getBrowserEvent().origin)) { // Inner peer received SETUP message but channel names did not match. // Start using the channel name sent from outer peer. The channel name // of the inner peer can easily become out of date, as iframe's and their // JS state get cached in many browsers upon page reload or history // navigation (particularly Firefox 1.5+). We can trust the outer peer, // since we only accept postMessage messages from the same hostname that // originally setup the channel. staleChannel.updateChannelNameAndCatalog(channelName); staleChannel.xpcDeliver(service, payload); return true; } } // Failed to find a channel to deliver this message to, so simply ignore it. goog.log.info(goog.net.xpc.logger, 'channel name mismatch; message ignored"'); return false; }; /** * Handles transport service messages. * @param {string} payload The message content. * @override */ goog.net.xpc.NativeMessagingTransport.prototype.transportServiceHandler = function(payload) { var transportParts = goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload); var transportMessageType = transportParts[0]; var peerEndpointId = transportParts[1]; switch (transportMessageType) { case goog.net.xpc.SETUP_ACK_: this.setPeerProtocolVersion_(1); if (!this.setupAckReceived_.hasFired()) { this.setupAckReceived_.callback(true); } break; case goog.net.xpc.SETUP_ACK_NTPV2: if (this.protocolVersion_ == 2) { this.setPeerProtocolVersion_(2); if (!this.setupAckReceived_.hasFired()) { this.setupAckReceived_.callback(true); } } break; case goog.net.xpc.SETUP: this.setPeerProtocolVersion_(1); this.sendSetupAckMessage_(1); break; case goog.net.xpc.SETUP_NTPV2: if (this.protocolVersion_ == 2) { var prevPeerProtocolVersion = this.peerProtocolVersion_; this.setPeerProtocolVersion_(2); this.sendSetupAckMessage_(2); if ((prevPeerProtocolVersion == 1 || this.peerEndpointId_ != null) && this.peerEndpointId_ != peerEndpointId) { // Send a new SETUP message since the peer has been replaced. goog.log.info( goog.net.xpc.logger, 'Sending SETUP and changing peer ID to: ' + peerEndpointId); this.sendSetupMessage_(); } this.peerEndpointId_ = peerEndpointId; } break; } }; /** * Sends a SETUP transport service message of the correct protocol number for * our current situation. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.sendSetupMessage_ = function() { // 'real' (legacy) v1 transports don't know about there being v2 ones out // there, and we shouldn't either. goog.asserts.assert( !(this.protocolVersion_ == 1 && this.peerProtocolVersion_ == 2)); if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2)) { var payload = goog.net.xpc.SETUP_NTPV2; payload += goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_; payload += this.endpointId_; this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload); } // For backward compatibility reasons, the V1 SETUP message can be sent by // both V1 and V2 transports. Once a V2 transport has 'heard' another V2 // transport it starts ignoring V1 messages, so the V2 message must be sent // first. if (this.couldPeerVersionBe_(1)) { this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP); } }; /** * Sends a SETUP_ACK transport service message of the correct protocol number * for our current situation. * @param {number} protocolVersion The protocol version of the SETUP message * which gave rise to this ack message. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.sendSetupAckMessage_ = function( protocolVersion) { goog.asserts.assert( this.protocolVersion_ != 1 || protocolVersion != 2, 'Shouldn\'t try to send a v2 setup ack in v1 mode.'); if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2) && protocolVersion == 2) { this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_NTPV2); } else if (this.couldPeerVersionBe_(1) && protocolVersion == 1) { this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_); } else { return; } if (!this.setupAckSent_.hasFired()) { this.setupAckSent_.callback(true); } }; /** * Attempts to set the peer protocol number. Downgrades from 2 to 1 are not * permitted. * @param {number} version The new protocol number. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.setPeerProtocolVersion_ = function(version) { if (version > this.peerProtocolVersion_) { this.peerProtocolVersion_ = version; } if (this.peerProtocolVersion_ == 1) { if (!this.setupAckSent_.hasFired() && !this.oneSidedHandshake_) { this.setupAckSent_.callback(true); } this.peerEndpointId_ = null; } }; /** * Connects this transport. * @override */ goog.net.xpc.NativeMessagingTransport.prototype.connect = function() { goog.net.xpc.NativeMessagingTransport.initialize_(this.getWindow()); this.initialized_ = true; this.maybeAttemptToConnect_(); }; /** * Connects to other peer. In the case of the outer peer, the setup messages are * likely sent before the inner peer is ready to receive them. Therefore, this * function will continue trying to send the SETUP message until the inner peer * responds. In the case of the inner peer, it will occasionally have its * channel name fall out of sync with the outer peer, particularly during * soft-reloads and history navigations. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.maybeAttemptToConnect_ = function() { // In a one-sided handshake, the outer frame does not send a SETUP message, // but the inner frame does. var outerFrame = this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER; if ((this.oneSidedHandshake_ && outerFrame) || this.channel_.isConnected() || this.isDisposed()) { this.maybeAttemptToConnectTimer_.stop(); return; } this.maybeAttemptToConnectTimer_.start(); this.sendSetupMessage_(); }; /** * Sends a message. * @param {string} service The name off the service the message is to be * delivered to. * @param {string} payload The message content. * @override */ goog.net.xpc.NativeMessagingTransport.prototype.send = function( service, payload) { var win = this.channel_.getPeerWindowObject(); if (!win) { goog.log.fine(goog.net.xpc.logger, 'send(): window not ready'); return; } this.send = function(service, payload) { // In IE8 (and perhaps elsewhere), it seems like postMessage is sometimes // implemented as a synchronous call. That is, calling it synchronously // calls whatever listeners it has, and control is not returned to the // calling thread until those listeners are run. This produces different // ordering to all other browsers, and breaks this protocol. This timer // callback is introduced to produce standard behavior across all browsers. var transport = this; var channelName = this.channel_.name; var sendFunctor = function() { transport.sendTimerId_ = 0; try { // postMessage is a method of the window object, except in some // versions of Opera, where it is a method of the document object. It // also seems that the appearance of postMessage on the peer window // object can sometimes be delayed. var obj = win.postMessage ? win : win.document; if (!obj.postMessage) { goog.log.warning( goog.net.xpc.logger, 'Peer window had no postMessage function.'); return; } obj.postMessage( channelName + '|' + service + ':' + payload, transport.peerHostname_); goog.log.fine( goog.net.xpc.logger, 'send(): service=' + service + ' payload=' + payload + ' to hostname=' + transport.peerHostname_); } catch (error) { // There is some evidence (not totally convincing) that postMessage can // be missing or throw errors during a narrow timing window during // startup. This protects against that. goog.log.warning( goog.net.xpc.logger, 'Error performing postMessage, ignoring.', error); } }; this.sendTimerId_ = goog.Timer.callOnce(sendFunctor, 0); }; this.send(service, payload); }; /** * Notify the channel that this transport is connected. If either transport is * protocol v1, a short delay is required to paper over timing vulnerabilities * in that protocol version. * @private */ goog.net.xpc.NativeMessagingTransport.prototype.notifyConnected_ = function() { var delay = (this.protocolVersion_ == 1 || this.peerProtocolVersion_ == 1) ? goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ : undefined; this.channel_.notifyConnected(delay); }; /** @override */ goog.net.xpc.NativeMessagingTransport.prototype.disposeInternal = function() { if (this.initialized_) { var listenWindow = this.getWindow(); var uid = goog.getUid(listenWindow); var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid]; goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value - 1; if (value == 1) { goog.events.unlisten( listenWindow.postMessage ? listenWindow : listenWindow.document, 'message', goog.net.xpc.NativeMessagingTransport.messageReceived_, false, goog.net.xpc.NativeMessagingTransport); } } if (this.sendTimerId_) { goog.Timer.clear(this.sendTimerId_); this.sendTimerId_ = 0; } goog.dispose(this.eventHandler_); delete this.eventHandler_; goog.dispose(this.maybeAttemptToConnectTimer_); delete this.maybeAttemptToConnectTimer_; this.setupAckReceived_.cancel(); delete this.setupAckReceived_; this.setupAckSent_.cancel(); delete this.setupAckSent_; this.connected_.cancel(); delete this.connected_; // Cleaning up this.send as it is an instance method, created in // goog.net.xpc.NativeMessagingTransport.prototype.send and has a closure over // this.channel_.peerWindowObject_. delete this.send; goog.net.xpc.NativeMessagingTransport.base(this, 'disposeInternal'); }; /** * Parse a transport service payload message. For v1, it is simply expected to * be 'SETUP' or 'SETUP_ACK'. For v2, an example setup message is * 'SETUP_NTPV2,abc123', where the second part is the endpoint id. The v2 setup * ack message is simply 'SETUP_ACK_NTPV2'. * @param {string} payload The payload. * @return {!Array} An array with the message type as the first member * and the endpoint id as the second, if one was sent, or null otherwise. * @private */ goog.net.xpc.NativeMessagingTransport.parseTransportPayload_ = function( payload) { var transportParts = /** @type {!Array} */ ( payload.split(goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_)); transportParts[1] = transportParts[1] || null; return transportParts; };