1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Connect set-up state machine for Me2Me and IT2Me
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
16 * @param {Element} pluginParent The node under which to add the client plugin.
17 * @param {function(remoting.ClientSession):void} onOk Callback on success.
18 * @param {function(remoting.Error):void} onError Callback on error.
21 remoting.SessionConnector = function(pluginParent, onOk, onError) {
26 this.pluginParent_ = pluginParent;
29 * @type {function(remoting.ClientSession):void}
35 * @type {function(remoting.Error):void}
38 this.onError_ = onError;
47 * @type {remoting.ClientSession.Mode}
50 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
53 * A timer that polls for an updated access token.
58 this.wcsAccessTokenRefreshTimer_ = 0;
60 // Initialize/declare per-connection state.
63 // Pre-load WCS to improve connection time.
64 remoting.identity.callWithToken(this.loadWcs_.bind(this), this.onError_);
68 * Reset the per-connection state so that the object can be re-used for a
69 * second connection. Note the none of the shared WCS state is reset.
71 remoting.SessionConnector.prototype.reset = function() {
73 * String used to identify the host to which to connect. For IT2Me, this is
74 * the first 7 digits of the access code; for Me2Me it is the host identifier.
82 * String used to authenticate to the host on connection. For IT2Me, this is
83 * the access code; for Me2Me it is the PIN.
88 this.passPhrase_ = '';
100 this.hostPublicKey_ = '';
106 this.refreshHostJidIfOffline_ = true;
109 * @type {remoting.ClientSession}
112 this.clientSession_ = null;
115 * @type {XMLHttpRequest}
118 this.pendingXhr_ = null;
121 * Function to interactively obtain the PIN from the user.
122 * @type {function(function(string):void):void}
125 this.fetchPin_ = function(onPinFetched) {};
128 * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
129 * this is the name of the host; for an IT2Me connection, it is the email
130 * address of the person sharing their computer.
135 this.hostDisplayName_ = '';
139 * Initiate a Me2Me connection.
141 * @param {remoting.Host} host The Me2Me host to which to connect.
142 * @param {function(function(string):void):void} fetchPin Function to
143 * interactively obtain the PIN from the user.
144 * @return {void} Nothing.
146 remoting.SessionConnector.prototype.connectMe2Me = function(host, fetchPin) {
147 // Cancel any existing connect operation.
150 this.hostId_ = host.hostId;
151 this.hostJid_ = host.jabberId;
152 this.fetchPin_ = fetchPin;
153 this.hostDisplayName_ = host.hostName;
154 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
155 this.createSessionIfReady_();
159 * Initiate an IT2Me connection.
161 * @param {string} accessCode The access code as entered by the user.
162 * @return {void} Nothing.
164 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
165 var kSupportIdLen = 7;
166 var kHostSecretLen = 5;
167 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
169 // Cancel any existing connect operation.
172 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
173 if (normalizedAccessCode.length != kAccessCodeLen) {
174 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
178 this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
179 this.passPhrase_ = normalizedAccessCode;
180 this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
181 remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
186 * Reconnect a closed connection.
188 * @return {void} Nothing.
190 remoting.SessionConnector.prototype.reconnect = function() {
191 if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
192 console.error('reconnect not supported for IT2Me.');
195 this.createSessionIfReady_();
199 * Cancel a connection-in-progress.
201 remoting.SessionConnector.prototype.cancel = function() {
202 if (this.clientSession_) {
203 this.clientSession_.removePlugin();
204 this.clientSession_ = null;
206 if (this.pendingXhr_) {
207 this.pendingXhr_.abort();
208 this.pendingXhr_ = null;
214 * Get the connection mode (Me2Me or IT2Me)
216 * @return {remoting.ClientSession.Mode}
218 remoting.SessionConnector.prototype.getConnectionMode = function() {
219 return this.connectionMode_;
223 * Continue an IT2Me connection once an access token has been obtained.
225 * @param {string} token An OAuth2 access token.
226 * @return {void} Nothing.
229 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
230 // Resolve the host id to get the host JID.
231 this.pendingXhr_ = remoting.xhr.get(
232 remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
233 encodeURIComponent(this.hostId_),
234 this.onIT2MeHostInfo_.bind(this),
236 { 'Authorization': 'OAuth ' + token });
240 * Continue an IT2Me connection once the host JID has been looked up.
242 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
243 * @return {void} Nothing.
246 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
247 this.pendingXhr_ = null;
248 if (xhr.status == 200) {
249 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
250 jsonParseSafe(xhr.responseText);
251 if (host && host.data && host.data.jabberId && host.data.publicKey) {
252 this.hostJid_ = host.data.jabberId;
253 this.hostPublicKey_ = host.data.publicKey;
254 this.hostDisplayName_ = this.hostJid_.split('/')[0];
255 this.createSessionIfReady_();
258 console.error('Invalid "support-hosts" response from server.');
261 this.onError_(this.translateSupportHostsError(xhr.status));
266 * Load the WCS driver script.
268 * @param {string} token An OAuth2 access token.
269 * @return {void} Nothing.
272 remoting.SessionConnector.prototype.loadWcs_ = function(token) {
273 remoting.wcsSandbox.setOnReady(this.onWcsLoaded_.bind(this));
274 remoting.wcsSandbox.setOnError(this.onError_);
275 remoting.wcsSandbox.setAccessToken(token);
276 this.startAccessTokenRefreshTimer_();
280 * Continue an IT2Me or Me2Me connection once WCS has been loaded.
282 * @param {string} clientJid The full JID of the WCS client.
283 * @return {void} Nothing.
286 remoting.SessionConnector.prototype.onWcsLoaded_ = function(clientJid) {
287 this.clientJid_ = clientJid;
288 this.createSessionIfReady_();
292 * If both the client and host JIDs are available, create a session and connect.
294 * @return {void} Nothing.
297 remoting.SessionConnector.prototype.createSessionIfReady_ = function() {
298 if (!this.clientJid_ || !this.hostJid_) {
302 var securityTypes = 'spake2_hmac,spake2_plain';
303 this.clientSession_ = new remoting.ClientSession(
304 this.hostJid_, this.clientJid_, this.hostPublicKey_, this.passPhrase_,
305 this.fetchPin_, securityTypes, this.hostId_, this.connectionMode_,
306 this.hostDisplayName_);
307 this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
308 this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
309 this.clientSession_.createPluginAndConnect(this.pluginParent_);
313 * Handle a change in the state of the client session prior to successful
314 * connection (after connection, this class no longer handles state change
315 * events). Errors that occur while connecting either trigger a reconnect
316 * or notify the onError handler.
318 * @param {number} oldState The previous state of the plugin.
319 * @param {number} newState The current state of the plugin.
320 * @return {void} Nothing.
323 remoting.SessionConnector.prototype.onStateChange_ =
324 function(oldState, newState) {
326 case remoting.ClientSession.State.CONNECTED:
327 // When the connection succeeds, deregister for state-change callbacks
328 // and pass the session to the onOk callback. It is expected that it
329 // will register a new state-change callback to handle disconnect
330 // or error conditions.
331 this.clientSession_.setOnStateChange(null);
332 this.onOk_(this.clientSession_);
335 case remoting.ClientSession.State.CREATED:
336 console.log('Created plugin');
339 case remoting.ClientSession.State.BAD_PLUGIN_VERSION:
340 this.onError_(remoting.Error.BAD_PLUGIN_VERSION);
343 case remoting.ClientSession.State.CONNECTING:
344 console.log('Connecting as ' + remoting.identity.getCachedEmail());
347 case remoting.ClientSession.State.INITIALIZING:
348 console.log('Initializing connection');
351 case remoting.ClientSession.State.CLOSED:
352 // This class deregisters for state-change callbacks when the CONNECTED
353 // state is reached, so it only sees the CLOSED state in exceptional
354 // circumstances. For example, a CONNECTING -> CLOSED transition happens
355 // if the host closes the connection without an error message instead of
356 // accepting it. Since there's no way of knowing exactly what went wrong,
357 // we rely on server-side logs in this case and report a generic error
359 this.onError_(remoting.Error.UNEXPECTED);
362 case remoting.ClientSession.State.FAILED:
363 var error = this.clientSession_.getError();
364 console.error('Client plugin reported connection failed: ' + error);
366 error = remoting.Error.UNEXPECTED;
368 if (error == remoting.Error.HOST_IS_OFFLINE &&
369 this.refreshHostJidIfOffline_) {
370 this.refreshHostJidIfOffline_ = false;
371 this.clientSession_.removePlugin();
372 this.clientSession_ = null;
373 remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
375 this.onError_(error);
380 console.error('Unexpected client plugin state: ' + newState);
381 // This should only happen if the web-app and client plugin get out of
382 // sync, and even then the version check should ensure compatibility.
383 this.onError_(remoting.Error.MISSING_PLUGIN);
388 * @param {boolean} success True if the host list was successfully refreshed;
389 * false if an error occurred.
392 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
394 var host = remoting.hostList.getHostForId(this.hostId_);
396 this.connectMe2Me(host, this.fetchPin_);
400 this.onError_(remoting.Error.HOST_IS_OFFLINE);
404 * Start a timer to periodically refresh the access token used by WCS. Access
405 * tokens have a limited lifespan, and since the WCS driver runs in a sandbox,
406 * it can't obtain a new one directly.
408 * @return {void} Nothing.
411 remoting.SessionConnector.prototype.startAccessTokenRefreshTimer_ = function() {
412 if (this.wcsAccessTokenRefreshTimer_ != 0) {
416 /** @type {remoting.SessionConnector} */
418 var refreshAccessToken = function() {
419 remoting.identity.callWithToken(
420 remoting.wcsSandbox.setAccessToken.bind(remoting.wcsSandbox),
424 * A timer that polls for an updated access token.
428 this.wcsAccessTokenRefreshTimer_ = setInterval(refreshAccessToken,
433 * @param {number} error An HTTP error code returned by the support-hosts
435 * @return {remoting.Error} The equivalent remoting.Error code.
438 remoting.SessionConnector.prototype.translateSupportHostsError =
441 case 0: return remoting.Error.NETWORK_FAILURE;
442 case 404: return remoting.Error.INVALID_ACCESS_CODE;
443 case 502: // No break
444 case 503: return remoting.Error.SERVICE_UNAVAILABLE;
445 default: return remoting.Error.UNEXPECTED;
450 * Normalize the access code entered by the user.
452 * @param {string} accessCode The access code, as entered by the user.
453 * @return {string} The normalized form of the code (whitespace removed).
455 remoting.SessionConnector.prototype.normalizeAccessCode_ =
456 function(accessCode) {
458 return accessCode.replace(/\s/g, '');