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