summaryrefslogtreecommitdiffstats
path: root/remoting
diff options
context:
space:
mode:
authorjamiewalch@google.com <jamiewalch@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-25 23:46:46 +0000
committerjamiewalch@google.com <jamiewalch@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-25 23:46:46 +0000
commit23e4353dfe2483dcfc7511bf2b23643fbdc5a949 (patch)
tree10a60918d09269d958da529185e920ee6cb6bce6 /remoting
parente335fd5150fc614e1b9df1d9e7e93ec7793d26f7 (diff)
downloadchromium_src-23e4353dfe2483dcfc7511bf2b23643fbdc5a949.zip
chromium_src-23e4353dfe2483dcfc7511bf2b23643fbdc5a949.tar.gz
chromium_src-23e4353dfe2483dcfc7511bf2b23643fbdc5a949.tar.bz2
Pass OAuth token refresh errors back to the caller.
This allows the app to advise the user to sign back in to Chromoting only when we believe it will actually help. As a bonus, the unhelpfully-named ERROR_GENERIC is now gone--apart from OAuth refresh failures, it was being used for edge-cases where we don't expect anything to go wrong (these now use the more suitably-named ERROR_UNEXPECTED). As part of fixing this, I have cleaned up some OAuth call points that were written before callWithToken existed, and which weren't using it. BUG=122899,130794 TEST=Corrupt the oauth2-refresh-token value in HTML Local Storage Review URL: https://chromiumcodereview.appspot.com/10579012 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@144059 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r--remoting/webapp/_locales/en/messages.json10
-rw-r--r--remoting/webapp/client_screen.js20
-rw-r--r--remoting/webapp/event_handlers.js15
-rw-r--r--remoting/webapp/host_controller.js14
-rw-r--r--remoting/webapp/host_list.js94
-rw-r--r--remoting/webapp/host_screen.js28
-rw-r--r--remoting/webapp/host_setup_dialog.js18
-rw-r--r--remoting/webapp/main.html30
-rw-r--r--remoting/webapp/oauth2.js81
-rw-r--r--remoting/webapp/remoting.js22
-rw-r--r--remoting/webapp/ui_mode.js1
-rw-r--r--remoting/webapp/wcs.js13
-rw-r--r--remoting/webapp/wcs_loader.js41
13 files changed, 256 insertions, 131 deletions
diff --git a/remoting/webapp/_locales/en/messages.json b/remoting/webapp/_locales/en/messages.json
index 4ca21a7..fe9a84f 100644
--- a/remoting/webapp/_locales/en/messages.json
+++ b/remoting/webapp/_locales/en/messages.json
@@ -127,17 +127,13 @@
"description": "Column header in the connection history table showing the length of time for which a connection was active, if available."
},
"ERROR_AUTHENTICATION_FAILED": {
- "message": "Authentication failed. Please sign out of Chromoting and try again.",
+ "message": "Authentication failed. Please sign in to Chromoting again.",
"description": "Error displayed if authentication fails. This can be caused by stale credentials, in which logging out of the web-app and retrying can fix the problem."
},
"ERROR_BAD_PLUGIN_VERSION": {
"message": "Some components required for Chromoting are missing. Please make sure you have installed the latest version and try again.",
"description": "Error displayed if the host or client plugin are missing or if they could not be loaded."
},
- "ERROR_GENERIC": {
- "message": "An unknown error occurred. Please sign out of Chromoting and try again.",
- "description": "Generic error message, displayed if something went wrong, but we aren't able to determine what, or the cause is too technical to be of use to a typical user."
- },
"ERROR_HOST_OVERLOAD": {
"message": "Connections to the remote computer are temporarily blocked because somebody was trying to connect to it with invalid PIN. Please try again later.",
"description": "Error that is shown on the client side when the host is blocking all connections due to failed authentication attempts."
@@ -428,6 +424,10 @@
"message": "Shrink to fit",
"description": "Menu option for enabling shrink-to-fit. When clicked, the remote desktop will be scaled down so that it fits in the browser window."
},
+ "SIGN_IN_BUTTON": {
+ "message": "Sign in",
+ "description": "Sign in button, visible if the user's authentication token is corrupt, in which case the user has to sign in (authenticate) to the app again."
+ },
"SIGN_OUT_BUTTON": {
"message": "Sign out",
"description": "Sign out button, visible if the user has authenticated. Clicking this clears authentication credentials and returns the web-app to the initial 'unauthenticated' state."
diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js
index f57c937..808c825 100644
--- a/remoting/webapp/client_screen.js
+++ b/remoting/webapp/client_screen.js
@@ -70,7 +70,8 @@ remoting.currentConnectionType = null;
*/
remoting.connectIt2Me = function() {
remoting.currentConnectionType = remoting.ConnectionType.It2Me;
- remoting.WcsLoader.load(connectIt2MeWithAccessToken_);
+ remoting.WcsLoader.load(connectIt2MeWithAccessToken_,
+ remoting.defaultOAuthErrorHandler);
};
/**
@@ -176,7 +177,7 @@ function connectIt2MeWithAccessToken_(token) {
} else {
var supportId = remoting.accessCode.substring(0, kSupportIdLen);
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
- resolveSupportId(supportId);
+ resolveSupportId(supportId, token);
}
} else {
showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
@@ -264,7 +265,7 @@ function onClientStateChange_(oldState, newState) {
remoting.ClientSession.ConnectionError.HOST_OVERLOAD) {
showConnectError_(remoting.Error.HOST_OVERLOAD);
} else {
- showConnectError_(remoting.Error.GENERIC);
+ showConnectError_(remoting.Error.UNEXPECTED);
}
if (clearPin) {
@@ -334,7 +335,8 @@ function startSession_() {
showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
}
};
- remoting.oauth2.callWithToken(createPluginAndConnect);
+ remoting.oauth2.callWithToken(createPluginAndConnect,
+ remoting.defaultOAuthErrorHandler);
}
/**
@@ -397,7 +399,7 @@ function parseServerResponse_(xhr) {
console.error('Invalid "support-hosts" response from server.');
}
}
- var errorMsg = remoting.Error.GENERIC;
+ var errorMsg = remoting.Error.UNEXPECTED;
if (xhr.status == 404) {
errorMsg = remoting.Error.INVALID_ACCESS_CODE;
} else if (xhr.status == 0) {
@@ -426,10 +428,11 @@ function normalizeAccessCode_(accessCode) {
* 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) {
+function resolveSupportId(supportId, token) {
var headers = {
- 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken()
+ 'Authorization': 'OAuth ' + token
};
remoting.supportHostsXhr_ = remoting.xhr.get(
@@ -506,7 +509,8 @@ remoting.connectMe2MeWithPin = function() {
document.title = chrome.i18n.getMessage('PRODUCT_NAME') + ': ' +
host.hostName;
- remoting.WcsLoader.load(connectMe2MeWithAccessToken_);
+ remoting.WcsLoader.load(connectMe2MeWithAccessToken_,
+ remoting.defaultOAuthErrorHandler);
};
/**
diff --git a/remoting/webapp/event_handlers.js b/remoting/webapp/event_handlers.js
index 778c160..338bd1e 100644
--- a/remoting/webapp/event_handlers.js
+++ b/remoting/webapp/event_handlers.js
@@ -12,7 +12,15 @@ function onLoad() {
window.location.replace(chrome.extension.getURL('main.html'));
};
var goEnterAccessCode = function() {
- remoting.setMode(remoting.AppMode.CLIENT_UNCONNECTED);
+ // We don't need a token until we authenticate, but asking for one here
+ // handles the token-expired case earlier, avoiding asking the user for
+ // the access code both before and after re-authentication.
+ remoting.oauth2.callWithToken(
+ /** @param {string} token */
+ function(token) {
+ remoting.setMode(remoting.AppMode.CLIENT_UNCONNECTED);
+ },
+ remoting.defaultOAuthErrorHandler);
};
var goFinishedIt2Me = function() {
if (remoting.currentMode == remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME) {
@@ -87,7 +95,10 @@ function onLoad() {
{ event: 'click', id: 'host-config-install-dismiss',
fn: function() { remoting.hostSetupDialog.hide(); } },
{ event: 'click', id: 'host-config-install-retry', fn: function() {
- remoting.hostSetupDialog.onInstallDialogRetry(); } }
+ remoting.hostSetupDialog.onInstallDialogRetry(); } },
+ { event: 'click', id: 'token-refresh-error-ok',
+ fn: function() { remoting.setMode(remoting.AppMode.HOME); } },
+ { event: 'click', id: 'token-refresh-error-sign-in', fn: doAuthRedirect }
];
for (var i = 0; i < actions.length; ++i) {
diff --git a/remoting/webapp/host_controller.js b/remoting/webapp/host_controller.js
index 63ef1f1..845e65d 100644
--- a/remoting/webapp/host_controller.js
+++ b/remoting/webapp/host_controller.js
@@ -225,14 +225,14 @@ remoting.HostController.prototype.start = function(hostPin, consent, callback) {
* @param {string} publicKey */
function onKeyGenerated(privateKey, publicKey) {
remoting.oauth2.callWithToken(
- /** @param {string?} oauthToken */
+ /** @param {string} oauthToken */
function(oauthToken) {
- if (oauthToken) {
- doRegisterHost(privateKey, publicKey, oauthToken);
- } else {
- // TODO(jamiewalch): Have a more specific error code here?
- callback(remoting.HostController.AsyncResult.FAILED);
- }
+ doRegisterHost(privateKey, publicKey, oauthToken);
+ },
+ /** @param {remoting.Error} error */
+ function(error) {
+ // TODO(jamiewalch): Have a more specific error code here?
+ callback(remoting.HostController.AsyncResult.FAILED);
});
};
diff --git a/remoting/webapp/host_list.js b/remoting/webapp/host_list.js
index dd5a546..540067e 100644
--- a/remoting/webapp/host_list.js
+++ b/remoting/webapp/host_list.js
@@ -19,9 +19,11 @@ var remoting = remoting || {};
*
* @constructor
* @param {Element} table The HTML <table> to contain host-list.
- * @param {Element} errorDiv The HTML <div> to display error messages.
+ * @param {Element} errorMsg The HTML <div> to display error messages.
+ * @param {Element} errorButton The HTML <button> to display the error
+ * resolution action.
*/
-remoting.HostList = function(table, errorDiv) {
+remoting.HostList = function(table, errorMsg, errorButton) {
/**
* @type {Element}
* @private
@@ -31,7 +33,12 @@ remoting.HostList = function(table, errorDiv) {
* @type {Element}
* @private
*/
- this.errorDiv_ = errorDiv;
+ this.errorMsg_ = errorMsg;
+ /**
+ * @type {Element}
+ * @private
+ */
+ this.errorButton_ = errorButton;
/**
* @type {Array.<remoting.HostTableEntry>}
* @private
@@ -48,6 +55,10 @@ remoting.HostList = function(table, errorDiv) {
*/
this.lastError_ = '';
+ this.errorButton_.addEventListener('click',
+ this.onErrorClick_.bind(this),
+ false);
+
// Load the cache of the last host-list, if present.
var cachedStr = /** @type {string} */
(window.localStorage.getItem(remoting.HostList.HOSTS_KEY));
@@ -88,21 +99,21 @@ remoting.HostList.prototype.refresh = function(onDone) {
var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone);
/** @type {remoting.HostList} */
var that = this;
- /** @param {string?} token The OAuth2 token. */
+ /** @param {string} token The OAuth2 token. */
var getHosts = function(token) {
- if (token) {
- var headers = { 'Authorization': 'OAuth ' + token };
- remoting.xhr.get(
- 'https://www.googleapis.com/chromoting/v1/@me/hosts',
- parseHostListResponse, '', headers);
- } else {
- that.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
- onDone(false);
- }
+ var headers = { 'Authorization': 'OAuth ' + token };
+ remoting.xhr.get(
+ 'https://www.googleapis.com/chromoting/v1/@me/hosts',
+ parseHostListResponse, '', headers);
+ };
+ /** @param {remoting.Error} error */
+ var onError = function(error) {
+ that.lastError_ = error;
+ onDone(false);
};
this.hosts_ = [];
this.lastError_ = '';
- remoting.oauth2.callWithToken(getHosts);
+ remoting.oauth2.callWithToken(getHosts, onError);
};
/**
@@ -141,11 +152,8 @@ remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
} else {
// Some other error.
console.error('Bad status on host list query: ', xhr);
- if (xhr.status == 403) {
- // The user's account is not enabled for Me2Me, so fail silently.
- } else if (xhr.status >= 400 && xhr.status < 500) {
- // For other errors, tell the user to re-authorize us.
- this.lastError_ = remoting.Error.GENERIC;
+ if (xhr.status == 401) {
+ this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
} else if (xhr.status == 503) {
this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
} else {
@@ -170,7 +178,7 @@ remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
*/
remoting.HostList.prototype.display = function(thisHostId) {
this.table_.innerText = '';
- this.errorDiv_.innerText = '';
+ this.errorMsg_.innerText = '';
this.hostTableEntries_ = [];
this.table_.hidden = (this.hosts_.length == 0);
@@ -193,9 +201,16 @@ remoting.HostList.prototype.display = function(thisHostId) {
}
if (this.lastError_ != '') {
- l10n.localizeElementFromTag(this.errorDiv_, this.lastError_);
+ l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
+ if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
+ l10n.localizeElementFromTag(this.errorButton_,
+ /*i18n-content*/'SIGN_IN_BUTTON');
+ } else {
+ l10n.localizeElementFromTag(this.errorButton_,
+ /*i18n-content*/'RETRY');
+ }
}
- this.errorDiv_.hidden = (this.lastError_ == '');
+ this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
};
/**
@@ -219,19 +234,14 @@ remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
* @return {void} Nothing.
*/
remoting.HostList.unregisterHostById = function(hostId) {
- /** @param {string?} token The OAuth2 token. */
+ /** @param {string} token The OAuth2 token. */
var deleteHost = function(token) {
- if (token) {
- var headers = { 'Authorization': 'OAuth ' + token };
- remoting.xhr.remove(
- 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostId,
- function() {}, '', headers);
- } else {
- console.error('Could not unregister host. Authentication failure.');
- // TODO(jamiewalch): Add a callback to signify success/failure?
- }
+ var headers = { 'Authorization': 'OAuth ' + token };
+ remoting.xhr.remove(
+ 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostId,
+ function() {}, '', headers);
}
- remoting.oauth2.callWithToken(deleteHost);
+ remoting.oauth2.callWithToken(deleteHost, remoting.defaultOAuthErrorHandler);
};
/**
@@ -271,7 +281,7 @@ remoting.HostList.prototype.renameHost = function(hostTableEntry) {
console.error('Could not rename host. Authentication failure.');
}
}
- remoting.oauth2.callWithToken(renameHost);
+ remoting.oauth2.callWithToken(renameHost, remoting.defaultOAuthErrorHandler);
};
/**
@@ -285,6 +295,22 @@ remoting.HostList.prototype.addHost = function(localHost) {
this.hosts_.push(localHost);
window.localStorage.setItem(remoting.HostList.HOSTS_KEY,
JSON.stringify(this.hosts_));
+};
+
+/**
+ * Called when the user clicks the button next to the error message. The action
+ * depends on the error.
+ *
+ * @private
+ */
+remoting.HostList.prototype.onErrorClick_ = function() {
+ if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
+ remoting.oauth2.doAuthRedirect();
+ } else {
+ this.lastError_ = '';
+ this.display(null);
+ this.refresh(remoting.extractThisHostAndDisplay);
+ }
}
/**
diff --git a/remoting/webapp/host_screen.js b/remoting/webapp/host_screen.js
index f904c96..7edb2f0 100644
--- a/remoting/webapp/host_screen.js
+++ b/remoting/webapp/host_screen.js
@@ -26,20 +26,16 @@ var lastShareWasCancelled_ = false;
*/
remoting.tryShare = function() {
console.log('Attempting to share...');
- lastShareWasCancelled_ = false;
- if (remoting.oauth2.needsNewAccessToken()) {
- console.log('Refreshing token...');
- remoting.oauth2.refreshAccessToken(function() {
- if (remoting.oauth2.needsNewAccessToken()) {
- // If we still need it, we're going to infinite loop.
- showShareError_(remoting.Error.AUTHENTICATION_FAILED);
- throw 'Unable to get access token';
- }
- remoting.tryShare();
- });
- return;
- }
+ remoting.oauth2.callWithToken(remoting.tryShareWithToken_,
+ remoting.defaultOAuthErrorHandler);
+};
+/**
+ * @param {string} token The OAuth access token.
+ * @private
+ */
+remoting.tryShareWithToken_ = function(token) {
+ lastShareWasCancelled_ = false;
onNatTraversalPolicyChanged_(true); // Hide warning by default.
remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE);
document.getElementById('cancel-share-button').disabled = false;
@@ -50,7 +46,7 @@ remoting.tryShare = function() {
remoting.hostSession.createPluginAndConnect(
document.getElementById('host-plugin-container'),
/** @type {string} */(remoting.oauth2.getCachedEmail()),
- remoting.oauth2.getAccessToken(),
+ token,
onNatTraversalPolicyChanged_,
onHostStateChanged_,
logDebugInfo_);
@@ -124,7 +120,7 @@ function onHostStateChanged_(state) {
} else if (state == remoting.HostSession.State.ERROR) {
console.error('Host plugin state: ERROR');
- showShareError_(remoting.Error.GENERIC);
+ showShareError_(remoting.Error.UNEXPECTED);
} else {
console.error('Unknown state -> ' + state);
}
@@ -172,7 +168,7 @@ remoting.cancelShare = function() {
// the host plugin, like we do for the client, which should handle crash
// reporting and it should use a more detailed error message than the
// default 'generic' one. See crbug.com/94624
- showShareError_(remoting.Error.GENERIC);
+ showShareError_(remoting.Error.UNEXPECTED);
}
disableTimeoutCountdown_();
};
diff --git a/remoting/webapp/host_setup_dialog.js b/remoting/webapp/host_setup_dialog.js
index fe60ecb..c0e5ae2 100644
--- a/remoting/webapp/host_setup_dialog.js
+++ b/remoting/webapp/host_setup_dialog.js
@@ -150,6 +150,18 @@ remoting.HostSetupDialog = function(hostController) {
* @return {void} Nothing.
*/
remoting.HostSetupDialog.prototype.showForStart = function() {
+ // Although we don't need an access token in order to start the host,
+ // using callWithToken here ensures consistent error handling in the
+ // case where the refresh token is invalid.
+ remoting.oauth2.callWithToken(this.showForStartWithToken_.bind(this),
+ remoting.defaultOAuthErrorHandler);
+};
+
+/**
+ * @param {string} token The OAuth2 token.
+ * @private
+ */
+remoting.HostSetupDialog.prototype.showForStartWithToken_ = function(token) {
/** @type {remoting.HostSetupDialog} */
var that = this;
@@ -423,7 +435,7 @@ remoting.HostSetupDialog.validPin_ = function(pin) {
}
}
return true;
-}
+};
/**
* @return {void} Nothing.
@@ -436,14 +448,14 @@ remoting.HostSetupDialog.prototype.onInstallDialogOk = function() {
} else {
remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL_PENDING);
}
-}
+};
/**
* @return {void} Nothing.
*/
remoting.HostSetupDialog.prototype.onInstallDialogRetry = function() {
remoting.setMode(remoting.AppMode.HOST_SETUP_INSTALL);
-}
+};
/** @type {remoting.HostSetupDialog} */
remoting.hostSetupDialog = null;
diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html
index 17a338c..a47b80e 100644
--- a/remoting/webapp/main.html
+++ b/remoting/webapp/main.html
@@ -140,7 +140,12 @@ found in the LICENSE file.
</div>
<div id="me2me-content">
<div id="host-list" hidden></div>
- <div id="host-list-error" class="error-state" hidden></div>
+ <div id="host-list-error" class="box" hidden>
+ <div id="host-list-error-message" class="error-state"></div>
+ <div class="box-spacer"></div>
+ <button type="button"
+ id="host-list-refresh-failed-button"></button>
+ </div>
<div id="daemon-control" data-daemon-state="enabled disabled" hidden>
<div class="section-row no-non-local-hosts"
data-daemon-state="disabled">
@@ -182,15 +187,34 @@ found in the LICENSE file.
</div> <!-- home -->
<div id="dialog-screen"
- data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup"
+ data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup home.token-refresh-failed"
hidden></div>
<div id="dialog-container"
- data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup"
+ data-ui-mode="home.host home.client home.auth home.history home.confirm-host-delete home.host-setup home.token-refresh-failed"
hidden>
<div class="box-spacer"></div>
+ <!-- TODO(jamiewalch): Refactor the various error-state divs -->
+ <div class="kd-modaldialog" data-ui-mode="home.token-refresh-failed">
+ <div class="message">
+ <span id="token-refresh-error-message" class="error-state"></span>
+ </div>
+ <div id="token-refresh-auth-failed" class="box">
+ <div class="box-spacer"></div>
+ <button id="token-refresh-error-sign-in"
+ type="button"
+ i18n-content="SIGN_IN_BUTTON"></button>
+ </div>
+ <div id="token-refresh-other-error" class="box">
+ <div class="box-spacer"></div>
+ <button id="token-refresh-error-ok"
+ type="button"
+ i18n-content="OK"></button>
+ </div>
+ </div> <!-- home.token-refresh-failed -->
+
<div id="host-setup-dialog"
class="kd-modaldialog"
data-ui-mode="home.host-setup">
diff --git a/remoting/webapp/oauth2.js b/remoting/webapp/oauth2.js
index 6024afc..e030822 100644
--- a/remoting/webapp/oauth2.js
+++ b/remoting/webapp/oauth2.js
@@ -184,8 +184,9 @@ remoting.OAuth2.prototype.needsNewAccessToken = function() {
* Will throw if !isAuthenticated() or needsNewAccessToken().
*
* @return {string} The access token.
+ * @private
*/
-remoting.OAuth2.prototype.getAccessToken = function() {
+remoting.OAuth2.prototype.getAccessToken_ = function() {
if (this.needsNewAccessToken()) {
throw 'Access Token expired.';
}
@@ -245,8 +246,9 @@ remoting.OAuth2.prototype.processTokenResponse_ = function(onDone, xhr) {
* @param {function(XMLHttpRequest): void} onDone Callback to invoke on
* completion.
* @return {void} Nothing.
+ * @private
*/
-remoting.OAuth2.prototype.refreshAccessToken = function(onDone) {
+remoting.OAuth2.prototype.refreshAccessToken_ = function(onDone) {
if (!this.isAuthenticated()) {
throw 'Not Authenticated.';
}
@@ -335,24 +337,51 @@ remoting.OAuth2.prototype.revokeToken_ = function(token) {
*
* The access token will remain valid for at least 2 minutes.
*
- * @param {function(string?):void} myfunc Function to invoke with access token.
+ * @param {function(string):void} onOk Function to invoke with access token if
+ * an access token was successfully retrieved.
+ * @param {function(remoting.Error):void} onError Function to invoke with an
+ * error code on failure.
* @return {void} Nothing.
*/
-remoting.OAuth2.prototype.callWithToken = function(myfunc) {
- /** @type {remoting.OAuth2} */
- var that = this;
+remoting.OAuth2.prototype.callWithToken = function(onOk, onError) {
if (this.needsNewAccessToken()) {
- var onRefresh = function() {
- if (that.needsNewAccessToken()) {
- myfunc(null);
- } else {
- myfunc(that.getAccessToken());
- }
- };
- this.refreshAccessToken(onRefresh);
+ this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError));
} else {
- myfunc(this.getAccessToken());
+ onOk(this.getAccessToken_());
+ }
+};
+
+/**
+ * Process token refresh results and notify caller.
+ *
+ * @param {function(string):void} onOk Function to invoke with access token if
+ * an access token was successfully retrieved.
+ * @param {function(remoting.Error):void} onError Function to invoke with an
+ * error code on failure.
+ * @param {XMLHttpRequest} xhr The result of the refresh operation.
+ * @private
+ */
+remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr) {
+ var error = remoting.Error.UNEXPECTED;
+ if (xhr.status == 200) {
+ onOk(this.getAccessToken_());
+ return;
+ } else if (xhr.status == 400) {
+ var result =
+ /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText));
+ if (result && result.error == 'invalid_grant') {
+ error = remoting.Error.AUTHENTICATION_FAILED;
+ }
+ } else if (xhr.status == 401) {
+ // According to the OAuth2 draft RFC, the server shouldn't return 401,
+ // but AUTHENTICATION_FAILED is the obvious interpretation if it does.
+ console.warn('Unexpected 401 in response to refresh.');
+ error = remoting.Error.AUTHENTICATION_FAILED;
+ } else if (xhr.status == 503) {
+ error = remoting.Error.SERVICE_UNAVAILABLE;
}
+ // TODO(jamiewalch): Add timeout support.
+ onError(error);
};
/**
@@ -378,20 +407,20 @@ remoting.OAuth2.prototype.getEmail = function(setEmail) {
setEmail(that.email);
};
- /** @param {string?} token The access token. */
+ /** @param {string} token The access token. */
var getEmailFromToken = function(token) {
- if (token) {
- var headers = { 'Authorization': 'OAuth ' + token };
- // TODO(ajwong): Update to new v2 API.
- remoting.xhr.get('https://www.googleapis.com/userinfo/email',
- onResponse, '', headers);
- } else {
- console.error('Unable to get email address: no access token');
- setEmail(null);
- }
+ var headers = { 'Authorization': 'OAuth ' + token };
+ // TODO(ajwong): Update to new v2 API.
+ remoting.xhr.get('https://www.googleapis.com/userinfo/email',
+ onResponse, '', headers);
+ };
+ /** @param {remoting.Error} error */
+ var onError = function(error) {
+ console.error('Unable to get email address: ' + error);
+ setEmail(null);
};
- this.callWithToken(getEmailFromToken);
+ this.callWithToken(getEmailFromToken, onError);
};
/**
diff --git a/remoting/webapp/remoting.js b/remoting/webapp/remoting.js
index 1737436..5fa18f4 100644
--- a/remoting/webapp/remoting.js
+++ b/remoting/webapp/remoting.js
@@ -22,7 +22,6 @@ remoting.Error = {
BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION',
NETWORK_FAILURE: /*i18n-content*/'ERROR_NETWORK_FAILURE',
HOST_OVERLOAD: /*i18n-content*/'ERROR_HOST_OVERLOAD',
- GENERIC: /*i18n-content*/'ERROR_GENERIC',
UNEXPECTED: /*i18n-content*/'ERROR_UNEXPECTED',
SERVICE_UNAVAILABLE: /*i18n-content*/'ERROR_SERVICE_UNAVAILABLE'
};
@@ -40,7 +39,8 @@ remoting.init = function() {
remoting.formatIq = new remoting.FormatIq();
remoting.hostList = new remoting.HostList(
document.getElementById('host-list'),
- document.getElementById('host-list-error'));
+ document.getElementById('host-list-error-message'),
+ document.getElementById('host-list-refresh-failed-button'));
remoting.toolbar = new remoting.Toolbar(
document.getElementById('session-toolbar'));
remoting.clipboard = new remoting.Clipboard();
@@ -322,3 +322,21 @@ remoting.timestamp = function() {
pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
return '[' + timestamp + ']';
};
+
+/**
+ * Default handler for OAuth token refresh failures. This switches the app mode
+ * to display an error message, optionally including a short-cut for signing in
+ * to Chromoting again.
+ *
+ * @param {remoting.Error} error
+ * @return {void} Nothing.
+ */
+remoting.defaultOAuthErrorHandler = function(error) {
+ l10n.localizeElementFromTag(
+ document.getElementById('token-refresh-error-message'),
+ error);
+ var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
+ document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
+ document.getElementById('token-refresh-other-error').hidden = auth_failed;
+ remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
+};
diff --git a/remoting/webapp/ui_mode.js b/remoting/webapp/ui_mode.js
index ea1862d..e800e4a 100644
--- a/remoting/webapp/ui_mode.js
+++ b/remoting/webapp/ui_mode.js
@@ -22,6 +22,7 @@ var remoting = remoting || {};
remoting.AppMode = {
HOME: 'home',
UNAUTHENTICATED: 'home.auth',
+ TOKEN_REFRESH_FAILED: 'home.token-refresh-failed',
HOST: 'home.host',
HOST_WAITING_FOR_CODE: 'home.host.waiting-for-code',
HOST_WAITING_FOR_CONNECTION: 'home.host.waiting-for-connection',
diff --git a/remoting/webapp/wcs.js b/remoting/webapp/wcs.js
index 4906c82..d5c4257 100644
--- a/remoting/webapp/wcs.js
+++ b/remoting/webapp/wcs.js
@@ -53,6 +53,10 @@ remoting.Wcs = function(wcsIqClient, token, onReady) {
this.clientFullJid_ = '';
var updateAccessToken = this.updateAccessToken_.bind(this);
+ /** @param {remoting.Error} error */
+ var onError = function(error) {
+ console.error('updateAccessToken: Authentication failed: ' + error);
+ };
/**
* A timer that polls for an updated access token.
@@ -60,7 +64,9 @@ remoting.Wcs = function(wcsIqClient, token, onReady) {
* @private
*/
this.pollForUpdatedToken_ = setInterval(
- function() { remoting.oauth2.callWithToken(updateAccessToken); },
+ function() {
+ remoting.oauth2.callWithToken(updateAccessToken, onError);
+ },
60 * 1000);
/**
@@ -85,10 +91,7 @@ remoting.Wcs = function(wcsIqClient, token, onReady) {
* @private
*/
remoting.Wcs.prototype.updateAccessToken_ = function(tokenNew) {
- if (!tokenNew) {
- console.error('updateAccessToken_: Authentication failed.');
- // No need to update the token. The hanging GET will fail anyway.
- } else if (tokenNew != this.token_) {
+ if (tokenNew != this.token_) {
this.token_ = tokenNew;
this.wcsIqClient_.updateAccessToken(this.token_);
}
diff --git a/remoting/webapp/wcs_loader.js b/remoting/webapp/wcs_loader.js
index fdd3736..000369d 100644
--- a/remoting/webapp/wcs_loader.js
+++ b/remoting/webapp/wcs_loader.js
@@ -33,24 +33,21 @@ remoting.WcsLoader = function() {
/**
* Load WCS if necessary, then invoke the callback with an access token.
*
- * @param {function(string?): void} onReady The callback function, called with
- * an OAuth2 access token when WCS has been loaded, or with null on error.
+ * @param {function(string): void} onReady The callback function, called with
+ * an OAuth2 access token when WCS has been loaded.
+ * @param {function(remoting.Error):void} onError Function to invoke with an
+ * error code on failure.
* @return {void} Nothing.
*/
-remoting.WcsLoader.load = function(onReady) {
+remoting.WcsLoader.load = function(onReady, onError) {
if (!remoting.wcsLoader) {
remoting.wcsLoader = new remoting.WcsLoader();
}
- /** @param {string?} token The OAuth2 access token. */
+ /** @param {string} token The OAuth2 access token. */
var start = function(token) {
- if (token) {
- remoting.wcsLoader.start_(token, onReady);
- } else {
- console.error('WcsLoader: Authentication failed.');
- onReady(null);
- }
+ remoting.wcsLoader.start_(token, onReady, onError);
};
- remoting.oauth2.callWithToken(start);
+ remoting.oauth2.callWithToken(start, onError);
};
/**
@@ -79,15 +76,18 @@ remoting.WcsLoader.prototype.SCRIPT_NODE_LOADED_FLAG_ = 'wcs-script-loaded';
* Starts loading the WCS IQ client.
*
* When it's loaded, construct remoting.wcs as a wrapper for it.
- * When the WCS connection is ready, or on error, call |onReady|.
+ * When the WCS connection is ready, or on error, call |onReady| or |onError|,
+ * respectively.
*
* @param {string} token An OAuth2 access token.
- * @param {function(string?): void} onReady The callback function, called with
- * an OAuth2 access token when WCS has been loaded, or with null on error.
+ * @param {function(string): void} onReady The callback function, called with
+ * an OAuth2 access token when WCS has been loaded.
+ * @param {function(remoting.Error):void} onError Function to invoke with an
+ * error code on failure.
* @return {void} Nothing.
* @private
*/
-remoting.WcsLoader.prototype.start_ = function(token, onReady) {
+remoting.WcsLoader.prototype.start_ = function(token, onReady, onError) {
var node = document.getElementById(this.SCRIPT_NODE_ID_);
if (!node) {
// The first time, there will be no script node, so create one.
@@ -111,21 +111,22 @@ remoting.WcsLoader.prototype.start_ = function(token, onReady) {
typedNode.setAttribute(that.SCRIPT_NODE_LOADED_FLAG_, true);
that.constructWcs_(token, onReady);
};
- var onError = function() {
+ var removeNodeAndNotify = function() {
var typedNode = /** @type {Element} */ (node);
typedNode.parentNode.removeChild(node);
- onReady(null);
+ // TODO(jamiewalch): See if we can do better by looking at the event.
+ onError(remoting.Error.NETWORK_FAILURE);
};
node.addEventListener('load', onLoad, false);
- node.addEventListener('error', onError, false);
+ node.addEventListener('error', removeNodeAndNotify, false);
};
/**
* Constructs the remoting.wcs object.
*
* @param {string} token An OAuth2 access token.
- * @param {function(string?): void} onReady The callback function, called with
- * an OAuth2 access token when WCS has been loaded, or with null on error.
+ * @param {function(string): void} onReady The callback function, called with
+ * an OAuth2 access token when WCS has been loaded.
* @return {void} Nothing.
* @private
*/