blob: 697e13aa865369e7a60bc1e12b8bcaeb6e889d83 [file] [log] [blame]
// Copyright 2015 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.
/** @suppress {duplicate} */
var remoting = remoting || {};
(function() {
'use strict';
/**
* @param {remoting.Host} host
* @param {remoting.HostList} hostList
*
* @constructor
* @implements {remoting.Activity}
*/
remoting.Me2MeActivity = function(host, hostList) {
/** @private */
this.host_ = host;
/** @private */
this.hostList_ = hostList;
/** @private */
this.pinDialog_ =
new remoting.PinDialog(base.getHtmlElement('pin-dialog'), host);
/** @private */
this.hostUpdateDialog_ = new remoting.HostNeedsUpdateDialog(
base.getHtmlElement('host-needs-update-dialog'), this.host_);
/** @private */
this.retryOnHostOffline_ = true;
/** @private {remoting.AutoReconnector} */
this.reconnector_ = null;
/** @private {remoting.SessionLogger} */
this.logger_ = null;
/** @private {remoting.DesktopRemotingActivity} */
this.desktopActivity_ = null;
/** @private {remoting.GnubbyAuthHandler} */
this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler();
/** @private {Array<string>} */
this.additionalCapabilities_ = [];
};
remoting.Me2MeActivity.prototype.dispose = function() {
base.dispose(this.desktopActivity_);
this.desktopActivity_ = null;
base.dispose(this.reconnector_);
this.reconnector_ = null;
};
remoting.Me2MeActivity.prototype.start = function() {
var webappVersion = chrome.runtime.getManifest().version;
var that = this;
var Event = remoting.ChromotingEvent;
this.logger_ = this.createLogger_(Event.SessionEntryPoint.CONNECT_BUTTON);
this.logger_.logSessionStateChange(Event.SessionState.STARTED);
function handleError(/** remoting.Error */ error) {
if (error.isCancel()) {
remoting.setMode(remoting.AppMode.HOME);
that.logger_.logSessionStateChange(
Event.SessionState.CONNECTION_CANCELED);
} else {
that.logger_.logSessionStateChange(
Event.SessionState.CONNECTION_FAILED, error);
that.showErrorMessage_(error);
}
}
this.hostUpdateDialog_.showIfNecessary(webappVersion).catch(
remoting.Error.handler(function(/** remoting.Error */ error) {
// User cancels the Host upgrade dialog. Report it as bad version.
throw new remoting.Error(remoting.Error.Tag.BAD_VERSION);
})).then(
this.gnubbyAuthHandler_.isGnubbyExtensionInstalled.bind(
this.gnubbyAuthHandler_)
).then(
function (extensionInstalled) {
if (extensionInstalled) {
this.additionalCapabilities_.push(
remoting.ClientSession.Capability.SECURITY_KEY);
}
}.bind(this)
).then(
this.connect_.bind(this)
).catch(remoting.Error.handler(handleError));
};
remoting.Me2MeActivity.prototype.stop = function() {
if (this.desktopActivity_) {
this.desktopActivity_.stop();
}
};
/** @return {remoting.DesktopRemotingActivity} */
remoting.Me2MeActivity.prototype.getDesktopActivity = function() {
return this.desktopActivity_;
};
/**
* @param {remoting.ChromotingEvent.SessionEntryPoint} entryPoint
* @return {!remoting.SessionLogger}
* @private
*/
remoting.Me2MeActivity.prototype.createLogger_ = function(entryPoint) {
var Mode = remoting.ChromotingEvent.Mode;
var logger = remoting.SessionLogger.createForClient();
logger.setEntryPoint(entryPoint);
logger.setHost(this.host_);
logger.setLogEntryMode(Mode.ME2ME);
logger.setHostStatusUpdateElapsedTime(
this.hostList_.getHostStatusUpdateElapsedTime());
return logger;
};
/**
* @param {remoting.ChromotingEvent.SessionEntryPoint} entryPoint
* @private
*/
remoting.Me2MeActivity.prototype.reconnect_ = function(entryPoint) {
console.assert(this.logger_, 'Reconnecting without a previous session.');
var previousSessionSummary = this.logger_.createSummary();
this.logger_ = this.createLogger_(entryPoint);
this.logger_.setPreviousSessionSummary(previousSessionSummary);
var Event = remoting.ChromotingEvent;
this.logger_.logSessionStateChange(Event.SessionState.STARTED);
this.connect_();
};
/**
* @private
*/
remoting.Me2MeActivity.prototype.connect_ = function() {
base.dispose(this.desktopActivity_);
this.desktopActivity_ = new remoting.DesktopRemotingActivity(
this, this.logger_, this.additionalCapabilities_);
this.desktopActivity_.getConnectingDialog().show();
this.host_.options.load().then(
function() {
this.desktopActivity_.start(this.host_,
this.createCredentialsProvider_());
}.bind(this));
};
/**
* @return {remoting.CredentialsProvider}
* @private
*/
remoting.Me2MeActivity.prototype.createCredentialsProvider_ = function() {
var host = this.host_;
var that = this;
/**
* @param {string} tokenUrl Token-issue URL received from the host.
* @param {string} hostPublicKey Host public key (DER and Base64 encoded).
* @param {string} scope OAuth scope to request the token for.
* @param {function(string, string):void} onThirdPartyTokenFetched Callback.
*/
var fetchThirdPartyToken = function(
tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) {
var thirdPartyTokenFetcher = new remoting.ThirdPartyTokenFetcher(
tokenUrl, hostPublicKey, scope, host.tokenUrlPatterns,
onThirdPartyTokenFetched);
thirdPartyTokenFetcher.fetchToken();
};
/**
* @param {boolean} supportsPairing
* @param {function(string):void} onPinFetched
*/
var requestPin = function(supportsPairing, onPinFetched) {
// Set time when PIN was requested.
var authStartTime = Date.now();
that.desktopActivity_.getConnectingDialog().hide();
that.pinDialog_.show(supportsPairing).then(function(/** string */ pin) {
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
// Done obtaining PIN information. Log time taken for PIN entry.
that.logger_.setAuthTotalTime(Date.now() - authStartTime);
onPinFetched(pin);
}).catch(remoting.Error.handler(function(/** remoting.Error */ error) {
console.assert(error.hasTag(remoting.Error.Tag.CANCELLED),
'Unexpected error: ' + error.getTag() + '.');
remoting.setMode(remoting.AppMode.HOME);
that.stop();
}));
};
return new remoting.CredentialsProvider({
fetchPin: requestPin,
pairingInfo: host.options.getPairingInfo(),
fetchThirdPartyToken: fetchThirdPartyToken
});
};
/**
* @param {!remoting.Error} error
* @private
*/
remoting.Me2MeActivity.prototype.reconnectOnHostOffline_ = function(error) {
var that = this;
var onHostListRefresh = function() {
var host = that.hostList_.getHostForId(that.host_.hostId);
var SessionEntryPoint = remoting.ChromotingEvent.SessionEntryPoint;
// Reconnect if the host's JID changes.
if (Boolean(host) &&
host.jabberId != that.host_.jabberId &&
host.status == 'ONLINE') {
that.host_ = host;
that.reconnect_(SessionEntryPoint.AUTO_RECONNECT_ON_HOST_OFFLINE);
return;
}
that.showErrorMessage_(error);
};
this.retryOnHostOffline_ = false;
// The plugin will be re-created when the host list has been refreshed.
this.hostList_.refreshAndDisplay().then(
onHostListRefresh
).catch(
this.showErrorMessage_.bind(this, error)
);
};
/**
* @param {!remoting.Error} error
*/
remoting.Me2MeActivity.prototype.onConnectionFailed = function(error) {
base.dispose(this.desktopActivity_);
this.desktopActivity_ = null;
if (error.isNone()) {
// This could happen if the host terminates the session before it is
// connected.
this.showFinishDialog_(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
} else if (error.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) &&
this.retryOnHostOffline_) {
this.reconnectOnHostOffline_(error);
} else {
this.showErrorMessage_(error);
}
};
/**
* @param {!remoting.ConnectionInfo} connectionInfo
*/
remoting.Me2MeActivity.prototype.onConnected = function(connectionInfo) {
// Reset the refresh flag so that the next connection will retry if needed.
this.retryOnHostOffline_ = true;
var plugin = connectionInfo.plugin();
// TODO(joedow): Do not register the GnubbyAuthHandler extension if the host
// does not support security key forwarding.
plugin.extensions().register(this.gnubbyAuthHandler_);
this.pinDialog_.requestPairingIfNecessary(plugin);
// Drop the session after 30s of suspension. If this timeout is too short, we
// risk dropping a connection that is self-recoverable. If this timeout is too
// long, the user may lose their patience and just manually reconnect.
this.desktopActivity_.getSession().dropSessionOnSuspend(30 * 1000);
};
remoting.Me2MeActivity.prototype.onDisconnected = function(error) {
base.dispose(this.desktopActivity_);
this.desktopActivity_ = null;
if (error.isNone()) {
this.showFinishDialog_(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
} else if (remoting.AutoReconnector.shouldAutoReconnect(error)) {
var SessionEntryPoint = remoting.ChromotingEvent.SessionEntryPoint;
this.reconnector_ = new remoting.AutoReconnector(
this.reconnect_.bind(
this, SessionEntryPoint.AUTO_RECONNECT_ON_CONNECTION_DROPPED));
} else {
this.showErrorMessage_(error);
}
};
/**
* @param {!remoting.Error} error
* @private
*/
remoting.Me2MeActivity.prototype.showErrorMessage_ = function(error) {
var errorDiv = document.getElementById('connect-error-message');
l10n.localizeElementFromTag(errorDiv, error.getTag());
this.showFinishDialog_(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
};
/**
* @param {remoting.AppMode} mode
* @private
*/
remoting.Me2MeActivity.prototype.showFinishDialog_ = function(mode) {
var dialog = remoting.modalDialogFactory.createMessageDialog(
mode,
base.getHtmlElement('client-finished-me2me-button'),
base.getHtmlElement('client-reconnect-button'));
/** @typedef {remoting.MessageDialog.Result} */
var Result = remoting.MessageDialog.Result;
var that = this;
dialog.show().then(function(/** Result */result) {
base.dispose(that.reconnector_);
that.reconnector_ = null;
if (result === Result.PRIMARY) {
remoting.setMode(remoting.AppMode.HOME);
} else {
that.reconnect_(
remoting.ChromotingEvent.SessionEntryPoint.RECONNECT_BUTTON);
}
});
};
/**
* @param {HTMLElement} rootElement
* @param {remoting.Host} host
* @constructor
*/
remoting.HostNeedsUpdateDialog = function(rootElement, host) {
/** @private */
this.host_ = host;
/** @private */
this.dialog_ = remoting.modalDialogFactory.createMessageDialog(
remoting.AppMode.CLIENT_HOST_NEEDS_UPGRADE,
rootElement.querySelector('.connect-button'),
rootElement.querySelector('.cancel-button'));
l10n.localizeElementFromTag(
rootElement.querySelector('.host-needs-update-message'),
/*i18n-content*/'HOST_NEEDS_UPDATE_TITLE', host.hostName);
};
/**
* Shows the HostNeedsUpdateDialog if the user is trying to connect to a legacy
* host.
*
* @param {string} webappVersion
* @return {Promise} Promise that resolves when no update is required or
* when the user ignores the update warning. Rejects with
* |remoting.Error.CANCELLED| if the user cancels the connection.
*/
remoting.HostNeedsUpdateDialog.prototype.showIfNecessary =
function(webappVersion) {
if (!remoting.Host.needsUpdate(this.host_, webappVersion)) {
return Promise.resolve();
}
/** @typedef {remoting.MessageDialog.Result} */
var Result = remoting.MessageDialog.Result;
return this.dialog_.show().then(function(/** Result */ result) {
if (result === Result.SECONDARY) {
return Promise.reject(new remoting.Error(remoting.Error.Tag.CANCELLED));
}
});
};
/**
* @param {HTMLElement} rootElement
* @param {remoting.Host} host
* @constructor
*/
remoting.PinDialog = function(rootElement, host) {
/** @private */
this.rootElement_ = rootElement;
/** @private */
this.pairingCheckbox_ = rootElement.querySelector('.pairing-checkbox');
/** @private */
this.pinInput_ = rootElement.querySelector('.pin-inputField');
/** @private */
this.host_ = host;
/** @private */
this.dialog_ = remoting.modalDialogFactory.createInputDialog(
remoting.AppMode.CLIENT_PIN_PROMPT,
this.rootElement_.querySelector('form'),
this.pinInput_,
this.rootElement_.querySelector('.cancel-button'));
};
/**
* @param {boolean} supportsPairing
* @return {Promise<string>} Promise that resolves with the PIN or rejects with
* |remoting.Error.CANCELLED| if the user cancels the connection.
*/
remoting.PinDialog.prototype.show = function(supportsPairing) {
// Reset the UI.
this.pairingCheckbox_.checked = false;
this.rootElement_.querySelector('.pairing-section').hidden = !supportsPairing;
var message = this.rootElement_.querySelector('.pin-message');
l10n.localizeElement(message, this.host_.hostName);
this.pinInput_.value = '';
return this.dialog_.show();
};
/**
* @param {remoting.ClientPlugin} plugin
*/
remoting.PinDialog.prototype.requestPairingIfNecessary = function(plugin) {
if (this.pairingCheckbox_.checked) {
var that = this;
/**
* @param {string} clientId
* @param {string} sharedSecret
*/
var onPairingComplete = function(clientId, sharedSecret) {
that.host_.options.setPairingInfo({'clientId': clientId,
'sharedSecret': sharedSecret});
that.host_.options.save();
};
// Use the platform name as a proxy for the local computer name.
// TODO(jamiewalch): Use a descriptive name for the local computer, for
// example, its Chrome Sync name.
var clientName = '';
if (remoting.platformIsMac()) {
clientName = 'Mac';
} else if (remoting.platformIsWindows()) {
clientName = 'Windows';
} else if (remoting.platformIsChromeOS()) {
clientName = 'ChromeOS';
} else if (remoting.platformIsLinux()) {
clientName = 'Linux';
} else {
console.log('Unrecognized client platform. Using navigator.platform.');
clientName = navigator.platform;
}
plugin.requestPairing(clientName, onPairingComplete);
}
};
/**
* A helper class to handle auto reconnect when the connection is dropped due to
* client connectivity issues.
*
* @param {Function} reconnectCallback callback to initiate the reconnect
*
* @constructor
* @implements {base.Disposable}
*
* @private
*/
remoting.AutoReconnector = function(reconnectCallback) {
/** @private */
this.reconnectCallback_ = reconnectCallback;
/** @private */
this.networkDetector_ = remoting.NetworkConnectivityDetector.create();
/** @private */
this.connectingDialog_ = remoting.modalDialogFactory.createConnectingDialog(
this.dispose.bind(this));
var that = this;
this.connectingDialog_.show();
this.networkDetector_.waitForOnline().then(function() {
if (that.reconnectCallback_) {
that.connectingDialog_.hide();
that.reconnectCallback_();
}
});
};
remoting.AutoReconnector.shouldAutoReconnect = function(error) {
return error.hasTag(remoting.Error.Tag.CLIENT_SUSPENDED);
};
remoting.AutoReconnector.prototype.dispose = function() {
base.dispose(this.networkDetector_);
this.networkDetector_ = null;
this.reconnectCallback_ = null;
this.connectingDialog_.hide();
};
})();