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.
7 * Class to communicate with the host daemon via Native Messaging.
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
18 remoting.HostDaemonFacade = function() {
19 /** @private {number} */
22 /** @private {Object<number, remoting.HostDaemonFacade.PendingReply>} */
23 this.pendingReplies_ = {};
25 /** @private {?Port} */
28 /** @private {string} */
31 /** @private {Array<remoting.HostController.Feature>} */
32 this.supportedFeatures_ = [];
34 /** @private {Array<function(boolean):void>} */
35 this.afterInitializationTasks_ = [];
38 * A promise that fulfills when the daemon finishes initializing.
39 * It will be set to null when the promise fulfills.
42 this.initializingPromise_ = null;
44 /** @private {!remoting.Error} */
45 this.error_ = remoting.Error.none();
48 this.onIncomingMessageCallback_ = this.onIncomingMessage_.bind(this);
51 this.onDisconnectCallback_ = this.onDisconnect_.bind(this);
54 this.debugMessageHandler_ =
55 new remoting.NativeMessageHostDebugMessageHandler();
61 * @return {Promise} A promise that fulfills when the daemon finishes
65 remoting.HostDaemonFacade.prototype.initialize_ = function() {
66 if (!this.initializingPromise_) {
68 return Promise.resolve();
71 /** @type {remoting.HostDaemonFacade} */
73 this.initializingPromise_ = this.connectNative_().then(function() {
74 that.initializingPromise_ = null;
76 that.initializingPromise_ = null;
77 throw new Error(that.error_);
80 return this.initializingPromise_;
84 * Connects to the native messaging host and sends a hello message.
86 * @return {Promise} A promise that fulfills when the connection attempt
90 remoting.HostDaemonFacade.prototype.connectNative_ = function() {
93 this.port_ = chrome.runtime.connectNative(
94 'com.google.chrome.remote_desktop');
95 this.port_.onMessage.addListener(this.onIncomingMessageCallback_);
96 this.port_.onDisconnect.addListener(this.onDisconnectCallback_);
97 return this.postMessageInternal_({type: 'hello'}).then(function(reply) {
98 that.version_ = base.getStringAttr(reply, 'version');
99 // Old versions of the native messaging host do not return this list.
100 // Those versions default to the empty list of supported features.
101 that.supportedFeatures_ =
102 base.getArrayAttr(reply, 'supportedFeatures', []);
104 } catch (/** @type {*} */ err) {
105 console.log('Native Messaging initialization failed: ', err);
106 throw remoting.Error.unexpected();
111 * Type used for entries of |pendingReplies_| list.
113 * @param {string} type Type of the originating request.
114 * @param {!base.Deferred} deferred Used to communicate returns back
118 remoting.HostDaemonFacade.PendingReply = function(type, deferred) {
123 this.deferred = deferred;
127 * @param {remoting.HostController.Feature} feature The feature to test for.
128 * @return {!Promise<boolean>} True if the implementation supports the
131 remoting.HostDaemonFacade.prototype.hasFeature = function(feature) {
132 /** @type {remoting.HostDaemonFacade} */
134 return this.initialize_().then(function() {
135 return that.supportedFeatures_.indexOf(feature) >= 0;
142 * Initializes that the Daemon if necessary and posts the supplied message.
144 * @param {{type: string}} message The message to post.
145 * @return {!Promise<!Object>}
148 remoting.HostDaemonFacade.prototype.postMessage_ =
150 /** @type {remoting.HostDaemonFacade} */
152 return this.initialize_().then(function() {
153 return that.postMessageInternal_(message);
160 * Attaches a new ID to the supplied message, and posts it to the
161 * Native Messaging port, adding a Deferred object to the list of
162 * pending replies. |message| should have its 'type' field set, and
163 * any other fields set depending on the message type.
165 * @param {{type: string}} message The message to post.
166 * @return {!Promise<!Object>}
169 remoting.HostDaemonFacade.prototype.postMessageInternal_ = function(message) {
170 var id = this.nextId_++;
172 var deferred = new base.Deferred();
173 this.pendingReplies_[id] = new remoting.HostDaemonFacade.PendingReply(
174 message.type + 'Response', deferred);
175 this.port_.postMessage(message);
176 return deferred.promise();
180 * Handler for incoming Native Messages.
182 * @param {Object} message The received message.
183 * @return {void} Nothing.
186 remoting.HostDaemonFacade.prototype.onIncomingMessage_ = function(message) {
187 if (this.debugMessageHandler_.handleMessage(message)) {
191 /** @type {number} */
192 var id = message['id'];
193 if (typeof(id) != 'number') {
194 console.error('NativeMessaging: missing or non-numeric id');
197 var reply = this.pendingReplies_[id];
199 console.error('NativeMessaging: unexpected id: ', id);
202 delete this.pendingReplies_[id];
205 var type = base.getStringAttr(message, 'type');
206 if (type != reply.type) {
207 throw 'Expected reply type: ' + reply.type + ', got: ' + type;
209 reply.deferred.resolve(message);
210 } catch (/** @type {*} */ e) {
211 console.error('Error while processing native message', e);
212 reply.deferred.reject(remoting.Error.unexpected());
217 * @return {void} Nothing.
220 remoting.HostDaemonFacade.prototype.onDisconnect_ = function() {
221 console.error('Native Message port disconnected');
223 this.port_.onDisconnect.removeListener(this.onDisconnectCallback_);
224 this.port_.onMessage.removeListener(this.onIncomingMessageCallback_);
227 // If initialization hasn't finished then assume that the port was
228 // disconnected because Native Messaging host is not installed.
229 this.error_ = this.initializingPromise_ ?
230 new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN) :
231 remoting.Error.unexpected();
233 // Notify the error-handlers of any requests that are still outstanding.
234 var pendingReplies = this.pendingReplies_;
235 this.pendingReplies_ = {};
236 for (var id in pendingReplies) {
237 var num_id = parseInt(id, 10);
238 pendingReplies[num_id].deferred.reject(this.error_);
243 * Gets local hostname.
245 * @return {!Promise<string>}
247 remoting.HostDaemonFacade.prototype.getHostName = function() {
248 return this.postMessage_({type: 'getHostName'}).then(function(reply) {
249 return base.getStringAttr(reply, 'hostname');
254 * Calculates PIN hash value to be stored in the config, passing the resulting
255 * hash value base64-encoded to the callback.
257 * @param {string} hostId The host ID.
258 * @param {string} pin The PIN.
259 * @return {!Promise<string>}
261 remoting.HostDaemonFacade.prototype.getPinHash = function(hostId, pin) {
262 return this.postMessage_({
266 }).then(function(reply) {
267 return base.getStringAttr(reply, 'hash');
272 * Generates new key pair to use for the host. The specified callback is called
273 * when the key is generated. The key is returned in format understood by the
274 * host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64).
276 * @return {!Promise<remoting.KeyPair>}
278 remoting.HostDaemonFacade.prototype.generateKeyPair = function() {
279 return this.postMessage_({type: 'generateKeyPair'}).then(function(reply) {
281 privateKey: base.getStringAttr(reply, 'privateKey'),
282 publicKey: base.getStringAttr(reply, 'publicKey')
288 * Updates host config with the values specified in |config|. All
289 * fields that are not specified in |config| remain
290 * unchanged. Following parameters cannot be changed using this
291 * function: host_id, xmpp_login. Error is returned if |config|
292 * includes these parameters. Changes take effect before the callback
295 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
297 * @param {Object} config The new config parameters.
298 * @return {!Promise<remoting.HostController.AsyncResult>}
300 remoting.HostDaemonFacade.prototype.updateDaemonConfig = function(config) {
301 return this.postMessage_({
302 type: 'updateDaemonConfig',
304 }).then(function(reply) {
305 return remoting.HostController.AsyncResult.fromString(
306 base.getStringAttr(reply, 'result'));
311 * Loads daemon config. The config is passed as a JSON formatted string to the
313 * @return {!Promise<Object>}
315 remoting.HostDaemonFacade.prototype.getDaemonConfig = function() {
316 return this.postMessage_({type: 'getDaemonConfig'}).then(function(reply) {
317 return base.getObjectAttr(reply, 'config');
322 * Retrieves daemon version. The version is returned as a dotted decimal
323 * string of the form major.minor.build.patch.
325 * @return {!Promise<string>}
327 remoting.HostDaemonFacade.prototype.getDaemonVersion = function() {
328 /** @type {remoting.HostDaemonFacade} */
330 return this.initialize_().then(function() {
331 return that.version_;
338 * Get the user's consent to crash reporting. The consent flags are passed to
339 * the callback as booleans: supported, allowed, set-by-policy.
341 * @return {!Promise<remoting.UsageStatsConsent>}
343 remoting.HostDaemonFacade.prototype.getUsageStatsConsent = function() {
344 return this.postMessage_({type: 'getUsageStatsConsent'}).
345 then(function(reply) {
347 supported: base.getBooleanAttr(reply, 'supported'),
348 allowed: base.getBooleanAttr(reply, 'allowed'),
349 setByPolicy: base.getBooleanAttr(reply, 'setByPolicy')
355 * Starts the daemon process with the specified configuration.
357 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
359 * @param {Object} config Host configuration.
360 * @param {boolean} consent Consent to report crash dumps.
361 * @return {!Promise<remoting.HostController.AsyncResult>}
363 remoting.HostDaemonFacade.prototype.startDaemon = function(config, consent) {
364 return this.postMessage_({
368 }).then(function(reply) {
369 return remoting.HostController.AsyncResult.fromString(
370 base.getStringAttr(reply, 'result'));
375 * Stops the daemon process.
377 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
379 * @return {!Promise<remoting.HostController.AsyncResult>}
381 remoting.HostDaemonFacade.prototype.stopDaemon =
383 return this.postMessage_({type: 'stopDaemon'}).then(function(reply) {
384 return remoting.HostController.AsyncResult.fromString(
385 base.getStringAttr(reply, 'result'));
390 * Gets the installed/running state of the Host process.
392 * @return {!Promise<remoting.HostController.State>}
394 remoting.HostDaemonFacade.prototype.getDaemonState = function() {
395 return this.postMessage_({type: 'getDaemonState'}).then(function(reply) {
396 return remoting.HostController.State.fromString(
397 base.getStringAttr(reply, 'state'));
402 * Retrieves the list of paired clients.
404 * @return {!Promise<Array<remoting.PairedClient>>}
406 remoting.HostDaemonFacade.prototype.getPairedClients = function() {
407 return this.postMessage_({type: 'getPairedClients'}).then(function(reply) {
408 var pairedClients =remoting.PairedClient.convertToPairedClientArray(
409 reply['pairedClients']);
410 if (pairedClients != null) {
411 return pairedClients;
413 throw remoting.Error.unexpected('No paired clients!');
419 * Clears all paired clients from the registry.
421 * @return {!Promise<boolean>}
423 remoting.HostDaemonFacade.prototype.clearPairedClients = function() {
424 return this.postMessage_({type: 'clearPairedClients'}).then(function(reply) {
425 return base.getBooleanAttr(reply, 'result');
430 * Deletes a paired client referenced by client id.
432 * @param {string} client Client to delete.
433 * @return {!Promise<boolean>}
435 remoting.HostDaemonFacade.prototype.deletePairedClient = function(client) {
436 return this.postMessage_({
437 type: 'deletePairedClient',
439 }).then(function(reply) {
440 return base.getBooleanAttr(reply, 'result');
445 * Gets the API keys to obtain/use service account credentials.
447 * @return {!Promise<string>}
449 remoting.HostDaemonFacade.prototype.getHostClientId = function() {
450 return this.postMessage_({type: 'getHostClientId'}).then(function(reply) {
451 return base.getStringAttr(reply, 'clientId');
456 * @param {string} authorizationCode OAuth authorization code.
457 * @return {!Promise<{remoting.XmppCredentials}>}
459 remoting.HostDaemonFacade.prototype.getCredentialsFromAuthCode =
460 function(authorizationCode) {
461 return this.postMessage_({
462 type: 'getCredentialsFromAuthCode',
463 authorizationCode: authorizationCode
464 }).then(function(reply) {
465 var userEmail = base.getStringAttr(reply, 'userEmail');
466 var refreshToken = base.getStringAttr(reply, 'refreshToken');
467 if (userEmail && refreshToken) {
469 userEmail: userEmail,
470 refreshToken: refreshToken
473 throw remoting.Error.unexpected('Missing userEmail or refreshToken');
479 * @param {string} authorizationCode OAuth authorization code.
480 * @return {!Promise<string>}
482 remoting.HostDaemonFacade.prototype.getRefreshTokenFromAuthCode =
483 function(authorizationCode) {
484 return this.postMessage_({
485 type: 'getRefreshTokenFromAuthCode',
486 authorizationCode: authorizationCode
487 }).then(function(reply) {
488 var refreshToken = base.getStringAttr(reply, 'refreshToken');
492 throw remoting.Error.unexpected('Missing refreshToken');