blob: 37f507d65afd616992f74efdc10ec1c73e3b8ec1 [file] [log] [blame] [edit]
// Copyright (c) 2012 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
* Functions related to the 'client screen' for Chromoting.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @type {remoting.ClientSession} The client session object, set once the
* access code has been successfully verified.
*/
remoting.clientSession = null;
/**
* @type {string} The normalized access code.
*/
remoting.accessCode = '';
/**
* @type {string} The host's JID, returned by the server.
*/
remoting.hostJid = '';
/**
* @type {string} For Me2Me connections, the id of the current host.
*/
remoting.hostId = '';
/**
* @type {boolean} For Me2Me connections. Set to true if connection
* must be retried on failure.
*/
remoting.retryIfOffline = false;
/**
* @type {string} The host's public key, returned by the server.
*/
remoting.hostPublicKey = '';
/**
* @type {XMLHttpRequest} The XHR object corresponding to the current
* support-hosts request, if there is one outstanding.
* @private
*/
remoting.supportHostsXhr_ = null;
/**
* @enum {string}
*/
remoting.ConnectionType = {
It2Me: 'It2Me',
Me2Me: 'Me2Me'
};
/**
* @type {remoting.ConnectionType?}
*/
remoting.currentConnectionType = null;
/**
* Entry point for the 'connect' functionality. This function defers to the
* WCS loader to call it back with an access token.
*/
remoting.connectIt2Me = function() {
remoting.currentConnectionType = remoting.ConnectionType.It2Me;
remoting.WcsLoader.load(connectIt2MeWithAccessToken_,
remoting.showErrorMessage);
};
/**
* Cancel an incomplete connect operation.
*
* @return {void} Nothing.
*/
remoting.cancelConnect = function() {
if (remoting.supportHostsXhr_) {
remoting.supportHostsXhr_.abort();
remoting.supportHostsXhr_ = null;
}
if (remoting.clientSession) {
remoting.clientSession.removePlugin();
remoting.clientSession = null;
}
if (remoting.currentConnectionType == remoting.ConnectionType.Me2Me) {
remoting.initDaemonUi();
} else {
remoting.setMode(remoting.AppMode.HOME);
document.getElementById('access-code-entry').value = '';
}
};
/**
* Toggle the scale-to-fit feature for the current client session.
*
* @return {void} Nothing.
*/
remoting.toggleScaleToFit = function() {
remoting.clientSession.setScaleToFit(!remoting.clientSession.getScaleToFit());
};
/**
* Update the remoting client layout in response to a resize event.
*
* @return {void} Nothing.
*/
remoting.onResize = function() {
if (remoting.clientSession)
remoting.clientSession.onResize();
};
/**
* Handle changes in the visibility of the window, for example by pausing video.
*
* @return {void} Nothing.
*/
remoting.onVisibilityChanged = function() {
if (remoting.clientSession)
remoting.clientSession.pauseVideo(document.webkitHidden);
}
/**
* Disconnect the remoting client.
*
* @return {void} Nothing.
*/
remoting.disconnect = function() {
if (remoting.clientSession) {
remoting.clientSession.disconnect();
remoting.clientSession = null;
console.log('Disconnected.');
if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
} else {
remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
}
}
};
/**
* Sends a Ctrl-Alt-Del sequence to the remoting client.
*
* @return {void} Nothing.
*/
remoting.sendCtrlAltDel = function() {
if (remoting.clientSession) {
console.log('Sending Ctrl-Alt-Del.');
remoting.clientSession.sendCtrlAltDel();
}
};
/**
* Sends a Print Screen keypress to the remoting client.
*
* @return {void} Nothing.
*/
remoting.sendPrintScreen = function() {
if (remoting.clientSession) {
console.log('Sending Print Screen.');
remoting.clientSession.sendPrintScreen();
}
};
/**
* If WCS was successfully loaded, proceed with the connection, otherwise
* report an error.
*
* @param {string?} token The OAuth2 access token, or null if an error occurred.
* @return {void} Nothing.
*/
function connectIt2MeWithAccessToken_(token) {
if (token) {
var accessCode = document.getElementById('access-code-entry').value;
remoting.accessCode = normalizeAccessCode_(accessCode);
// At present, only 12-digit access codes are supported, of which the first
// 7 characters are the supportId.
var kSupportIdLen = 7;
var kHostSecretLen = 5;
var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
if (remoting.accessCode.length != kAccessCodeLen) {
console.error('Bad access code length');
showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
} else {
var supportId = remoting.accessCode.substring(0, kSupportIdLen);
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
resolveSupportId(supportId, token);
}
} else {
showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
}
}
/**
* Callback function called when the state of the client plugin changes. The
* current state is available via the |state| member variable.
*
* @param {number} oldState The previous state of the plugin.
* @param {number} newState The current state of the plugin.
*/
// TODO(jamiewalch): Make this pass both the current and old states to avoid
// race conditions.
function onClientStateChange_(oldState, newState) {
if (!remoting.clientSession) {
// If the connection has been cancelled, then we no longer have a reference
// to the session object and should ignore any state changes.
return;
}
// Clear the PIN on successful connection, or on error if we're not going to
// automatically retry.
var clearPin = false;
if (newState == remoting.ClientSession.State.CREATED) {
console.log('Created plugin');
} else if (newState == remoting.ClientSession.State.BAD_PLUGIN_VERSION) {
showConnectError_(remoting.Error.BAD_PLUGIN_VERSION);
} else if (newState == remoting.ClientSession.State.CONNECTING) {
console.log('Connecting as ' + remoting.oauth2.getCachedEmail());
} else if (newState == remoting.ClientSession.State.INITIALIZING) {
console.log('Initializing connection');
} else if (newState == remoting.ClientSession.State.CONNECTED) {
if (remoting.clientSession) {
clearPin = true;
setConnectionInterruptedButtonsText_();
remoting.setMode(remoting.AppMode.IN_SESSION);
remoting.toolbar.center();
remoting.toolbar.preview();
remoting.clipboard.startSession();
updateStatistics_();
}
} else if (newState == remoting.ClientSession.State.CLOSED) {
if (oldState == remoting.ClientSession.State.CONNECTED) {
remoting.clientSession.removePlugin();
remoting.clientSession = null;
console.log('Connection closed by host');
if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
} else {
remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
}
} else {
// The transition from CONNECTING to CLOSED state may happen
// only with older client plugins. Current version should go the
// FAILED state when connection fails.
showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
}
} else if (newState == remoting.ClientSession.State.FAILED) {
console.error('Client plugin reported connection failed: ' +
remoting.clientSession.error);
clearPin = true;
if (remoting.clientSession.error ==
remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) {
clearPin = false;
retryConnectOrReportOffline_();
} else if (remoting.clientSession.error ==
remoting.ClientSession.ConnectionError.SESSION_REJECTED) {
showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
} else if (remoting.clientSession.error ==
remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) {
showConnectError_(remoting.Error.INCOMPATIBLE_PROTOCOL);
} else if (remoting.clientSession.error ==
remoting.ClientSession.ConnectionError.NETWORK_FAILURE) {
showConnectError_(remoting.Error.NETWORK_FAILURE);
} else if (remoting.clientSession.error ==
remoting.ClientSession.ConnectionError.HOST_OVERLOAD) {
showConnectError_(remoting.Error.HOST_OVERLOAD);
} else {
showConnectError_(remoting.Error.UNEXPECTED);
}
if (clearPin) {
document.getElementById('pin-entry').value = '';
}
} else {
console.error('Unexpected client plugin state: ' + newState);
// This should only happen if the web-app and client plugin get out of
// sync, and even then the version check should allow compatibility.
showConnectError_(remoting.Error.MISSING_PLUGIN);
}
}
/**
* If we have a hostId to retry, try refreshing it and connecting again. If not,
* then show the 'host offline' error message.
*
* @return {void} Nothing.
*/
function retryConnectOrReportOffline_() {
if (remoting.clientSession) {
remoting.clientSession.removePlugin();
remoting.clientSession = null;
}
if (remoting.hostId && remoting.retryIfOffline) {
console.warn('Connection failed. Retrying.');
/** @param {boolean} success True if the refresh was successful. */
var onDone = function(success) {
if (success) {
remoting.retryIfOffline = false;
remoting.connectMe2MeWithPin();
} else {
showConnectError_(remoting.Error.HOST_IS_OFFLINE);
}
};
remoting.hostList.refresh(onDone);
} else {
console.error('Connection failed. Not retrying.');
showConnectError_(remoting.Error.HOST_IS_OFFLINE);
}
}
/**
* Create the client session object and initiate the connection.
*
* @return {void} Nothing.
*/
function startSession_() {
console.log('Starting session...');
var accessCode = document.getElementById('access-code-entry');
accessCode.value = ''; // The code has been validated and won't work again.
remoting.clientSession =
new remoting.ClientSession(
remoting.hostJid, remoting.hostPublicKey,
remoting.accessCode, 'spake2_plain', '',
/** @type {string} */ (remoting.oauth2.getCachedEmail()),
remoting.ClientSession.Mode.IT2ME,
onClientStateChange_);
/** @param {string?} token The auth token. */
var createPluginAndConnect = function(token) {
if (token) {
remoting.clientSession.createPluginAndConnect(
document.getElementById('session-mode'),
token);
} else {
showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
}
};
remoting.oauth2.callWithToken(createPluginAndConnect,
remoting.showErrorMessage);
}
/**
* Show a client-side error message.
*
* @param {remoting.Error} errorTag The error to be localized and
* displayed.
* @return {void} Nothing.
*/
function showConnectError_(errorTag) {
console.error('Connection failed: ' + errorTag);
var errorDiv = document.getElementById('connect-error-message');
l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag));
remoting.accessCode = '';
if (remoting.clientSession) {
remoting.clientSession.disconnect();
remoting.clientSession = null;
}
if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
} else {
remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
}
}
/**
* Set the text on the buttons shown under the error message so that they are
* easy to understand in the case where a successful connection failed, as
* opposed to the case where a connection never succeeded.
*/
function setConnectionInterruptedButtonsText_() {
var button1 = document.getElementById('client-reconnect-button');
l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT');
button1.removeAttribute('autofocus');
var button2 = document.getElementById('client-finished-me2me-button');
l10n.localizeElementFromTag(button2, /*i18n-content*/'OK');
button2.setAttribute('autofocus', 'autofocus');
}
/**
* Parse the response from the server to a request to resolve a support id.
*
* @param {XMLHttpRequest} xhr The XMLHttpRequest object.
* @return {void} Nothing.
*/
function parseServerResponse_(xhr) {
remoting.supportHostsXhr_ = null;
console.log('parseServerResponse: xhr =', xhr);
if (xhr.status == 200) {
var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
jsonParseSafe(xhr.responseText);
if (host && host.data && host.data.jabberId && host.data.publicKey) {
remoting.hostJid = host.data.jabberId;
remoting.hostPublicKey = host.data.publicKey;
var split = remoting.hostJid.split('/');
document.getElementById('connected-to').innerText = split[0];
startSession_();
return;
} else {
console.error('Invalid "support-hosts" response from server.');
}
}
var errorMsg = remoting.Error.UNEXPECTED;
if (xhr.status == 404) {
errorMsg = remoting.Error.INVALID_ACCESS_CODE;
} else if (xhr.status == 0) {
errorMsg = remoting.Error.NO_RESPONSE;
} else if (xhr.status == 503) {
errorMsg = remoting.Error.SERVICE_UNAVAILABLE;
} else {
console.error('The server responded: ' + xhr.responseText);
}
showConnectError_(errorMsg);
}
/**
* Normalize the access code entered by the user.
*
* @param {string} accessCode The access code, as entered by the user.
* @return {string} The normalized form of the code (whitespace removed).
*/
function normalizeAccessCode_(accessCode) {
// Trim whitespace.
// TODO(sergeyu): Do we need to do any other normalization here?
return accessCode.replace(/\s/g, '');
}
/**
* Initiate a request to the server to resolve a support ID.
*
* @param {string} supportId The canonicalized support ID.
* @param {string} token The OAuth access token.
*/
function resolveSupportId(supportId, token) {
var headers = {
'Authorization': 'OAuth ' + token
};
remoting.supportHostsXhr_ = remoting.xhr.get(
'https://www.googleapis.com/chromoting/v1/support-hosts/' +
encodeURIComponent(supportId),
parseServerResponse_,
'',
headers);
}
/**
* Timer callback to update the statistics panel.
*/
function updateStatistics_() {
if (!remoting.clientSession ||
remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) {
return;
}
var perfstats = remoting.clientSession.getPerfStats();
remoting.stats.update(perfstats);
remoting.clientSession.logStatistics(perfstats);
// Update the stats once per second.
window.setTimeout(updateStatistics_, 1000);
}
/**
* Shows PIN entry screen.
*
* @param {string} hostId The unique id of the host.
* @param {boolean} retryIfOffline If true and the host can't be contacted,
* refresh the host list and try again. This allows bookmarked hosts to
* work even if they reregister with Talk and get a different Jid.
* @return {void} Nothing.
*/
remoting.connectMe2Me = function(hostId, retryIfOffline) {
remoting.currentConnectionType = remoting.ConnectionType.Me2Me;
remoting.hostId = hostId;
remoting.retryIfOffline = retryIfOffline;
var host = remoting.hostList.getHostForId(remoting.hostId);
// If we're re-loading a tab for a host that has since been unregistered
// then the hostId may no longer resolve.
if (!host) {
showConnectError_(remoting.Error.HOST_IS_OFFLINE);
return;
}
var message = document.getElementById('pin-message');
l10n.localizeElement(message, host.hostName);
remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT);
};
/**
* Start a connection to the specified host, using the cached details
* and the PIN entered by the user.
*
* @return {void} Nothing.
*/
remoting.connectMe2MeWithPin = function() {
console.log('Connecting to host...');
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
var host = remoting.hostList.getHostForId(remoting.hostId);
// If the user clicked on a cached host that has since been removed then we
// won't find the hostId. If the user clicked on the entry for the local host
// immediately after having enabled it then we won't know it's JID or public
// key until the host heartbeats and we pull a fresh host list.
if (!host || !host.jabberId || !host.publicKey) {
retryConnectOrReportOffline_();
return;
}
remoting.hostJid = host.jabberId;
remoting.hostPublicKey = host.publicKey;
document.getElementById('connected-to').innerText = host.hostName;
document.title = chrome.i18n.getMessage('PRODUCT_NAME') + ': ' +
host.hostName;
remoting.WcsLoader.load(connectMe2MeWithAccessToken_,
remoting.showErrorMessage);
};
/**
* Continue making the connection to a host, once WCS has initialized.
*
* @param {string?} token The OAuth2 access token, or null if an error occurred.
* @return {void} Nothing.
*/
function connectMe2MeWithAccessToken_(token) {
if (token) {
/** @type {string} */
var pin = document.getElementById('pin-entry').value;
remoting.clientSession =
new remoting.ClientSession(
remoting.hostJid, remoting.hostPublicKey,
pin, 'spake2_hmac,spake2_plain', remoting.hostId,
/** @type {string} */ (remoting.oauth2.getCachedEmail()),
remoting.ClientSession.Mode.ME2ME, onClientStateChange_);
// Don't log errors for cached JIDs.
remoting.clientSession.logErrors(!remoting.retryIfOffline);
remoting.clientSession.createPluginAndConnect(
document.getElementById('session-mode'),
token);
} else {
showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
}
}