Home | History | Annotate | Download | only in webapp
      1 // Copyright 2013 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  * Connect set-up state machine for Me2Me and IT2Me
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     15 /**
     16  * @param {Element} pluginParent The node under which to add the client plugin.
     17  * @param {function(remoting.ClientSession):void} onOk Callback on success.
     18  * @param {function(remoting.Error):void} onError Callback on error.
     19  * @constructor
     20  */
     21 remoting.SessionConnector = function(pluginParent, onOk, onError) {
     22   /**
     23    * @type {Element}
     24    * @private
     25    */
     26   this.pluginParent_ = pluginParent;
     27 
     28   /**
     29    * @type {function(remoting.ClientSession):void}
     30    * @private
     31    */
     32   this.onOk_ = onOk;
     33 
     34   /**
     35    * @type {function(remoting.Error):void}
     36    * @private
     37    */
     38   this.onError_ = onError;
     39 
     40   /**
     41    * @type {string}
     42    * @private
     43    */
     44   this.clientJid_ = '';
     45 
     46   /**
     47    * @type {remoting.ClientSession.Mode}
     48    * @private
     49    */
     50   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
     51 
     52   // Initialize/declare per-connection state.
     53   this.reset();
     54 };
     55 
     56 /**
     57  * Reset the per-connection state so that the object can be re-used for a
     58  * second connection. Note the none of the shared WCS state is reset.
     59  */
     60 remoting.SessionConnector.prototype.reset = function() {
     61   /**
     62    * Set to true to indicate that the user requested pairing when entering
     63    * their PIN for a Me2Me connection.
     64    *
     65    * @type {boolean}
     66    */
     67   this.pairingRequested = false;
     68 
     69   /**
     70    * String used to identify the host to which to connect. For IT2Me, this is
     71    * the first 7 digits of the access code; for Me2Me it is the host identifier.
     72    *
     73    * @type {string}
     74    * @private
     75    */
     76   this.hostId_ = '';
     77 
     78   /**
     79    * For paired connections, the client id of this device, issued by the host.
     80    *
     81    * @type {string}
     82    * @private
     83    */
     84   this.clientPairingId_ = '';
     85 
     86   /**
     87    * For paired connections, the paired secret for this device, issued by the
     88    * host.
     89    *
     90    * @type {string}
     91    * @private
     92    */
     93   this.clientPairedSecret_ = '';
     94 
     95   /**
     96    * String used to authenticate to the host on connection. For IT2Me, this is
     97    * the access code; for Me2Me it is the PIN.
     98    *
     99    * @type {string}
    100    * @private
    101    */
    102   this.passPhrase_ = '';
    103 
    104   /**
    105    * @type {string}
    106    * @private
    107    */
    108   this.hostJid_ = '';
    109 
    110   /**
    111    * @type {string}
    112    * @private
    113    */
    114   this.hostPublicKey_ = '';
    115 
    116   /**
    117    * @type {boolean}
    118    * @private
    119    */
    120   this.refreshHostJidIfOffline_ = false;
    121 
    122   /**
    123    * @type {remoting.ClientSession}
    124    * @private
    125    */
    126   this.clientSession_ = null;
    127 
    128   /**
    129    * @type {XMLHttpRequest}
    130    * @private
    131    */
    132   this.pendingXhr_ = null;
    133 
    134   /**
    135    * Function to interactively obtain the PIN from the user.
    136    * @type {function(boolean, function(string):void):void}
    137    * @private
    138    */
    139   this.fetchPin_ = function(onPinFetched) {};
    140 
    141   /**
    142    * @type {function(string, string, string,
    143    *                 function(string, string):void): void}
    144    * @private
    145    */
    146   this.fetchThirdPartyToken_ = function(
    147       tokenUrl, scope, onThirdPartyTokenFetched) {};
    148 
    149   /**
    150    * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
    151    * this is the name of the host; for an IT2Me connection, it is the email
    152    * address of the person sharing their computer.
    153    *
    154    * @type {string}
    155    * @private
    156    */
    157   this.hostDisplayName_ = '';
    158 };
    159 
    160 /**
    161  * Initiate a Me2Me connection.
    162  *
    163  * @param {remoting.Host} host The Me2Me host to which to connect.
    164  * @param {function(boolean, function(string):void):void} fetchPin Function to
    165  *     interactively obtain the PIN from the user.
    166  * @param {function(string, string, string,
    167  *                  function(string, string): void): void}
    168  *     fetchThirdPartyToken Function to obtain a token from a third party
    169  *     authenticaiton server.
    170  * @param {string} clientPairingId The client id issued by the host when
    171  *     this device was paired, if it is already paired.
    172  * @param {string} clientPairedSecret The shared secret issued by the host when
    173  *     this device was paired, if it is already paired.
    174  * @return {void} Nothing.
    175  */
    176 remoting.SessionConnector.prototype.connectMe2Me =
    177     function(host, fetchPin, fetchThirdPartyToken,
    178              clientPairingId, clientPairedSecret) {
    179   this.connectMe2MeInternal_(
    180       host.hostId, host.jabberId, host.publicKey, host.hostName,
    181       fetchPin, fetchThirdPartyToken,
    182       clientPairingId, clientPairedSecret, true);
    183 };
    184 
    185 /**
    186  * Update the pairing info so that the reconnect function will work correctly.
    187  *
    188  * @param {string} clientId The paired client id.
    189  * @param {string} sharedSecret The shared secret.
    190  */
    191 remoting.SessionConnector.prototype.updatePairingInfo =
    192     function(clientId, sharedSecret) {
    193   this.clientPairingId_ = clientId;
    194   this.clientPairedSecret_ = sharedSecret;
    195 };
    196 
    197 /**
    198  * Initiate a Me2Me connection.
    199  *
    200  * @param {string} hostId ID of the Me2Me host.
    201  * @param {string} hostJid XMPP JID of the host.
    202  * @param {string} hostPublicKey Public Key of the host.
    203  * @param {string} hostDisplayName Display name (friendly name) of the host.
    204  * @param {function(boolean, function(string):void):void} fetchPin Function to
    205  *     interactively obtain the PIN from the user.
    206  * @param {function(string, string, string,
    207  *                  function(string, string): void): void}
    208  *     fetchThirdPartyToken Function to obtain a token from a third party
    209  *     authenticaiton server.
    210  * @param {string} clientPairingId The client id issued by the host when
    211  *     this device was paired, if it is already paired.
    212  * @param {string} clientPairedSecret The shared secret issued by the host when
    213  *     this device was paired, if it is already paired.
    214  * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
    215  *     the connection if the current JID is offline.
    216  * @return {void} Nothing.
    217  * @private
    218  */
    219 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
    220     function(hostId, hostJid, hostPublicKey, hostDisplayName,
    221              fetchPin, fetchThirdPartyToken,
    222              clientPairingId, clientPairedSecret,
    223              refreshHostJidIfOffline) {
    224   // Cancel any existing connect operation.
    225   this.cancel();
    226 
    227   this.hostId_ = hostId;
    228   this.hostJid_ = hostJid;
    229   this.hostPublicKey_ = hostPublicKey;
    230   this.fetchPin_ = fetchPin;
    231   this.fetchThirdPartyToken_ = fetchThirdPartyToken;
    232   this.hostDisplayName_ = hostDisplayName;
    233   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
    234   this.refreshHostJidIfOffline_ = refreshHostJidIfOffline;
    235   this.updatePairingInfo(clientPairingId, clientPairedSecret);
    236   this.createSession_();
    237 };
    238 
    239 /**
    240  * Initiate an IT2Me connection.
    241  *
    242  * @param {string} accessCode The access code as entered by the user.
    243  * @return {void} Nothing.
    244  */
    245 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
    246   var kSupportIdLen = 7;
    247   var kHostSecretLen = 5;
    248   var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
    249 
    250   // Cancel any existing connect operation.
    251   this.cancel();
    252 
    253   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
    254   if (normalizedAccessCode.length != kAccessCodeLen) {
    255     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
    256     return;
    257   }
    258 
    259   this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
    260   this.passPhrase_ = normalizedAccessCode;
    261   this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
    262   remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
    263                                   this.onError_);
    264 };
    265 
    266 /**
    267  * Reconnect a closed connection.
    268  *
    269  * @return {void} Nothing.
    270  */
    271 remoting.SessionConnector.prototype.reconnect = function() {
    272   if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
    273     console.error('reconnect not supported for IT2Me.');
    274     return;
    275   }
    276   this.connectMe2MeInternal_(
    277       this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
    278       this.fetchPin_, this.fetchThirdPartyToken_,
    279       this.clientPairingId_, this.clientPairedSecret_, true);
    280 };
    281 
    282 /**
    283  * Cancel a connection-in-progress.
    284  */
    285 remoting.SessionConnector.prototype.cancel = function() {
    286   if (this.clientSession_) {
    287     this.clientSession_.removePlugin();
    288     this.clientSession_ = null;
    289   }
    290   if (this.pendingXhr_) {
    291     this.pendingXhr_.abort();
    292     this.pendingXhr_ = null;
    293   }
    294   this.reset();
    295 };
    296 
    297 /**
    298  * Get the connection mode (Me2Me or IT2Me)
    299  *
    300  * @return {remoting.ClientSession.Mode}
    301  */
    302 remoting.SessionConnector.prototype.getConnectionMode = function() {
    303   return this.connectionMode_;
    304 };
    305 
    306 /**
    307  * Get host ID.
    308  *
    309  * @return {string}
    310  */
    311 remoting.SessionConnector.prototype.getHostId = function() {
    312   return this.hostId_;
    313 };
    314 
    315 /**
    316  * Get host display name.
    317  *
    318  * @return {string}
    319  */
    320 remoting.SessionConnector.prototype.getHostDisplayName = function() {
    321   return this.hostDisplayName_;
    322 };
    323 
    324 /**
    325  * Continue an IT2Me connection once an access token has been obtained.
    326  *
    327  * @param {string} token An OAuth2 access token.
    328  * @return {void} Nothing.
    329  * @private
    330  */
    331 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
    332   // Resolve the host id to get the host JID.
    333   this.pendingXhr_ = remoting.xhr.get(
    334       remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
    335           encodeURIComponent(this.hostId_),
    336       this.onIT2MeHostInfo_.bind(this),
    337       '',
    338       { 'Authorization': 'OAuth ' + token });
    339 };
    340 
    341 /**
    342  * Continue an IT2Me connection once the host JID has been looked up.
    343  *
    344  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
    345  * @return {void} Nothing.
    346  * @private
    347  */
    348 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
    349   this.pendingXhr_ = null;
    350   if (xhr.status == 200) {
    351     var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
    352         jsonParseSafe(xhr.responseText);
    353     if (host && host.data && host.data.jabberId && host.data.publicKey) {
    354       this.hostJid_ = host.data.jabberId;
    355       this.hostPublicKey_ = host.data.publicKey;
    356       this.hostDisplayName_ = this.hostJid_.split('/')[0];
    357       this.createSession_();
    358       return;
    359     } else {
    360       console.error('Invalid "support-hosts" response from server.');
    361     }
    362   } else {
    363     this.onError_(this.translateSupportHostsError(xhr.status));
    364   }
    365 };
    366 
    367 /**
    368  * Creates ClientSession object.
    369  */
    370 remoting.SessionConnector.prototype.createSession_ = function() {
    371   // In some circumstances, the WCS <iframe> can get reloaded, which results
    372   // in a new clientJid and a new callback. In this case, remove the old
    373   // client plugin before instantiating a new one.
    374   if (this.clientSession_) {
    375     this.clientSession_.removePlugin();
    376     this.clientSession_ = null;
    377   }
    378 
    379   var authenticationMethods =
    380      'third_party,spake2_pair,spake2_hmac,spake2_plain';
    381   this.clientSession_ = new remoting.ClientSession(
    382       this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_,
    383       authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
    384       this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
    385   this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
    386   this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
    387   this.clientSession_.createPluginAndConnect(this.pluginParent_);
    388 };
    389 
    390 /**
    391  * Handle a change in the state of the client session prior to successful
    392  * connection (after connection, this class no longer handles state change
    393  * events). Errors that occur while connecting either trigger a reconnect
    394  * or notify the onError handler.
    395  *
    396  * @param {number} oldState The previous state of the plugin.
    397  * @param {number} newState The current state of the plugin.
    398  * @return {void} Nothing.
    399  * @private
    400  */
    401 remoting.SessionConnector.prototype.onStateChange_ =
    402     function(oldState, newState) {
    403   switch (newState) {
    404     case remoting.ClientSession.State.CONNECTED:
    405       // When the connection succeeds, deregister for state-change callbacks
    406       // and pass the session to the onOk callback. It is expected that it
    407       // will register a new state-change callback to handle disconnect
    408       // or error conditions.
    409       this.clientSession_.setOnStateChange(null);
    410       this.onOk_(this.clientSession_);
    411       break;
    412 
    413     case remoting.ClientSession.State.CREATED:
    414       console.log('Created plugin');
    415       break;
    416 
    417     case remoting.ClientSession.State.CONNECTING:
    418       console.log('Connecting as ' + remoting.identity.getCachedEmail());
    419       break;
    420 
    421     case remoting.ClientSession.State.INITIALIZING:
    422       console.log('Initializing connection');
    423       break;
    424 
    425     case remoting.ClientSession.State.CLOSED:
    426       // This class deregisters for state-change callbacks when the CONNECTED
    427       // state is reached, so it only sees the CLOSED state in exceptional
    428       // circumstances. For example, a CONNECTING -> CLOSED transition happens
    429       // if the host closes the connection without an error message instead of
    430       // accepting it. Since there's no way of knowing exactly what went wrong,
    431       // we rely on server-side logs in this case and report a generic error
    432       // message.
    433       this.onError_(remoting.Error.UNEXPECTED);
    434       break;
    435 
    436     case remoting.ClientSession.State.FAILED:
    437       var error = this.clientSession_.getError();
    438       console.error('Client plugin reported connection failed: ' + error);
    439       if (error == null) {
    440         error = remoting.Error.UNEXPECTED;
    441       }
    442       if (error == remoting.Error.HOST_IS_OFFLINE &&
    443           this.refreshHostJidIfOffline_) {
    444         remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
    445       } else {
    446         this.onError_(error);
    447       }
    448       break;
    449 
    450     default:
    451       console.error('Unexpected client plugin state: ' + newState);
    452       // This should only happen if the web-app and client plugin get out of
    453       // sync, and even then the version check should ensure compatibility.
    454       this.onError_(remoting.Error.MISSING_PLUGIN);
    455   }
    456 };
    457 
    458 /**
    459  * @param {boolean} success True if the host list was successfully refreshed;
    460  *     false if an error occurred.
    461  * @private
    462  */
    463 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
    464   if (success) {
    465     var host = remoting.hostList.getHostForId(this.hostId_);
    466     if (host) {
    467       this.connectMe2MeInternal_(
    468           host.hostId, host.jabberId, host.publicKey, host.hostName,
    469           this.fetchPin_, this.fetchThirdPartyToken_,
    470           this.clientPairingId_, this.clientPairedSecret_, false);
    471       return;
    472     }
    473   }
    474   this.onError_(remoting.Error.HOST_IS_OFFLINE);
    475 };
    476 
    477 /**
    478  * @param {number} error An HTTP error code returned by the support-hosts
    479  *     endpoint.
    480  * @return {remoting.Error} The equivalent remoting.Error code.
    481  * @private
    482  */
    483 remoting.SessionConnector.prototype.translateSupportHostsError =
    484     function(error) {
    485   switch (error) {
    486     case 0: return remoting.Error.NETWORK_FAILURE;
    487     case 404: return remoting.Error.INVALID_ACCESS_CODE;
    488     case 502: // No break
    489     case 503: return remoting.Error.SERVICE_UNAVAILABLE;
    490     default: return remoting.Error.UNEXPECTED;
    491   }
    492 };
    493 
    494 /**
    495  * Normalize the access code entered by the user.
    496  *
    497  * @param {string} accessCode The access code, as entered by the user.
    498  * @return {string} The normalized form of the code (whitespace removed).
    499  */
    500 remoting.SessionConnector.prototype.normalizeAccessCode_ =
    501     function(accessCode) {
    502   // Trim whitespace.
    503   return accessCode.replace(/\s/g, '');
    504 };
    505