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