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