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