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  * This class provides an interface between the HostController and either the
      8  * NativeMessaging Host or the Host NPAPI plugin, depending on whether or not
      9  * NativeMessaging is supported. Since the test for NativeMessaging support is
     10  * asynchronous, this class stores any requests on a queue, pending the result
     11  * of the test.
     12  * Once the test is complete, the pending requests are performed on either the
     13  * NativeMessaging host, or the NPAPI plugin.
     14  *
     15  * If necessary, the HostController is instructed (via a callback) to
     16  * instantiate the NPAPI plugin, and return a reference to it here.
     17  */
     18 
     19 'use strict';
     20 
     21 /** @suppress {duplicate} */
     22 var remoting = remoting || {};
     23 
     24 /**
     25  * @constructor
     26  * @param {function():remoting.HostPlugin} createPluginCallback Callback to
     27  *     instantiate the NPAPI plugin when NativeMessaging is determined to be
     28  *     unsupported.
     29  */
     30 remoting.HostDispatcher = function(createPluginCallback) {
     31   /** @type {remoting.HostDispatcher} */
     32   var that = this;
     33 
     34   /** @type {remoting.HostNativeMessaging} @private */
     35   this.nativeMessagingHost_ = new remoting.HostNativeMessaging();
     36 
     37   /** @type {remoting.HostPlugin} @private */
     38   this.npapiHost_ = null;
     39 
     40   /** @type {remoting.HostDispatcher.State} */
     41   this.state_ = remoting.HostDispatcher.State.UNKNOWN;
     42 
     43   /** @type {Array.<function()>} */
     44   this.pendingRequests_ = [];
     45 
     46   function sendPendingRequests() {
     47     for (var i = 0; i < that.pendingRequests_.length; i++) {
     48       that.pendingRequests_[i]();
     49     }
     50     that.pendingRequests_ = null;
     51   }
     52 
     53   function onNativeMessagingInit() {
     54     console.log('Native Messaging supported.');
     55     that.state_ = remoting.HostDispatcher.State.NATIVE_MESSAGING;
     56     sendPendingRequests();
     57   }
     58 
     59   function onNativeMessagingFailed(error) {
     60     console.log('Native Messaging unsupported, falling back to NPAPI.');
     61     that.npapiHost_ = createPluginCallback();
     62     that.state_ = remoting.HostDispatcher.State.NPAPI;
     63     sendPendingRequests();
     64   }
     65 
     66   this.nativeMessagingHost_.initialize(onNativeMessagingInit,
     67                                        onNativeMessagingFailed);
     68 };
     69 
     70 /** @enum {number} */
     71 remoting.HostDispatcher.State = {
     72   UNKNOWN: 0,
     73   NATIVE_MESSAGING: 1,
     74   NPAPI: 2
     75 };
     76 
     77 /**
     78  * @param {remoting.HostController.Feature} feature The feature to test for.
     79  * @param {function(boolean):void} onDone
     80  * @return {void}
     81  */
     82 remoting.HostDispatcher.prototype.hasFeature = function(
     83     feature, onDone) {
     84   switch (this.state_) {
     85     case remoting.HostDispatcher.State.UNKNOWN:
     86       this.pendingRequests_.push(
     87           this.hasFeature.bind(this, feature, onDone));
     88       break;
     89     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
     90       onDone(this.nativeMessagingHost_.hasFeature(feature));
     91       break;
     92     case remoting.HostDispatcher.State.NPAPI:
     93       // If this is an old NPAPI plugin that doesn't list supportedFeatures,
     94       // assume it is an old plugin that doesn't support any new feature.
     95       var supportedFeatures = [];
     96       if (typeof(this.npapiHost_.supportedFeatures) == 'string') {
     97         supportedFeatures = this.npapiHost_.supportedFeatures.split(' ');
     98       }
     99       onDone(supportedFeatures.indexOf(feature) >= 0);
    100       break;
    101   }
    102 };
    103 
    104 /**
    105  * @param {function(string):void} onDone
    106  * @param {function(remoting.Error):void} onError
    107  * @return {void}
    108  */
    109 remoting.HostDispatcher.prototype.getHostName = function(onDone, onError) {
    110   switch (this.state_) {
    111     case remoting.HostDispatcher.State.UNKNOWN:
    112       this.pendingRequests_.push(
    113           this.getHostName.bind(this, onDone, onError));
    114       break;
    115     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    116       this.nativeMessagingHost_.getHostName(onDone, onError);
    117       break;
    118     case remoting.HostDispatcher.State.NPAPI:
    119       try {
    120         this.npapiHost_.getHostName(onDone);
    121       } catch (err) {
    122         onError(remoting.Error.MISSING_PLUGIN);
    123       }
    124       break;
    125   }
    126 };
    127 
    128 /**
    129  * @param {string} hostId
    130  * @param {string} pin
    131  * @param {function(string):void} onDone
    132  * @param {function(remoting.Error):void} onError
    133  * @return {void}
    134  */
    135 remoting.HostDispatcher.prototype.getPinHash =
    136     function(hostId, pin, onDone, onError) {
    137   switch (this.state_) {
    138     case remoting.HostDispatcher.State.UNKNOWN:
    139       this.pendingRequests_.push(
    140           this.getPinHash.bind(this, hostId, pin, onDone, onError));
    141       break;
    142     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    143       this.nativeMessagingHost_.getPinHash(hostId, pin, onDone, onError);
    144       break;
    145     case remoting.HostDispatcher.State.NPAPI:
    146       try {
    147         this.npapiHost_.getPinHash(hostId, pin, onDone);
    148       } catch (err) {
    149         onError(remoting.Error.MISSING_PLUGIN);
    150       }
    151       break;
    152   }
    153 };
    154 
    155 /**
    156  * @param {function(string, string):void} onDone
    157  * @param {function(remoting.Error):void} onError
    158  * @return {void}
    159  */
    160 remoting.HostDispatcher.prototype.generateKeyPair = function(onDone, onError) {
    161   switch (this.state_) {
    162     case remoting.HostDispatcher.State.UNKNOWN:
    163       this.pendingRequests_.push(
    164           this.generateKeyPair.bind(this, onDone, onError));
    165       break;
    166     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    167       this.nativeMessagingHost_.generateKeyPair(onDone, onError);
    168       break;
    169     case remoting.HostDispatcher.State.NPAPI:
    170       try {
    171         this.npapiHost_.generateKeyPair(onDone);
    172       } catch (err) {
    173         onError(remoting.Error.MISSING_PLUGIN);
    174       }
    175       break;
    176   }
    177 };
    178 
    179 /**
    180  * @param {Object} config
    181  * @param {function(remoting.HostController.AsyncResult):void} onDone
    182  * @param {function(remoting.Error):void} onError
    183  * @return {void}
    184  */
    185 remoting.HostDispatcher.prototype.updateDaemonConfig =
    186     function(config, onDone, onError) {
    187   switch (this.state_) {
    188     case remoting.HostDispatcher.State.UNKNOWN:
    189       this.pendingRequests_.push(
    190           this.updateDaemonConfig.bind(this, config, onDone, onError));
    191       break;
    192     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    193       this.nativeMessagingHost_.updateDaemonConfig(config, onDone, onError);
    194       break;
    195     case remoting.HostDispatcher.State.NPAPI:
    196       try {
    197         this.npapiHost_.updateDaemonConfig(JSON.stringify(config), onDone);
    198       } catch (err) {
    199         onError(remoting.Error.MISSING_PLUGIN);
    200       }
    201       break;
    202   }
    203 };
    204 
    205 /**
    206  * @param {function(Object):void} onDone
    207  * @param {function(remoting.Error):void} onError
    208  * @return {void}
    209  */
    210 remoting.HostDispatcher.prototype.getDaemonConfig = function(onDone, onError) {
    211   /**
    212    * Converts the config string from the NPAPI plugin to an Object, to pass to
    213    * |onDone|.
    214    * @param {string} configStr
    215    * @return {void}
    216    */
    217   function callbackForNpapi(configStr) {
    218     var config = jsonParseSafe(configStr);
    219     if (typeof(config) != 'object') {
    220       onError(remoting.Error.UNEXPECTED);
    221     } else {
    222       onDone(/** @type {Object} */ (config));
    223     }
    224   }
    225 
    226   switch (this.state_) {
    227     case remoting.HostDispatcher.State.UNKNOWN:
    228       this.pendingRequests_.push(
    229           this.getDaemonConfig.bind(this, onDone, onError));
    230       break;
    231     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    232       this.nativeMessagingHost_.getDaemonConfig(onDone, onError);
    233       break;
    234     case remoting.HostDispatcher.State.NPAPI:
    235       try {
    236         this.npapiHost_.getDaemonConfig(callbackForNpapi);
    237       } catch (err) {
    238         onError(remoting.Error.MISSING_PLUGIN);
    239       }
    240       break;
    241   }
    242 };
    243 
    244 /**
    245  * @param {function(string):void} onDone
    246  * @param {function(remoting.Error):void} onError
    247  * @return {void}
    248  */
    249 remoting.HostDispatcher.prototype.getDaemonVersion = function(onDone, onError) {
    250   switch (this.state_) {
    251     case remoting.HostDispatcher.State.UNKNOWN:
    252       this.pendingRequests_.push(
    253           this.getDaemonVersion.bind(this, onDone, onError));
    254       break;
    255     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    256       onDone(this.nativeMessagingHost_.getDaemonVersion());
    257       break;
    258     case remoting.HostDispatcher.State.NPAPI:
    259       try {
    260         this.npapiHost_.getDaemonVersion(onDone);
    261       } catch (err) {
    262         onError(remoting.Error.MISSING_PLUGIN);
    263       }
    264       break;
    265   }
    266 };
    267 
    268 /**
    269  * @param {function(boolean, boolean, boolean):void} onDone
    270  * @param {function(remoting.Error):void} onError
    271  * @return {void}
    272  */
    273 remoting.HostDispatcher.prototype.getUsageStatsConsent =
    274     function(onDone, onError) {
    275   switch (this.state_) {
    276     case remoting.HostDispatcher.State.UNKNOWN:
    277       this.pendingRequests_.push(
    278           this.getUsageStatsConsent.bind(this, onDone, onError));
    279       break;
    280     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    281       this.nativeMessagingHost_.getUsageStatsConsent(onDone, onError);
    282       break;
    283     case remoting.HostDispatcher.State.NPAPI:
    284       try {
    285         this.npapiHost_.getUsageStatsConsent(onDone);
    286       } catch (err) {
    287         onError(remoting.Error.MISSING_PLUGIN);
    288       }
    289       break;
    290   }
    291 };
    292 
    293 /**
    294  * @param {Object} config
    295  * @param {boolean} consent
    296  * @param {function(remoting.HostController.AsyncResult):void} onDone
    297  * @param {function(remoting.Error):void} onError
    298  * @return {void}
    299  */
    300 remoting.HostDispatcher.prototype.startDaemon =
    301     function(config, consent, onDone, onError) {
    302   switch (this.state_) {
    303     case remoting.HostDispatcher.State.UNKNOWN:
    304       this.pendingRequests_.push(
    305           this.startDaemon.bind(this, config, consent, onDone, onError));
    306       break;
    307     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    308       this.nativeMessagingHost_.startDaemon(config, consent, onDone, onError);
    309       break;
    310     case remoting.HostDispatcher.State.NPAPI:
    311       try {
    312         this.npapiHost_.startDaemon(JSON.stringify(config), consent, onDone);
    313       } catch (err) {
    314         onError(remoting.Error.MISSING_PLUGIN);
    315       }
    316       break;
    317   }
    318 };
    319 
    320 /**
    321  * @param {function(remoting.HostController.AsyncResult):void} onDone
    322  * @param {function(remoting.Error):void} onError
    323  * @return {void}
    324  */
    325 remoting.HostDispatcher.prototype.stopDaemon = function(onDone, onError) {
    326   switch (this.state_) {
    327     case remoting.HostDispatcher.State.UNKNOWN:
    328       this.pendingRequests_.push(this.stopDaemon.bind(this, onDone, onError));
    329       break;
    330     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    331       this.nativeMessagingHost_.stopDaemon(onDone, onError);
    332       break;
    333     case remoting.HostDispatcher.State.NPAPI:
    334       try {
    335         this.npapiHost_.stopDaemon(onDone);
    336       } catch (err) {
    337         onError(remoting.Error.MISSING_PLUGIN);
    338       }
    339       break;
    340   }
    341 };
    342 
    343 /**
    344  * @param {function(remoting.HostController.State):void} onDone
    345  * @param {function(remoting.Error):void} onError
    346  * @return {void}
    347  */
    348 remoting.HostDispatcher.prototype.getDaemonState = function(onDone, onError) {
    349   switch (this.state_) {
    350     case remoting.HostDispatcher.State.UNKNOWN:
    351       this.pendingRequests_.push(
    352           this.getDaemonState.bind(this, onDone, onError));
    353       break;
    354     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    355       this.nativeMessagingHost_.getDaemonState(onDone, onError);
    356       break;
    357     case remoting.HostDispatcher.State.NPAPI:
    358       // Call the callback directly, since NPAPI exposes the state directly as
    359       // a field member, rather than an asynchronous method.
    360       var state = this.npapiHost_.daemonState;
    361       if (state === undefined) {
    362         onError(remoting.Error.MISSING_PLUGIN);
    363       } else {
    364         onDone(state);
    365       }
    366       break;
    367   }
    368 };
    369 
    370 /**
    371  * @param {function(Array.<remoting.PairedClient>):void} onDone
    372  * @param {function(remoting.Error):void} onError
    373  * @return {void}
    374  */
    375 remoting.HostDispatcher.prototype.getPairedClients = function(onDone, onError) {
    376   /**
    377    * Converts the JSON string from the NPAPI plugin to Array.<PairedClient>, to
    378    * pass to |onDone|.
    379    * @param {string} pairedClientsJson
    380    * @return {void}
    381    */
    382   function callbackForNpapi(pairedClientsJson) {
    383     var pairedClients = remoting.PairedClient.convertToPairedClientArray(
    384         jsonParseSafe(pairedClientsJson));
    385     if (pairedClients != null) {
    386       onDone(pairedClients);
    387     } else {
    388       onError(remoting.Error.UNEXPECTED);
    389     }
    390   }
    391 
    392   switch (this.state_) {
    393     case remoting.HostDispatcher.State.UNKNOWN:
    394       this.pendingRequests_.push(
    395           this.getPairedClients.bind(this, onDone, onError));
    396       break;
    397     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    398       this.nativeMessagingHost_.getPairedClients(onDone, onError);
    399       break;
    400     case remoting.HostDispatcher.State.NPAPI:
    401       try {
    402         this.npapiHost_.getPairedClients(callbackForNpapi);
    403       } catch (err) {
    404         onError(remoting.Error.MISSING_PLUGIN);
    405       }
    406       break;
    407   }
    408 };
    409 
    410 /**
    411  * The pairing API returns a boolean to indicate success or failure, but
    412  * the JS API is defined in terms of onDone and onError callbacks. This
    413  * function converts one to the other.
    414  *
    415  * @param {function():void} onDone Success callback.
    416  * @param {function(remoting.Error):void} onError Error callback.
    417  * @param {boolean} success True if the operation succeeded; false otherwise.
    418  * @private
    419  */
    420 remoting.HostDispatcher.runCallback_ = function(onDone, onError, success) {
    421   if (success) {
    422     onDone();
    423   } else {
    424     onError(remoting.Error.UNEXPECTED);
    425   }
    426 };
    427 
    428 /**
    429  * @param {function():void} onDone
    430  * @param {function(remoting.Error):void} onError
    431  * @return {void}
    432  */
    433 remoting.HostDispatcher.prototype.clearPairedClients =
    434     function(onDone, onError) {
    435   var callback =
    436       remoting.HostDispatcher.runCallback_.bind(null, onDone, onError);
    437   switch (this.state_) {
    438     case remoting.HostDispatcher.State.UNKNOWN:
    439       this.pendingRequests_.push(
    440         this.clearPairedClients.bind(this, onDone, onError));
    441       break;
    442     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    443       this.nativeMessagingHost_.clearPairedClients(callback, onError);
    444       break;
    445     case remoting.HostDispatcher.State.NPAPI:
    446       try {
    447         this.npapiHost_.clearPairedClients(callback);
    448       } catch (err) {
    449         onError(remoting.Error.MISSING_PLUGIN);
    450       }
    451       break;
    452   }
    453 };
    454 
    455 /**
    456  * @param {string} client
    457  * @param {function():void} onDone
    458  * @param {function(remoting.Error):void} onError
    459  * @return {void}
    460  */
    461 remoting.HostDispatcher.prototype.deletePairedClient =
    462     function(client, onDone, onError) {
    463   var callback =
    464       remoting.HostDispatcher.runCallback_.bind(null, onDone, onError);
    465   switch (this.state_) {
    466     case remoting.HostDispatcher.State.UNKNOWN:
    467       this.pendingRequests_.push(
    468         this.deletePairedClient.bind(this, client, onDone, onError));
    469       break;
    470     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    471       this.nativeMessagingHost_.deletePairedClient(client, callback, onError);
    472       break;
    473     case remoting.HostDispatcher.State.NPAPI:
    474       try {
    475         this.npapiHost_.deletePairedClient(client, callback);
    476       } catch (err) {
    477         onError(remoting.Error.MISSING_PLUGIN);
    478       }
    479       break;
    480   }
    481 };
    482 
    483 /**
    484  * @param {function(string):void} onDone
    485  * @param {function(remoting.Error):void} onError
    486  * @return {void}
    487  */
    488 remoting.HostDispatcher.prototype.getHostClientId =
    489     function(onDone, onError) {
    490   switch (this.state_) {
    491     case remoting.HostDispatcher.State.UNKNOWN:
    492       this.pendingRequests_.push(
    493           this.getHostClientId.bind(this, onDone, onError));
    494       break;
    495     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    496       this.nativeMessagingHost_.getHostClientId(onDone, onError);
    497       break;
    498     case remoting.HostDispatcher.State.NPAPI:
    499       // The NPAPI plugin is packaged with the webapp, not the host, so it
    500       // doesn't have access to the API keys baked into the installed host.
    501       onError(remoting.Error.UNEXPECTED);
    502       break;
    503   }
    504 };
    505 
    506 /**
    507  * @param {string} authorizationCode
    508  * @param {function(string, string):void} onDone
    509  * @param {function(remoting.Error):void} onError
    510  * @return {void}
    511  */
    512 remoting.HostDispatcher.prototype.getCredentialsFromAuthCode =
    513     function(authorizationCode, onDone, onError) {
    514   switch (this.state_) {
    515     case remoting.HostDispatcher.State.UNKNOWN:
    516       this.pendingRequests_.push(
    517           this.getCredentialsFromAuthCode.bind(
    518               this, authorizationCode, onDone, onError));
    519       break;
    520     case remoting.HostDispatcher.State.NATIVE_MESSAGING:
    521       this.nativeMessagingHost_.getCredentialsFromAuthCode(
    522           authorizationCode, onDone, onError);
    523       break;
    524     case remoting.HostDispatcher.State.NPAPI:
    525       // The NPAPI plugin is packaged with the webapp, not the host, so it
    526       // doesn't have access to the API keys baked into the installed host.
    527       onError(remoting.Error.UNEXPECTED);
    528       break;
    529   }
    530 };
    531