blob: 62ae4ee2f79eb66d30c8345393acbe01c5213648 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Class to communicate with the host daemon via Native Messaging.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
*/
remoting.HostDaemonFacade = function() {
/** @private {number} */
this.nextId_ = 0;
/** @private {Object<number, remoting.HostDaemonFacade.PendingReply>} */
this.pendingReplies_ = {};
/** @private {?Port} */
this.port_ = null;
/** @private {string} */
this.version_ = '';
/** @private {Array<remoting.HostController.Feature>} */
this.supportedFeatures_ = [];
/** @private {Array<function(boolean):void>} */
this.afterInitializationTasks_ = [];
/**
* A promise that fulfills when the daemon finishes initializing.
* It will be set to null when the promise fulfills.
* @private {Promise}
*/
this.initializingPromise_ = null;
/** @private {!remoting.Error} */
this.error_ = remoting.Error.none();
/** @private */
this.onIncomingMessageCallback_ = this.onIncomingMessage_.bind(this);
/** @private */
this.onDisconnectCallback_ = this.onDisconnect_.bind(this);
/** @private */
this.debugMessageHandler_ =
new remoting.NativeMessageHostDebugMessageHandler();
this.initialize_();
};
/**
* @return {Promise} A promise that fulfills when the daemon finishes
* initializing
* @private
*/
remoting.HostDaemonFacade.prototype.initialize_ = function() {
if (!this.initializingPromise_) {
if (this.port_) {
return Promise.resolve();
}
/** @type {remoting.HostDaemonFacade} */
var that = this;
this.initializingPromise_ = this.connectNative_().then(function() {
that.initializingPromise_ = null;
}, function() {
that.initializingPromise_ = null;
throw new Error(that.error_);
});
}
return this.initializingPromise_;
};
/**
* Connects to the native messaging host and sends a hello message.
*
* @return {Promise} A promise that fulfills when the connection attempt
* succeeds or fails.
* @private
*/
remoting.HostDaemonFacade.prototype.connectNative_ = function() {
var that = this;
try {
this.port_ = chrome.runtime.connectNative(
'com.google.chrome.remote_desktop');
this.port_.onMessage.addListener(this.onIncomingMessageCallback_);
this.port_.onDisconnect.addListener(this.onDisconnectCallback_);
return this.postMessageInternal_({type: 'hello'}).then(function(reply) {
that.version_ = base.getStringAttr(reply, 'version');
// Old versions of the native messaging host do not return this list.
// Those versions default to the empty list of supported features.
that.supportedFeatures_ =
base.getArrayAttr(reply, 'supportedFeatures', []);
});
} catch (/** @type {*} */ err) {
console.log('Native Messaging initialization failed: ', err);
throw remoting.Error.unexpected();
}
};
/**
* Type used for entries of |pendingReplies_| list.
*
* @param {string} type Type of the originating request.
* @param {!base.Deferred} deferred Used to communicate returns back
* to the caller.
* @constructor
*/
remoting.HostDaemonFacade.PendingReply = function(type, deferred) {
/** @const */
this.type = type;
/** @const */
this.deferred = deferred;
};
/**
* @param {remoting.HostController.Feature} feature The feature to test for.
* @return {!Promise<boolean>} True if the implementation supports the
* named feature.
*/
remoting.HostDaemonFacade.prototype.hasFeature = function(feature) {
/** @type {remoting.HostDaemonFacade} */
var that = this;
return this.initialize_().then(function() {
return that.supportedFeatures_.indexOf(feature) >= 0;
}, function () {
return false;
});
};
/**
* Initializes that the Daemon if necessary and posts the supplied message.
*
* @param {{type: string}} message The message to post.
* @return {!Promise<!Object>}
* @private
*/
remoting.HostDaemonFacade.prototype.postMessage_ =
function(message) {
/** @type {remoting.HostDaemonFacade} */
var that = this;
return this.initialize_().then(function() {
return that.postMessageInternal_(message);
}, function() {
throw that.error_;
});
};
/**
* Attaches a new ID to the supplied message, and posts it to the
* Native Messaging port, adding a Deferred object to the list of
* pending replies. |message| should have its 'type' field set, and
* any other fields set depending on the message type.
*
* @param {{type: string}} message The message to post.
* @return {!Promise<!Object>}
* @private
*/
remoting.HostDaemonFacade.prototype.postMessageInternal_ = function(message) {
var id = this.nextId_++;
message['id'] = id;
var deferred = new base.Deferred();
this.pendingReplies_[id] = new remoting.HostDaemonFacade.PendingReply(
message.type + 'Response', deferred);
this.port_.postMessage(message);
return deferred.promise();
};
/**
* Handler for incoming Native Messages.
*
* @param {Object} message The received message.
* @return {void} Nothing.
* @private
*/
remoting.HostDaemonFacade.prototype.onIncomingMessage_ = function(message) {
if (this.debugMessageHandler_.handleMessage(message)) {
return;
}
/** @type {number} */
var id = message['id'];
if (typeof(id) != 'number') {
console.error('NativeMessaging: missing or non-numeric id');
return;
}
var reply = this.pendingReplies_[id];
if (!reply) {
console.error('NativeMessaging: unexpected id: ', id);
return;
}
delete this.pendingReplies_[id];
try {
var type = base.getStringAttr(message, 'type');
if (type != reply.type) {
throw 'Expected reply type: ' + reply.type + ', got: ' + type;
}
reply.deferred.resolve(message);
} catch (/** @type {*} */ e) {
console.error('Error while processing native message', e);
reply.deferred.reject(remoting.Error.unexpected());
}
};
/**
* @return {void} Nothing.
* @private
*/
remoting.HostDaemonFacade.prototype.onDisconnect_ = function() {
console.error('Native Message port disconnected');
this.port_.onDisconnect.removeListener(this.onDisconnectCallback_);
this.port_.onMessage.removeListener(this.onIncomingMessageCallback_);
this.port_ = null;
// If initialization hasn't finished then assume that the port was
// disconnected because Native Messaging host is not installed.
this.error_ = this.initializingPromise_ ?
new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN) :
remoting.Error.unexpected();
// Notify the error-handlers of any requests that are still outstanding.
var pendingReplies = this.pendingReplies_;
this.pendingReplies_ = {};
for (var id in pendingReplies) {
var num_id = parseInt(id, 10);
pendingReplies[num_id].deferred.reject(this.error_);
}
}
/**
* Gets local hostname.
*
* @return {!Promise<string>}
*/
remoting.HostDaemonFacade.prototype.getHostName = function() {
return this.postMessage_({type: 'getHostName'}).then(function(reply) {
return base.getStringAttr(reply, 'hostname');
});
};
/**
* Calculates PIN hash value to be stored in the config, passing the resulting
* hash value base64-encoded to the callback.
*
* @param {string} hostId The host ID.
* @param {string} pin The PIN.
* @return {!Promise<string>}
*/
remoting.HostDaemonFacade.prototype.getPinHash = function(hostId, pin) {
return this.postMessage_({
type: 'getPinHash',
hostId: hostId,
pin: pin
}).then(function(reply) {
return base.getStringAttr(reply, 'hash');
});
};
/**
* Generates new key pair to use for the host. The specified callback is called
* when the key is generated. The key is returned in format understood by the
* host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64).
*
* @return {!Promise<remoting.KeyPair>}
*/
remoting.HostDaemonFacade.prototype.generateKeyPair = function() {
return this.postMessage_({type: 'generateKeyPair'}).then(function(reply) {
return {
privateKey: base.getStringAttr(reply, 'privateKey'),
publicKey: base.getStringAttr(reply, 'publicKey')
};
});
};
/**
* Updates host config with the values specified in |config|. All
* fields that are not specified in |config| remain
* unchanged. Following parameters cannot be changed using this
* function: host_id, xmpp_login. Error is returned if |config|
* includes these parameters. Changes take effect before the callback
* is called.
*
* TODO(jrw): Consider conversion exceptions to AsyncResult values.
*
* @param {Object} config The new config parameters.
* @return {!Promise<remoting.HostController.AsyncResult>}
*/
remoting.HostDaemonFacade.prototype.updateDaemonConfig = function(config) {
return this.postMessage_({
type: 'updateDaemonConfig',
config: config
}).then(function(reply) {
return remoting.HostController.AsyncResult.fromString(
base.getStringAttr(reply, 'result'));
});
};
/**
* Loads daemon config. The config is passed as a JSON formatted string to the
* callback.
* @return {!Promise<Object>}
*/
remoting.HostDaemonFacade.prototype.getDaemonConfig = function() {
return this.postMessage_({type: 'getDaemonConfig'}).then(function(reply) {
return base.getObjectAttr(reply, 'config');
});
};
/**
* Retrieves daemon version. The version is returned as a dotted decimal
* string of the form major.minor.build.patch.
*
* @return {!Promise<string>}
*/
remoting.HostDaemonFacade.prototype.getDaemonVersion = function() {
/** @type {remoting.HostDaemonFacade} */
var that = this;
return this.initialize_().then(function() {
return that.version_;
}, function() {
throw that.error_;
});
};
/**
* Get the user's consent to crash reporting. The consent flags are passed to
* the callback as booleans: supported, allowed, set-by-policy.
*
* @return {!Promise<remoting.UsageStatsConsent>}
*/
remoting.HostDaemonFacade.prototype.getUsageStatsConsent = function() {
return this.postMessage_({type: 'getUsageStatsConsent'}).
then(function(reply) {
return {
supported: base.getBooleanAttr(reply, 'supported'),
allowed: base.getBooleanAttr(reply, 'allowed'),
setByPolicy: base.getBooleanAttr(reply, 'setByPolicy')
};
});
};
/**
* Starts the daemon process with the specified configuration.
*
* TODO(jrw): Consider conversion exceptions to AsyncResult values.
*
* @param {Object} config Host configuration.
* @param {boolean} consent Consent to report crash dumps.
* @return {!Promise<remoting.HostController.AsyncResult>}
*/
remoting.HostDaemonFacade.prototype.startDaemon = function(config, consent) {
return this.postMessage_({
type: 'startDaemon',
config: config,
consent: consent
}).then(function(reply) {
return remoting.HostController.AsyncResult.fromString(
base.getStringAttr(reply, 'result'));
});
};
/**
* Stops the daemon process.
*
* TODO(jrw): Consider conversion exceptions to AsyncResult values.
*
* @return {!Promise<remoting.HostController.AsyncResult>}
*/
remoting.HostDaemonFacade.prototype.stopDaemon =
function() {
return this.postMessage_({type: 'stopDaemon'}).then(function(reply) {
return remoting.HostController.AsyncResult.fromString(
base.getStringAttr(reply, 'result'));
});
};
/**
* Gets the installed/running state of the Host process.
*
* @return {!Promise<remoting.HostController.State>}
*/
remoting.HostDaemonFacade.prototype.getDaemonState = function() {
return this.postMessage_({type: 'getDaemonState'}).then(function(reply) {
return remoting.HostController.State.fromString(
base.getStringAttr(reply, 'state'));
});
};
/**
* Retrieves the list of paired clients.
*
* @return {!Promise<Array<remoting.PairedClient>>}
*/
remoting.HostDaemonFacade.prototype.getPairedClients = function() {
return this.postMessage_({type: 'getPairedClients'}).then(function(reply) {
var pairedClients =remoting.PairedClient.convertToPairedClientArray(
reply['pairedClients']);
if (pairedClients != null) {
return pairedClients;
} else {
throw remoting.Error.unexpected('No paired clients!');
}
});
};
/**
* Clears all paired clients from the registry.
*
* @return {!Promise<boolean>}
*/
remoting.HostDaemonFacade.prototype.clearPairedClients = function() {
return this.postMessage_({type: 'clearPairedClients'}).then(function(reply) {
return base.getBooleanAttr(reply, 'result');
});
};
/**
* Deletes a paired client referenced by client id.
*
* @param {string} client Client to delete.
* @return {!Promise<boolean>}
*/
remoting.HostDaemonFacade.prototype.deletePairedClient = function(client) {
return this.postMessage_({
type: 'deletePairedClient',
clientId: client
}).then(function(reply) {
return base.getBooleanAttr(reply, 'result');
});
};
/**
* Gets the API keys to obtain/use service account credentials.
*
* @return {!Promise<string>}
*/
remoting.HostDaemonFacade.prototype.getHostClientId = function() {
return this.postMessage_({type: 'getHostClientId'}).then(function(reply) {
return base.getStringAttr(reply, 'clientId');
});
};
/**
* @param {string} authorizationCode OAuth authorization code.
* @return {!Promise<{remoting.XmppCredentials}>}
*/
remoting.HostDaemonFacade.prototype.getCredentialsFromAuthCode =
function(authorizationCode) {
return this.postMessage_({
type: 'getCredentialsFromAuthCode',
authorizationCode: authorizationCode
}).then(function(reply) {
var userEmail = base.getStringAttr(reply, 'userEmail');
var refreshToken = base.getStringAttr(reply, 'refreshToken');
if (userEmail && refreshToken) {
return {
userEmail: userEmail,
refreshToken: refreshToken
};
} else {
throw remoting.Error.unexpected('Missing userEmail or refreshToken');
}
});
};
/**
* @param {string} authorizationCode OAuth authorization code.
* @return {!Promise<string>}
*/
remoting.HostDaemonFacade.prototype.getRefreshTokenFromAuthCode =
function(authorizationCode) {
return this.postMessage_({
type: 'getRefreshTokenFromAuthCode',
authorizationCode: authorizationCode
}).then(function(reply) {
var refreshToken = base.getStringAttr(reply, 'refreshToken');
if (refreshToken) {
return refreshToken
} else {
throw remoting.Error.unexpected('Missing refreshToken');
}
});
};