text: // 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<!goog.net.xpc.NativeMessagingTransport>}
     * @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<number>}
   * @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)) {
        // 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<?string>} 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<?string>} */ (payload.split(
        goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_));
    transportParts[1] = transportParts[1] || null;
    return transportParts;
  };
  