Path: blob/trunk/third_party/closure/goog/net/xpc/nativemessagingtransport.js
1865 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Contains the class which uses native messaging16* facilities for cross domain communication.17*18*/192021goog.provide('goog.net.xpc.NativeMessagingTransport');2223goog.require('goog.Timer');24goog.require('goog.asserts');25goog.require('goog.async.Deferred');26goog.require('goog.events');27goog.require('goog.events.EventHandler');28goog.require('goog.log');29goog.require('goog.net.xpc');30goog.require('goog.net.xpc.CrossPageChannelRole');31goog.require('goog.net.xpc.Transport');32goog.require('goog.net.xpc.TransportTypes');33343536/**37* The native messaging transport38*39* Uses document.postMessage() to send messages to other documents.40* Receiving is done by listening on 'message'-events on the document.41*42* @param {goog.net.xpc.CrossPageChannel} channel The channel this43* transport belongs to.44* @param {string} peerHostname The hostname (protocol, domain, and port) of the45* peer.46* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for47* finding the correct window/document.48* @param {boolean=} opt_oneSidedHandshake If this is true, only the outer49* transport sends a SETUP message and expects a SETUP_ACK. The inner50* transport goes connected when it receives the SETUP.51* @param {number=} opt_protocolVersion Which version of its setup protocol the52* transport should use. The default is '2'.53* @constructor54* @extends {goog.net.xpc.Transport}55* @final56*/57goog.net.xpc.NativeMessagingTransport = function(58channel, peerHostname, opt_domHelper, opt_oneSidedHandshake,59opt_protocolVersion) {60goog.net.xpc.NativeMessagingTransport.base(61this, 'constructor', opt_domHelper);6263/**64* The channel this transport belongs to.65* @type {goog.net.xpc.CrossPageChannel}66* @private67*/68this.channel_ = channel;6970/**71* Which version of the transport's protocol should be used.72* @type {number}73* @private74*/75this.protocolVersion_ = opt_protocolVersion || 2;76goog.asserts.assert(this.protocolVersion_ >= 1);77goog.asserts.assert(this.protocolVersion_ <= 2);7879/**80* The hostname of the peer. This parameterizes all calls to postMessage, and81* should contain the precise protocol, domain, and port of the peer window.82* @type {string}83* @private84*/85this.peerHostname_ = peerHostname || '*';8687/**88* The event handler.89* @type {!goog.events.EventHandler<!goog.net.xpc.NativeMessagingTransport>}90* @private91*/92this.eventHandler_ = new goog.events.EventHandler(this);9394/**95* Timer for connection reattempts.96* @type {!goog.Timer}97* @private98*/99this.maybeAttemptToConnectTimer_ = new goog.Timer(100, this.getWindow());100101/**102* Whether one-sided handshakes are enabled.103* @type {boolean}104* @private105*/106this.oneSidedHandshake_ = !!opt_oneSidedHandshake;107108/**109* Fires once we've received our SETUP_ACK message.110* @type {!goog.async.Deferred}111* @private112*/113this.setupAckReceived_ = new goog.async.Deferred();114115/**116* Fires once we've sent our SETUP_ACK message.117* @type {!goog.async.Deferred}118* @private119*/120this.setupAckSent_ = new goog.async.Deferred();121122/**123* Fires once we're marked connected.124* @type {!goog.async.Deferred}125* @private126*/127this.connected_ = new goog.async.Deferred();128129/**130* The unique ID of this side of the connection. Used to determine when a peer131* is reloaded.132* @type {string}133* @private134*/135this.endpointId_ = goog.net.xpc.getRandomString(10);136137/**138* The unique ID of the peer. If we get a message from a peer with an ID we139* don't expect, we reset the connection.140* @type {?string}141* @private142*/143this.peerEndpointId_ = null;144145// We don't want to mark ourselves connected until we have sent whatever146// message will cause our counterpart in the other frame to also declare147// itself connected, if there is such a message. Otherwise we risk a user148// message being sent in advance of that message, and it being discarded.149if (this.oneSidedHandshake_) {150if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) {151// One sided handshake, inner frame:152// SETUP_ACK must be received.153this.connected_.awaitDeferred(this.setupAckReceived_);154} else {155// One sided handshake, outer frame:156// SETUP_ACK must be sent.157this.connected_.awaitDeferred(this.setupAckSent_);158}159} else {160// Two sided handshake:161// SETUP_ACK has to have been received, and sent.162this.connected_.awaitDeferred(this.setupAckReceived_);163if (this.protocolVersion_ == 2) {164this.connected_.awaitDeferred(this.setupAckSent_);165}166}167this.connected_.addCallback(this.notifyConnected_, this);168this.connected_.callback(true);169170this.eventHandler_.listen(171this.maybeAttemptToConnectTimer_, goog.Timer.TICK,172this.maybeAttemptToConnect_);173174goog.log.info(175goog.net.xpc.logger, 'NativeMessagingTransport created. ' +176'protocolVersion=' + this.protocolVersion_ + ', oneSidedHandshake=' +177this.oneSidedHandshake_ + ', role=' + this.channel_.getRole());178};179goog.inherits(goog.net.xpc.NativeMessagingTransport, goog.net.xpc.Transport);180181182/**183* Length of the delay in milliseconds between the channel being connected and184* the connection callback being called, in cases where coverage of timing flaws185* is required.186* @type {number}187* @private188*/189goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ = 200;190191192/**193* Current determination of peer's protocol version, or null for unknown.194* @type {?number}195* @private196*/197goog.net.xpc.NativeMessagingTransport.prototype.peerProtocolVersion_ = null;198199200/**201* Flag indicating if this instance of the transport has been initialized.202* @type {boolean}203* @private204*/205goog.net.xpc.NativeMessagingTransport.prototype.initialized_ = false;206207208/**209* The transport type.210* @type {number}211* @override212*/213goog.net.xpc.NativeMessagingTransport.prototype.transportType =214goog.net.xpc.TransportTypes.NATIVE_MESSAGING;215216217/**218* The delimiter used for transport service messages.219* @type {string}220* @private221*/222goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_ = ',';223224225/**226* Tracks the number of NativeMessagingTransport channels that have been227* initialized but not disposed yet in a map keyed by the UID of the window228* object. This allows for multiple windows to be initiallized and listening229* for messages.230* @type {Object<number>}231* @private232*/233goog.net.xpc.NativeMessagingTransport.activeCount_ = {};234235236/**237* Id of a timer user during postMessage sends.238* @type {number}239* @private240*/241goog.net.xpc.NativeMessagingTransport.prototype.sendTimerId_ = 0;242243244/**245* Checks whether the peer transport protocol version could be as indicated.246* @param {number} version The version to check for.247* @return {boolean} Whether the peer transport protocol version is as248* indicated, or null.249* @private250*/251goog.net.xpc.NativeMessagingTransport.prototype.couldPeerVersionBe_ = function(252version) {253return this.peerProtocolVersion_ == null ||254this.peerProtocolVersion_ == version;255};256257258/**259* Initializes this transport. Registers a listener for 'message'-events260* on the document.261* @param {Window} listenWindow The window to listen to events on.262* @private263*/264goog.net.xpc.NativeMessagingTransport.initialize_ = function(listenWindow) {265var uid = goog.getUid(listenWindow);266var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];267if (!goog.isNumber(value)) {268value = 0;269}270if (value == 0) {271// Listen for message-events. These are fired on window in FF3 and on272// document in Opera.273goog.events.listen(274listenWindow.postMessage ? listenWindow : listenWindow.document,275'message', goog.net.xpc.NativeMessagingTransport.messageReceived_,276false, goog.net.xpc.NativeMessagingTransport);277}278goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value + 1;279};280281282/**283* Processes an incoming message-event.284* @param {goog.events.BrowserEvent} msgEvt The message event.285* @return {boolean} True if message was successfully delivered to a channel.286* @private287*/288goog.net.xpc.NativeMessagingTransport.messageReceived_ = function(msgEvt) {289var data = msgEvt.getBrowserEvent().data;290291if (!goog.isString(data)) {292return false;293}294295var headDelim = data.indexOf('|');296var serviceDelim = data.indexOf(':');297298// make sure we got something reasonable299if (headDelim == -1 || serviceDelim == -1) {300return false;301}302303var channelName = data.substring(0, headDelim);304var service = data.substring(headDelim + 1, serviceDelim);305var payload = data.substring(serviceDelim + 1);306307goog.log.fine(308goog.net.xpc.logger, 'messageReceived: channel=' + channelName +309', service=' + service + ', payload=' + payload);310311// Attempt to deliver message to the channel. Keep in mind that it may not312// exist for several reasons, including but not limited to:313// - a malformed message314// - the channel simply has not been created315// - channel was created in a different namespace316// - message was sent to the wrong window317// - channel has become stale (e.g. caching iframes and back clicks)318var channel = goog.net.xpc.channels[channelName];319if (channel) {320channel.xpcDeliver(321service, payload,322/** @type {!MessageEvent} */ (msgEvt.getBrowserEvent()).origin);323return true;324}325326var transportMessageType =327goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload)[0];328329// Check if there are any stale channel names that can be updated.330for (var staleChannelName in goog.net.xpc.channels) {331var staleChannel = goog.net.xpc.channels[staleChannelName];332if (staleChannel.getRole() == goog.net.xpc.CrossPageChannelRole.INNER &&333!staleChannel.isConnected() &&334service == goog.net.xpc.TRANSPORT_SERVICE_ &&335(transportMessageType == goog.net.xpc.SETUP ||336transportMessageType == goog.net.xpc.SETUP_NTPV2) &&337staleChannel.isMessageOriginAcceptable(338msgEvt.getBrowserEvent().origin)) {339// Inner peer received SETUP message but channel names did not match.340// Start using the channel name sent from outer peer. The channel name341// of the inner peer can easily become out of date, as iframe's and their342// JS state get cached in many browsers upon page reload or history343// navigation (particularly Firefox 1.5+). We can trust the outer peer,344// since we only accept postMessage messages from the same hostname that345// originally setup the channel.346staleChannel.updateChannelNameAndCatalog(channelName);347staleChannel.xpcDeliver(service, payload);348return true;349}350}351352// Failed to find a channel to deliver this message to, so simply ignore it.353goog.log.info(goog.net.xpc.logger, 'channel name mismatch; message ignored"');354return false;355};356357358/**359* Handles transport service messages.360* @param {string} payload The message content.361* @override362*/363goog.net.xpc.NativeMessagingTransport.prototype.transportServiceHandler =364function(payload) {365var transportParts =366goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload);367var transportMessageType = transportParts[0];368var peerEndpointId = transportParts[1];369switch (transportMessageType) {370case goog.net.xpc.SETUP_ACK_:371this.setPeerProtocolVersion_(1);372if (!this.setupAckReceived_.hasFired()) {373this.setupAckReceived_.callback(true);374}375break;376case goog.net.xpc.SETUP_ACK_NTPV2:377if (this.protocolVersion_ == 2) {378this.setPeerProtocolVersion_(2);379if (!this.setupAckReceived_.hasFired()) {380this.setupAckReceived_.callback(true);381}382}383break;384case goog.net.xpc.SETUP:385this.setPeerProtocolVersion_(1);386this.sendSetupAckMessage_(1);387break;388case goog.net.xpc.SETUP_NTPV2:389if (this.protocolVersion_ == 2) {390var prevPeerProtocolVersion = this.peerProtocolVersion_;391this.setPeerProtocolVersion_(2);392this.sendSetupAckMessage_(2);393if ((prevPeerProtocolVersion == 1 || this.peerEndpointId_ != null) &&394this.peerEndpointId_ != peerEndpointId) {395// Send a new SETUP message since the peer has been replaced.396goog.log.info(397goog.net.xpc.logger,398'Sending SETUP and changing peer ID to: ' + peerEndpointId);399this.sendSetupMessage_();400}401this.peerEndpointId_ = peerEndpointId;402}403break;404}405};406407408/**409* Sends a SETUP transport service message of the correct protocol number for410* our current situation.411* @private412*/413goog.net.xpc.NativeMessagingTransport.prototype.sendSetupMessage_ = function() {414// 'real' (legacy) v1 transports don't know about there being v2 ones out415// there, and we shouldn't either.416goog.asserts.assert(417!(this.protocolVersion_ == 1 && this.peerProtocolVersion_ == 2));418419if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2)) {420var payload = goog.net.xpc.SETUP_NTPV2;421payload += goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_;422payload += this.endpointId_;423this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload);424}425426// For backward compatibility reasons, the V1 SETUP message can be sent by427// both V1 and V2 transports. Once a V2 transport has 'heard' another V2428// transport it starts ignoring V1 messages, so the V2 message must be sent429// first.430if (this.couldPeerVersionBe_(1)) {431this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP);432}433};434435436/**437* Sends a SETUP_ACK transport service message of the correct protocol number438* for our current situation.439* @param {number} protocolVersion The protocol version of the SETUP message440* which gave rise to this ack message.441* @private442*/443goog.net.xpc.NativeMessagingTransport.prototype.sendSetupAckMessage_ = function(444protocolVersion) {445goog.asserts.assert(446this.protocolVersion_ != 1 || protocolVersion != 2,447'Shouldn\'t try to send a v2 setup ack in v1 mode.');448if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2) &&449protocolVersion == 2) {450this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_NTPV2);451} else if (this.couldPeerVersionBe_(1) && protocolVersion == 1) {452this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);453} else {454return;455}456457if (!this.setupAckSent_.hasFired()) {458this.setupAckSent_.callback(true);459}460};461462463/**464* Attempts to set the peer protocol number. Downgrades from 2 to 1 are not465* permitted.466* @param {number} version The new protocol number.467* @private468*/469goog.net.xpc.NativeMessagingTransport.prototype.setPeerProtocolVersion_ =470function(version) {471if (version > this.peerProtocolVersion_) {472this.peerProtocolVersion_ = version;473}474if (this.peerProtocolVersion_ == 1) {475if (!this.setupAckSent_.hasFired() && !this.oneSidedHandshake_) {476this.setupAckSent_.callback(true);477}478this.peerEndpointId_ = null;479}480};481482483/**484* Connects this transport.485* @override486*/487goog.net.xpc.NativeMessagingTransport.prototype.connect = function() {488goog.net.xpc.NativeMessagingTransport.initialize_(this.getWindow());489this.initialized_ = true;490this.maybeAttemptToConnect_();491};492493494/**495* Connects to other peer. In the case of the outer peer, the setup messages are496* likely sent before the inner peer is ready to receive them. Therefore, this497* function will continue trying to send the SETUP message until the inner peer498* responds. In the case of the inner peer, it will occasionally have its499* channel name fall out of sync with the outer peer, particularly during500* soft-reloads and history navigations.501* @private502*/503goog.net.xpc.NativeMessagingTransport.prototype.maybeAttemptToConnect_ =504function() {505// In a one-sided handshake, the outer frame does not send a SETUP message,506// but the inner frame does.507var outerFrame =508this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER;509if ((this.oneSidedHandshake_ && outerFrame) || this.channel_.isConnected() ||510this.isDisposed()) {511this.maybeAttemptToConnectTimer_.stop();512return;513}514this.maybeAttemptToConnectTimer_.start();515this.sendSetupMessage_();516};517518519/**520* Sends a message.521* @param {string} service The name off the service the message is to be522* delivered to.523* @param {string} payload The message content.524* @override525*/526goog.net.xpc.NativeMessagingTransport.prototype.send = function(527service, payload) {528var win = this.channel_.getPeerWindowObject();529if (!win) {530goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');531return;532}533534this.send = function(service, payload) {535// In IE8 (and perhaps elsewhere), it seems like postMessage is sometimes536// implemented as a synchronous call. That is, calling it synchronously537// calls whatever listeners it has, and control is not returned to the538// calling thread until those listeners are run. This produces different539// ordering to all other browsers, and breaks this protocol. This timer540// callback is introduced to produce standard behavior across all browsers.541var transport = this;542var channelName = this.channel_.name;543var sendFunctor = function() {544transport.sendTimerId_ = 0;545546try {547// postMessage is a method of the window object, except in some548// versions of Opera, where it is a method of the document object. It549// also seems that the appearance of postMessage on the peer window550// object can sometimes be delayed.551var obj = win.postMessage ? win : win.document;552if (!obj.postMessage) {553goog.log.warning(554goog.net.xpc.logger, 'Peer window had no postMessage function.');555return;556}557558obj.postMessage(559channelName + '|' + service + ':' + payload,560transport.peerHostname_);561goog.log.fine(562goog.net.xpc.logger, 'send(): service=' + service + ' payload=' +563payload + ' to hostname=' + transport.peerHostname_);564} catch (error) {565// There is some evidence (not totally convincing) that postMessage can566// be missing or throw errors during a narrow timing window during567// startup. This protects against that.568goog.log.warning(569goog.net.xpc.logger, 'Error performing postMessage, ignoring.',570error);571}572};573this.sendTimerId_ = goog.Timer.callOnce(sendFunctor, 0);574};575this.send(service, payload);576};577578579/**580* Notify the channel that this transport is connected. If either transport is581* protocol v1, a short delay is required to paper over timing vulnerabilities582* in that protocol version.583* @private584*/585goog.net.xpc.NativeMessagingTransport.prototype.notifyConnected_ = function() {586var delay = (this.protocolVersion_ == 1 || this.peerProtocolVersion_ == 1) ?587goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ :588undefined;589this.channel_.notifyConnected(delay);590};591592593/** @override */594goog.net.xpc.NativeMessagingTransport.prototype.disposeInternal = function() {595if (this.initialized_) {596var listenWindow = this.getWindow();597var uid = goog.getUid(listenWindow);598var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];599goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value - 1;600if (value == 1) {601goog.events.unlisten(602listenWindow.postMessage ? listenWindow : listenWindow.document,603'message', goog.net.xpc.NativeMessagingTransport.messageReceived_,604false, goog.net.xpc.NativeMessagingTransport);605}606}607608if (this.sendTimerId_) {609goog.Timer.clear(this.sendTimerId_);610this.sendTimerId_ = 0;611}612613goog.dispose(this.eventHandler_);614delete this.eventHandler_;615616goog.dispose(this.maybeAttemptToConnectTimer_);617delete this.maybeAttemptToConnectTimer_;618619this.setupAckReceived_.cancel();620delete this.setupAckReceived_;621this.setupAckSent_.cancel();622delete this.setupAckSent_;623this.connected_.cancel();624delete this.connected_;625626// Cleaning up this.send as it is an instance method, created in627// goog.net.xpc.NativeMessagingTransport.prototype.send and has a closure over628// this.channel_.peerWindowObject_.629delete this.send;630631goog.net.xpc.NativeMessagingTransport.base(this, 'disposeInternal');632};633634635/**636* Parse a transport service payload message. For v1, it is simply expected to637* be 'SETUP' or 'SETUP_ACK'. For v2, an example setup message is638* 'SETUP_NTPV2,abc123', where the second part is the endpoint id. The v2 setup639* ack message is simply 'SETUP_ACK_NTPV2'.640* @param {string} payload The payload.641* @return {!Array<?string>} An array with the message type as the first member642* and the endpoint id as the second, if one was sent, or null otherwise.643* @private644*/645goog.net.xpc.NativeMessagingTransport.parseTransportPayload_ = function(646payload) {647var transportParts = /** @type {!Array<?string>} */ (648payload.split(goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_));649transportParts[1] = transportParts[1] || null;650return transportParts;651};652653654