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} */ 13var remoting = remoting || {}; 14 15/** 16 * @constructor 17 */ 18remoting.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 */ 51remoting.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. If Native Messaging is not available or the host 60 * process is not installed, this returns false to the callback. 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 */ 67remoting.HostNativeMessaging.prototype.initialize = function(onDone, onError) { 68 if (!chrome.runtime.connectNative) { 69 console.log('Native Messaging API not available'); 70 onError(remoting.Error.UNEXPECTED); 71 return; 72 } 73 74 // NativeMessaging API exists on Chrome 26.xxx but fails to notify 75 // onDisconnect in the case where the Host components are not installed. Need 76 // to blacklist these versions of Chrome. 77 var majorVersion = navigator.appVersion.match('Chrome/(\\d+)\.')[1]; 78 if (!majorVersion || majorVersion <= 26) { 79 console.log('Native Messaging not supported on this version of Chrome'); 80 onError(remoting.Error.UNEXPECTED); 81 return; 82 } 83 84 try { 85 this.port_ = chrome.runtime.connectNative( 86 'com.google.chrome.remote_desktop'); 87 this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this)); 88 this.port_.onDisconnect.addListener(this.onDisconnect_.bind(this)); 89 this.postMessage_({type: 'hello'}, onDone, 90 onError.bind(null, remoting.Error.UNEXPECTED)); 91 } catch (err) { 92 console.log('Native Messaging initialization failed: ', 93 /** @type {*} */ (err)); 94 onError(remoting.Error.UNEXPECTED); 95 return; 96 } 97}; 98 99/** 100 * Verifies that |object| is of type |type|, logging an error if not. 101 * 102 * @param {string} name Name of the object, to be included in the error log. 103 * @param {*} object Object to test. 104 * @param {string} type Expected type of the object. 105 * @return {boolean} Result of test. 106 */ 107function checkType_(name, object, type) { 108 if (typeof(object) !== type) { 109 console.error('NativeMessaging: "' + name + '" expected to be of type "' + 110 type + '", got: ' + object); 111 return false; 112 } 113 return true; 114} 115 116/** 117 * Returns |result| as an AsyncResult. If |result| is not valid, returns null 118 * and logs an error. 119 * 120 * @param {*} result 121 * @return {remoting.HostController.AsyncResult?} Converted result. 122 */ 123function asAsyncResult_(result) { 124 if (!checkType_('result', result, 'string')) { 125 return null; 126 } 127 if (!remoting.HostController.AsyncResult.hasOwnProperty(result)) { 128 console.error('NativeMessaging: unexpected result code: ', result); 129 return null; 130 } 131 return remoting.HostController.AsyncResult[result]; 132} 133 134/** 135 * Returns |result| as a HostController.State. If |result| is not valid, 136 * returns null and logs an error. 137 * 138 * @param {*} result 139 * @return {remoting.HostController.State?} Converted result. 140 */ 141function asHostState_(result) { 142 if (!checkType_('result', result, 'string')) { 143 return null; 144 } 145 if (!remoting.HostController.State.hasOwnProperty(result)) { 146 console.error('NativeMessaging: unexpected result code: ', result); 147 return null; 148 } 149 return remoting.HostController.State[result]; 150} 151 152/** 153 * @param {remoting.HostController.Feature} feature The feature to test for. 154 * @return {boolean} True if the implementation supports the named feature. 155 */ 156remoting.HostNativeMessaging.prototype.hasFeature = function(feature) { 157 return this.supportedFeatures_.indexOf(feature) >= 0; 158}; 159 160/** 161 * Attaches a new ID to the supplied message, and posts it to the Native 162 * Messaging port, adding |onDone| to the list of pending replies. 163 * |message| should have its 'type' field set, and any other fields set 164 * depending on the message type. 165 * 166 * @param {{type: string}} message The message to post. 167 * @param {?function(...):void} onDone The callback, if any, to be triggered 168 * on response. 169 * @param {function(remoting.Error):void} onError The callback to be triggered 170 * on error. 171 * @return {void} Nothing. 172 * @private 173 */ 174remoting.HostNativeMessaging.prototype.postMessage_ = 175 function(message, onDone, onError) { 176 var id = this.nextId_++; 177 message['id'] = id; 178 this.pendingReplies_[id] = new remoting.HostNativeMessaging.PendingReply( 179 message.type + 'Response', onDone, onError); 180 this.port_.postMessage(message); 181}; 182 183/** 184 * Handler for incoming Native Messages. 185 * 186 * @param {Object} message The received message. 187 * @return {void} Nothing. 188 * @private 189 */ 190remoting.HostNativeMessaging.prototype.onIncomingMessage_ = function(message) { 191 /** @type {number} */ 192 var id = message['id']; 193 if (typeof(id) != 'number') { 194 console.error('NativeMessaging: missing or non-numeric id'); 195 return; 196 } 197 var reply = this.pendingReplies_[id]; 198 if (!reply) { 199 console.error('NativeMessaging: unexpected id: ', id); 200 return; 201 } 202 delete this.pendingReplies_[id]; 203 204 var onDone = reply.onDone; 205 var onError = reply.onError; 206 207 /** @type {string} */ 208 var type = message['type']; 209 if (!checkType_('type', type, 'string')) { 210 onError(remoting.Error.UNEXPECTED); 211 return; 212 } 213 if (type != reply.type) { 214 console.error('NativeMessaging: expected reply type: ', reply.type, 215 ', got: ', type); 216 onError(remoting.Error.UNEXPECTED); 217 return; 218 } 219 220 switch (type) { 221 case 'helloResponse': 222 /** @type {string} */ 223 var version = message['version']; 224 if (checkType_('version', version, 'string')) { 225 this.version_ = version; 226 if (message['supportedFeatures'] instanceof Array) { 227 this.supportedFeatures_ = message['supportedFeatures']; 228 } else { 229 // Old versions of the native messaging host do not return this list. 230 // Those versions don't support any new feature. 231 this.supportedFeatures_ = []; 232 } 233 onDone(); 234 } else { 235 onError(remoting.Error.UNEXPECTED); 236 } 237 break; 238 239 case 'getHostNameResponse': 240 /** @type {*} */ 241 var hostname = message['hostname']; 242 if (checkType_('hostname', hostname, 'string')) { 243 onDone(hostname); 244 } else { 245 onError(remoting.Error.UNEXPECTED); 246 } 247 break; 248 249 case 'getPinHashResponse': 250 /** @type {*} */ 251 var hash = message['hash']; 252 if (checkType_('hash', hash, 'string')) { 253 onDone(hash); 254 } else { 255 onError(remoting.Error.UNEXPECTED); 256 } 257 break; 258 259 case 'generateKeyPairResponse': 260 /** @type {*} */ 261 var privateKey = message['privateKey']; 262 /** @type {*} */ 263 var publicKey = message['publicKey']; 264 if (checkType_('privateKey', privateKey, 'string') && 265 checkType_('publicKey', publicKey, 'string')) { 266 onDone(privateKey, publicKey); 267 } else { 268 onError(remoting.Error.UNEXPECTED); 269 } 270 break; 271 272 case 'updateDaemonConfigResponse': 273 var result = asAsyncResult_(message['result']); 274 if (result != null) { 275 onDone(result); 276 } else { 277 onError(remoting.Error.UNEXPECTED); 278 } 279 break; 280 281 case 'getDaemonConfigResponse': 282 /** @type {*} */ 283 var config = message['config']; 284 if (checkType_('config', config, 'object')) { 285 onDone(config); 286 } else { 287 onError(remoting.Error.UNEXPECTED); 288 } 289 break; 290 291 case 'getUsageStatsConsentResponse': 292 /** @type {*} */ 293 var supported = message['supported']; 294 /** @type {*} */ 295 var allowed = message['allowed']; 296 /** @type {*} */ 297 var setByPolicy = message['setByPolicy']; 298 if (checkType_('supported', supported, 'boolean') && 299 checkType_('allowed', allowed, 'boolean') && 300 checkType_('setByPolicy', setByPolicy, 'boolean')) { 301 onDone(supported, allowed, setByPolicy); 302 } else { 303 onError(remoting.Error.UNEXPECTED); 304 } 305 break; 306 307 case 'startDaemonResponse': 308 case 'stopDaemonResponse': 309 var result = asAsyncResult_(message['result']); 310 if (result != null) { 311 onDone(result); 312 } else { 313 onError(remoting.Error.UNEXPECTED); 314 } 315 break; 316 317 case 'getDaemonStateResponse': 318 var state = asHostState_(message['state']); 319 if (state != null) { 320 onDone(state); 321 } else { 322 onError(remoting.Error.UNEXPECTED); 323 } 324 break; 325 326 case 'getPairedClientsResponse': 327 var pairedClients = remoting.PairedClient.convertToPairedClientArray( 328 message['pairedClients']); 329 if (pairedClients != null) { 330 onDone(pairedClients); 331 } else { 332 onError(remoting.Error.UNEXPECTED); 333 } 334 break; 335 336 case 'clearPairedClientsResponse': 337 case 'deletePairedClientResponse': 338 /** @type {boolean} */ 339 var success = message['result']; 340 if (checkType_('success', success, 'boolean')) { 341 onDone(success); 342 } else { 343 onError(remoting.Error.UNEXPECTED); 344 } 345 break; 346 347 case 'getHostClientIdResponse': 348 /** @type {string} */ 349 var clientId = message['clientId']; 350 if (checkType_('clientId', clientId, 'string')) { 351 onDone(clientId); 352 } else { 353 onError(remoting.Error.UNEXPECTED); 354 } 355 break; 356 357 case 'getCredentialsFromAuthCodeResponse': 358 /** @type {string} */ 359 var userEmail = message['userEmail']; 360 /** @type {string} */ 361 var refreshToken = message['refreshToken']; 362 if (checkType_('userEmail', userEmail, 'string') && userEmail && 363 checkType_('refreshToken', refreshToken, 'string') && refreshToken) { 364 onDone(userEmail, refreshToken); 365 } else { 366 onError(remoting.Error.UNEXPECTED); 367 } 368 break; 369 370 default: 371 console.error('Unexpected native message: ', message); 372 onError(remoting.Error.UNEXPECTED); 373 } 374}; 375 376/** 377 * @return {void} Nothing. 378 * @private 379 */ 380remoting.HostNativeMessaging.prototype.onDisconnect_ = function() { 381 console.error('Native Message port disconnected'); 382 383 // Notify the error-handlers of any requests that are still outstanding. 384 for (var id in this.pendingReplies_) { 385 this.pendingReplies_[/** @type {number} */(id)].onError( 386 remoting.Error.UNEXPECTED); 387 } 388 this.pendingReplies_ = {}; 389} 390 391/** 392 * @param {function(string):void} onDone Callback to be called with the 393 * local hostname. 394 * @param {function(remoting.Error):void} onError The callback to be triggered 395 * on error. 396 * @return {void} Nothing. 397 */ 398remoting.HostNativeMessaging.prototype.getHostName = 399 function(onDone, onError) { 400 this.postMessage_({type: 'getHostName'}, onDone, onError); 401}; 402 403/** 404 * Calculates PIN hash value to be stored in the config, passing the resulting 405 * hash value base64-encoded to the callback. 406 * 407 * @param {string} hostId The host ID. 408 * @param {string} pin The PIN. 409 * @param {function(string):void} onDone Callback. 410 * @param {function(remoting.Error):void} onError The callback to be triggered 411 * on error. 412 * @return {void} Nothing. 413 */ 414remoting.HostNativeMessaging.prototype.getPinHash = 415 function(hostId, pin, onDone, onError) { 416 this.postMessage_({ 417 type: 'getPinHash', 418 hostId: hostId, 419 pin: pin 420 }, onDone, onError); 421}; 422 423/** 424 * Generates new key pair to use for the host. The specified callback is called 425 * when the key is generated. The key is returned in format understood by the 426 * host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64). 427 * 428 * @param {function(string, string):void} onDone Callback. 429 * @param {function(remoting.Error):void} onError The callback to be triggered 430 * on error. 431 * @return {void} Nothing. 432 */ 433remoting.HostNativeMessaging.prototype.generateKeyPair = 434 function(onDone, onError) { 435 this.postMessage_({type: 'generateKeyPair'}, onDone, onError); 436}; 437 438/** 439 * Updates host config with the values specified in |config|. All 440 * fields that are not specified in |config| remain 441 * unchanged. Following parameters cannot be changed using this 442 * function: host_id, xmpp_login. Error is returned if |config| 443 * includes these parameters. Changes take effect before the callback 444 * is called. 445 * 446 * @param {Object} config The new config parameters. 447 * @param {function(remoting.HostController.AsyncResult):void} onDone 448 * Callback to be called when finished. 449 * @param {function(remoting.Error):void} onError The callback to be triggered 450 * on error. 451 * @return {void} Nothing. 452 */ 453remoting.HostNativeMessaging.prototype.updateDaemonConfig = 454 function(config, onDone, onError) { 455 this.postMessage_({ 456 type: 'updateDaemonConfig', 457 config: config 458 }, onDone, onError); 459}; 460 461/** 462 * Loads daemon config. The config is passed as a JSON formatted string to the 463 * callback. 464 * 465 * @param {function(Object):void} onDone Callback. 466 * @param {function(remoting.Error):void} onError The callback to be triggered 467 * on error. 468 * @return {void} Nothing. 469 */ 470remoting.HostNativeMessaging.prototype.getDaemonConfig = 471 function(onDone, onError) { 472 this.postMessage_({type: 'getDaemonConfig'}, onDone, onError); 473}; 474 475/** 476 * Retrieves daemon version. The version is returned as a dotted decimal string 477 * of the form major.minor.build.patch. 478 * @return {string} The daemon version, or the empty string if not available. 479 */ 480remoting.HostNativeMessaging.prototype.getDaemonVersion = function() { 481 // Return the cached version from the 'hello' exchange. 482 return this.version_; 483}; 484 485/** 486 * Get the user's consent to crash reporting. The consent flags are passed to 487 * the callback as booleans: supported, allowed, set-by-policy. 488 * 489 * @param {function(boolean, boolean, boolean):void} onDone Callback. 490 * @param {function(remoting.Error):void} onError The callback to be triggered 491 * on error. 492 * @return {void} Nothing. 493 */ 494remoting.HostNativeMessaging.prototype.getUsageStatsConsent = 495 function(onDone, onError) { 496 this.postMessage_({type: 'getUsageStatsConsent'}, onDone, onError); 497}; 498 499/** 500 * Starts the daemon process with the specified configuration. 501 * 502 * @param {Object} config Host configuration. 503 * @param {boolean} consent Consent to report crash dumps. 504 * @param {function(remoting.HostController.AsyncResult):void} onDone 505 * Callback. 506 * @param {function(remoting.Error):void} onError The callback to be triggered 507 * on error. 508 * @return {void} Nothing. 509 */ 510remoting.HostNativeMessaging.prototype.startDaemon = 511 function(config, consent, onDone, onError) { 512 this.postMessage_({ 513 type: 'startDaemon', 514 config: config, 515 consent: consent 516 }, onDone, onError); 517}; 518 519/** 520 * Stops the daemon process. 521 * 522 * @param {function(remoting.HostController.AsyncResult):void} onDone 523 * Callback. 524 * @param {function(remoting.Error):void} onError The callback to be triggered 525 * on error. 526 * @return {void} Nothing. 527 */ 528remoting.HostNativeMessaging.prototype.stopDaemon = 529 function(onDone, onError) { 530 this.postMessage_({type: 'stopDaemon'}, onDone, onError); 531}; 532 533/** 534 * Gets the installed/running state of the Host process. 535 * 536 * @param {function(remoting.HostController.State):void} onDone Callback. 537 * @param {function(remoting.Error):void} onError The callback to be triggered 538 * on error. 539 * @return {void} Nothing. 540 */ 541remoting.HostNativeMessaging.prototype.getDaemonState = 542 function(onDone, onError) { 543 this.postMessage_({type: 'getDaemonState'}, onDone, onError); 544} 545 546/** 547 * Retrieves the list of paired clients. 548 * 549 * @param {function(Array.<remoting.PairedClient>):void} onDone Callback to be 550 * called with the result. 551 * @param {function(remoting.Error):void} onError Callback to be triggered 552 * on error. 553 */ 554remoting.HostNativeMessaging.prototype.getPairedClients = 555 function(onDone, onError) { 556 this.postMessage_({type: 'getPairedClients'}, onDone, onError); 557} 558 559/** 560 * Clears all paired clients from the registry. 561 * 562 * @param {function(boolean):void} onDone Callback to be called when finished. 563 * @param {function(remoting.Error):void} onError Callback to be triggered 564 * on error. 565 */ 566remoting.HostNativeMessaging.prototype.clearPairedClients = 567 function(onDone, onError) { 568 this.postMessage_({type: 'clearPairedClients'}, onDone, onError); 569} 570 571/** 572 * Deletes a paired client referenced by client id. 573 * 574 * @param {string} client Client to delete. 575 * @param {function(boolean):void} onDone Callback to be called when finished. 576 * @param {function(remoting.Error):void} onError Callback to be triggered 577 * on error. 578 */ 579remoting.HostNativeMessaging.prototype.deletePairedClient = 580 function(client, onDone, onError) { 581 this.postMessage_({ 582 type: 'deletePairedClient', 583 clientId: client 584 }, onDone, onError); 585} 586 587/** 588 * Gets the API keys to obtain/use service account credentials. 589 * 590 * @param {function(string):void} onDone Callback. 591 * @param {function(remoting.Error):void} onError The callback to be triggered 592 * on error. 593 * @return {void} Nothing. 594 */ 595remoting.HostNativeMessaging.prototype.getHostClientId = 596 function(onDone, onError) { 597 this.postMessage_({type: 'getHostClientId'}, onDone, onError); 598}; 599 600/** 601 * 602 * @param {string} authorizationCode OAuth authorization code. 603 * @param {function(string, string):void} onDone Callback. 604 * @param {function(remoting.Error):void} onError The callback to be triggered 605 * on error. 606 * @return {void} Nothing. 607 */ 608remoting.HostNativeMessaging.prototype.getCredentialsFromAuthCode = 609 function(authorizationCode, onDone, onError) { 610 this.postMessage_({ 611 type: 'getCredentialsFromAuthCode', 612 authorizationCode: authorizationCode 613 }, onDone, onError); 614}; 615