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