nixtransport.js

// Copyright 2008 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 NIX (Native IE XDC) method transport for
 * cross-domain communication. It exploits the fact that Internet Explorer
 * allows a window that is the parent of an iframe to set said iframe window's
 * opener property to an object. This object can be a function that in turn
 * can be used to send a message despite same-origin constraints. Note that
 * this function, if a pure JavaScript object, opens up the possibilitiy of
 * gaining a hold of the context of the other window and in turn, attacking
 * it. This implementation therefore wraps the JavaScript objects used inside
 * a VBScript class. Since VBScript objects are passed in JavaScript as a COM
 * wrapper (like DOM objects), they are thus opaque to JavaScript
 * (except for the interface they expose). This therefore provides a safe
 * method of transport.
 *
 *
 * Initially based on FrameElementTransport which shares some similarities
 * to this method.
 */

goog.provide('goog.net.xpc.NixTransport');

goog.require('goog.log');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.reflect');



/**
 * NIX method transport.
 *
 * NOTE(user): NIX method tested in all IE versions starting from 6.0.
 *
 * @param {goog.net.xpc.CrossPageChannel} channel The channel this transport
 *     belongs to.
 * @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
 *     the correct window.
 * @constructor
 * @extends {goog.net.xpc.Transport}
 * @final
 */
goog.net.xpc.NixTransport = function(channel, opt_domHelper) {
  goog.net.xpc.NixTransport.base(this, 'constructor', opt_domHelper);

  /**
   * The channel this transport belongs to.
   * @type {goog.net.xpc.CrossPageChannel}
   * @private
   */
  this.channel_ = channel;

  /**
   * The authorization token, if any, used by this transport.
   * @type {?string}
   * @private
   */
  this.authToken_ = channel[goog.net.xpc.CfgFields.AUTH_TOKEN] || '';

  /**
   * The authorization token, if any, that must be sent by the other party
   * for setup to occur.
   * @type {?string}
   * @private
   */
  this.remoteAuthToken_ =
      channel[goog.net.xpc.CfgFields.REMOTE_AUTH_TOKEN] || '';

  // Conduct the setup work for NIX in general, if need be.
  goog.net.xpc.NixTransport.conductGlobalSetup_(this.getWindow());

  // Setup aliases so that VBScript can call these methods
  // on the transport class, even if they are renamed during
  // compression.
  this[goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE] = this.handleMessage_;
  this[goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL] = this.createChannel_;
};
goog.inherits(goog.net.xpc.NixTransport, goog.net.xpc.Transport);


// Consts for NIX. VBScript doesn't allow items to start with _ for some
// reason, so we need to make these names quite unique, as they will go into
// the global namespace.


/**
 * Global name of the Wrapper VBScript class.
 * Note that this class will be stored in the *global*
 * namespace (i.e. window in browsers).
 * @type {string}
 */
goog.net.xpc.NixTransport.NIX_WRAPPER = 'GCXPC____NIXVBS_wrapper';


/**
 * Global name of the GetWrapper VBScript function. This
 * constant is used by JavaScript to call this function.
 * Note that this function will be stored in the *global*
 * namespace (i.e. window in browsers).
 * @type {string}
 */
goog.net.xpc.NixTransport.NIX_GET_WRAPPER = 'GCXPC____NIXVBS_get_wrapper';


/**
 * The name of the handle message method used by the wrapper class
 * when calling the transport.
 * @type {string}
 */
goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE = 'GCXPC____NIXJS_handle_message';


/**
 * The name of the create channel method used by the wrapper class
 * when calling the transport.
 * @type {string}
 */
goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL = 'GCXPC____NIXJS_create_channel';


/**
 * A "unique" identifier that is stored in the wrapper
 * class so that the wrapper can be distinguished from
 * other objects easily.
 * @type {string}
 */
goog.net.xpc.NixTransport.NIX_ID_FIELD = 'GCXPC____NIXVBS_container';


/**
 * Determines if the installed version of IE supports accessing window.opener
 * after it has been set to a non-Window/null value. NIX relies on this being
 * possible.
 * @return {boolean} Whether window.opener behavior is compatible with NIX.
 */
goog.net.xpc.NixTransport.isNixSupported = function() {
  var isSupported = false;
  try {
    var oldOpener = window.opener;
    // The compiler complains (as it should!) if we set window.opener to
    // something other than a window or null.
    window.opener = /** @type {Window} */ ({});
    isSupported = goog.reflect.canAccessProperty(window, 'opener');
    window.opener = oldOpener;
  } catch (e) { }
  return isSupported;
};


/**
 * Conducts the global setup work for the NIX transport method.
 * This function creates and then injects into the page the
 * VBScript code necessary to create the NIX wrapper class.
 * Note that this method can be called multiple times, as
 * it internally checks whether the work is necessary before
 * proceeding.
 * @param {Window} listenWindow The window containing the affected page.
 * @private
 */
goog.net.xpc.NixTransport.conductGlobalSetup_ = function(listenWindow) {
  if (listenWindow['nix_setup_complete']) {
    return;
  }

  // Inject the VBScript code needed.
  var vbscript =
      // We create a class to act as a wrapper for
      // a Javascript call, to prevent a break in of
      // the context.
      'Class ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n ' +

      // An internal member for keeping track of the
      // transport for which this wrapper exists.
      'Private m_Transport\n' +

      // An internal member for keeping track of the
      // auth token associated with the context that
      // created this wrapper. Used for validation
      // purposes.
      'Private m_Auth\n' +

      // Method for internally setting the value
      // of the m_Transport property. We have the
      // isEmpty check to prevent the transport
      // from being overridden with an illicit
      // object by a malicious party.
      'Public Sub SetTransport(transport)\n' +
      'If isEmpty(m_Transport) Then\n' +
      'Set m_Transport = transport\n' +
      'End If\n' +
      'End Sub\n' +

      // Method for internally setting the value
      // of the m_Auth property. We have the
      // isEmpty check to prevent the transport
      // from being overridden with an illicit
      // object by a malicious party.
      'Public Sub SetAuth(auth)\n' +
      'If isEmpty(m_Auth) Then\n' +
      'm_Auth = auth\n' +
      'End If\n' +
      'End Sub\n' +

      // Returns the auth token to the gadget, so it can
      // confirm a match before initiating the connection
      'Public Function GetAuthToken()\n ' +
      'GetAuthToken = m_Auth\n' +
      'End Function\n' +

      // A wrapper method which causes a
      // message to be sent to the other context.
      'Public Sub SendMessage(service, payload)\n ' +
      'Call m_Transport.' +
      goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE + '(service, payload)\n' +
      'End Sub\n' +

      // Method for setting up the inner->outer
      // channel.
      'Public Sub CreateChannel(channel)\n ' +
      'Call m_Transport.' +
      goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL + '(channel)\n' +
      'End Sub\n' +

      // An empty field with a unique identifier to
      // prevent the code from confusing this wrapper
      // with a run-of-the-mill value found in window.opener.
      'Public Sub ' + goog.net.xpc.NixTransport.NIX_ID_FIELD + '()\n ' +
      'End Sub\n' +
      'End Class\n ' +

      // Function to get a reference to the wrapper.
      'Function ' +
      goog.net.xpc.NixTransport.NIX_GET_WRAPPER + '(transport, auth)\n' +
      'Dim wrap\n' +
      'Set wrap = New ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n' +
      'wrap.SetTransport transport\n' +
      'wrap.SetAuth auth\n' +
      'Set ' + goog.net.xpc.NixTransport.NIX_GET_WRAPPER + ' = wrap\n' +
      'End Function';

  try {
    listenWindow.execScript(vbscript, 'vbscript');
    listenWindow['nix_setup_complete'] = true;
  }
  catch (e) {
    goog.log.error(goog.net.xpc.logger,
        'exception caught while attempting global setup: ' + e);
  }
};


/**
 * The transport type.
 * @type {number}
 * @protected
 * @override
 */
goog.net.xpc.NixTransport.prototype.transportType =
    goog.net.xpc.TransportTypes.NIX;


/**
 * Keeps track of whether the local setup has completed (i.e.
 * the initial work towards setting the channel up has been
 * completed for this end).
 * @type {boolean}
 * @private
 */
goog.net.xpc.NixTransport.prototype.localSetupCompleted_ = false;


/**
 * The NIX channel used to talk to the other page. This
 * object is in fact a reference to a VBScript class
 * (see above) and as such, is in fact a COM wrapper.
 * When using this object, make sure to not access methods
 * without calling them, otherwise a COM error will be thrown.
 * @type {Object}
 * @private
 */
goog.net.xpc.NixTransport.prototype.nixChannel_ = null;


/**
 * Connect this transport.
 * @override
 */
goog.net.xpc.NixTransport.prototype.connect = function() {
  if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
    this.attemptOuterSetup_();
  } else {
    this.attemptInnerSetup_();
  }
};


/**
 * Attempts to setup the channel from the perspective
 * of the outer (read: container) page. This method
 * will attempt to create a NIX wrapper for this transport
 * and place it into the "opener" property of the inner
 * page's window object. If it fails, it will continue
 * to loop until it does so.
 *
 * @private
 */
goog.net.xpc.NixTransport.prototype.attemptOuterSetup_ = function() {
  if (this.localSetupCompleted_) {
    return;
  }

  // Get shortcut to iframe-element that contains the inner
  // page.
  var innerFrame = this.channel_.getIframeElement();

  try {
    // Attempt to place the NIX wrapper object into the inner
    // frame's opener property.
    var theWindow = this.getWindow();
    var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
    innerFrame.contentWindow.opener = getWrapper(this, this.authToken_);
    this.localSetupCompleted_ = true;
  }
  catch (e) {
    goog.log.error(goog.net.xpc.logger,
        'exception caught while attempting setup: ' + e);
  }

  // If the retry is necessary, reattempt this setup.
  if (!this.localSetupCompleted_) {
    this.getWindow().setTimeout(goog.bind(this.attemptOuterSetup_, this), 100);
  }
};


/**
 * Attempts to setup the channel from the perspective
 * of the inner (read: iframe) page. This method
 * will attempt to *read* the opener object from the
 * page's opener property. If it succeeds, this object
 * is saved into nixChannel_ and the channel is confirmed
 * with the container by calling CreateChannel with an instance
 * of a wrapper for *this* page. Note that if this method
 * fails, it will continue to loop until it succeeds.
 *
 * @private
 */
goog.net.xpc.NixTransport.prototype.attemptInnerSetup_ = function() {
  if (this.localSetupCompleted_) {
    return;
  }

  try {
    var opener = this.getWindow().opener;

    // Ensure that the object contained inside the opener
    // property is in fact a NIX wrapper.
    if (opener && goog.net.xpc.NixTransport.NIX_ID_FIELD in opener) {
      this.nixChannel_ = opener;

      // Ensure that the NIX channel given to use is valid.
      var remoteAuthToken = this.nixChannel_['GetAuthToken']();

      if (remoteAuthToken != this.remoteAuthToken_) {
        goog.log.error(goog.net.xpc.logger,
            'Invalid auth token from other party');
        return;
      }

      // Complete the construction of the channel by sending our own
      // wrapper to the container via the channel they gave us.
      var theWindow = this.getWindow();
      var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
      this.nixChannel_['CreateChannel'](getWrapper(this, this.authToken_));

      this.localSetupCompleted_ = true;

      // Notify channel that the transport is ready.
      this.channel_.notifyConnected();
    }
  }
  catch (e) {
    goog.log.error(goog.net.xpc.logger,
        'exception caught while attempting setup: ' + e);
    return;
  }

  // If the retry is necessary, reattempt this setup.
  if (!this.localSetupCompleted_) {
    this.getWindow().setTimeout(goog.bind(this.attemptInnerSetup_, this), 100);
  }
};


/**
 * Internal method called by the inner page, via the
 * NIX wrapper, to complete the setup of the channel.
 *
 * @param {Object} channel The NIX wrapper of the
 *  inner page.
 * @private
 */
goog.net.xpc.NixTransport.prototype.createChannel_ = function(channel) {
  // Verify that the channel is in fact a NIX wrapper.
  if (typeof channel != 'unknown' ||
      !(goog.net.xpc.NixTransport.NIX_ID_FIELD in channel)) {
    goog.log.error(goog.net.xpc.logger,
        'Invalid NIX channel given to createChannel_');
  }

  this.nixChannel_ = channel;

  // Ensure that the NIX channel given to use is valid.
  var remoteAuthToken = this.nixChannel_['GetAuthToken']();

  if (remoteAuthToken != this.remoteAuthToken_) {
    goog.log.error(goog.net.xpc.logger, 'Invalid auth token from other party');
    return;
  }

  // Indicate to the CrossPageChannel that the channel is setup
  // and ready to use.
  this.channel_.notifyConnected();
};


/**
 * Internal method called by the other page, via the NIX wrapper,
 * to deliver a message.
 * @param {string} serviceName The name of the service the message is to be
 *   delivered to.
 * @param {string} payload The message to process.
 * @private
 */
goog.net.xpc.NixTransport.prototype.handleMessage_ =
    function(serviceName, payload) {
  /** @this {goog.net.xpc.NixTransport} */
  var deliveryHandler = function() {
    this.channel_.xpcDeliver(serviceName, payload);
  };
  this.getWindow().setTimeout(goog.bind(deliveryHandler, this), 1);
};


/**
 * Sends a message.
 * @param {string} service The name of the service the message is to be
 *   delivered to.
 * @param {string} payload The message content.
 * @override
 */
goog.net.xpc.NixTransport.prototype.send = function(service, payload) {
  // Verify that the NIX channel we have is valid.
  if (typeof(this.nixChannel_) !== 'unknown') {
    goog.log.error(goog.net.xpc.logger, 'NIX channel not connected');
  }

  // Send the message via the NIX wrapper object.
  this.nixChannel_['SendMessage'](service, payload);
};


/** @override */
goog.net.xpc.NixTransport.prototype.disposeInternal = function() {
  goog.net.xpc.NixTransport.base(this, 'disposeInternal');
  this.nixChannel_ = null;
};