diff options
author | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-07 04:10:10 +0000 |
---|---|---|
committer | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-07 04:10:10 +0000 |
commit | 79ff6327718beb0ecbbc5b5c79d1d426acf8f669 (patch) | |
tree | dac87d3aa9c8c79f0d6ff9dc10886af95092f2a9 /remoting | |
parent | cec910c0d1b82303a8e94503eb3feab5e9353a6c (diff) | |
download | chromium_src-79ff6327718beb0ecbbc5b5c79d1d426acf8f669.zip chromium_src-79ff6327718beb0ecbbc5b5c79d1d426acf8f669.tar.gz chromium_src-79ff6327718beb0ecbbc5b5c79d1d426acf8f669.tar.bz2 |
Factor connection setup out of client_screen.js.
client_screen.js is a bit of a mixed bag of functions with only a loose
common theme. This CL pulls out the largest single feature, connection setup,
into a separate class, eliminating lots of global state as a result.
As a side-effect, it speeds up connections by parallelizing the WCS driver load
and fixes the referenced bugs.
BUG=173788,174113
Review URL: https://chromiumcodereview.appspot.com/12177003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@181187 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/remoting.gyp | 1 | ||||
-rw-r--r-- | remoting/webapp/all_js_load.gtestjs | 1 | ||||
-rw-r--r-- | remoting/webapp/client_screen.js | 506 | ||||
-rw-r--r-- | remoting/webapp/client_session.js | 67 | ||||
-rw-r--r-- | remoting/webapp/event_handlers.js | 14 | ||||
-rw-r--r-- | remoting/webapp/main.html | 1 | ||||
-rw-r--r-- | remoting/webapp/remoting.js | 9 | ||||
-rw-r--r-- | remoting/webapp/session_connector.js | 404 | ||||
-rw-r--r-- | remoting/webapp/ui_mode.js | 4 |
9 files changed, 549 insertions, 458 deletions
diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index 82361fe..e3c2716 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -186,6 +186,7 @@ 'webapp/plugin_settings.js', 'webapp/xhr_proxy.js', 'webapp/remoting.js', + 'webapp/session_connector.js', 'webapp/server_log_entry.js', 'webapp/stats_accumulator.js', 'webapp/storage.js', diff --git a/remoting/webapp/all_js_load.gtestjs b/remoting/webapp/all_js_load.gtestjs index 59164bc..57d719f 100644 --- a/remoting/webapp/all_js_load.gtestjs +++ b/remoting/webapp/all_js_load.gtestjs @@ -49,6 +49,7 @@ AllJsLoadTest.prototype = { 'plugin_settings.js', 'xhr_proxy.js', 'remoting.js', + 'session_connector.js', 'server_log_entry.js', 'stats_accumulator.js', 'storage.js', diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js index a5922e1..1140dc7 100644 --- a/remoting/webapp/client_screen.js +++ b/remoting/webapp/client_screen.js @@ -13,93 +13,29 @@ 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 + * @type {remoting.SessionConnector} The connector object, set when a connection + * is initiated. */ -remoting.supportHostsXhr_ = null; +remoting.connector = null; /** - * @type {remoting.ClientSession.Mode?} + * @type {remoting.ClientSession} The client session object, set once the + * connector has invoked its onOk callback. */ -remoting.currentConnectionType = null; +remoting.clientSession = null; /** - * Entry point for the 'connect' functionality. This function defers to the - * WCS loader to call it back with an access token. + * Initiate an IT2Me connection. */ -remoting.connectIt2Me = function() { - remoting.currentConnectionType = remoting.ClientSession.Mode.IT2ME; - /** @param {string} token */ - var startWcsAndConnect = function(token) { - remoting.wcsSandbox.setOnReady( - connectIt2MeWithAccessToken_.bind(null, token)); - remoting.wcsSandbox.setOnError(remoting.showErrorMessage); - remoting.wcsSandbox.setAccessToken(token); - startAccessTokenRefreshTimer_(); - }; - remoting.identity.callWithToken(startWcsAndConnect, - remoting.showErrorMessage); -}; - -/** - * Cancel an incomplete connect operation. - * - * Note that this function is not currently used. It is here for reference - * because we'll need to reinstate something very like it when we transition - * to Apps v2 where we can no longer change the URL (which is what we do in - * lieu of calling this function to ensure correct Reload behaviour). - * - * @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 = ''; - } +remoting.connectIT2Me = function() { + remoting.connector = new remoting.SessionConnector( + document.getElementById('session-mode'), + remoting.onConnected, + remoting.showErrorMessage); + var accessCode = document.getElementById('access-code-entry').value; + remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); + remoting.connector.connectIT2Me(accessCode); }; -*/ /** * Update the remoting client layout in response to a resize event. @@ -107,8 +43,9 @@ remoting.cancelConnect = function() { * @return {void} Nothing. */ remoting.onResize = function() { - if (remoting.clientSession) + if (remoting.clientSession) { remoting.clientSession.onResize(); + } }; /** @@ -117,8 +54,9 @@ remoting.onResize = function() { * @return {void} Nothing. */ remoting.onVisibilityChanged = function() { - if (remoting.clientSession) + if (remoting.clientSession) { remoting.clientSession.pauseVideo(document.webkitHidden); + } } /** @@ -127,16 +65,17 @@ remoting.onVisibilityChanged = function() { * @return {void} Nothing. */ remoting.disconnect = function() { - if (remoting.clientSession) { - remoting.clientSession.disconnect(true); - remoting.clientSession = null; - console.log('Disconnected.'); - if (remoting.currentConnectionType == remoting.ClientSession.Mode.IT2ME) { - remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); - } else { - remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); - } + if (!remoting.clientSession) { + return; } + if (remoting.clientSession.mode == remoting.ClientSession.Mode.IT2ME) { + remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); + } else { + remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); + } + remoting.clientSession.disconnect(true); + remoting.clientSession = null; + console.log('Disconnected.'); }; /** @@ -164,178 +103,41 @@ remoting.sendPrintScreen = function() { }; /** - * If WCS was successfully loaded, proceed with the connection, otherwise - * report an error. - * - * @param {string} token The OAuth2 access token. - * @param {string} clientJid The full JID of the WCS client. - * @return {void} Nothing. - */ -function connectIt2MeWithAccessToken_(token, clientJid) { - 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(clientJid, supportId, token); - } -} - -/** * 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.identity.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.retryIfOffline = false; - 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; + switch (newState) { + case remoting.ClientSession.State.CLOSED: console.log('Connection closed by host'); - if (remoting.currentConnectionType == remoting.ClientSession.Mode.IT2ME) { + if (remoting.clientSession.mode == remoting.ClientSession.Mode.IT2ME) { remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); } else { remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); } - } else { - // A state transition from CONNECTING -> CLOSED can happen if the host - // closes the connection without an error message instead of accepting it. - // For example, it does this if it fails to activate curtain mode. Since - // there's no way of knowing exactly what went wrong, we rely on server- - // side logs in this case and show a generic error message. - showConnectError_(remoting.Error.UNEXPECTED); - } - - } 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 = ''; - } + break; - } 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); + case remoting.ClientSession.State.FAILED: + var error = remoting.clientSession.getError(); + console.error('Client plugin reported connection failed: ' + error); + if (error == null) { + error = remoting.Error.UNEXPECTED; } - }; - 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. - * - * @param {string} clientJid The full JID of the WCS client. - * @return {void} Nothing. - */ -function startSession_(clientJid) { - 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, clientJid, - remoting.hostPublicKey, - remoting.accessCode, 'spake2_plain', '', - remoting.ClientSession.Mode.IT2ME, - onClientStateChange_); - remoting.clientSession.createPluginAndConnect( - document.getElementById('session-mode')); + showConnectError_(error); + break; + + default: + console.error('Unexpected client plugin state: ' + newState); + // This should only happen if the web-app and client plugin get out of + // sync, so MISSING_PLUGIN is a suitable error. + showConnectError_(remoting.Error.MISSING_PLUGIN); + break; + } + remoting.clientSession.removePlugin(); + remoting.clientSession = null; } /** @@ -350,15 +152,15 @@ function showConnectError_(errorTag) { var errorDiv = document.getElementById('connect-error-message'); l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); remoting.accessCode = ''; - if (remoting.clientSession) { - remoting.clientSession.disconnect(false); - remoting.clientSession = null; - } - if (remoting.currentConnectionType == remoting.ClientSession.Mode.IT2ME) { + if (remoting.clientSession.mode == remoting.ClientSession.Mode.IT2ME) { remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME); } else { remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME); } + if (remoting.clientSession) { + remoting.clientSession.disconnect(false); + remoting.clientSession = null; + } } /** @@ -376,75 +178,6 @@ function setConnectionInterruptedButtonsText_() { } /** - * Parse the response from the server to a request to resolve a support id. - * - * @param {string} clientJid The full JID of the WCS client. - * @param {XMLHttpRequest} xhr The XMLHttpRequest object. - * @return {void} Nothing. - */ -function parseServerResponse_(clientJid, 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_(clientJid); - 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 == 502 || 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} clientJid The full JID of the WCS client. - * @param {string} supportId The canonicalized support ID. - * @param {string} token The OAuth access token. - */ -function resolveSupportId(clientJid, supportId, token) { - var headers = { - 'Authorization': 'OAuth ' + token - }; - - remoting.supportHostsXhr_ = remoting.xhr.get( - remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + - encodeURIComponent(supportId), - parseServerResponse_.bind(null, clientJid), - '', - headers); -} - -/** * Timer callback to update the statistics panel. */ function updateStatistics_() { @@ -460,114 +193,49 @@ function updateStatistics_() { } /** - * Shows PIN entry screen. + * Shows PIN entry screen localized to include the host name, and registers + * a host-specific one-shot event handler for the form submission. * * @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.ClientSession.Mode.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. +remoting.connectMe2Me = function(hostId) { + var host = remoting.hostList.getHostForId(hostId); if (!host) { showConnectError_(remoting.Error.HOST_IS_OFFLINE); return; } + + remoting.connector = new remoting.SessionConnector( + document.getElementById('session-mode'), + remoting.onConnected, + remoting.showErrorMessage); + /** @type {Element} */ + var pinForm = document.getElementById('pin-form'); + /** @param {Event} event */ + var onSubmit = function(event) { + pinForm.removeEventListener('submit', onSubmit, false); + var pin = document.getElementById('pin-entry').value; + remoting.connector.connectMe2Me(host, pin); + remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); + event.preventDefault(); + }; + pinForm.addEventListener('submit', onSubmit, false); + 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 = host.hostName + ' - ' + - chrome.i18n.getMessage('PRODUCT_NAME'); - - /** @param {string} token */ - var startWcsAndConnect = function(token) { - remoting.wcsSandbox.setOnReady( - connectMe2MeWithAccessToken_.bind(null, token)); - remoting.wcsSandbox.setOnError(remoting.showErrorMessage); - remoting.wcsSandbox.setAccessToken(token); - startAccessTokenRefreshTimer_(); - }; - remoting.identity.callWithToken(startWcsAndConnect, - remoting.showErrorMessage); +/** @param {remoting.ClientSession} clientSession */ +remoting.onConnected = function(clientSession) { + remoting.connector = null; + remoting.clientSession = clientSession; + remoting.clientSession.setOnStateChange(onClientStateChange_); + setConnectionInterruptedButtonsText_(); + remoting.setMode(remoting.AppMode.IN_SESSION); + remoting.toolbar.center(); + remoting.toolbar.preview(); + remoting.clipboard.startSession(); + updateStatistics_(); }; - -/** - * Continue making the connection to a host, once WCS has initialized. - * - * @param {string} token The OAuth2 access token. - * @param {string} clientJid The full JID of the WCS client. - * @return {void} Nothing. - */ -function connectMe2MeWithAccessToken_(token, clientJid) { - /** @type {string} */ - var pin = document.getElementById('pin-entry').value; - - remoting.clientSession = - new remoting.ClientSession( - remoting.hostJid, clientJid, remoting.hostPublicKey, - pin, 'spake2_hmac,spake2_plain', remoting.hostId, - remoting.ClientSession.Mode.ME2ME, onClientStateChange_); - // Don't log host offline errors for cached JIDs. - remoting.clientSession.logHostOfflineErrors(!remoting.retryIfOffline); - remoting.clientSession.createPluginAndConnect( - document.getElementById('session-mode')); -} - -/** @type {number} */ -remoting.wcsAccessTokenRefreshTimer = 0; - -function startAccessTokenRefreshTimer_() { - if (remoting.wcsAccessTokenRefreshTimer != 0) { - return; - } - - /** @param {string} token */ - var updateAccessToken = function(token) { - remoting.wcsSandbox.setAccessToken(token); - }; - /** @param {remoting.Error} error */ - var logError = function(error) { - console.error('updateAccessToken: Authentication failed: ' + error); - }; - var refreshAccessToken = function() { - remoting.identity.callWithToken(updateAccessToken, logError); - }; - /** - * A timer that polls for an updated access token. - * @type {number} - * @private - */ - remoting.wcsAccessTokenRefreshTimer = setInterval(refreshAccessToken, - 60 * 1000); -} diff --git a/remoting/webapp/client_session.js b/remoting/webapp/client_session.js index 4fa0563..522bca8 100644 --- a/remoting/webapp/client_session.js +++ b/remoting/webapp/client_session.js @@ -34,15 +34,11 @@ var remoting = remoting || {}; * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me. * Mixed into authentication hashes for some authentication methods. * @param {remoting.ClientSession.Mode} mode The mode of this connection. - * @param {function(remoting.ClientSession.State, - remoting.ClientSession.State):void} onStateChange - * The callback to invoke when the session changes state. * @constructor */ remoting.ClientSession = function(hostJid, clientJid, hostPublicKey, sharedSecret, - authenticationMethods, hostId, - mode, onStateChange) { + authenticationMethods, hostId, mode) { this.state = remoting.ClientSession.State.CREATED; this.hostJid = hostJid; @@ -51,6 +47,7 @@ remoting.ClientSession = function(hostJid, clientJid, this.sharedSecret = sharedSecret; this.authenticationMethods = authenticationMethods; this.hostId = hostId; + /** @type {remoting.ClientSession.Mode} */ this.mode = mode; this.sessionId = ''; /** @type {remoting.ClientPlugin} */ @@ -62,7 +59,9 @@ remoting.ClientSession = function(hostJid, clientJid, /** @private */ this.hasReceivedFrame_ = false; this.logToServer = new remoting.LogToServer(); - this.onStateChange = onStateChange; + /** @type {?function(remoting.ClientSession.State, + remoting.ClientSession.State):void} */ + this.onStateChange_ = null; /** @type {number?} @private */ this.notifyClientDimensionsTimer_ = null; @@ -118,9 +117,18 @@ remoting.ClientSession = function(hostJid, clientJid, 'click', this.callToggleFullScreen_, false); }; +/** + * @param {?function(remoting.ClientSession.State, + remoting.ClientSession.State):void} onStateChange + * The callback to invoke when the session changes state. + */ +remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) { + this.onStateChange_ = onStateChange; +}; + // Note that the positive values in both of these enums are copied directly // from chromoting_scriptable_object.h and must be kept in sync. The negative -// values represent states transitions that occur within the web-app that have +// values represent state transitions that occur within the web-app that have // no corresponding plugin state transition. /** @enum {number} */ remoting.ClientSession.State = { @@ -197,8 +205,9 @@ remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN; /** * The last connection error. Set when state is set to FAILED. * @type {remoting.ClientSession.ConnectionError} + * @private */ -remoting.ClientSession.prototype.error = +remoting.ClientSession.prototype.error_ = remoting.ClientSession.ConnectionError.NONE; /** @@ -209,15 +218,6 @@ remoting.ClientSession.prototype.error = remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; /** - * Callback to invoke when the state is changed. - * - * @param {remoting.ClientSession.State} oldState The previous state. - * @param {remoting.ClientSession.State} newState The current state. - */ -remoting.ClientSession.prototype.onStateChange = - function(oldState, newState) { }; - -/** * @param {Element} container The element to add the plugin to. * @param {string} id Id to use for the plugin element . * @return {remoting.ClientPlugin} Create plugin object for the locally @@ -411,6 +411,27 @@ remoting.ClientSession.prototype.disconnect = function(isUserInitiated) { }; /** + * @return {?remoting.Error} The current error code, or null if the connection + * is not in an error state. + */ +remoting.ClientSession.prototype.getError = function() { + switch (this.error_) { + case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: + return remoting.Error.HOST_IS_OFFLINE; + case remoting.ClientSession.ConnectionError.SESSION_REJECTED: + return remoting.Error.INVALID_ACCESS_CODE; + case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: + return remoting.Error.INCOMPATIBLE_PROTOCOL; + case remoting.ClientSession.ConnectionError.NETWORK_FAILURE: + return remoting.Error.NETWORK_FAILURE; + case remoting.ClientSession.ConnectionError.HOST_OVERLOAD: + return remoting.Error.HOST_OVERLOAD; + default: + return null; + } +}; + +/** * Sends a key combination to the remoting client, by sending down events for * the given keys, followed by up events in reverse order. * @@ -609,7 +630,7 @@ remoting.ClientSession.prototype.onConnectionStatusUpdate_ = this.plugin.notifyClientDimensions(window.innerWidth, window.innerHeight); } } else if (status == remoting.ClientSession.State.FAILED) { - this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error); + this.error_ = /** @type {remoting.ClientSession.ConnectionError} */ (error); } this.setState_(/** @type {remoting.ClientSession.State} */ (status)); }; @@ -642,9 +663,9 @@ remoting.ClientSession.prototype.setState_ = function(newState) { if (this.state == remoting.ClientSession.State.CLOSED) { state = remoting.ClientSession.State.CONNECTION_CANCELED; } else if (this.state == remoting.ClientSession.State.FAILED && - this.error == remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE && + this.error_ == remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE && !this.logHostOfflineErrors_) { - // The upper layer requested host-offline errors to be suppressed, for + // The application requested host-offline errors to be suppressed, for // example, because this connection attempt is using a cached host JID. console.log('Suppressing host-offline error.'); state = remoting.ClientSession.State.CONNECTION_CANCELED; @@ -653,9 +674,9 @@ remoting.ClientSession.prototype.setState_ = function(newState) { this.state == remoting.ClientSession.State.FAILED) { state = remoting.ClientSession.State.CONNECTION_DROPPED; } - this.logToServer.logClientSessionStateChange(state, this.error, this.mode); - if (this.onStateChange) { - this.onStateChange(oldState, newState); + this.logToServer.logClientSessionStateChange(state, this.error_, this.mode); + if (this.onStateChange_) { + this.onStateChange_(oldState, newState); } }; diff --git a/remoting/webapp/event_handlers.js b/remoting/webapp/event_handlers.js index f3a8c24..5dfc368 100644 --- a/remoting/webapp/event_handlers.js +++ b/remoting/webapp/event_handlers.js @@ -22,7 +22,7 @@ function onLoad() { }, remoting.showErrorMessage); }; - var goFinishedIt2Me = function() { + var goFinishedIT2Me = function() { if (remoting.currentMode == remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME) { remoting.setMode(remoting.AppMode.CLIENT_UNCONNECTED); } else { @@ -34,12 +34,7 @@ function onLoad() { }; /** @param {Event} event The event. */ var sendAccessCode = function(event) { - remoting.connectIt2Me(); - event.preventDefault(); - }; - /** @param {Event} event The event. */ - var connectHostWithPin = function(event) { - remoting.connectMe2MeWithPin(); + remoting.connectIT2Me(); event.preventDefault(); }; var doAuthRedirect = function() { @@ -69,7 +64,7 @@ function onLoad() { { event: 'click', id: 'stop-sharing-button', fn: remoting.cancelShare }, { event: 'click', id: 'host-finished-button', fn: restartWebapp }, { event: 'click', id: 'client-finished-it2me-button', - fn: goFinishedIt2Me }, + fn: goFinishedIT2Me }, { event: 'click', id: 'client-finished-me2me-button', fn: restartWebapp }, { event: 'click', id: 'cancel-pin-entry-button', fn: restartWebapp }, { event: 'click', id: 'client-reconnect-button', fn: reload }, @@ -83,9 +78,8 @@ function onLoad() { fn: function() { remoting.hostSetupDialog.showForPin(); } }, { event: 'click', id: 'stop-daemon', fn: stopDaemon }, { event: 'submit', id: 'access-code-form', fn: sendAccessCode }, - { event: 'submit', id: 'pin-form', fn: connectHostWithPin }, { event: 'click', id: 'get-started-it2me', - fn: remoting.showIt2MeUiAndSave }, + fn: remoting.showIT2MeUiAndSave }, { event: 'click', id: 'get-started-me2me', fn: remoting.showMe2MeUiAndSave }, { event: 'click', id: 'daemon-pin-cancel', diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html index cb64271..31a426f 100644 --- a/remoting/webapp/main.html +++ b/remoting/webapp/main.html @@ -39,6 +39,7 @@ found in the LICENSE file. <script src="oauth2.js"></script> <script src="plugin_settings.js"></script> <script src="remoting.js"></script> + <script src="session_connector.js"></script> <script src="server_log_entry.js"></script> <script src="stats_accumulator.js"></script> <script src="storage.js"></script> diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js index 8da7e15..f89072a 100644 --- a/remoting/webapp/remoting.js +++ b/remoting/webapp/remoting.js @@ -64,7 +64,7 @@ remoting.init = function() { remoting.identity.getEmail(remoting.onEmail, remoting.showErrorMessage); - remoting.showOrHideIt2MeUi(); + remoting.showOrHideIT2MeUi(); remoting.showOrHideMe2MeUi(); // The plugin's onFocus handler sends a paste command to |window|, because @@ -88,7 +88,7 @@ remoting.init = function() { if ('mode' in urlParams) { if (urlParams['mode'] == 'me2me') { var hostId = urlParams['hostId']; - remoting.connectMe2Me(hostId, true); + remoting.connectMe2Me(hostId); return; } } @@ -166,14 +166,15 @@ remoting.logExtensionInfoAsync_ = function() { }; /** - * If an It2Me client or host is active then prompt the user before closing. + * If an IT2Me client or host is active then prompt the user before closing. * If a Me2Me client is active then don't bother, since closing the window is * the more intuitive way to end a Me2Me session, and re-connecting is easy. * * @return {?string} The prompt string if a connection is active. */ remoting.promptClose = function() { - if (remoting.currentConnectionType == remoting.ClientSession.Mode.ME2ME) { + if (!remoting.clientSession || + remoting.clientSession.mode == remoting.ClientSession.Mode.ME2ME) { return null; } switch (remoting.currentMode) { diff --git a/remoting/webapp/session_connector.js b/remoting/webapp/session_connector.js new file mode 100644 index 0000000..0cb9105 --- /dev/null +++ b/remoting/webapp/session_connector.js @@ -0,0 +1,404 @@ +// 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 + * Connect set-up state machine for Me2Me and IT2Me + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @param {Element} pluginParent The node under which to add the client plugin. + * @param {function(remoting.ClientSession):void} onOk Callback on success. + * @param {function(remoting.Error):void} onError Callback on error. + * @constructor + */ +remoting.SessionConnector = function(pluginParent, onOk, onError) { + /** + * @type {Element} + * @private + */ + this.pluginParent_ = pluginParent; + + /** + * @type {function(remoting.ClientSession):void} + * @private + */ + this.onOk_ = onOk; + + /** + * @type {function(remoting.Error):void} + * @private + */ + this.onError_ = onError; + + /** + * @type {remoting.ClientSession.Mode} + * @private + */ + this.connectionMode_ = remoting.ClientSession.Mode.ME2ME; + + /** + * String used to identify the host to which to connect. For IT2Me, this is + * the first 7 digits of the access code; for Me2Me it is the host identifier. + * + * @type {string} + * @private + */ + this.hostId_ = ''; + + /** + * String used to authenticate to the host on connection. For IT2Me, this is + * the access code; for Me2Me it is the PIN. + * + * @type {string} + * @private + */ + this.passPhrase_ = ''; + + /** + * @type {string} + * @private + */ + this.hostJid_ = ''; + + /** + * @type {string} + * @private + */ + this.hostPublicKey_ = ''; + + /** + * @type {string} + * @private + */ + this.clientJid_ = ''; + + /** + * @type {boolean} + * @private + */ + this.refreshHostJidIfOffline_ = true; + + /** + * @type {remoting.ClientSession} + * @private + */ + this.clientSession_ = null; + + /** + * @type {XMLHttpRequest} + * @private + */ + this.pendingXhr_ = null; + + /** + * A timer that polls for an updated access token. + * @type {number} + * @private + */ + this.wcsAccessTokenRefreshTimer_ = 0; + + // Pre-load WCS to improve connection time. + remoting.identity.callWithToken(this.loadWcs_.bind(this), this.onError_); +}; + +/** + * Initiate a Me2Me connection. + * + * @param {remoting.Host} host The Me2Me host to which to connect. + * @param {string} pin The PIN as entered by the user. + * @return {void} Nothing. + */ +remoting.SessionConnector.prototype.connectMe2Me = function(host, pin) { + this.hostId_ = host.hostId; + this.hostJid_ = host.jabberId; + this.passPhrase_ = pin; + this.createSessionIfReady_(); +}; + +/** + * Initiate an IT2Me connection. + * + * @param {string} accessCode The access code as entered by the user. + * @return {void} Nothing. + */ +remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) { + var kSupportIdLen = 7; + var kHostSecretLen = 5; + var kAccessCodeLen = kSupportIdLen + kHostSecretLen; + + var normalizedAccessCode = this.normalizeAccessCode_(accessCode); + if (normalizedAccessCode.length != kAccessCodeLen) { + this.onError_(remoting.Error.INVALID_ACCESS_CODE); + return; + } + + this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen); + this.passPhrase_ = normalizedAccessCode; + this.connectionMode_ = remoting.ClientSession.Mode.IT2ME; + remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this), + this.onError_); +}; + +/** + * Cancel a connection-in-progress. + */ +remoting.SessionConnector.prototype.cancel = function() { + if (this.clientSession_) { + this.clientSession_.removePlugin(); + this.clientSession_ = null; + } + if (this.pendingXhr_) { + this.pendingXhr_.abort(); + this.pendingXhr_ = null; + } + this.hostId_ = ''; + this.hostJid_ = ''; + this.passPhrase_ = ''; + this.hostPublicKey_ = ''; + this.refreshHostJidIfOffline_ = true; +}; + +/** + * Continue an IT2Me connection once an access token has been obtained. + * + * @param {string} token An OAuth2 access token. + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) { + // Resolve the host id to get the host JID. + this.pendingXhr_ = remoting.xhr.get( + remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + + encodeURIComponent(this.hostId_), + this.onIT2MeHostInfo_.bind(this), + '', + { 'Authorization': 'OAuth ' + token }); +}; + +/** + * Continue an IT2Me connection once the host JID has been looked up. + * + * @param {XMLHttpRequest} xhr The server response to the support-hosts query. + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) { + this.pendingXhr_ = null; + 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) { + this.hostJid_ = host.data.jabberId; + this.hostPublicKey_ = host.data.publicKey; + this.createSessionIfReady_(); + return; + } else { + console.error('Invalid "support-hosts" response from server.'); + } + } else { + this.onError_(this.translateSupportHostsError(xhr.status)); + } +}; + +/** + * Load the WCS driver script. + * + * @param {string} token An OAuth2 access token. + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.loadWcs_ = function(token) { + remoting.wcsSandbox.setOnReady(this.onWcsLoaded_.bind(this)); + remoting.wcsSandbox.setOnError(this.onError_); + remoting.wcsSandbox.setAccessToken(token); + this.startAccessTokenRefreshTimer_(); +}; + +/** + * Continue an IT2Me or Me2Me connection once WCS has been loaded. + * + * @param {string} clientJid The full JID of the WCS client. + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.onWcsLoaded_ = function(clientJid) { + this.clientJid_ = clientJid; + this.createSessionIfReady_(); +}; + +/** + * If both the client and host JIDs are available, create a session and connect. + * + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.createSessionIfReady_ = function() { + if (!this.clientJid_ || !this.hostJid_) { + return; + } + + var securityTypes = 'spake2_hmac,spake2_plain'; + this.clientSession_ = new remoting.ClientSession( + this.hostJid_, this.clientJid_, this.hostPublicKey_, + this.passPhrase_, securityTypes, this.hostId_, + this.connectionMode_); + this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_); + this.clientSession_.setOnStateChange(this.onStateChange_.bind(this)); + this.clientSession_.createPluginAndConnect(this.pluginParent_); +}; + +/** + * Handle a change in the state of the client session prior to successful + * connection (after connection, this class no longer handles state change + * events). Errors that occur while connecting either trigger a reconnect + * or notify the onError handler. + * + * @param {number} oldState The previous state of the plugin. + * @param {number} newState The current state of the plugin. + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.onStateChange_ = + function(oldState, newState) { + switch (newState) { + case remoting.ClientSession.State.CONNECTED: + // When the connection succeeds, deregister for state-change callbacks + // and pass the session to the onOk callback. It is expected that it + // will register a new state-change callback to handle disconnect + // or error conditions. + this.clientSession_.setOnStateChange(null); + this.onOk_(this.clientSession_); + break; + + case remoting.ClientSession.State.CREATED: + console.log('Created plugin'); + break; + + case remoting.ClientSession.State.BAD_PLUGIN_VERSION: + this.onError_(remoting.Error.BAD_PLUGIN_VERSION); + break; + + case remoting.ClientSession.State.CONNECTING: + console.log('Connecting as ' + remoting.identity.getCachedEmail()); + break; + + case remoting.ClientSession.State.INITIALIZING: + console.log('Initializing connection'); + break; + + case remoting.ClientSession.State.CLOSED: + // This class deregisters for state-change callbacks when the CONNECTED + // state is reached, so it only sees the CLOSED state in exceptional + // circumstances. For example, a CONNECTING -> CLOSED transition happens + // if the host closes the connection without an error message instead of + // accepting it. Since there's no way of knowing exactly what went wrong, + // we rely on server-side logs in this case and report a generic error + // message. + this.onError_(remoting.Error.UNEXPECTED); + break; + + case remoting.ClientSession.State.FAILED: + var error = this.clientSession_.getError(); + console.error('Client plugin reported connection failed: ' + error); + if (error == null) { + error = remoting.Error.UNEXPECTED; + } + if (error == remoting.Error.HOST_IS_OFFLINE && + this.refreshHostJidIfOffline_) { + this.refreshHostJidIfOffline_ = false; + this.clientSession_.removePlugin(); + this.clientSession_ = null; + remoting.hostList.refresh(this.onHostListRefresh_.bind(this)); + } else { + this.onError_(error); + } + break; + + default: + 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 ensure compatibility. + this.onError_(remoting.Error.MISSING_PLUGIN); + } +}; + +/** + * @param {boolean} success True if the host list was successfully refreshed; + * false if an error occurred. + * @private + */ +remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) { + if (success) { + var host = remoting.hostList.getHostForId(this.hostId_); + if (host) { + this.connectMe2Me(host, this.passPhrase_); + return; + } + } + this.onError_(remoting.Error.HOST_IS_OFFLINE); +}; + +/** + * Start a timer to periodically refresh the access token used by WCS. Access + * tokens have a limited lifespan, and since the WCS driver runs in a sandbox, + * it can't obtain a new one directly. + * + * @return {void} Nothing. + * @private + */ +remoting.SessionConnector.prototype.startAccessTokenRefreshTimer_ = function() { + if (this.wcsAccessTokenRefreshTimer_ != 0) { + return; + } + + /** @type {remoting.SessionConnector} */ + var that = this; + var refreshAccessToken = function() { + remoting.identity.callWithToken( + remoting.wcsSandbox.setAccessToken.bind(remoting.wcsSandbox), + that.onError_); + }; + /** + * A timer that polls for an updated access token. + * @type {number} + * @private + */ + this.wcsAccessTokenRefreshTimer_ = setInterval(refreshAccessToken, + 60 * 1000); +} + +/** + * @param {number} error An HTTP error code returned by the support-hosts + * endpoint. + * @return {remoting.Error} The equivalent remoting.Error code. + * @private + */ +remoting.SessionConnector.prototype.translateSupportHostsError = + function(error) { + switch (error) { + case 0: return remoting.Error.NO_RESPONSE; + case 404: return remoting.Error.INVALID_ACCESS_CODE; + case 502: // No break + case 503: return remoting.Error.SERVICE_UNAVAILABLE; + default: return remoting.Error.UNEXPECTED; + } +}; + +/** + * 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). + */ +remoting.SessionConnector.prototype.normalizeAccessCode_ = + function(accessCode) { + // Trim whitespace. + return accessCode.replace(/\s/g, ''); +}; diff --git a/remoting/webapp/ui_mode.js b/remoting/webapp/ui_mode.js index fecc2b2..1b59c96 100644 --- a/remoting/webapp/ui_mode.js +++ b/remoting/webapp/ui_mode.js @@ -149,7 +149,7 @@ remoting.showOrHideCallback = function(mode, items) { document.getElementById(mode + '-content').hidden = !visited; }; -remoting.showOrHideIt2MeUi = function() { +remoting.showOrHideIT2MeUi = function() { chrome.storage.local.get('it2me-visited', remoting.showOrHideCallback.bind(null, 'it2me')); }; @@ -159,7 +159,7 @@ remoting.showOrHideMe2MeUi = function() { remoting.showOrHideCallback.bind(null, 'me2me')); }; -remoting.showIt2MeUiAndSave = function() { +remoting.showIT2MeUiAndSave = function() { var items = {}; items['it2me-visited'] = true; chrome.storage.local.set(items); |