From 99f344d9b8b2231d96f865e2cfb227dafe68b71c Mon Sep 17 00:00:00 2001 From: "sergeyu@chromium.org" Date: Thu, 29 Mar 2012 21:20:47 +0000 Subject: Implement Me2Me setup flow. Review URL: https://chromiumcodereview.appspot.com/9921001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@129692 0039d316-1c4b-4281-b951-d872f2087c98 --- remoting/webapp/_locales/en/messages.json | 28 ++- remoting/webapp/ask_pin_dialog.js | 111 ---------- remoting/webapp/daemon_plugin.js | 43 +++- remoting/webapp/event_handlers.js | 4 +- remoting/webapp/host_plugin_proto.js | 16 +- remoting/webapp/host_setup_dialog.js | 351 ++++++++++++++++++++++++++++++ remoting/webapp/main.css | 2 +- remoting/webapp/main.html | 26 ++- remoting/webapp/remoting.js | 3 +- remoting/webapp/ui_mode.js | 6 +- remoting/webapp/xhr.js | 4 +- 11 files changed, 452 insertions(+), 142 deletions(-) delete mode 100644 remoting/webapp/ask_pin_dialog.js create mode 100644 remoting/webapp/host_setup_dialog.js (limited to 'remoting/webapp') diff --git a/remoting/webapp/_locales/en/messages.json b/remoting/webapp/_locales/en/messages.json index ee8791b..a0a41c3 100644 --- a/remoting/webapp/_locales/en/messages.json +++ b/remoting/webapp/_locales/en/messages.json @@ -17,10 +17,6 @@ "message": "All connections", "description": "In the connection history dialog, clicking this button shows all recent connections unfiltered." }, - "ASK_PIN_DIALOG_DESCRIPTION": { - "message": "To protect access to this computer, please choose a PIN. This PIN will be required when connecting from another location.", - "description": "Explanatory text displayed when the user enables remote access or changes the PIN." - }, "ASK_PIN_DIALOG_LABEL": { "message": "PIN", "description": "Label next to the PIN entry edit box. The user must enter a PIN before enabling remote access to their computer." @@ -222,6 +218,30 @@ "message": "To securely access this computer from anywhere you sign in to Chromoting, you must first enable remote connections.", "description": "Message displayed when the current computer is not accepting remote connections, instructing the user how to enable them." }, + "HOST_SETUP_DIALOG_DESCRIPTION": { + "message": "To protect access to this computer, please choose a PIN. This PIN will be required when connecting from another location.", + "description": "Explanatory text displayed when the user enables remote access or changes the PIN." + }, + "HOST_SETUP_HOST_FAILED": { + "message": "Failed to start remote access service.", + "description": "Message shown when host service fails to start when enabling the host on local computer." + }, + "HOST_SETUP_REGISTRATION_FAILED": { + "message": "Failed to register this computer.", + "description": "Message shown when host registration fails when enabling the host on local computer." + }, + "HOST_SETUP_STARTED": { + "message": "Remote connections for this computer have been enabled.", + "description": "Message shown after access to local computer has been enabled successfully." + }, + "HOST_SETUP_STARTING": { + "message": "Enabling remote connections for this computer.", + "description": "Message shown when local machine is being registered in the directory and when starting the host." + }, + "HOST_SETUP_UPDATING_PIN": { + "message": "PIN for this computer is being updated.", + "description": "Message shown while changing PIN for the local computer." + }, "HOME_SHARE_BUTTON": { "message": "Share", "description": "Clicking this button starts the desktop sharing process." diff --git a/remoting/webapp/ask_pin_dialog.js b/remoting/webapp/ask_pin_dialog.js deleted file mode 100644 index 176fdf4..0000000 --- a/remoting/webapp/ask_pin_dialog.js +++ /dev/null @@ -1,111 +0,0 @@ -// 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. - -'use strict'; - -/** @suppress {duplicate} */ -var remoting = remoting || {}; - -/** - * @param {remoting.DaemonPlugin} daemon The parent daemon plugin instance. - * @constructor - */ -remoting.AskPinDialog = function(daemon) { - this.startDaemon_ = false; - this.daemon_ = daemon; - this.okButton_ = document.getElementById('daemon-pin-ok'); - this.spinner_ = document.getElementById('start-daemon-spinner'); - this.pinEntry_ = document.getElementById('daemon-pin-entry'); - this.pinConfirm_ = document.getElementById('daemon-pin-confirm'); - /** @type {remoting.AskPinDialog} */ - var that = this; - /** @param {Event} event The event. */ - var onSubmit = function(event) { - event.preventDefault(); - that.onSubmit_(); - }; - var form = document.getElementById('ask-pin-form'); - form.addEventListener('submit', onSubmit, false); -}; - -/** - * Show the dialog in order to get a PIN prior to starting the daemon. When the - * user clicks OK, the dialog shows a spinner until the daemon has started. - * - * @return {void} Nothing. - */ -remoting.AskPinDialog.prototype.showForStart = function() { - remoting.setMode(remoting.AppMode.ASK_PIN); - this.startDaemon_ = true; -}; - -/** - * Show the dialog in order to change the PIN associated with a running daemon. - * - * @return {void} Nothing. - */ -remoting.AskPinDialog.prototype.showForPin = function() { - remoting.setMode(remoting.AppMode.ASK_PIN); - this.startDaemon_ = false; -}; - -/** - * @return {void} Nothing. - */ -remoting.AskPinDialog.prototype.hide = function() { - remoting.setMode(remoting.AppMode.HOME); -}; - -/** @private */ -remoting.AskPinDialog.prototype.onSubmit_ = function() { - // TODO(jamiewalch): Add validation and error checks when we improve the UI. - var pin = this.pinEntry_.value; - this.daemon_.setPin(pin); - if (this.startDaemon_) { - this.daemon_.start(); - this.pollDaemonState_(); - } else { - this.hide(); - } -}; - -/** - * @return {void} Nothing. - * @private - */ -remoting.AskPinDialog.prototype.pollDaemonState_ = function() { - var state = this.daemon_.state(); - var retry = false; // Set to true if we haven't finished yet. - switch (state) { - case remoting.DaemonPlugin.State.STOPPED: - case remoting.DaemonPlugin.State.NOT_INSTALLED: - retry = true; - break; - case remoting.DaemonPlugin.State.STARTED: - this.hide(); - this.daemon_.updateDom(); - break; - case remoting.DaemonPlugin.State.START_FAILED: - // TODO(jamiewalch): Show an error message. - break; - default: - // TODO(jamiewalch): Show an error message. - console.error('Unexpected daemon state', state); - break; - } - if (retry) { - this.okButton_.hidden = true; - this.spinner_.hidden = false; - /** @type {remoting.AskPinDialog} */ - var that = this; - var pollDaemonState = function() { that.pollDaemonState_(); } - window.setTimeout(pollDaemonState, 1000); - } else { - this.okButton_.hidden = false; - this.spinner_.hidden = true; - } -}; - -/** @type {remoting.AskPinDialog} */ -remoting.askPinDialog = null; diff --git a/remoting/webapp/daemon_plugin.js b/remoting/webapp/daemon_plugin.js index cec5a8f..2f16fda 100644 --- a/remoting/webapp/daemon_plugin.js +++ b/remoting/webapp/daemon_plugin.js @@ -44,6 +44,8 @@ remoting.DaemonPlugin.prototype.state = function() { * @return {void} Nothing. */ remoting.DaemonPlugin.prototype.updateDom = function() { + // TODO(sergeyu): This code updates UI state. Does it belong here, + // or should it moved somewhere else? var match = ''; switch (this.state()) { case remoting.DaemonPlugin.State.STARTED: @@ -59,27 +61,54 @@ remoting.DaemonPlugin.prototype.updateDom = function() { }; /** + * Generates new host key pair. + * @param {function(string,string):void} callback Callback for the + * generated key pair. + * @return {void} Nothing. + */ +remoting.DaemonPlugin.prototype.generateKeyPair = function(callback) { + this.plugin_.generateKeyPair(callback); +}; + +/** + * @return {string} Local hostname + */ +remoting.DaemonPlugin.prototype.getHostName = function() { + return this.plugin_.getHostName(); +}; + +/** + * Read current host configuration. + * @param {function(string):void} callback Host config callback. + * @return {void} Nothing. + */ +remoting.DaemonPlugin.prototype.getConfig = function(callback) { + this.plugin_.getDaemonConfig(callback); +}; + +/** * Start the daemon process. - * @return {boolean} False if insufficient state has been set. + * @param {string} config Host config. + * @return {void} Nothing. */ -remoting.DaemonPlugin.prototype.start = function() { - return this.plugin_.startDaemon(); +remoting.DaemonPlugin.prototype.start = function(config) { + this.plugin_.startDaemon(config); }; /** * Stop the daemon process. - * @return {boolean} False if insufficient state has been set. + * @return {void} Nothing. */ remoting.DaemonPlugin.prototype.stop = function() { - return this.plugin_.stopDaemon(); + this.plugin_.stopDaemon(); }; /** * @param {string} pin The new PIN for the daemon process. - * @return {boolean} True if the PIN was set successfully. + * @return {void} Nothing. */ remoting.DaemonPlugin.prototype.setPin = function(pin) { - return this.plugin_.setDaemonPin(pin); + this.plugin_.setDaemonPin(pin); }; /** @type {remoting.DaemonPlugin} */ diff --git a/remoting/webapp/event_handlers.js b/remoting/webapp/event_handlers.js index a245908..c36787a 100644 --- a/remoting/webapp/event_handlers.js +++ b/remoting/webapp/event_handlers.js @@ -66,9 +66,9 @@ function onLoad() { { event: 'click', id: 'toolbar-stub', fn: function() { remoting.toolbar.toggle(); } }, { event: 'click', id: 'start-daemon', - fn: function() { remoting.askPinDialog.showForStart(); } }, + fn: function() { remoting.hostSetupDialog.showForStart(); } }, { event: 'click', id: 'change-daemon-pin', - fn: function() { remoting.askPinDialog.showForPin(); } }, + 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 }, diff --git a/remoting/webapp/host_plugin_proto.js b/remoting/webapp/host_plugin_proto.js index f555d10..6760fe4 100644 --- a/remoting/webapp/host_plugin_proto.js +++ b/remoting/webapp/host_plugin_proto.js @@ -25,11 +25,20 @@ remoting.HostPlugin.prototype.disconnect = function() {}; * @return {void} Nothing. */ remoting.HostPlugin.prototype.localize = function(callback) {}; +/** @return {string} Local hostname. */ +remoting.HostPlugin.prototype.getHostName = function() {}; + +/** @param {function(string, string):void} callback Callback to be called + * after new key is generated. + * @return {void} Nothing. */ +remoting.HostPlugin.prototype.generateKeyPair = function(callback) {}; + /** @param {string} pin The new PIN. * @return {void} Nothing. */ remoting.HostPlugin.prototype.setDaemonPin = function(pin) {}; -/** @param {string} callback Callback to be called for the config. +/** @param {function(string):void} callback Callback to be called for + * the config. * @return {void} Nothing. */ remoting.HostPlugin.prototype.getDaemonConfig = function(callback) {}; @@ -40,11 +49,6 @@ remoting.HostPlugin.prototype.startDaemon = function(config) {}; /** @return {void} Nothing. */ remoting.HostPlugin.prototype.stopDaemon = function() {}; -/** @param {function(string):void} callback Callback to be called - * after new key is generated. - * @return {void} Nothing. */ -remoting.HostPlugin.prototype.generateKeyPair = function(callback) {}; - /** @type {number} */ remoting.HostPlugin.prototype.state; /** @type {number} */ remoting.HostPlugin.prototype.STARTING; diff --git a/remoting/webapp/host_setup_dialog.js b/remoting/webapp/host_setup_dialog.js new file mode 100644 index 0000000..1e9e66c --- /dev/null +++ b/remoting/webapp/host_setup_dialog.js @@ -0,0 +1,351 @@ +// 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. + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @param {Array.} sequence Sequence of + * steps for the flow. + * @constructor + */ +remoting.HostSetupFlow = function(sequence) { + this.sequence_ = sequence; + this.currentStep_ = 0; + this.state_ = sequence[0]; + + this.pin = ''; + this.hostConfig = ''; +}; + +/** @enum {number} */ +remoting.HostSetupFlow.State = { + NONE: 0, + ASK_PIN: 1, + REGISTER_HOST: 2, + START_HOST: 3, + HOST_STARTED: 4, + UPDATE_PIN: 5, + REGISTRATION_FAILED: 6, + HOST_START_FAILED: 7 +}; + +/** @return {remoting.HostSetupFlow.State} Current state of the flow. */ +remoting.HostSetupFlow.prototype.getState = function() { + return this.state_; +}; + +/** + * @param {boolean} success + * @return {remoting.HostSetupFlow.State} New state. + */ +remoting.HostSetupFlow.prototype.switchToNextStep = function(success) { + if (this.state_ == remoting.HostSetupFlow.State.NONE) { + return this.state_; + } + if (success) { + // If the current step was successful then switch to the next + // step in the sequence. + if (this.currentStep_ < this.sequence_.length - 1) { + this.currentStep_ += 1; + this.state_ = this.sequence_[this.currentStep_]; + } else { + this.state_ = remoting.HostSetupFlow.State.NONE; + } + } else { + // Current step failed, so switch to corresponding error state. + if (this.state_ == remoting.HostSetupFlow.State.REGISTER_HOST) { + this.state_ = remoting.HostSetupFlow.State.REGISTRATION_FAILED; + } else { + // TODO(sergeyu): Add other error states and use them here. + this.state_ = remoting.HostSetupFlow.State.HOST_START_FAILED; + } + } + return this.state_; +}; + +/** + * @param {remoting.DaemonPlugin} daemon The parent daemon plugin instance. + * @constructor + */ +remoting.HostSetupDialog = function(daemon) { + this.daemon_ = daemon; + this.pinEntry_ = document.getElementById('daemon-pin-entry'); + this.pinConfirm_ = document.getElementById('daemon-pin-confirm'); + + /** @type {remoting.HostSetupFlow} */ + this.flow_ = new remoting.HostSetupFlow([remoting.HostSetupFlow.State.NONE]); + + /** @type {remoting.HostSetupDialog} */ + var that = this; + /** @param {Event} event The event. */ + var onPinSubmit = function(event) { + event.preventDefault(); + that.onPinSubmit_(); + }; + var form = document.getElementById('ask-pin-form'); + form.addEventListener('submit', onPinSubmit, false); +}; + +/** + * Show the dialog in order to get a PIN prior to starting the daemon. When the + * user clicks OK, the dialog shows a spinner until the daemon has started. + * + * @return {void} Nothing. + */ +remoting.HostSetupDialog.prototype.showForStart = function() { + this.startNewFlow_( + [remoting.HostSetupFlow.State.ASK_PIN, + remoting.HostSetupFlow.State.REGISTER_HOST, + remoting.HostSetupFlow.State.START_HOST, + remoting.HostSetupFlow.State.HOST_STARTED]); +}; + +/** + * Show the dialog in order to change the PIN associated with a running daemon. + * + * @return {void} Nothing. + */ +remoting.HostSetupDialog.prototype.showForPin = function() { + this.startNewFlow_( + [remoting.HostSetupFlow.State.ASK_PIN, + remoting.HostSetupFlow.State.UPDATE_PIN]); +}; + +/** + * @return {void} Nothing. + */ +remoting.HostSetupDialog.prototype.hide = function() { + remoting.setMode(remoting.AppMode.HOME); +}; + +/** + * Starts new flow with the specified sequence of steps. + * @param {Array.} sequence Sequence of steps. + * @private + */ +remoting.HostSetupDialog.prototype.startNewFlow_ = function(sequence) { + this.flow_ = new remoting.HostSetupFlow(sequence); + this.pinEntry_.text = ''; + this.pinConfirm_.text = ''; + this.updateState_(); +}; + +/** + * Updates current UI mode according to the current state of the setup + * flow and start the action corresponding to the current step (if + * any). + * @private + */ +remoting.HostSetupDialog.prototype.updateState_ = function() { + /** @param {string} tag */ + function showProcessingMessage(tag) { + var errorDiv = document.getElementById('host-setup-processing-message'); + l10n.localizeElementFromTag(errorDiv, tag); + remoting.setMode(remoting.AppMode.HOST_SETUP_PROCESSING); + } + /** @param {string} tag */ + function showDoneMessage(tag) { + var errorDiv = document.getElementById('host-setup-done-message'); + l10n.localizeElementFromTag(errorDiv, tag); + remoting.setMode(remoting.AppMode.HOST_SETUP_DONE); + } + /** @param {string} tag */ + function showErrorMessage(tag) { + var errorDiv = document.getElementById('host-setup-error-message'); + l10n.localizeElementFromTag(errorDiv, tag); + remoting.setMode(remoting.AppMode.HOST_SETUP_ERROR); + } + + var state = this.flow_.getState(); + if (state == remoting.HostSetupFlow.State.NONE) { + this.hide(); + } else if (state == remoting.HostSetupFlow.State.ASK_PIN) { + remoting.setMode(remoting.AppMode.HOST_SETUP_ASK_PIN); + } else if (state == remoting.HostSetupFlow.State.REGISTER_HOST) { + showProcessingMessage(/*i18n-content*/'HOST_SETUP_STARTING'); + this.registerHost_(); + } else if (state == remoting.HostSetupFlow.State.START_HOST) { + showProcessingMessage(/*i18n-content*/'HOST_SETUP_STARTING'); + this.startHost_(); + } else if (state == remoting.HostSetupFlow.State.UPDATE_PIN) { + showProcessingMessage(/*i18n-content*/'HOST_SETUP_UPDATING_PIN'); + this.updatePin_(); + } else if (state == remoting.HostSetupFlow.State.HOST_STARTED) { + showDoneMessage(/*i18n-content*/'HOST_SETUP_STARTED'); + } else if (state == remoting.HostSetupFlow.State.REGISTRATION_FAILED) { + showErrorMessage(/*i18n-content*/'HOST_SETUP_REGISTRATION_FAILED'); + } else if (state == remoting.HostSetupFlow.State.HOST_START_FAILED) { + showErrorMessage(/*i18n-content*/'HOST_SETUP_HOST_FAILED'); + } +}; + +/** + * Registers new host. + */ +remoting.HostSetupDialog.prototype.registerHost_ = function() { + /** @type {remoting.HostSetupDialog} */ + var that = this; + /** @type {remoting.HostSetupFlow} */ + var flow = this.flow_; + + /** @return {string} */ + function generateUuid() { + var random = new Uint16Array(8); + window.crypto.getRandomValues(random); + /** @type {Array.} */ + var e = new Array(); + for (var i = 0; i < 8; i++) { + e[i] = (/** @type {number} */random[i] + 0x10000). + toString(16).substring(1); + } + return e[0] + e[1] + '-' + e[2] + "-" + e[3] + '-' + + e[4] + '-' + e[5] + e[6] + e[7]; + } + + var newHostId = generateUuid(); + + /** @param {string} privateKey + * @param {XMLHttpRequest} xhr */ + function onRegistered(privateKey, xhr) { + if (flow !== that.flow_ || + flow.getState() != remoting.HostSetupFlow.State.REGISTER_HOST) { + console.error('Host setup was interrupted when registering the host'); + return; + } + + var success = (xhr.status == 200); + + if (success) { + // TODO(sergeyu): Calculate HMAC of the PIN instead of storing it + // in plaintext. + flow.hostConfig = JSON.stringify({ + xmpp_login: remoting.oauth2.getCachedEmail(), + oauth_refresh_token: remoting.oauth2.getRefreshToken(), + host_id: newHostId, + host_name: that.daemon_.getHostName(), + host_secret_hash: 'plain:' + flow.pin, + private_key: privateKey + }); + } else { + console.log('Failed to register the host. Status: ' + xhr.status + + ' response: ' + xhr.responseText); + } + + flow.switchToNextStep(success); + that.updateState_(); + } + + /** + * @param {string} privateKey + * @param {string} publicKey + * @param {string} oauthToken + */ + function doRegisterHost(privateKey, publicKey, oauthToken) { + if (flow !== that.flow_ || + flow.getState() != remoting.HostSetupFlow.State.REGISTER_HOST) { + console.error('Host setup was interrupted when generating key pair'); + return; + } + + var headers = { + 'Authorization': 'OAuth ' + oauthToken, + 'Content-type' : 'application/json; charset=UTF-8' + }; + + var newHostDetails = { data: { + hostId: newHostId, + hostName: that.daemon_.getHostName(), + publicKey: publicKey + } }; + remoting.xhr.post( + 'https://www.googleapis.com/chromoting/v1/@me/hosts/', + /** @param {XMLHttpRequest} xhr */ + function (xhr) { onRegistered(privateKey, xhr); }, + JSON.stringify(newHostDetails), + headers); + } + + this.daemon_.generateKeyPair( + /** @param {string} privateKey + * @param {string} publicKey */ + function(privateKey, publicKey) { + remoting.oauth2.callWithToken( + /** @param {string} oauthToken */ + function(oauthToken) { + doRegisterHost(privateKey, publicKey, oauthToken); + }); + }); +}; + +/** + * Starts the host process after it's registered. + */ +remoting.HostSetupDialog.prototype.startHost_ = function() { + this.daemon_.start(this.flow_.hostConfig); + this.pollDaemonState_(); +}; + +remoting.HostSetupDialog.prototype.updatePin_ = function() { + this.daemon_.setPin(this.flow_.pin); + this.pollDaemonState_(); +} + +/** @private */ +remoting.HostSetupDialog.prototype.onPinSubmit_ = function() { + if (this.flow_.getState() != remoting.HostSetupFlow.State.ASK_PIN) { + console.error('PIN submitted in an invalid state', this.flow_.getState()); + return; + } + // TODO(jamiewalch): Add validation and error checks when we improve the UI. + var pin = this.pinEntry_.value; + this.flow_.pin = pin; + this.flow_.switchToNextStep(true); + this.updateState_(); +}; + +/** + * @return {void} Nothing. + * @private + */ +remoting.HostSetupDialog.prototype.pollDaemonState_ = function() { + var state = this.daemon_.state(); + var retry = false; // Set to true if we haven't finished yet. + switch (state) { + case remoting.DaemonPlugin.State.STOPPED: + case remoting.DaemonPlugin.State.NOT_INSTALLED: + retry = true; + break; + case remoting.DaemonPlugin.State.STARTED: + if (this.flow_.getState() == remoting.HostSetupFlow.State.START_HOST || + this.flow_.getState() == remoting.HostSetupFlow.State.UPDATE_PIN) { + this.flow_.switchToNextStep(true); + this.updateState_(); + } + this.daemon_.updateDom(); + break; + case remoting.DaemonPlugin.State.START_FAILED: + if (this.flow_.getState() == remoting.HostSetupFlow.State.START_HOST || + this.flow_.getState() == remoting.HostSetupFlow.State.UPDATE_PIN) { + this.flow_.switchToNextStep(false); + this.updateState_(); + } + break; + default: + // TODO(jamiewalch): Show an error message. + console.error('Unexpected daemon state', state); + break; + } + if (retry) { + /** @type {remoting.HostSetupDialog} */ + var that = this; + var pollDaemonState = function() { that.pollDaemonState_(); } + window.setTimeout(pollDaemonState, 1000); + } +}; + +/** @type {remoting.HostSetupDialog} */ +remoting.hostSetupDialog = null; diff --git a/remoting/webapp/main.css b/remoting/webapp/main.css index 8c2b153..26f0132 100644 --- a/remoting/webapp/main.css +++ b/remoting/webapp/main.css @@ -438,7 +438,7 @@ button { margin-top: 24px; } -#ask-pin-dialog { +#host-setup-dialog { position: absolute; top: 50%; left: 50%; diff --git a/remoting/webapp/main.html b/remoting/webapp/main.html index 9a3826a..7722ff5 100644 --- a/remoting/webapp/main.html +++ b/remoting/webapp/main.html @@ -16,7 +16,7 @@ found in the LICENSE file. - + @@ -185,18 +185,19 @@ found in the LICENSE file.