Home | History | Annotate | Download | only in background
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview
      7  *
      8  * It2MeHelpeeChannel relays messages between the Hangouts web page (Hangouts)
      9  * and the It2Me Native Messaging Host (It2MeHost) for the helpee (the Hangouts
     10  * participant who is receiving remoting assistance).
     11  *
     12  * It runs in the background page. It contains a chrome.runtime.Port object,
     13  * representing a connection to Hangouts, and a remoting.It2MeHostFacade object,
     14  * representing a connection to the IT2Me Native Messaging Host.
     15  *
     16  *   Hangouts                       It2MeHelpeeChannel                 It2MeHost
     17  *      |---------runtime.connect()-------->|                              |
     18  *      |-----------hello message---------->|                              |
     19  *      |<-----helloResponse message------->|                              |
     20  *      |----------connect message--------->|                              |
     21  *      |                                   |-----showConfirmDialog()----->|
     22  *      |                                   |----------connect()---------->|
     23  *      |                                   |<-------hostStateChanged------|
     24  *      |                                   |    (RECEIVED_ACCESS_CODE)    |
     25  *      |<---connect response (access code)-|                              |
     26  *      |                                   |                              |
     27  *
     28  * Hangouts will send the access code to the web app on the helper side.
     29  * The helper will then connect to the It2MeHost using the access code.
     30  *
     31  *   Hangouts                       It2MeHelpeeChannel                 It2MeHost
     32  *      |                                   |<-------hostStateChanged------|
     33  *      |                                   |          (CONNECTED)         |
     34  *      |<-- hostStateChanged(CONNECTED)----|                              |
     35  *      |-------disconnect message--------->|                              |
     36  *      |<--hostStateChanged(DISCONNECTED)--|                              |
     37  *
     38  *
     39  * It also handles host downloads and install status queries:
     40  *
     41  *   Hangouts                       It2MeHelpeeChannel
     42  *      |------isHostInstalled message----->|
     43  *      |<-isHostInstalled response(false)--|
     44  *      |                                   |
     45  *      |--------downloadHost message------>|
     46  *      |                                   |
     47  *      |------isHostInstalled message----->|
     48  *      |<-isHostInstalled response(false)--|
     49  *      |                                   |
     50  *      |------isHostInstalled message----->|
     51  *      |<-isHostInstalled response(true)---|
     52  */
     53 
     54 'use strict';
     55 
     56 /** @suppress {duplicate} */
     57 var remoting = remoting || {};
     58 
     59 /**
     60  * @param {chrome.runtime.Port} hangoutPort
     61  * @param {remoting.It2MeHostFacade} host
     62  * @param {remoting.HostInstaller} hostInstaller
     63  * @param {function()} onDisposedCallback Callback to notify the client when
     64  *    the connection is torn down.
     65  *
     66  * @constructor
     67  * @implements {base.Disposable}
     68  */
     69 remoting.It2MeHelpeeChannel =
     70     function(hangoutPort, host, hostInstaller, onDisposedCallback) {
     71   /**
     72    * @type {chrome.runtime.Port}
     73    * @private
     74    */
     75   this.hangoutPort_ = hangoutPort;
     76 
     77   /**
     78    * @type {remoting.It2MeHostFacade}
     79    * @private
     80    */
     81   this.host_ = host;
     82 
     83   /**
     84    * @type {?remoting.HostInstaller}
     85    * @private
     86    */
     87   this.hostInstaller_ = hostInstaller;
     88 
     89   /**
     90    * @type {remoting.HostSession.State}
     91    * @private
     92    */
     93   this.hostState_ = remoting.HostSession.State.UNKNOWN;
     94 
     95   /**
     96    * @type {?function()}
     97    * @private
     98    */
     99   this.onDisposedCallback_ = onDisposedCallback;
    100 
    101   this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this);
    102   this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this);
    103 };
    104 
    105 /** @enum {string} */
    106 remoting.It2MeHelpeeChannel.HangoutMessageTypes = {
    107   CONNECT: 'connect',
    108   CONNECT_RESPONSE: 'connectResponse',
    109   DISCONNECT: 'disconnect',
    110   DOWNLOAD_HOST: 'downloadHost',
    111   ERROR: 'error',
    112   HELLO: 'hello',
    113   HELLO_RESPONSE: 'helloResponse',
    114   HOST_STATE_CHANGED: 'hostStateChanged',
    115   IS_HOST_INSTALLED: 'isHostInstalled',
    116   IS_HOST_INSTALLED_RESPONSE: 'isHostInstalledResponse'
    117 };
    118 
    119 /** @enum {string} */
    120 remoting.It2MeHelpeeChannel.Features = {
    121   REMOTE_ASSISTANCE: 'remoteAssistance'
    122 };
    123 
    124 remoting.It2MeHelpeeChannel.prototype.init = function() {
    125   this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
    126   this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
    127 };
    128 
    129 remoting.It2MeHelpeeChannel.prototype.dispose = function() {
    130   if (this.host_ !== null) {
    131     this.host_.unhookCallbacks();
    132     this.host_.disconnect();
    133     this.host_ = null;
    134   }
    135 
    136   if (this.hangoutPort_ !== null) {
    137     this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_);
    138     this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_);
    139     this.hostState_ = remoting.HostSession.State.DISCONNECTED;
    140 
    141     try {
    142       var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
    143       this.hangoutPort_.postMessage({
    144         method: MessageTypes.HOST_STATE_CHANGED,
    145         state: this.hostState_
    146       });
    147     } catch (e) {
    148       // |postMessage| throws if |this.hangoutPort_| is disconnected
    149       // It is safe to ignore the exception.
    150     }
    151     this.hangoutPort_.disconnect();
    152     this.hangoutPort_ = null;
    153   }
    154 
    155   if (this.onDisposedCallback_ !== null) {
    156     this.onDisposedCallback_();
    157     this.onDisposedCallback_ = null;
    158   }
    159 };
    160 
    161 /**
    162  * Message Handler for incoming runtime messages from Hangouts.
    163  *
    164  * @param {{method:string, data:Object.<string,*>}} message
    165  * @private
    166  */
    167 remoting.It2MeHelpeeChannel.prototype.onHangoutMessage_ = function(message) {
    168   try {
    169     var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
    170     switch (message.method) {
    171       case MessageTypes.HELLO:
    172         this.hangoutPort_.postMessage({
    173           method: MessageTypes.HELLO_RESPONSE,
    174           supportedFeatures: base.values(remoting.It2MeHelpeeChannel.Features)
    175         });
    176         return true;
    177       case MessageTypes.IS_HOST_INSTALLED:
    178         this.handleIsHostInstalled_(message);
    179         return true;
    180       case MessageTypes.DOWNLOAD_HOST:
    181         this.handleDownloadHost_(message);
    182         return true;
    183       case MessageTypes.CONNECT:
    184         this.handleConnect_(message);
    185         return true;
    186       case MessageTypes.DISCONNECT:
    187         this.dispose();
    188         return true;
    189     }
    190     throw new Error('Unsupported message method=' + message.method);
    191   } catch(e) {
    192     var error = /** @type {Error} */ e;
    193     this.sendErrorResponse_(message, error.message);
    194   }
    195   return false;
    196 };
    197 
    198 /**
    199  * Queries the |hostInstaller| for the installation status.
    200  *
    201  * @param {{method:string, data:Object.<string,*>}} message
    202  * @private
    203  */
    204 remoting.It2MeHelpeeChannel.prototype.handleIsHostInstalled_ =
    205     function(message) {
    206   /** @type {remoting.It2MeHelpeeChannel} */
    207   var that = this;
    208 
    209   /** @param {boolean} installed */
    210   function sendResponse(installed) {
    211     var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
    212     that.hangoutPort_.postMessage({
    213       method: MessageTypes.IS_HOST_INSTALLED_RESPONSE,
    214       result: installed
    215     });
    216   }
    217 
    218   this.hostInstaller_.isInstalled().then(
    219     sendResponse,
    220     this.sendErrorResponse_.bind(this, message)
    221   );
    222 };
    223 
    224 /**
    225  * @param {{method:string, data:Object.<string,*>}} message
    226  * @private
    227  */
    228 remoting.It2MeHelpeeChannel.prototype.handleDownloadHost_ = function(message) {
    229   try {
    230     this.hostInstaller_.download();
    231   } catch (e) {
    232     var error = /** @type {Error} */ e;
    233     this.sendErrorResponse_(message, error.message);
    234   }
    235 };
    236 
    237 /**
    238  * Disconnect the session if the |hangoutPort| gets disconnected.
    239  * @private
    240  */
    241 remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() {
    242   this.dispose();
    243 };
    244 
    245 /**
    246  * Connects to the It2Me Native messaging Host and retrieves the access code.
    247  *
    248  * @param {{method:string, data:Object.<string,*>}} message
    249  * @private
    250  */
    251 remoting.It2MeHelpeeChannel.prototype.handleConnect_ =
    252     function(message) {
    253   var email = getStringAttr(message, 'email');
    254 
    255   if (!email) {
    256     throw new Error('Missing required parameter: email');
    257   }
    258 
    259   if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) {
    260     throw new Error('An existing connection is in progress.');
    261   }
    262 
    263   this.showConfirmDialog_().then(
    264     this.initializeHost_.bind(this)
    265   ).then(
    266     this.fetchOAuthToken_.bind(this)
    267   ).then(
    268     this.connectToHost_.bind(this, email),
    269     this.sendErrorResponse_.bind(this, message)
    270   );
    271 };
    272 
    273 /**
    274  * Prompts the user before starting the It2Me Native Messaging Host.  This
    275  * ensures that even if Hangouts is compromised, an attacker cannot start the
    276  * host without explicit user confirmation.
    277  *
    278  * @return {Promise} A promise that resolves to a boolean value, indicating
    279  *     whether the user accepts the remote assistance or not.
    280  * @private
    281  */
    282 remoting.It2MeHelpeeChannel.prototype.showConfirmDialog_ = function() {
    283   if (base.isAppsV2()) {
    284     return this.showConfirmDialogV2_();
    285   } else {
    286     return this.showConfirmDialogV1_();
    287   }
    288 };
    289 
    290 /**
    291  * @return {Promise} A promise that resolves to a boolean value, indicating
    292  *     whether the user accepts the remote assistance or not.
    293  * @private
    294  */
    295 remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV1_ = function() {
    296   var messageHeader = l10n.getTranslationOrError(
    297       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1');
    298   var message1 = l10n.getTranslationOrError(
    299       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2');
    300   var message2 = l10n.getTranslationOrError(
    301       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3');
    302   var message = base.escapeHTML(messageHeader) + '\n' +
    303                 '- ' + base.escapeHTML(message1) + '\n' +
    304                 '- ' + base.escapeHTML(message2) + '\n';
    305 
    306   if(window.confirm(message)) {
    307     return Promise.resolve();
    308   } else {
    309     return Promise.reject(new Error(remoting.Error.CANCELLED));
    310   }
    311 };
    312 
    313 /**
    314  * @return {Promise} A promise that resolves to a boolean value, indicating
    315  *     whether the user accepts the remote assistance or not.
    316  * @private
    317  */
    318 remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV2_ = function() {
    319   var messageHeader = l10n.getTranslationOrError(
    320       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1');
    321   var message1 = l10n.getTranslationOrError(
    322       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2');
    323   var message2 = l10n.getTranslationOrError(
    324       /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3');
    325   var message = '<div>' + base.escapeHTML(messageHeader) + '</div>' +
    326                 '<ul class="insetList">' +
    327                   '<li>' + base.escapeHTML(message1) + '</li>' +
    328                   '<li>' + base.escapeHTML(message2) + '</li>' +
    329                 '</ul>';
    330   /**
    331    * @param {function(*=):void} resolve
    332    * @param {function(*=):void} reject
    333    */
    334   return new Promise(function(resolve, reject) {
    335     /** @param {number} result */
    336     function confirmDialogCallback(result) {
    337       if (result === 1) {
    338         resolve();
    339       } else {
    340         reject(new Error(remoting.Error.CANCELLED));
    341       }
    342     }
    343     remoting.MessageWindow.showConfirmWindow(
    344         '', // Empty string to use the package name as the dialog title.
    345         message,
    346         l10n.getTranslationOrError(
    347             /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_ACCEPT'),
    348         l10n.getTranslationOrError(
    349             /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_DECLINE'),
    350         confirmDialogCallback
    351     );
    352   });
    353 };
    354 
    355 /**
    356  * @return {Promise} A promise that resolves when the host is initialized.
    357  * @private
    358  */
    359 remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() {
    360   /** @type {remoting.It2MeHostFacade} */
    361   var host = this.host_;
    362 
    363   /**
    364    * @param {function(*=):void} resolve
    365    * @param {function(*=):void} reject
    366    */
    367   return new Promise(function(resolve, reject) {
    368     if (host.initialized()) {
    369       resolve();
    370     } else {
    371       host.initialize(resolve, reject);
    372     }
    373   });
    374 };
    375 
    376 /**
    377  * TODO(kelvinp): The existing implementation only works in the v2 app
    378  * We need to implement token fetching for the v1 app using remoting.OAuth2
    379  * before launch (crbug.com/405130).
    380  *
    381  * @return {Promise} Promise that resolves with the OAuth token as the value.
    382  */
    383 remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
    384   if (!base.isAppsV2()) {
    385     throw new Error('fetchOAuthToken_ is not implemented in the v1 app.');
    386   }
    387 
    388   /**
    389    * @param {function(*=):void} resolve
    390    */
    391   return new Promise(function(resolve){
    392     chrome.identity.getAuthToken({ 'interactive': false }, resolve);
    393   });
    394 };
    395 
    396 /**
    397  * Connects to the It2Me Native Messaging Host and retrieves the access code
    398  * in the |onHostStateChanged_| callback.
    399  *
    400  * @param {string} email
    401  * @param {string} accessToken
    402  * @private
    403  */
    404 remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
    405     function(email, accessToken) {
    406   base.debug.assert(this.host_.initialized());
    407   this.host_.connect(
    408     email,
    409     'oauth2:' + accessToken,
    410     this.onHostStateChanged_.bind(this),
    411     base.doNothing, // Ignore |onNatPolicyChanged|.
    412     console.log.bind(console), // Forward logDebugInfo to console.log.
    413     remoting.settings.XMPP_SERVER_ADDRESS,
    414     remoting.settings.XMPP_SERVER_USE_TLS,
    415     remoting.settings.DIRECTORY_BOT_JID,
    416     this.onHostConnectError_);
    417 };
    418 
    419 /**
    420  * @param {remoting.Error} error
    421  * @private
    422  */
    423 remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
    424   this.sendErrorResponse_(null, error);
    425 };
    426 
    427 /**
    428  * @param {remoting.HostSession.State} state
    429  * @private
    430  */
    431 remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
    432   this.hostState_ = state;
    433   var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
    434   var HostState = remoting.HostSession.State;
    435 
    436   switch (state) {
    437     case HostState.RECEIVED_ACCESS_CODE:
    438       var accessCode = this.host_.getAccessCode();
    439       this.hangoutPort_.postMessage({
    440         method: MessageTypes.CONNECT_RESPONSE,
    441         accessCode: accessCode
    442       });
    443       break;
    444     case HostState.CONNECTED:
    445     case HostState.DISCONNECTED:
    446       this.hangoutPort_.postMessage({
    447         method: MessageTypes.HOST_STATE_CHANGED,
    448         state: state
    449       });
    450       break;
    451     case HostState.ERROR:
    452       this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
    453       break;
    454     case HostState.INVALID_DOMAIN_ERROR:
    455       this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
    456       break;
    457     default:
    458       // It is safe to ignore other state changes.
    459   }
    460 };
    461 
    462 /**
    463  * @param {?{method:string, data:Object.<string,*>}} incomingMessage
    464  * @param {string|Error} error
    465  * @private
    466  */
    467 remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
    468     function(incomingMessage, error) {
    469   if (error instanceof Error) {
    470     error = error.message;
    471   }
    472 
    473   console.error('Error responding to message method:' +
    474                 (incomingMessage ? incomingMessage.method : 'null') +
    475                 ' error:' + error);
    476   this.hangoutPort_.postMessage({
    477     method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
    478     message: error,
    479     request: incomingMessage
    480   });
    481 };
    482