Home | History | Annotate | Download | only in webapp
      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  * Class to communicate with the host daemon via Native Messaging.
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     15 /**
     16  * @constructor
     17  */
     18 remoting.HostDaemonFacade = function() {
     19   /**
     20    * @type {number}
     21    * @private
     22    */
     23   this.nextId_ = 0;
     24 
     25   /**
     26    * @type {Object.<number, remoting.HostDaemonFacade.PendingReply>}
     27    * @private
     28    */
     29   this.pendingReplies_ = {};
     30 
     31   /** @type {?chrome.runtime.Port} @private */
     32   this.port_ = null;
     33 
     34   /** @type {string} @private */
     35   this.version_ = '';
     36 
     37   /** @type {Array.<remoting.HostController.Feature>} @private */
     38   this.supportedFeatures_ = [];
     39 
     40   /** @type {Array.<function(boolean):void>} @private */
     41   this.afterInitializationTasks_ = [];
     42 
     43   /** @private */
     44   this.initializationFinished_ = false;
     45 
     46   /** @type {remoting.Error} @private */
     47   this.error_ = remoting.Error.NONE;
     48 
     49   try {
     50     this.port_ = chrome.runtime.connectNative(
     51         'com.google.chrome.remote_desktop');
     52     this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this));
     53     this.port_.onDisconnect.addListener(this.onDisconnect_.bind(this));
     54     this.postMessage_({type: 'hello'},
     55                       this.onInitialized_.bind(this, true),
     56                       this.onInitialized_.bind(this, false));
     57   } catch (err) {
     58     console.log('Native Messaging initialization failed: ',
     59                 /** @type {*} */ (err));
     60     this.onInitialized_(false);
     61   }
     62 };
     63 
     64 /**
     65  * Type used for entries of |pendingReplies_| list.
     66  *
     67  * @param {string} type Type of the originating request.
     68  * @param {function(...):void} onDone Response callback. Parameters depend on
     69  *     the request type.
     70  * @param {function(remoting.Error):void} onError Callback to call on error.
     71  * @constructor
     72  */
     73 remoting.HostDaemonFacade.PendingReply = function(type, onDone, onError) {
     74   this.type = type;
     75   this.onDone = onDone;
     76   this.onError = onError;
     77 };
     78 
     79 /**
     80  * @param {boolean} success
     81  * @return {void} Nothing.
     82  * @private
     83  */
     84 remoting.HostDaemonFacade.prototype.onInitialized_ = function(success) {
     85   this.initializationFinished_ = true;
     86   var afterInitializationTasks = this.afterInitializationTasks_;
     87   this.afterInitializationTasks_ = [];
     88   for (var id in afterInitializationTasks) {
     89     afterInitializationTasks[/** @type {number} */(id)](success);
     90   }
     91 };
     92 
     93 /**
     94  * @param {remoting.HostController.Feature} feature The feature to test for.
     95  * @param {function(boolean):void} onDone Callback to return result.
     96  * @return {boolean} True if the implementation supports the named feature.
     97  */
     98 remoting.HostDaemonFacade.prototype.hasFeature = function(feature, onDone) {
     99   if (!this.port_) {
    100     onDone(false);
    101   } else if (this.initializationFinished_) {
    102     onDone(this.supportedFeatures_.indexOf(feature) >= 0);
    103   } else {
    104     /** @type remoting.HostDaemonFacade */
    105     var that = this;
    106     this.afterInitializationTasks_.push(
    107         /** @param {boolean} success */
    108         function(success) {
    109           onDone(that.supportedFeatures_.indexOf(feature) >= 0);
    110         });
    111   }
    112 };
    113 
    114 /**
    115  * Attaches a new ID to the supplied message, and posts it to the Native
    116  * Messaging port, adding |onDone| to the list of pending replies.
    117  * |message| should have its 'type' field set, and any other fields set
    118  * depending on the message type.
    119  *
    120  * @param {{type: string}} message The message to post.
    121  * @param {function(...):void} onDone The callback, if any, to be triggered
    122  *     on response.
    123  * @param {function(remoting.Error):void} onError Callback to call on error.
    124  * @return {void} Nothing.
    125  * @private
    126  */
    127 remoting.HostDaemonFacade.prototype.postMessage_ =
    128     function(message, onDone, onError) {
    129   if (!this.port_) {
    130     onError(this.error_);
    131     return;
    132   }
    133   var id = this.nextId_++;
    134   message['id'] = id;
    135   this.pendingReplies_[id] = new remoting.HostDaemonFacade.PendingReply(
    136     message.type + 'Response', onDone, onError);
    137   this.port_.postMessage(message);
    138 };
    139 
    140 /**
    141  * Handler for incoming Native Messages.
    142  *
    143  * @param {Object} message The received message.
    144  * @return {void} Nothing.
    145  * @private
    146  */
    147 remoting.HostDaemonFacade.prototype.onIncomingMessage_ = function(message) {
    148   /** @type {number} */
    149   var id = message['id'];
    150   if (typeof(id) != 'number') {
    151     console.error('NativeMessaging: missing or non-numeric id');
    152     return;
    153   }
    154   var reply = this.pendingReplies_[id];
    155   if (!reply) {
    156     console.error('NativeMessaging: unexpected id: ', id);
    157     return;
    158   }
    159   delete this.pendingReplies_[id];
    160 
    161   try {
    162     var type = getStringAttr(message, 'type');
    163     if (type != reply.type) {
    164       throw 'Expected reply type: ' + reply.type + ', got: ' + type;
    165     }
    166 
    167     this.handleIncomingMessage_(message, reply.onDone);
    168   } catch (e) {
    169     console.error('Error while processing native message' +
    170                   /** @type {*} */ (e));
    171     reply.onError(remoting.Error.UNEXPECTED);
    172   }
    173 }
    174 
    175 /**
    176  * Handler for incoming Native Messages.
    177  *
    178  * @param {Object} message The received message.
    179  * @param {function(...):void} onDone Function to call when we're done
    180  *     processing the message.
    181  * @return {void} Nothing.
    182  * @private
    183  */
    184 remoting.HostDaemonFacade.prototype.handleIncomingMessage_ =
    185     function(message, onDone) {
    186   var type = getStringAttr(message, 'type');
    187 
    188   switch (type) {
    189     case 'helloResponse':
    190       this.version_ = getStringAttr(message, 'version');
    191       // Old versions of the native messaging host do not return this list.
    192       // Those versions default to the empty list of supported features.
    193       this.supportedFeatures_ = getArrayAttr(message, 'supportedFeatures', []);
    194       onDone();
    195       break;
    196 
    197     case 'getHostNameResponse':
    198       onDone(getStringAttr(message, 'hostname'));
    199       break;
    200 
    201     case 'getPinHashResponse':
    202       onDone(getStringAttr(message, 'hash'));
    203       break;
    204 
    205     case 'generateKeyPairResponse':
    206       var privateKey = getStringAttr(message, 'privateKey');
    207       var publicKey = getStringAttr(message, 'publicKey');
    208       onDone(privateKey, publicKey);
    209       break;
    210 
    211     case 'updateDaemonConfigResponse':
    212       var result = remoting.HostController.AsyncResult.fromString(
    213           getStringAttr(message, 'result'));
    214       onDone(result);
    215       break;
    216 
    217     case 'getDaemonConfigResponse':
    218       onDone(getObjectAttr(message, 'config'));
    219       break;
    220 
    221     case 'getUsageStatsConsentResponse':
    222       var supported = getBooleanAttr(message, 'supported');
    223       var allowed = getBooleanAttr(message, 'allowed');
    224       var setByPolicy = getBooleanAttr(message, 'setByPolicy');
    225       onDone(supported, allowed, setByPolicy);
    226       break;
    227 
    228     case 'startDaemonResponse':
    229     case 'stopDaemonResponse':
    230       var result = remoting.HostController.AsyncResult.fromString(
    231           getStringAttr(message, 'result'));
    232       onDone(result);
    233       break;
    234 
    235     case 'getDaemonStateResponse':
    236       var state = remoting.HostController.State.fromString(
    237         getStringAttr(message, 'state'));
    238       onDone(state);
    239       break;
    240 
    241     case 'getPairedClientsResponse':
    242       var pairedClients = remoting.PairedClient.convertToPairedClientArray(
    243           message['pairedClients']);
    244       if (pairedClients != null) {
    245         onDone(pairedClients);
    246       } else {
    247         throw 'No paired clients!';
    248       }
    249       break;
    250 
    251     case 'clearPairedClientsResponse':
    252     case 'deletePairedClientResponse':
    253       onDone(getBooleanAttr(message, 'result'));
    254       break;
    255 
    256     case 'getHostClientIdResponse':
    257       onDone(getStringAttr(message, 'clientId'));
    258       break;
    259 
    260     case 'getCredentialsFromAuthCodeResponse':
    261       var userEmail = getStringAttr(message, 'userEmail');
    262       var refreshToken = getStringAttr(message, 'refreshToken');
    263       if (userEmail && refreshToken) {
    264         onDone(userEmail, refreshToken);
    265       } else {
    266         throw 'Missing userEmail or refreshToken';
    267       }
    268       break;
    269 
    270     default:
    271       throw 'Unexpected native message: ' + message;
    272   }
    273 };
    274 
    275 /**
    276  * @return {void} Nothing.
    277  * @private
    278  */
    279 remoting.HostDaemonFacade.prototype.onDisconnect_ = function() {
    280   console.error('Native Message port disconnected');
    281 
    282   this.port_ = null;
    283 
    284   // If initialization hasn't finished then assume that the port was
    285   // disconnected because Native Messaging host is not installed.
    286   this.error_ = this.initializationFinished_ ? remoting.Error.UNEXPECTED :
    287                                                remoting.Error.MISSING_PLUGIN;
    288 
    289   // Notify the error-handlers of any requests that are still outstanding.
    290   var pendingReplies = this.pendingReplies_;
    291   this.pendingReplies_ = {};
    292   for (var id in pendingReplies) {
    293     pendingReplies[/** @type {number} */(id)].onError(this.error_);
    294   }
    295 }
    296 
    297 /**
    298  * Gets local hostname.
    299  *
    300  * @param {function(string):void} onDone Callback to return result.
    301  * @param {function(remoting.Error):void} onError Callback to call on error.
    302  * @return {void} Nothing.
    303  */
    304 remoting.HostDaemonFacade.prototype.getHostName =
    305     function(onDone, onError) {
    306   this.postMessage_({type: 'getHostName'}, onDone, onError);
    307 };
    308 
    309 /**
    310  * Calculates PIN hash value to be stored in the config, passing the resulting
    311  * hash value base64-encoded to the callback.
    312  *
    313  * @param {string} hostId The host ID.
    314  * @param {string} pin The PIN.
    315  * @param {function(string):void} onDone Callback to return result.
    316  * @param {function(remoting.Error):void} onError Callback to call on error.
    317  * @return {void} Nothing.
    318  */
    319 remoting.HostDaemonFacade.prototype.getPinHash =
    320     function(hostId, pin, onDone, onError) {
    321   this.postMessage_({
    322       type: 'getPinHash',
    323       hostId: hostId,
    324       pin: pin
    325   }, onDone, onError);
    326 };
    327 
    328 /**
    329  * Generates new key pair to use for the host. The specified callback is called
    330  * when the key is generated. The key is returned in format understood by the
    331  * host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64).
    332  *
    333  * @param {function(string, string):void} onDone Callback to return result.
    334  * @param {function(remoting.Error):void} onError Callback to call on error.
    335  * @return {void} Nothing.
    336  */
    337 remoting.HostDaemonFacade.prototype.generateKeyPair =
    338     function(onDone, onError) {
    339   this.postMessage_({type: 'generateKeyPair'}, onDone, onError);
    340 };
    341 
    342 /**
    343  * Updates host config with the values specified in |config|. All
    344  * fields that are not specified in |config| remain
    345  * unchanged. Following parameters cannot be changed using this
    346  * function: host_id, xmpp_login. Error is returned if |config|
    347  * includes these parameters. Changes take effect before the callback
    348  * is called.
    349  *
    350  * @param {Object} config The new config parameters.
    351  * @param {function(remoting.HostController.AsyncResult):void} onDone
    352  *     Callback to be called when finished.
    353  * @param {function(remoting.Error):void} onError Callback to call on error.
    354  * @return {void} Nothing.
    355  */
    356 remoting.HostDaemonFacade.prototype.updateDaemonConfig =
    357     function(config, onDone, onError) {
    358   this.postMessage_({
    359       type: 'updateDaemonConfig',
    360       config: config
    361   }, onDone, onError);
    362 };
    363 
    364 /**
    365  * Loads daemon config. The config is passed as a JSON formatted string to the
    366  * callback.
    367  *
    368  * @param {function(Object):void} onDone Callback to return result.
    369  * @param {function(remoting.Error):void} onError Callback to call on error.
    370  * @return {void} Nothing.
    371  */
    372 remoting.HostDaemonFacade.prototype.getDaemonConfig =
    373     function(onDone, onError) {
    374   this.postMessage_({type: 'getDaemonConfig'}, onDone, onError);
    375 };
    376 
    377 /**
    378  * Retrieves daemon version. The version is passed to onDone as a dotted decimal
    379  * string of the form major.minor.build.patch.
    380  * @param {function(string):void} onDone Callback to be called to return result.
    381  * @param {function(remoting.Error):void} onError Callback to call on error.
    382  * @return {void}
    383  */
    384 remoting.HostDaemonFacade.prototype.getDaemonVersion =
    385     function(onDone, onError) {
    386   if (!this.port_) {
    387     onError(remoting.Error.UNEXPECTED);
    388   } else if (this.initializationFinished_) {
    389     onDone(this.version_);
    390   } else {
    391     /** @type remoting.HostDaemonFacade */
    392     var that = this;
    393     this.afterInitializationTasks_.push(
    394         /** @param {boolean} success */
    395         function(success) {
    396           if (success) {
    397             onDone(that.version_);
    398           } else {
    399             onError(that.error_);
    400           }
    401         });
    402   }
    403 };
    404 
    405 /**
    406  * Get the user's consent to crash reporting. The consent flags are passed to
    407  * the callback as booleans: supported, allowed, set-by-policy.
    408  *
    409  * @param {function(boolean, boolean, boolean):void} onDone Callback to return
    410  *     result.
    411  * @param {function(remoting.Error):void} onError Callback to call on error.
    412  * @return {void} Nothing.
    413  */
    414 remoting.HostDaemonFacade.prototype.getUsageStatsConsent =
    415     function(onDone, onError) {
    416   this.postMessage_({type: 'getUsageStatsConsent'}, onDone, onError);
    417 };
    418 
    419 /**
    420  * Starts the daemon process with the specified configuration.
    421  *
    422  * @param {Object} config Host configuration.
    423  * @param {boolean} consent Consent to report crash dumps.
    424  * @param {function(remoting.HostController.AsyncResult):void} onDone
    425  *     Callback to return result.
    426  * @param {function(remoting.Error):void} onError Callback to call on error.
    427  * @return {void} Nothing.
    428  */
    429 remoting.HostDaemonFacade.prototype.startDaemon =
    430     function(config, consent, onDone, onError) {
    431   this.postMessage_({
    432       type: 'startDaemon',
    433       config: config,
    434       consent: consent
    435   }, onDone, onError);
    436 };
    437 
    438 /**
    439  * Stops the daemon process.
    440  *
    441  * @param {function(remoting.HostController.AsyncResult):void} onDone
    442  *     Callback to return result.
    443  * @param {function(remoting.Error):void} onError Callback to call on error.
    444  * @return {void} Nothing.
    445  */
    446 remoting.HostDaemonFacade.prototype.stopDaemon =
    447     function(onDone, onError) {
    448   this.postMessage_({type: 'stopDaemon'}, onDone, onError);
    449 };
    450 
    451 /**
    452  * Gets the installed/running state of the Host process.
    453  *
    454  * @param {function(remoting.HostController.State):void} onDone Callback to
    455 *      return result.
    456  * @param {function(remoting.Error):void} onError Callback to call on error.
    457  * @return {void} Nothing.
    458  */
    459 remoting.HostDaemonFacade.prototype.getDaemonState =
    460     function(onDone, onError) {
    461   this.postMessage_({type: 'getDaemonState'}, onDone, onError);
    462 }
    463 
    464 /**
    465  * Retrieves the list of paired clients.
    466  *
    467  * @param {function(Array.<remoting.PairedClient>):void} onDone Callback to
    468  *     return result.
    469  * @param {function(remoting.Error):void} onError Callback to call on error.
    470  */
    471 remoting.HostDaemonFacade.prototype.getPairedClients =
    472     function(onDone, onError) {
    473   this.postMessage_({type: 'getPairedClients'}, onDone, onError);
    474 }
    475 
    476 /**
    477  * Clears all paired clients from the registry.
    478  *
    479  * @param {function(boolean):void} onDone Callback to be called when finished.
    480  * @param {function(remoting.Error):void} onError Callback to call on error.
    481  */
    482 remoting.HostDaemonFacade.prototype.clearPairedClients =
    483     function(onDone, onError) {
    484   this.postMessage_({type: 'clearPairedClients'}, onDone, onError);
    485 }
    486 
    487 /**
    488  * Deletes a paired client referenced by client id.
    489  *
    490  * @param {string} client Client to delete.
    491  * @param {function(boolean):void} onDone Callback to be called when finished.
    492  * @param {function(remoting.Error):void} onError Callback to call on error.
    493  */
    494 remoting.HostDaemonFacade.prototype.deletePairedClient =
    495     function(client, onDone, onError) {
    496   this.postMessage_({
    497     type: 'deletePairedClient',
    498     clientId: client
    499   }, onDone, onError);
    500 }
    501 
    502 /**
    503  * Gets the API keys to obtain/use service account credentials.
    504  *
    505  * @param {function(string):void} onDone Callback to return result.
    506  * @param {function(remoting.Error):void} onError Callback to call on error.
    507  * @return {void} Nothing.
    508  */
    509 remoting.HostDaemonFacade.prototype.getHostClientId =
    510     function(onDone, onError) {
    511   this.postMessage_({type: 'getHostClientId'}, onDone, onError);
    512 };
    513 
    514 /**
    515  *
    516  * @param {string} authorizationCode OAuth authorization code.
    517  * @param {function(string, string):void} onDone Callback to return result.
    518  * @param {function(remoting.Error):void} onError Callback to call on error.
    519  * @return {void} Nothing.
    520  */
    521 remoting.HostDaemonFacade.prototype.getCredentialsFromAuthCode =
    522     function(authorizationCode, onDone, onError) {
    523   this.postMessage_({
    524     type: 'getCredentialsFromAuthCode',
    525     authorizationCode: authorizationCode
    526   }, onDone, onError);
    527 };
    528