Home | History | Annotate | Download | only in webapp
      1 // Copyright (c) 2012 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 'use strict';
      6 
      7 /** @suppress {duplicate} */
      8 var remoting = remoting || {};
      9 
     10 /** @constructor */
     11 remoting.HostController = function() {
     12   this.hostDaemonFacade_ = this.createDaemonFacade_();
     13 };
     14 
     15 // Note that the values in the enums below are copied from
     16 // daemon_controller.h and must be kept in sync.
     17 /** @enum {number} */
     18 remoting.HostController.State = {
     19   NOT_IMPLEMENTED: -1,
     20   NOT_INSTALLED: 0,
     21   INSTALLING: 1,
     22   STOPPED: 2,
     23   STARTING: 3,
     24   STARTED: 4,
     25   STOPPING: 5,
     26   UNKNOWN: 6
     27 };
     28 
     29 /**
     30  * @param {string} state The host controller state name.
     31  * @return {remoting.HostController.State} The state enum value.
     32  */
     33 remoting.HostController.State.fromString = function(state) {
     34   if (!remoting.HostController.State.hasOwnProperty(state)) {
     35     throw "Invalid HostController.State: " + state;
     36   }
     37   return remoting.HostController.State[state];
     38 }
     39 
     40 /** @enum {number} */
     41 remoting.HostController.AsyncResult = {
     42   OK: 0,
     43   FAILED: 1,
     44   CANCELLED: 2,
     45   FAILED_DIRECTORY: 3
     46 };
     47 
     48 /**
     49  * @param {string} result The async result name.
     50  * @return {remoting.HostController.AsyncResult} The result enum value.
     51  */
     52 remoting.HostController.AsyncResult.fromString = function(result) {
     53   if (!remoting.HostController.AsyncResult.hasOwnProperty(result)) {
     54     throw "Invalid HostController.AsyncResult: " + result;
     55   }
     56   return remoting.HostController.AsyncResult[result];
     57 }
     58 
     59 /**
     60  * @return {remoting.HostDaemonFacade}
     61  * @private
     62  */
     63 remoting.HostController.prototype.createDaemonFacade_ = function() {
     64   /** @type {remoting.HostDaemonFacade} @private */
     65   var hostDaemonFacade = new remoting.HostDaemonFacade();
     66 
     67   /** @param {string} version */
     68   var printVersion = function(version) {
     69     if (version == '') {
     70       console.log('Host not installed.');
     71     } else {
     72       console.log('Host version: ' + version);
     73     }
     74   };
     75 
     76   hostDaemonFacade.getDaemonVersion(printVersion, function() {
     77     console.log('Host version not available.');
     78   });
     79 
     80   return hostDaemonFacade;
     81 };
     82 
     83 /**
     84  * Set of features for which hasFeature() can be used to test.
     85  *
     86  * @enum {string}
     87  */
     88 remoting.HostController.Feature = {
     89   PAIRING_REGISTRY: 'pairingRegistry',
     90   OAUTH_CLIENT: 'oauthClient'
     91 };
     92 
     93 /**
     94  * @param {remoting.HostController.Feature} feature The feature to test for.
     95  * @param {function(boolean):void} callback
     96  * @return {void}
     97  */
     98 remoting.HostController.prototype.hasFeature = function(feature, callback) {
     99   // TODO(rmsousa): This could synchronously return a boolean, provided it were
    100   // only called after native messaging is completely initialized.
    101   this.hostDaemonFacade_.hasFeature(feature, callback);
    102 };
    103 
    104 /**
    105  * @param {function(boolean, boolean, boolean):void} onDone Callback to be
    106  *     called when done.
    107  * @param {function(remoting.Error):void} onError Callback to be called on
    108  *     error.
    109  */
    110 remoting.HostController.prototype.getConsent = function(onDone, onError) {
    111   this.hostDaemonFacade_.getUsageStatsConsent(onDone, onError);
    112 };
    113 
    114 /**
    115  * Registers and starts the host.
    116  *
    117  * @param {string} hostPin Host PIN.
    118  * @param {boolean} consent The user's consent to crash dump reporting.
    119  * @param {function():void} onDone Callback to be called when done.
    120  * @param {function(remoting.Error):void} onError Callback to be called on
    121  *     error.
    122  * @return {void} Nothing.
    123  */
    124 remoting.HostController.prototype.start = function(hostPin, consent, onDone,
    125                                                    onError) {
    126   /** @type {remoting.HostController} */
    127   var that = this;
    128 
    129   /** @return {string} */
    130   function generateUuid() {
    131     var random = new Uint16Array(8);
    132     window.crypto.getRandomValues(random);
    133     /** @type {Array.<string>} */
    134     var e = new Array();
    135     for (var i = 0; i < 8; i++) {
    136       e[i] = (/** @type {number} */random[i] + 0x10000).
    137           toString(16).substring(1);
    138     }
    139     return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
    140         e[4] + '-' + e[5] + e[6] + e[7];
    141   };
    142 
    143   var newHostId = generateUuid();
    144 
    145   /** @param {remoting.Error} error */
    146   function onStartError(error) {
    147     // Unregister the host if we failed to start it.
    148     remoting.HostList.unregisterHostById(newHostId);
    149     onError(error);
    150   }
    151 
    152   /**
    153    * @param {string} hostName
    154    * @param {string} publicKey
    155    * @param {remoting.HostController.AsyncResult} result
    156    */
    157   function onStarted(hostName, publicKey, result) {
    158     if (result == remoting.HostController.AsyncResult.OK) {
    159       remoting.hostList.onLocalHostStarted(hostName, newHostId, publicKey);
    160       onDone();
    161     } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
    162       onStartError(remoting.Error.CANCELLED);
    163     } else {
    164       onStartError(remoting.Error.UNEXPECTED);
    165     }
    166   }
    167 
    168   /**
    169    * @param {string} hostName
    170    * @param {string} publicKey
    171    * @param {string} privateKey
    172    * @param {string} xmppLogin
    173    * @param {string} refreshToken
    174    * @param {string} clientBaseJid
    175    * @param {string} hostSecretHash
    176    */
    177   function startHostWithHash(hostName, publicKey, privateKey, xmppLogin,
    178                              refreshToken, clientBaseJid, hostSecretHash) {
    179     var hostConfig = {
    180       xmpp_login: xmppLogin,
    181       oauth_refresh_token: refreshToken,
    182       host_id: newHostId,
    183       host_name: hostName,
    184       host_secret_hash: hostSecretHash,
    185       private_key: privateKey
    186     };
    187     var hostOwner = clientBaseJid;
    188     var hostOwnerEmail = remoting.identity.getCachedEmail();
    189     if (hostOwner != xmppLogin) {
    190       hostConfig['host_owner'] = hostOwner;
    191       if (hostOwnerEmail != hostOwner) {
    192         hostConfig['host_owner_email'] = hostOwnerEmail;
    193       }
    194     }
    195     that.hostDaemonFacade_.startDaemon(
    196         hostConfig, consent, onStarted.bind(null, hostName, publicKey),
    197         onStartError);
    198   }
    199 
    200   /**
    201    * @param {string} hostName
    202    * @param {string} publicKey
    203    * @param {string} privateKey
    204    * @param {string} email
    205    * @param {string} refreshToken
    206    * @param {string} clientBaseJid
    207    */
    208   function onClientBaseJid(
    209       hostName, publicKey, privateKey, email, refreshToken, clientBaseJid) {
    210     that.hostDaemonFacade_.getPinHash(
    211         newHostId, hostPin,
    212         startHostWithHash.bind(null, hostName, publicKey, privateKey,
    213                                email, refreshToken, clientBaseJid),
    214         onError);
    215   }
    216 
    217   /**
    218    * @param {string} hostName
    219    * @param {string} publicKey
    220    * @param {string} privateKey
    221    * @param {string} email
    222    * @param {string} refreshToken
    223    */
    224   function onServiceAccountCredentials(
    225       hostName, publicKey, privateKey, email, refreshToken) {
    226     that.getClientBaseJid_(
    227         onClientBaseJid.bind(
    228             null, hostName, publicKey, privateKey, email, refreshToken),
    229         onStartError);
    230   }
    231 
    232   /**
    233    * @param {string} hostName
    234    * @param {string} publicKey
    235    * @param {string} privateKey
    236    * @param {XMLHttpRequest} xhr
    237    */
    238   function onRegistered(
    239       hostName, publicKey, privateKey, xhr) {
    240     var success = (xhr.status == 200);
    241 
    242     if (success) {
    243       var result = jsonParseSafe(xhr.responseText);
    244       if ('data' in result && 'authorizationCode' in result['data']) {
    245         that.hostDaemonFacade_.getCredentialsFromAuthCode(
    246             result['data']['authorizationCode'],
    247             onServiceAccountCredentials.bind(
    248                 null, hostName, publicKey, privateKey),
    249             onError);
    250       } else {
    251         // No authorization code returned, use regular user credential flow.
    252         that.hostDaemonFacade_.getPinHash(
    253             newHostId, hostPin, startHostWithHash.bind(
    254                 null, hostName, publicKey, privateKey,
    255                 remoting.identity.getCachedEmail(),
    256                 remoting.oauth2.getRefreshToken()),
    257           onError);
    258       }
    259     } else {
    260       console.log('Failed to register the host. Status: ' + xhr.status +
    261                   ' response: ' + xhr.responseText);
    262       onError(remoting.Error.REGISTRATION_FAILED);
    263     }
    264   }
    265 
    266   /**
    267    * @param {string} hostName
    268    * @param {string} privateKey
    269    * @param {string} publicKey
    270    * @param {string} hostClientId
    271    * @param {string} oauthToken
    272    */
    273   function doRegisterHost(
    274       hostName, privateKey, publicKey, hostClientId, oauthToken) {
    275     var headers = {
    276       'Authorization': 'OAuth ' + oauthToken,
    277       'Content-type' : 'application/json; charset=UTF-8'
    278     };
    279 
    280     var newHostDetails = { data: {
    281        hostId: newHostId,
    282        hostName: hostName,
    283        publicKey: publicKey
    284     } };
    285 
    286     var registerHostUrl =
    287         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts';
    288 
    289     if (hostClientId) {
    290       registerHostUrl += '?' + remoting.xhr.urlencodeParamHash(
    291           { hostClientId: hostClientId });
    292     }
    293 
    294     remoting.xhr.post(
    295         registerHostUrl,
    296         onRegistered.bind(null, hostName, publicKey, privateKey),
    297         JSON.stringify(newHostDetails),
    298         headers);
    299   }
    300 
    301   /**
    302    * @param {string} hostName
    303    * @param {string} privateKey
    304    * @param {string} publicKey
    305    * @param {string} hostClientId
    306    */
    307   function onHostClientId(
    308       hostName, privateKey, publicKey, hostClientId) {
    309     remoting.identity.callWithToken(
    310         doRegisterHost.bind(
    311             null, hostName, privateKey, publicKey, hostClientId), onError);
    312   }
    313 
    314   /**
    315    * @param {string} hostName
    316    * @param {string} privateKey
    317    * @param {string} publicKey
    318    * @param {boolean} hasFeature
    319    */
    320   function onHasFeatureOAuthClient(
    321       hostName, privateKey, publicKey, hasFeature) {
    322     if (hasFeature) {
    323       that.hostDaemonFacade_.getHostClientId(
    324           onHostClientId.bind(null, hostName, privateKey, publicKey), onError);
    325     } else {
    326       remoting.identity.callWithToken(
    327           doRegisterHost.bind(
    328               null, hostName, privateKey, publicKey, null), onError);
    329     }
    330   }
    331 
    332   /**
    333    * @param {string} hostName
    334    * @param {string} privateKey
    335    * @param {string} publicKey
    336    */
    337   function onKeyGenerated(hostName, privateKey, publicKey) {
    338     that.hasFeature(
    339         remoting.HostController.Feature.OAUTH_CLIENT,
    340         onHasFeatureOAuthClient.bind(null, hostName, privateKey, publicKey));
    341   }
    342 
    343   /**
    344    * @param {string} hostName
    345    * @return {void} Nothing.
    346    */
    347   function startWithHostname(hostName) {
    348     that.hostDaemonFacade_.generateKeyPair(onKeyGenerated.bind(null, hostName),
    349                                          onError);
    350   }
    351 
    352   this.hostDaemonFacade_.getHostName(startWithHostname, onError);
    353 };
    354 
    355 /**
    356  * Stop the daemon process.
    357  * @param {function():void} onDone Callback to be called when done.
    358  * @param {function(remoting.Error):void} onError Callback to be called on
    359  *     error.
    360  * @return {void} Nothing.
    361  */
    362 remoting.HostController.prototype.stop = function(onDone, onError) {
    363   /** @type {remoting.HostController} */
    364   var that = this;
    365 
    366   /** @param {string?} hostId The host id of the local host. */
    367   function unregisterHost(hostId) {
    368     if (hostId) {
    369       remoting.HostList.unregisterHostById(hostId);
    370     }
    371     onDone();
    372   }
    373 
    374   /** @param {remoting.HostController.AsyncResult} result */
    375   function onStopped(result) {
    376     if (result == remoting.HostController.AsyncResult.OK) {
    377       that.getLocalHostId(unregisterHost);
    378     } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
    379       onError(remoting.Error.CANCELLED);
    380     } else {
    381       onError(remoting.Error.UNEXPECTED);
    382     }
    383   }
    384 
    385   this.hostDaemonFacade_.stopDaemon(onStopped, onError);
    386 };
    387 
    388 /**
    389  * Check the host configuration is valid (non-null, and contains both host_id
    390  * and xmpp_login keys).
    391  * @param {Object} config The host configuration.
    392  * @return {boolean} True if it is valid.
    393  */
    394 function isHostConfigValid_(config) {
    395   return !!config && typeof config['host_id'] == 'string' &&
    396       typeof config['xmpp_login'] == 'string';
    397 }
    398 
    399 /**
    400  * @param {string} newPin The new PIN to set
    401  * @param {function():void} onDone Callback to be called when done.
    402  * @param {function(remoting.Error):void} onError Callback to be called on
    403  *     error.
    404  * @return {void} Nothing.
    405  */
    406 remoting.HostController.prototype.updatePin = function(newPin, onDone,
    407                                                        onError) {
    408   /** @type {remoting.HostController} */
    409   var that = this;
    410 
    411   /** @param {remoting.HostController.AsyncResult} result */
    412   function onConfigUpdated(result) {
    413     if (result == remoting.HostController.AsyncResult.OK) {
    414       onDone();
    415     } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
    416       onError(remoting.Error.CANCELLED);
    417     } else {
    418       onError(remoting.Error.UNEXPECTED);
    419     }
    420   }
    421 
    422   /** @param {string} pinHash */
    423   function updateDaemonConfigWithHash(pinHash) {
    424     var newConfig = {
    425       host_secret_hash: pinHash
    426     };
    427     that.hostDaemonFacade_.updateDaemonConfig(newConfig, onConfigUpdated,
    428                                             onError);
    429   }
    430 
    431   /** @param {Object} config */
    432   function onConfig(config) {
    433     if (!isHostConfigValid_(config)) {
    434       onError(remoting.Error.UNEXPECTED);
    435       return;
    436     }
    437     /** @type {string} */
    438     var hostId = config['host_id'];
    439     that.hostDaemonFacade_.getPinHash(
    440         hostId, newPin, updateDaemonConfigWithHash, onError);
    441   }
    442 
    443   // TODO(sergeyu): When crbug.com/121518 is fixed: replace this call
    444   // with an unprivileged version if that is necessary.
    445   this.hostDaemonFacade_.getDaemonConfig(onConfig, onError);
    446 };
    447 
    448 /**
    449  * Get the state of the local host.
    450  *
    451  * @param {function(remoting.HostController.State):void} onDone Completion
    452  *     callback.
    453  */
    454 remoting.HostController.prototype.getLocalHostState = function(onDone) {
    455   /** @param {remoting.Error} error */
    456   function onError(error) {
    457     onDone((error == remoting.Error.MISSING_PLUGIN) ?
    458                remoting.HostController.State.NOT_INSTALLED :
    459                remoting.HostController.State.UNKNOWN);
    460   }
    461   this.hostDaemonFacade_.getDaemonState(onDone, onError);
    462 };
    463 
    464 /**
    465  * Get the id of the local host, or null if it is not registered.
    466  *
    467  * @param {function(string?):void} onDone Completion callback.
    468  */
    469 remoting.HostController.prototype.getLocalHostId = function(onDone) {
    470   /** @type {remoting.HostController} */
    471   var that = this;
    472   /** @param {Object} config */
    473   function onConfig(config) {
    474     var hostId = null;
    475     if (isHostConfigValid_(config)) {
    476       hostId = /** @type {string} */ config['host_id'];
    477     }
    478     onDone(hostId);
    479   };
    480 
    481   this.hostDaemonFacade_.getDaemonConfig(onConfig, function(error) {
    482     onDone(null);
    483   });
    484 };
    485 
    486 /**
    487  * Fetch the list of paired clients for this host.
    488  *
    489  * @param {function(Array.<remoting.PairedClient>):void} onDone
    490  * @param {function(remoting.Error):void} onError
    491  * @return {void}
    492  */
    493 remoting.HostController.prototype.getPairedClients = function(onDone,
    494                                                               onError) {
    495   this.hostDaemonFacade_.getPairedClients(onDone, onError);
    496 };
    497 
    498 /**
    499  * Delete a single paired client.
    500  *
    501  * @param {string} client The client id of the pairing to delete.
    502  * @param {function():void} onDone Completion callback.
    503  * @param {function(remoting.Error):void} onError Error callback.
    504  * @return {void}
    505  */
    506 remoting.HostController.prototype.deletePairedClient = function(
    507     client, onDone, onError) {
    508   this.hostDaemonFacade_.deletePairedClient(client, onDone, onError);
    509 };
    510 
    511 /**
    512  * Delete all paired clients.
    513  *
    514  * @param {function():void} onDone Completion callback.
    515  * @param {function(remoting.Error):void} onError Error callback.
    516  * @return {void}
    517  */
    518 remoting.HostController.prototype.clearPairedClients = function(
    519     onDone, onError) {
    520   this.hostDaemonFacade_.clearPairedClients(onDone, onError);
    521 };
    522 
    523 /**
    524  * Gets the host owner's base JID, used by the host for client authorization.
    525  * In most cases this is the same as the owner's email address, but for
    526  * non-Gmail accounts, it may be different.
    527  *
    528  * @private
    529  * @param {function(string): void} onSuccess
    530  * @param {function(remoting.Error): void} onError
    531  */
    532 remoting.HostController.prototype.getClientBaseJid_ = function(
    533     onSuccess, onError) {
    534   var signalStrategy = null;
    535 
    536   var onState = function(state) {
    537     switch (state) {
    538       case remoting.SignalStrategy.State.CONNECTED:
    539         var jid = signalStrategy.getJid().split('/')[0].toLowerCase();
    540         base.dispose(signalStrategy);
    541         signalStrategy = null;
    542         onSuccess(jid);
    543         break;
    544 
    545       case remoting.SignalStrategy.State.FAILED:
    546         var error = signalStrategy.getError();
    547         base.dispose(signalStrategy);
    548         signalStrategy = null;
    549         onError(error);
    550         break;
    551     }
    552   };
    553 
    554   signalStrategy = remoting.SignalStrategy.create(onState);
    555 
    556   /** @param {string} token */
    557   function connectSignalingWithToken(token) {
    558     remoting.identity.getEmail(
    559         connectSignalingWithTokenAndEmail.bind(null, token), onError);
    560   }
    561 
    562   /**
    563    * @param {string} token
    564    * @param {string} email
    565    */
    566   function connectSignalingWithTokenAndEmail(token, email) {
    567     signalStrategy.connect(
    568         remoting.settings.XMPP_SERVER_ADDRESS, email, token);
    569   }
    570 
    571   remoting.identity.callWithToken(connectSignalingWithToken, onError);
    572 };
    573 
    574 /** @type {remoting.HostController} */
    575 remoting.hostController = null;
    576