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);
57 * @return {Promise} A promise that fulfills when the daemon finishes
61 remoting.HostDaemonFacade.prototype.initialize_ = function() {
62 if (!this.initializingPromise_) {
64 return Promise.resolve();
67 /** @type {remoting.HostDaemonFacade} */
69 this.initializingPromise_ = this.connectNative_().then(function() {
70 that.initializingPromise_ = null;
72 that.initializingPromise_ = null;
73 throw new Error(that.error_);
76 return this.initializingPromise_;
80 * Connects to the native messaging host and sends a hello message.
82 * @return {Promise} A promise that fulfills when the connection attempt
86 remoting.HostDaemonFacade.prototype.connectNative_ = function() {
89 this.port_ = chrome.runtime.connectNative(
90 'com.google.chrome.remote_desktop');
91 this.port_.onMessage.addListener(this.onIncomingMessageCallback_);
92 this.port_.onDisconnect.addListener(this.onDisconnectCallback_);
93 return this.postMessageInternal_({type: 'hello'}).then(function(reply) {
94 that.version_ = base.getStringAttr(reply, 'version');
95 // Old versions of the native messaging host do not return this list.
96 // Those versions default to the empty list of supported features.
97 that.supportedFeatures_ =
98 base.getArrayAttr(reply, 'supportedFeatures', []);
100 } catch (/** @type {*} */ err) {
101 console.log('Native Messaging initialization failed: ', err);
102 throw remoting.Error.unexpected();
107 * Type used for entries of |pendingReplies_| list.
109 * @param {string} type Type of the originating request.
110 * @param {!base.Deferred} deferred Used to communicate returns back
114 remoting.HostDaemonFacade.PendingReply = function(type, deferred) {
119 this.deferred = deferred;
123 * @param {remoting.HostController.Feature} feature The feature to test for.
124 * @return {!Promise<boolean>} True if the implementation supports the
127 remoting.HostDaemonFacade.prototype.hasFeature = function(feature) {
128 /** @type {remoting.HostDaemonFacade} */
130 return this.initialize_().then(function() {
131 return that.supportedFeatures_.indexOf(feature) >= 0;
138 * Initializes that the Daemon if necessary and posts the supplied message.
140 * @param {{type: string}} message The message to post.
141 * @return {!Promise<!Object>}
144 remoting.HostDaemonFacade.prototype.postMessage_ =
146 /** @type {remoting.HostDaemonFacade} */
148 return this.initialize_().then(function() {
149 return that.postMessageInternal_(message);
156 * Attaches a new ID to the supplied message, and posts it to the
157 * Native Messaging port, adding a Deferred object to the list of
158 * pending replies. |message| should have its 'type' field set, and
159 * any other fields set depending on the message type.
161 * @param {{type: string}} message The message to post.
162 * @return {!Promise<!Object>}
165 remoting.HostDaemonFacade.prototype.postMessageInternal_ = function(message) {
166 var id = this.nextId_++;
168 var deferred = new base.Deferred();
169 this.pendingReplies_[id] = new remoting.HostDaemonFacade.PendingReply(
170 message.type + 'Response', deferred);
171 this.port_.postMessage(message);
172 return deferred.promise();
176 * Handler for incoming Native Messages.
178 * @param {Object} message The received message.
179 * @return {void} Nothing.
182 remoting.HostDaemonFacade.prototype.onIncomingMessage_ = function(message) {
183 /** @type {number} */
184 var id = message['id'];
185 if (typeof(id) != 'number') {
186 console.error('NativeMessaging: missing or non-numeric id');
189 var reply = this.pendingReplies_[id];
191 console.error('NativeMessaging: unexpected id: ', id);
194 delete this.pendingReplies_[id];
197 var type = base.getStringAttr(message, 'type');
198 if (type != reply.type) {
199 throw 'Expected reply type: ' + reply.type + ', got: ' + type;
201 reply.deferred.resolve(message);
202 } catch (/** @type {*} */ e) {
203 console.error('Error while processing native message', e);
204 reply.deferred.reject(remoting.Error.unexpected());
209 * @return {void} Nothing.
212 remoting.HostDaemonFacade.prototype.onDisconnect_ = function() {
213 console.error('Native Message port disconnected');
215 this.port_.onDisconnect.removeListener(this.onDisconnectCallback_);
216 this.port_.onMessage.removeListener(this.onIncomingMessageCallback_);
219 // If initialization hasn't finished then assume that the port was
220 // disconnected because Native Messaging host is not installed.
221 this.error_ = this.initializingPromise_ ?
222 new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN) :
223 remoting.Error.unexpected();
225 // Notify the error-handlers of any requests that are still outstanding.
226 var pendingReplies = this.pendingReplies_;
227 this.pendingReplies_ = {};
228 for (var id in pendingReplies) {
229 var num_id = parseInt(id, 10);
230 pendingReplies[num_id].deferred.reject(this.error_);
235 * Gets local hostname.
237 * @return {!Promise<string>}
239 remoting.HostDaemonFacade.prototype.getHostName = function() {
240 return this.postMessage_({type: 'getHostName'}).then(function(reply) {
241 return base.getStringAttr(reply, 'hostname');
246 * Calculates PIN hash value to be stored in the config, passing the resulting
247 * hash value base64-encoded to the callback.
249 * @param {string} hostId The host ID.
250 * @param {string} pin The PIN.
251 * @return {!Promise<string>}
253 remoting.HostDaemonFacade.prototype.getPinHash = function(hostId, pin) {
254 return this.postMessage_({
258 }).then(function(reply) {
259 return base.getStringAttr(reply, 'hash');
264 * Generates new key pair to use for the host. The specified callback is called
265 * when the key is generated. The key is returned in format understood by the
266 * host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64).
268 * @return {!Promise<remoting.KeyPair>}
270 remoting.HostDaemonFacade.prototype.generateKeyPair = function() {
271 return this.postMessage_({type: 'generateKeyPair'}).then(function(reply) {
273 privateKey: base.getStringAttr(reply, 'privateKey'),
274 publicKey: base.getStringAttr(reply, 'publicKey')
280 * Updates host config with the values specified in |config|. All
281 * fields that are not specified in |config| remain
282 * unchanged. Following parameters cannot be changed using this
283 * function: host_id, xmpp_login. Error is returned if |config|
284 * includes these parameters. Changes take effect before the callback
287 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
289 * @param {Object} config The new config parameters.
290 * @return {!Promise<remoting.HostController.AsyncResult>}
292 remoting.HostDaemonFacade.prototype.updateDaemonConfig = function(config) {
293 return this.postMessage_({
294 type: 'updateDaemonConfig',
296 }).then(function(reply) {
297 return remoting.HostController.AsyncResult.fromString(
298 base.getStringAttr(reply, 'result'));
303 * Loads daemon config. The config is passed as a JSON formatted string to the
305 * @return {!Promise<Object>}
307 remoting.HostDaemonFacade.prototype.getDaemonConfig = function() {
308 return this.postMessage_({type: 'getDaemonConfig'}).then(function(reply) {
309 return base.getObjectAttr(reply, 'config');
314 * Retrieves daemon version. The version is returned as a dotted decimal
315 * string of the form major.minor.build.patch.
317 * @return {!Promise<string>}
319 remoting.HostDaemonFacade.prototype.getDaemonVersion = function() {
320 /** @type {remoting.HostDaemonFacade} */
322 return this.initialize_().then(function() {
323 return that.version_;
330 * Get the user's consent to crash reporting. The consent flags are passed to
331 * the callback as booleans: supported, allowed, set-by-policy.
333 * @return {!Promise<remoting.UsageStatsConsent>}
335 remoting.HostDaemonFacade.prototype.getUsageStatsConsent = function() {
336 return this.postMessage_({type: 'getUsageStatsConsent'}).
337 then(function(reply) {
339 supported: base.getBooleanAttr(reply, 'supported'),
340 allowed: base.getBooleanAttr(reply, 'allowed'),
341 setByPolicy: base.getBooleanAttr(reply, 'setByPolicy')
347 * Starts the daemon process with the specified configuration.
349 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
351 * @param {Object} config Host configuration.
352 * @param {boolean} consent Consent to report crash dumps.
353 * @return {!Promise<remoting.HostController.AsyncResult>}
355 remoting.HostDaemonFacade.prototype.startDaemon = function(config, consent) {
356 return this.postMessage_({
360 }).then(function(reply) {
361 return remoting.HostController.AsyncResult.fromString(
362 base.getStringAttr(reply, 'result'));
367 * Stops the daemon process.
369 * TODO(jrw): Consider conversion exceptions to AsyncResult values.
371 * @return {!Promise<remoting.HostController.AsyncResult>}
373 remoting.HostDaemonFacade.prototype.stopDaemon =
375 return this.postMessage_({type: 'stopDaemon'}).then(function(reply) {
376 return remoting.HostController.AsyncResult.fromString(
377 base.getStringAttr(reply, 'result'));
382 * Gets the installed/running state of the Host process.
384 * @return {!Promise<remoting.HostController.State>}
386 remoting.HostDaemonFacade.prototype.getDaemonState = function() {
387 return this.postMessage_({type: 'getDaemonState'}).then(function(reply) {
388 return remoting.HostController.State.fromString(
389 base.getStringAttr(reply, 'state'));
394 * Retrieves the list of paired clients.
396 * @return {!Promise<Array<remoting.PairedClient>>}
398 remoting.HostDaemonFacade.prototype.getPairedClients = function() {
399 return this.postMessage_({type: 'getPairedClients'}).then(function(reply) {
400 var pairedClients =remoting.PairedClient.convertToPairedClientArray(
401 reply['pairedClients']);
402 if (pairedClients != null) {
403 return pairedClients;
405 throw remoting.Error.unexpected('No paired clients!');
411 * Clears all paired clients from the registry.
413 * @return {!Promise<boolean>}
415 remoting.HostDaemonFacade.prototype.clearPairedClients = function() {
416 return this.postMessage_({type: 'clearPairedClients'}).then(function(reply) {
417 return base.getBooleanAttr(reply, 'result');
422 * Deletes a paired client referenced by client id.
424 * @param {string} client Client to delete.
425 * @return {!Promise<boolean>}
427 remoting.HostDaemonFacade.prototype.deletePairedClient = function(client) {
428 return this.postMessage_({
429 type: 'deletePairedClient',
431 }).then(function(reply) {
432 return base.getBooleanAttr(reply, 'result');
437 * Gets the API keys to obtain/use service account credentials.
439 * @return {!Promise<string>}
441 remoting.HostDaemonFacade.prototype.getHostClientId = function() {
442 return this.postMessage_({type: 'getHostClientId'}).then(function(reply) {
443 return base.getStringAttr(reply, 'clientId');
448 * @param {string} authorizationCode OAuth authorization code.
449 * @return {!Promise<{remoting.XmppCredentials}>}
451 remoting.HostDaemonFacade.prototype.getCredentialsFromAuthCode =
452 function(authorizationCode) {
453 return this.postMessage_({
454 type: 'getCredentialsFromAuthCode',
455 authorizationCode: authorizationCode
456 }).then(function(reply) {
457 var userEmail = base.getStringAttr(reply, 'userEmail');
458 var refreshToken = base.getStringAttr(reply, 'refreshToken');
459 if (userEmail && refreshToken) {
461 userEmail: userEmail,
462 refreshToken: refreshToken
465 throw remoting.Error.unexpected('Missing userEmail or refreshToken');
471 * @param {string} authorizationCode OAuth authorization code.
472 * @return {!Promise<string>}
474 remoting.HostDaemonFacade.prototype.getRefreshTokenFromAuthCode =
475 function(authorizationCode) {
476 return this.postMessage_({
477 type: 'getRefreshTokenFromAuthCode',
478 authorizationCode: authorizationCode
479 }).then(function(reply) {
480 var refreshToken = base.getStringAttr(reply, 'refreshToken');
484 throw remoting.Error.unexpected('Missing refreshToken');