// Copyright 2013 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 components via Native Messaging.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
*/
remoting.HostNativeMessaging = function() {
/**
* @type {number}
* @private
*/
this.nextId_ = 0;
/**
* @type {Object.<number, remoting.HostNativeMessaging.PendingReply>}
* @private
*/
this.pendingReplies_ = {};
/** @type {?chrome.extension.Port} @private */
this.port_ = null;
/** @type {string} @private */
this.version_ = '';
/** @type {Array.<remoting.HostController.Feature>} @private */
this.supportedFeatures_ = [];
};
/**
* Type used for entries of |pendingReplies_| list.
*
* @param {string} type Type of the originating request.
* @param {function(...):void} onDone The callback, if any, to be triggered
* on response. The actual parameters depend on the original request type.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @constructor
*/
remoting.HostNativeMessaging.PendingReply = function(type, onDone, onError) {
this.type = type;
this.onDone = onDone;
this.onError = onError;
};
/**
* Sets up connection to the Native Messaging host process and exchanges
* 'hello' messages. Invokes onDone on success and onError on failure (the
* native messaging host is probably not installed).
*
* @param {function(): void} onDone Called after successful initialization.
* @param {function(remoting.Error): void} onError Called if initialization
* failed.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.initialize = function(onDone, onError) {
try {
this.port_ = chrome.runtime.connectNative(
'com.google.chrome.remote_desktop');
this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this));
this.port_.onDisconnect.addListener(this.onDisconnect_.bind(this));
this.postMessage_({type: 'hello'}, onDone,
onError.bind(null, remoting.Error.UNEXPECTED));
} catch (err) {
console.log('Native Messaging initialization failed: ',
/** @type {*} */ (err));
onError(remoting.Error.UNEXPECTED);
return;
}
};
/**
* @param {remoting.HostController.Feature} feature The feature to test for.
* @return {boolean} True if the implementation supports the named feature.
*/
remoting.HostNativeMessaging.prototype.hasFeature = function(feature) {
return this.supportedFeatures_.indexOf(feature) >= 0;
};
/**
* Attaches a new ID to the supplied message, and posts it to the Native
* Messaging port, adding |onDone| 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.
* @param {function(...):void} onDone The callback, if any, to be triggered
* on response.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
* @private
*/
remoting.HostNativeMessaging.prototype.postMessage_ =
function(message, onDone, onError) {
var id = this.nextId_++;
message['id'] = id;
this.pendingReplies_[id] = new remoting.HostNativeMessaging.PendingReply(
message.type + 'Response', onDone, onError);
this.port_.postMessage(message);
};
/**
* Handler for incoming Native Messages.
*
* @param {Object} message The received message.
* @return {void} Nothing.
* @private
*/
remoting.HostNativeMessaging.prototype.onIncomingMessage_ = function(message) {
/** @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 = getStringAttr(message, 'type');
if (type != reply.type) {
throw 'Expected reply type: ' + reply.type + ', got: ' + type;
}
this.handleIncomingMessage_(message, reply.onDone);
} catch (e) {
console.error('Error while processing native message' +
/** @type {*} */ (e));
reply.onError(remoting.Error.UNEXPECTED);
}
}
/**
* Handler for incoming Native Messages.
*
* @param {Object} message The received message.
* @param {function(...):void} onDone Function to call when we're done
* processing the message.
* @return {void} Nothing.
* @private
*/
remoting.HostNativeMessaging.prototype.handleIncomingMessage_ =
function(message, onDone) {
var type = getStringAttr(message, 'type');
switch (type) {
case 'helloResponse':
this.version_ = getStringAttr(message, 'version');
// Old versions of the native messaging host do not return this list.
// Those versions default to the empty list of supported features.
this.supportedFeatures_ = getArrayAttr(message, 'supportedFeatures', []);
onDone();
break;
case 'getHostNameResponse':
onDone(getStringAttr(message, 'hostname'));
break;
case 'getPinHashResponse':
onDone(getStringAttr(message, 'hash'));
break;
case 'generateKeyPairResponse':
var privateKey = getStringAttr(message, 'privateKey');
var publicKey = getStringAttr(message, 'publicKey');
onDone(privateKey, publicKey);
break;
case 'updateDaemonConfigResponse':
var result = remoting.HostController.AsyncResult.fromString(
getStringAttr(message, 'result'));
onDone(result);
break;
case 'getDaemonConfigResponse':
onDone(getObjectAttr(message, 'config'));
break;
case 'getUsageStatsConsentResponse':
var supported = getBooleanAttr(message, 'supported');
var allowed = getBooleanAttr(message, 'allowed');
var setByPolicy = getBooleanAttr(message, 'setByPolicy');
onDone(supported, allowed, setByPolicy);
break;
case 'startDaemonResponse':
case 'stopDaemonResponse':
var result = remoting.HostController.AsyncResult.fromString(
getStringAttr(message, 'result'));
onDone(result);
break;
case 'getDaemonStateResponse':
var state = remoting.HostController.State.fromString(
getStringAttr(message, 'state'));
onDone(state);
break;
case 'getPairedClientsResponse':
var pairedClients = remoting.PairedClient.convertToPairedClientArray(
message['pairedClients']);
if (pairedClients != null) {
onDone(pairedClients);
} else {
throw 'No paired clients!';
}
break;
case 'clearPairedClientsResponse':
case 'deletePairedClientResponse':
onDone(getBooleanAttr(message, 'result'));
break;
case 'getHostClientIdResponse':
onDone(getStringAttr(message, 'clientId'));
break;
case 'getCredentialsFromAuthCodeResponse':
var userEmail = getStringAttr(message, 'userEmail');
var refreshToken = getStringAttr(message, 'refreshToken');
if (userEmail && refreshToken) {
onDone(userEmail, refreshToken);
} else {
throw 'Missing userEmail or refreshToken';
}
break;
default:
throw 'Unexpected native message: ' + message;
}
};
/**
* @return {void} Nothing.
* @private
*/
remoting.HostNativeMessaging.prototype.onDisconnect_ = function() {
console.error('Native Message port disconnected');
// Notify the error-handlers of any requests that are still outstanding.
var pendingReplies = this.pendingReplies_;
this.pendingReplies_ = {};
for (var id in pendingReplies) {
pendingReplies[/** @type {number} */(id)].onError(
remoting.Error.UNEXPECTED);
}
}
/**
* @param {function(string):void} onDone Callback to be called with the
* local hostname.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getHostName =
function(onDone, onError) {
this.postMessage_({type: 'getHostName'}, onDone, onError);
};
/**
* 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.
* @param {function(string):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getPinHash =
function(hostId, pin, onDone, onError) {
this.postMessage_({
type: 'getPinHash',
hostId: hostId,
pin: pin
}, onDone, onError);
};
/**
* 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).
*
* @param {function(string, string):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.generateKeyPair =
function(onDone, onError) {
this.postMessage_({type: 'generateKeyPair'}, onDone, onError);
};
/**
* 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.
*
* @param {Object} config The new config parameters.
* @param {function(remoting.HostController.AsyncResult):void} onDone
* Callback to be called when finished.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.updateDaemonConfig =
function(config, onDone, onError) {
this.postMessage_({
type: 'updateDaemonConfig',
config: config
}, onDone, onError);
};
/**
* Loads daemon config. The config is passed as a JSON formatted string to the
* callback.
*
* @param {function(Object):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getDaemonConfig =
function(onDone, onError) {
this.postMessage_({type: 'getDaemonConfig'}, onDone, onError);
};
/**
* Retrieves daemon version. The version is returned as a dotted decimal string
* of the form major.minor.build.patch.
* @return {string} The daemon version, or the empty string if not available.
*/
remoting.HostNativeMessaging.prototype.getDaemonVersion = function() {
// Return the cached version from the 'hello' exchange.
return this.version_;
};
/**
* Get the user's consent to crash reporting. The consent flags are passed to
* the callback as booleans: supported, allowed, set-by-policy.
*
* @param {function(boolean, boolean, boolean):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getUsageStatsConsent =
function(onDone, onError) {
this.postMessage_({type: 'getUsageStatsConsent'}, onDone, onError);
};
/**
* Starts the daemon process with the specified configuration.
*
* @param {Object} config Host configuration.
* @param {boolean} consent Consent to report crash dumps.
* @param {function(remoting.HostController.AsyncResult):void} onDone
* Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.startDaemon =
function(config, consent, onDone, onError) {
this.postMessage_({
type: 'startDaemon',
config: config,
consent: consent
}, onDone, onError);
};
/**
* Stops the daemon process.
*
* @param {function(remoting.HostController.AsyncResult):void} onDone
* Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.stopDaemon =
function(onDone, onError) {
this.postMessage_({type: 'stopDaemon'}, onDone, onError);
};
/**
* Gets the installed/running state of the Host process.
*
* @param {function(remoting.HostController.State):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getDaemonState =
function(onDone, onError) {
this.postMessage_({type: 'getDaemonState'}, onDone, onError);
}
/**
* Retrieves the list of paired clients.
*
* @param {function(Array.<remoting.PairedClient>):void} onDone Callback to be
* called with the result.
* @param {function(remoting.Error):void} onError Callback to be triggered
* on error.
*/
remoting.HostNativeMessaging.prototype.getPairedClients =
function(onDone, onError) {
this.postMessage_({type: 'getPairedClients'}, onDone, onError);
}
/**
* Clears all paired clients from the registry.
*
* @param {function(boolean):void} onDone Callback to be called when finished.
* @param {function(remoting.Error):void} onError Callback to be triggered
* on error.
*/
remoting.HostNativeMessaging.prototype.clearPairedClients =
function(onDone, onError) {
this.postMessage_({type: 'clearPairedClients'}, onDone, onError);
}
/**
* Deletes a paired client referenced by client id.
*
* @param {string} client Client to delete.
* @param {function(boolean):void} onDone Callback to be called when finished.
* @param {function(remoting.Error):void} onError Callback to be triggered
* on error.
*/
remoting.HostNativeMessaging.prototype.deletePairedClient =
function(client, onDone, onError) {
this.postMessage_({
type: 'deletePairedClient',
clientId: client
}, onDone, onError);
}
/**
* Gets the API keys to obtain/use service account credentials.
*
* @param {function(string):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getHostClientId =
function(onDone, onError) {
this.postMessage_({type: 'getHostClientId'}, onDone, onError);
};
/**
*
* @param {string} authorizationCode OAuth authorization code.
* @param {function(string, string):void} onDone Callback.
* @param {function(remoting.Error):void} onError The callback to be triggered
* on error.
* @return {void} Nothing.
*/
remoting.HostNativeMessaging.prototype.getCredentialsFromAuthCode =
function(authorizationCode, onDone, onError) {
this.postMessage_({
type: 'getCredentialsFromAuthCode',
authorizationCode: authorizationCode
}, onDone, onError);
};