blob: b53f79858fc635d9bdff346bd190046cfc9bd857 [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
*
* It2MeHelpeeChannel relays messages between the Hangouts web page (Hangouts)
* and the It2Me Native Messaging Host (It2MeHost) for the helpee (the Hangouts
* participant who is receiving remoting assistance).
*
* It runs in the background page. It contains a chrome.runtime.Port object,
* representing a connection to Hangouts, and a remoting.It2MeHostFacade object,
* representing a connection to the IT2Me Native Messaging Host.
*
* Hangouts It2MeHelpeeChannel It2MeHost
* |---------runtime.connect()-------->| |
* |-----------hello message---------->| |
* |<-----helloResponse message------->| |
* |----------connect message--------->| |
* | |-----showConfirmDialog()----->|
* | |----------connect()---------->|
* | |<-------hostStateChanged------|
* | | (RECEIVED_ACCESS_CODE) |
* |<---connect response (access code)-| |
* | | |
*
* Hangouts will send the access code to the web app on the helper side.
* The helper will then connect to the It2MeHost using the access code.
*
* Hangouts It2MeHelpeeChannel It2MeHost
* | |<-------hostStateChanged------|
* | | (CONNECTED) |
* |<-- hostStateChanged(CONNECTED)----| |
* |-------disconnect message--------->| |
* |<--hostStateChanged(DISCONNECTED)--| |
*
*
* It also handles host downloads and install status queries:
*
* Hangouts It2MeHelpeeChannel
* |------isHostInstalled message----->|
* |<-isHostInstalled response(false)--|
* | |
* |--------downloadHost message------>|
* | |
* |------isHostInstalled message----->|
* |<-isHostInstalled response(false)--|
* | |
* |------isHostInstalled message----->|
* |<-isHostInstalled response(true)---|
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @param {chrome.runtime.Port} hangoutPort
* @param {remoting.It2MeHostFacade} host
* @param {remoting.HostInstaller} hostInstaller
* @param {function()} onDisposedCallback Callback to notify the client when
* the connection is torn down.
*
* @constructor
* @implements {base.Disposable}
*/
remoting.It2MeHelpeeChannel =
function(hangoutPort, host, hostInstaller, onDisposedCallback) {
/**
* @type {chrome.runtime.Port}
* @private
*/
this.hangoutPort_ = hangoutPort;
/**
* @type {remoting.It2MeHostFacade}
* @private
*/
this.host_ = host;
/**
* @type {?remoting.HostInstaller}
* @private
*/
this.hostInstaller_ = hostInstaller;
/**
* @type {remoting.HostSession.State}
* @private
*/
this.hostState_ = remoting.HostSession.State.UNKNOWN;
/**
* @type {?function()}
* @private
*/
this.onDisposedCallback_ = onDisposedCallback;
this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this);
this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this);
};
/** @enum {string} */
remoting.It2MeHelpeeChannel.HangoutMessageTypes = {
CONNECT: 'connect',
CONNECT_RESPONSE: 'connectResponse',
DISCONNECT: 'disconnect',
DOWNLOAD_HOST: 'downloadHost',
ERROR: 'error',
HELLO: 'hello',
HELLO_RESPONSE: 'helloResponse',
HOST_STATE_CHANGED: 'hostStateChanged',
IS_HOST_INSTALLED: 'isHostInstalled',
IS_HOST_INSTALLED_RESPONSE: 'isHostInstalledResponse'
};
/** @enum {string} */
remoting.It2MeHelpeeChannel.Features = {
REMOTE_ASSISTANCE: 'remoteAssistance'
};
remoting.It2MeHelpeeChannel.prototype.init = function() {
this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
};
remoting.It2MeHelpeeChannel.prototype.dispose = function() {
if (this.host_ !== null) {
this.host_.unhookCallbacks();
this.host_.disconnect();
this.host_ = null;
}
if (this.hangoutPort_ !== null) {
this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_);
this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_);
this.hostState_ = remoting.HostSession.State.DISCONNECTED;
try {
var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
this.hangoutPort_.postMessage({
method: MessageTypes.HOST_STATE_CHANGED,
state: this.hostState_
});
} catch (e) {
// |postMessage| throws if |this.hangoutPort_| is disconnected
// It is safe to ignore the exception.
}
this.hangoutPort_.disconnect();
this.hangoutPort_ = null;
}
if (this.onDisposedCallback_ !== null) {
this.onDisposedCallback_();
this.onDisposedCallback_ = null;
}
};
/**
* Message Handler for incoming runtime messages from Hangouts.
*
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelpeeChannel.prototype.onHangoutMessage_ = function(message) {
try {
var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
switch (message.method) {
case MessageTypes.HELLO:
this.hangoutPort_.postMessage({
method: MessageTypes.HELLO_RESPONSE,
supportedFeatures: base.values(remoting.It2MeHelpeeChannel.Features)
});
return true;
case MessageTypes.IS_HOST_INSTALLED:
this.handleIsHostInstalled_(message);
return true;
case MessageTypes.DOWNLOAD_HOST:
this.handleDownloadHost_(message);
return true;
case MessageTypes.CONNECT:
this.handleConnect_(message);
return true;
case MessageTypes.DISCONNECT:
this.dispose();
return true;
}
throw new Error('Unsupported message method=' + message.method);
} catch(e) {
var error = /** @type {Error} */ e;
this.sendErrorResponse_(message, error.message);
}
return false;
};
/**
* Queries the |hostInstaller| for the installation status.
*
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelpeeChannel.prototype.handleIsHostInstalled_ =
function(message) {
/** @type {remoting.It2MeHelpeeChannel} */
var that = this;
/** @param {boolean} installed */
function sendResponse(installed) {
var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
that.hangoutPort_.postMessage({
method: MessageTypes.IS_HOST_INSTALLED_RESPONSE,
result: installed
});
}
this.hostInstaller_.isInstalled().then(
sendResponse,
this.sendErrorResponse_.bind(this, message)
);
};
/**
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelpeeChannel.prototype.handleDownloadHost_ = function(message) {
try {
this.hostInstaller_.download();
} catch (e) {
var error = /** @type {Error} */ e;
this.sendErrorResponse_(message, error.message);
}
};
/**
* Disconnect the session if the |hangoutPort| gets disconnected.
* @private
*/
remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() {
this.dispose();
};
/**
* Connects to the It2Me Native messaging Host and retrieves the access code.
*
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelpeeChannel.prototype.handleConnect_ =
function(message) {
var email = getStringAttr(message, 'email');
if (!email) {
throw new Error('Missing required parameter: email');
}
if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) {
throw new Error('An existing connection is in progress.');
}
this.showConfirmDialog_().then(
this.initializeHost_.bind(this)
).then(
this.fetchOAuthToken_.bind(this)
).then(
this.connectToHost_.bind(this, email),
this.sendErrorResponse_.bind(this, message)
);
};
/**
* Prompts the user before starting the It2Me Native Messaging Host. This
* ensures that even if Hangouts is compromised, an attacker cannot start the
* host without explicit user confirmation.
*
* @return {Promise} A promise that resolves to a boolean value, indicating
* whether the user accepts the remote assistance or not.
* @private
*/
remoting.It2MeHelpeeChannel.prototype.showConfirmDialog_ = function() {
if (base.isAppsV2()) {
return this.showConfirmDialogV2_();
} else {
return this.showConfirmDialogV1_();
}
};
/**
* @return {Promise} A promise that resolves to a boolean value, indicating
* whether the user accepts the remote assistance or not.
* @private
*/
remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV1_ = function() {
var messageHeader = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1');
var message1 = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2');
var message2 = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3');
var message = base.escapeHTML(messageHeader) + '\n' +
'- ' + base.escapeHTML(message1) + '\n' +
'- ' + base.escapeHTML(message2) + '\n';
if(window.confirm(message)) {
return Promise.resolve();
} else {
return Promise.reject(new Error(remoting.Error.CANCELLED));
}
};
/**
* @return {Promise} A promise that resolves to a boolean value, indicating
* whether the user accepts the remote assistance or not.
* @private
*/
remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV2_ = function() {
var messageHeader = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1');
var message1 = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2');
var message2 = l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3');
var message = '<div>' + base.escapeHTML(messageHeader) + '</div>' +
'<ul class="insetList">' +
'<li>' + base.escapeHTML(message1) + '</li>' +
'<li>' + base.escapeHTML(message2) + '</li>' +
'</ul>';
/**
* @param {function(*=):void} resolve
* @param {function(*=):void} reject
*/
return new Promise(function(resolve, reject) {
/** @param {number} result */
function confirmDialogCallback(result) {
if (result === 1) {
resolve();
} else {
reject(new Error(remoting.Error.CANCELLED));
}
}
remoting.MessageWindow.showConfirmWindow(
'', // Empty string to use the package name as the dialog title.
message,
l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_ACCEPT'),
l10n.getTranslationOrError(
/*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_DECLINE'),
confirmDialogCallback
);
});
};
/**
* @return {Promise} A promise that resolves when the host is initialized.
* @private
*/
remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() {
/** @type {remoting.It2MeHostFacade} */
var host = this.host_;
/**
* @param {function(*=):void} resolve
* @param {function(*=):void} reject
*/
return new Promise(function(resolve, reject) {
if (host.initialized()) {
resolve();
} else {
host.initialize(resolve, reject);
}
});
};
/**
* TODO(kelvinp): The existing implementation only works in the v2 app
* We need to implement token fetching for the v1 app using remoting.OAuth2
* before launch (crbug.com/405130).
*
* @return {Promise} Promise that resolves with the OAuth token as the value.
*/
remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
if (!base.isAppsV2()) {
throw new Error('fetchOAuthToken_ is not implemented in the v1 app.');
}
/**
* @param {function(*=):void} resolve
*/
return new Promise(function(resolve){
chrome.identity.getAuthToken({ 'interactive': false }, resolve);
});
};
/**
* Connects to the It2Me Native Messaging Host and retrieves the access code
* in the |onHostStateChanged_| callback.
*
* @param {string} email
* @param {string} accessToken
* @private
*/
remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
function(email, accessToken) {
base.debug.assert(this.host_.initialized());
this.host_.connect(
email,
'oauth2:' + accessToken,
this.onHostStateChanged_.bind(this),
base.doNothing, // Ignore |onNatPolicyChanged|.
console.log.bind(console), // Forward logDebugInfo to console.log.
remoting.settings.XMPP_SERVER_ADDRESS,
remoting.settings.XMPP_SERVER_USE_TLS,
remoting.settings.DIRECTORY_BOT_JID,
this.onHostConnectError_);
};
/**
* @param {remoting.Error} error
* @private
*/
remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
this.sendErrorResponse_(null, error);
};
/**
* @param {remoting.HostSession.State} state
* @private
*/
remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
this.hostState_ = state;
var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
var HostState = remoting.HostSession.State;
switch (state) {
case HostState.RECEIVED_ACCESS_CODE:
var accessCode = this.host_.getAccessCode();
this.hangoutPort_.postMessage({
method: MessageTypes.CONNECT_RESPONSE,
accessCode: accessCode
});
break;
case HostState.CONNECTED:
case HostState.DISCONNECTED:
this.hangoutPort_.postMessage({
method: MessageTypes.HOST_STATE_CHANGED,
state: state
});
break;
case HostState.ERROR:
this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
break;
case HostState.INVALID_DOMAIN_ERROR:
this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
break;
default:
// It is safe to ignore other state changes.
}
};
/**
* @param {?{method:string, data:Object.<string,*>}} incomingMessage
* @param {string|Error} error
* @private
*/
remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
function(incomingMessage, error) {
if (error instanceof Error) {
error = error.message;
}
console.error('Error responding to message method:' +
(incomingMessage ? incomingMessage.method : 'null') +
' error:' + error);
this.hangoutPort_.postMessage({
method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
message: error,
request: incomingMessage
});
};