Javascript  |  298行  |  9.34 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Authenticator class wraps the communications between Gaia and its host.
 */
function Authenticator() {
}

/**
 * Gaia auth extension url origin.
 * @type {string}
 */
Authenticator.THIS_EXTENSION_ORIGIN =
    'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';

/**
 * Singleton getter of Authenticator.
 * @return {Object} The singleton instance of Authenticator.
 */
Authenticator.getInstance = function() {
  if (!Authenticator.instance_) {
    Authenticator.instance_ = new Authenticator();
  }
  return Authenticator.instance_;
};

Authenticator.prototype = {
  email_: null,
  password_: null,
  attemptToken_: null,

  // Input params from extension initialization URL.
  inputLang_: undefined,
  intputEmail_: undefined,

  samlPageLoaded_: false,
  samlSupportChannel_: null,

  GAIA_URL: 'https://accounts.google.com/',
  GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
  PARENT_PAGE: 'chrome://oobe/',
  SERVICE_ID: 'chromeoslogin',
  CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',

  initialize: function() {
    var params = getUrlSearchParams(location.search);
    this.parentPage_ = params.parentPage || this.PARENT_PAGE;
    this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
    this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
    this.inputLang_ = params.hl;
    this.inputEmail_ = params.email;
    this.service_ = params.service || this.SERVICE_ID;
    this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
    this.continueUrlWithoutParams_ = stripParams(this.continueUrl_);
    this.inlineMode_ = params.inlineMode == '1';
    this.constrained_ = params.constrained == '1';
    this.partitionId_ = params.partitionId || '';
    this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
    this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
    this.loaded_ = false;

    document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this));
    document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
  },

  isGaiaMessage_: function(msg) {
    // Not quite right, but good enough.
    return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
           this.GAIA_URL.indexOf(msg.origin) == 0;
  },

  isInternalMessage_: function(msg) {
    return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
  },

  isParentMessage_: function(msg) {
    return msg.origin == this.parentPage_;
  },

  constructInitialFrameUrl_: function() {
    var url = this.gaiaUrl_ + this.gaiaPath_;

    url = appendParam(url, 'service', this.service_);
    url = appendParam(url, 'continue', this.continueUrl_);
    if (this.inputLang_)
      url = appendParam(url, 'hl', this.inputLang_);
    if (this.inputEmail_)
      url = appendParam(url, 'Email', this.inputEmail_);

    return url;
  },

  /** Callback when all loads in the gaia webview is complete. */
  onWebviewLoadstop_: function(gaiaFrame) {
    // Report the current state to the parent which will then update the
    // browser history so that later it could respond properly to back/forward.
    var msg = {
      'method': 'reportState',
      'src': gaiaFrame.src
    };
    window.parent.postMessage(msg, this.parentPage_);

    if (gaiaFrame.src.lastIndexOf(
        this.continueUrlWithoutParams_, 0) == 0) {
      // Detect when login is finished by the load stop event of the continue
      // URL. Cannot reuse the login complete flow in success.html, because
      // webview does not support extension pages yet.
      gaiaFrame.hidden = true;
      msg = {'method': 'completeLogin'};
      window.parent.postMessage(msg, this.parentPage_);
      return;
    }

    if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
      gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
        // Send an initial message to gaia so that it has an JavaScript
        // reference to the embedder.
        gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
      });
    }

    this.loaded_ || this.onLoginUILoaded();
  },

  /**
   * Callback when the gaia webview attempts to open a new window.
   */
  onWebviewNewWindow_: function(gaiaFrame, e) {
    window.open(e.targetUrl, '_blank');
    e.window.discard();
  },

  onWebviewRequestCompleted_: function(details) {
    if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
      return;
    }

    var headers = details.responseHeaders;
    for (var i = 0; headers && i < headers.length; ++i) {
      if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
        return;
      }
    }
    var msg = {
      'method': 'switchToFullTab',
      'url': details.url
    };
    window.parent.postMessage(msg, this.parentPage_);
  },

  loadFrame_: function() {
    var gaiaFrame = $('gaia-frame');
    gaiaFrame.partition = this.partitionId_;
    gaiaFrame.src = this.initialFrameUrl_;
    if (this.inlineMode_) {
      gaiaFrame.addEventListener(
          'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame));
      gaiaFrame.addEventListener(
          'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame));
    }
    if (this.constrained_) {
      gaiaFrame.request.onCompleted.addListener(
          this.onWebviewRequestCompleted_.bind(this),
          {urls: ['<all_urls>'], types: ['main_frame']},
          ['responseHeaders']);
    }
  },

  completeLogin: function(username, password) {
    var msg = {
      'method': 'completeLogin',
      'email': username,
      'password': password
    };
    window.parent.postMessage(msg, this.parentPage_);
    if (this.samlSupportChannel_)
      this.samlSupportChannel_.send({name: 'resetAuth'});
  },

  onPageLoad: function(e) {
    window.addEventListener('message', this.onMessage.bind(this), false);
    this.loadFrame_();
  },

  /**
   * Invoked when 'enableSAML' event is received to initialize SAML support.
   */
  onEnableSAML_: function() {
    this.samlPageLoaded_ = false;

    this.samlSupportChannel_ = new Channel();
    this.samlSupportChannel_.connect('authMain');
    this.samlSupportChannel_.registerMessage(
        'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
    this.samlSupportChannel_.send({
      name: 'setGaiaUrl',
      gaiaUrl: this.gaiaUrl_
    });
  },

  /**
   * Invoked when the background page sends 'onHostedPageLoaded' message.
   * @param {!Object} msg Details sent with the message.
   */
  onAuthPageLoaded_: function(msg) {
    this.samlPageLoaded_ = msg.url.indexOf(this.gaiaUrl_) != 0;
    window.parent.postMessage({
      'method': 'authPageLoaded',
      'isSAML': this.samlPageLoaded_
    }, this.parentPage_);
  },

  onLoginUILoaded: function() {
    var msg = {
      'method': 'loginUILoaded'
    };
    window.parent.postMessage(msg, this.parentPage_);
    if (this.inlineMode_) {
      $('gaia-frame').focus();
    }
    this.loaded_ = true;
  },

  onConfirmLogin_: function() {
    if (!this.samlPageLoaded_) {
      this.completeLogin(this.email_, this.password_);
      return;
    }

    this.samlSupportChannel_.sendWithCallback(
        {name: 'getScrapedPasswords'},
        function(passwords) {
          if (passwords.length == 0) {
            window.parent.postMessage(
                {method: 'noPassword', email: this.email_},
                this.parentPage_);
          } else {
            window.parent.postMessage(
                {method: 'confirmPassword', email: this.email_},
                this.parentPage_);
          }
        }.bind(this));
  },

  onVerifyConfirmedPassword_: function(password) {
    this.samlSupportChannel_.sendWithCallback(
        {name: 'getScrapedPasswords'},
        function(passwords) {
          for (var i = 0; i < passwords.length; ++i) {
            if (passwords[i] == password) {
              this.completeLogin(this.email_, passwords[i]);
              return;
            }
          }
          window.parent.postMessage(
              {method: 'confirmPassword', email: this.email_},
              this.parentPage_);
        }.bind(this));
  },

  onMessage: function(e) {
    var msg = e.data;
    if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
      this.email_ = msg.email;
      this.password_ = msg.password;
      this.attemptToken_ = msg.attemptToken;
      this.samlPageLoaded_ = false;
      if (this.samlSupportChannel_)
        this.samlSupportChannel_.send({name: 'startAuth'});
    } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
      this.email_ = null;
      this.password_ = null;
      this.attemptToken_ = null;
      this.samlPageLoaded_ = false;
      this.onLoginUILoaded();
      if (this.samlSupportChannel_)
        this.samlSupportChannel_.send({name: 'resetAuth'});
    } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
      if (this.attemptToken_ == msg.attemptToken)
        this.onConfirmLogin_();
      else
        console.error('Authenticator.onMessage: unexpected attemptToken!?');
    } else if (msg.method == 'verifyConfirmedPassword' &&
               this.isParentMessage_(e)) {
      this.onVerifyConfirmedPassword_(msg.password);
    } else if (msg.method == 'navigate' &&
               this.isParentMessage_(e)) {
       $('gaia-frame').src = msg.src;
    } else {
      console.error('Authenticator.onMessage: unknown message + origin!?');
    }
  }
};

Authenticator.getInstance().initialize();