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.
19 * @param {function(string, string):boolean} onExtensionMessage The handler for
20 * protocol extension messages. Returns true if a message is recognized;
24 remoting.SessionConnector = function(pluginParent, onOk, onError,
30 this.pluginParent_ = pluginParent;
33 * @type {function(remoting.ClientSession):void}
39 * @type {function(remoting.Error):void}
42 this.onError_ = onError;
45 * @type {function(string, string):boolean}
48 this.onExtensionMessage_ = onExtensionMessage;
57 * @type {remoting.ClientSession.Mode}
60 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
62 // Initialize/declare per-connection state.
67 * Reset the per-connection state so that the object can be re-used for a
68 * second connection. Note the none of the shared WCS state is reset.
70 remoting.SessionConnector.prototype.reset = function() {
72 * Set to true to indicate that the user requested pairing when entering
73 * their PIN for a Me2Me connection.
77 this.pairingRequested = false;
80 * String used to identify the host to which to connect. For IT2Me, this is
81 * the first 7 digits of the access code; for Me2Me it is the host identifier.
89 * For paired connections, the client id of this device, issued by the host.
94 this.clientPairingId_ = '';
97 * For paired connections, the paired secret for this device, issued by the
103 this.clientPairedSecret_ = '';
106 * String used to authenticate to the host on connection. For IT2Me, this is
107 * the access code; for Me2Me it is the PIN.
112 this.passPhrase_ = '';
124 this.hostPublicKey_ = '';
130 this.refreshHostJidIfOffline_ = false;
133 * @type {remoting.ClientSession}
136 this.clientSession_ = null;
139 * @type {XMLHttpRequest}
142 this.pendingXhr_ = null;
145 * Function to interactively obtain the PIN from the user.
146 * @type {function(boolean, function(string):void):void}
149 this.fetchPin_ = function(onPinFetched) {};
152 * @type {function(string, string, string,
153 * function(string, string):void): void}
156 this.fetchThirdPartyToken_ = function(
157 tokenUrl, scope, onThirdPartyTokenFetched) {};
160 * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
161 * this is the name of the host; for an IT2Me connection, it is the email
162 * address of the person sharing their computer.
167 this.hostDisplayName_ = '';
171 * Initiate a Me2Me connection.
173 * @param {remoting.Host} host The Me2Me host to which to connect.
174 * @param {function(boolean, function(string):void):void} fetchPin Function to
175 * interactively obtain the PIN from the user.
176 * @param {function(string, string, string,
177 * function(string, string): void): void}
178 * fetchThirdPartyToken Function to obtain a token from a third party
179 * authenticaiton server.
180 * @param {string} clientPairingId The client id issued by the host when
181 * this device was paired, if it is already paired.
182 * @param {string} clientPairedSecret The shared secret issued by the host when
183 * this device was paired, if it is already paired.
184 * @return {void} Nothing.
186 remoting.SessionConnector.prototype.connectMe2Me =
187 function(host, fetchPin, fetchThirdPartyToken,
188 clientPairingId, clientPairedSecret) {
189 this.connectMe2MeInternal_(
190 host.hostId, host.jabberId, host.publicKey, host.hostName,
191 fetchPin, fetchThirdPartyToken,
192 clientPairingId, clientPairedSecret, true);
196 * Update the pairing info so that the reconnect function will work correctly.
198 * @param {string} clientId The paired client id.
199 * @param {string} sharedSecret The shared secret.
201 remoting.SessionConnector.prototype.updatePairingInfo =
202 function(clientId, sharedSecret) {
203 this.clientPairingId_ = clientId;
204 this.clientPairedSecret_ = sharedSecret;
208 * Initiate a Me2Me connection.
210 * @param {string} hostId ID of the Me2Me host.
211 * @param {string} hostJid XMPP JID of the host.
212 * @param {string} hostPublicKey Public Key of the host.
213 * @param {string} hostDisplayName Display name (friendly name) of the host.
214 * @param {function(boolean, function(string):void):void} fetchPin Function to
215 * interactively obtain the PIN from the user.
216 * @param {function(string, string, string,
217 * function(string, string): void): void}
218 * fetchThirdPartyToken Function to obtain a token from a third party
219 * authenticaiton server.
220 * @param {string} clientPairingId The client id issued by the host when
221 * this device was paired, if it is already paired.
222 * @param {string} clientPairedSecret The shared secret issued by the host when
223 * this device was paired, if it is already paired.
224 * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
225 * the connection if the current JID is offline.
226 * @return {void} Nothing.
229 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
230 function(hostId, hostJid, hostPublicKey, hostDisplayName,
231 fetchPin, fetchThirdPartyToken,
232 clientPairingId, clientPairedSecret,
233 refreshHostJidIfOffline) {
234 // Cancel any existing connect operation.
237 this.hostId_ = hostId;
238 this.hostJid_ = hostJid;
239 this.hostPublicKey_ = hostPublicKey;
240 this.fetchPin_ = fetchPin;
241 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
242 this.hostDisplayName_ = hostDisplayName;
243 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
244 this.refreshHostJidIfOffline_ = refreshHostJidIfOffline;
245 this.updatePairingInfo(clientPairingId, clientPairedSecret);
246 this.createSession_();
250 * Initiate an IT2Me connection.
252 * @param {string} accessCode The access code as entered by the user.
253 * @return {void} Nothing.
255 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
256 var kSupportIdLen = 7;
257 var kHostSecretLen = 5;
258 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
260 // Cancel any existing connect operation.
263 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
264 if (normalizedAccessCode.length != kAccessCodeLen) {
265 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
269 this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
270 this.passPhrase_ = normalizedAccessCode;
271 this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
272 remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
277 * Reconnect a closed connection.
279 * @return {void} Nothing.
281 remoting.SessionConnector.prototype.reconnect = function() {
282 if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
283 console.error('reconnect not supported for IT2Me.');
286 this.connectMe2MeInternal_(
287 this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
288 this.fetchPin_, this.fetchThirdPartyToken_,
289 this.clientPairingId_, this.clientPairedSecret_, true);
293 * Cancel a connection-in-progress.
295 remoting.SessionConnector.prototype.cancel = function() {
296 if (this.clientSession_) {
297 this.clientSession_.removePlugin();
298 this.clientSession_ = null;
300 if (this.pendingXhr_) {
301 this.pendingXhr_.abort();
302 this.pendingXhr_ = null;
308 * Get the connection mode (Me2Me or IT2Me)
310 * @return {remoting.ClientSession.Mode}
312 remoting.SessionConnector.prototype.getConnectionMode = function() {
313 return this.connectionMode_;
321 remoting.SessionConnector.prototype.getHostId = function() {
326 * Get host display name.
330 remoting.SessionConnector.prototype.getHostDisplayName = function() {
331 return this.hostDisplayName_;
335 * Continue an IT2Me connection once an access token has been obtained.
337 * @param {string} token An OAuth2 access token.
338 * @return {void} Nothing.
341 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
342 // Resolve the host id to get the host JID.
343 this.pendingXhr_ = remoting.xhr.get(
344 remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
345 encodeURIComponent(this.hostId_),
346 this.onIT2MeHostInfo_.bind(this),
348 { 'Authorization': 'OAuth ' + token });
352 * Continue an IT2Me connection once the host JID has been looked up.
354 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
355 * @return {void} Nothing.
358 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
359 this.pendingXhr_ = null;
360 if (xhr.status == 200) {
361 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
362 jsonParseSafe(xhr.responseText);
363 if (host && host.data && host.data.jabberId && host.data.publicKey) {
364 this.hostJid_ = host.data.jabberId;
365 this.hostPublicKey_ = host.data.publicKey;
366 this.hostDisplayName_ = this.hostJid_.split('/')[0];
367 this.createSession_();
370 console.error('Invalid "support-hosts" response from server.');
373 this.onError_(this.translateSupportHostsError(xhr.status));
378 * Creates ClientSession object.
380 remoting.SessionConnector.prototype.createSession_ = function() {
381 // In some circumstances, the WCS <iframe> can get reloaded, which results
382 // in a new clientJid and a new callback. In this case, remove the old
383 // client plugin before instantiating a new one.
384 if (this.clientSession_) {
385 this.clientSession_.removePlugin();
386 this.clientSession_ = null;
389 var authenticationMethods =
390 'third_party,spake2_pair,spake2_hmac,spake2_plain';
391 this.clientSession_ = new remoting.ClientSession(
392 this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_,
393 authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
394 this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
395 this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
396 this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
397 this.clientSession_.createPluginAndConnect(this.pluginParent_,
398 this.onExtensionMessage_);
402 * Handle a change in the state of the client session prior to successful
403 * connection (after connection, this class no longer handles state change
404 * events). Errors that occur while connecting either trigger a reconnect
405 * or notify the onError handler.
407 * @param {number} oldState The previous state of the plugin.
408 * @param {number} newState The current state of the plugin.
409 * @return {void} Nothing.
412 remoting.SessionConnector.prototype.onStateChange_ =
413 function(oldState, newState) {
415 case remoting.ClientSession.State.CONNECTED:
416 // When the connection succeeds, deregister for state-change callbacks
417 // and pass the session to the onOk callback. It is expected that it
418 // will register a new state-change callback to handle disconnect
419 // or error conditions.
420 this.clientSession_.setOnStateChange(null);
421 this.onOk_(this.clientSession_);
424 case remoting.ClientSession.State.CREATED:
425 console.log('Created plugin');
428 case remoting.ClientSession.State.CONNECTING:
429 console.log('Connecting as ' + remoting.identity.getCachedEmail());
432 case remoting.ClientSession.State.INITIALIZING:
433 console.log('Initializing connection');
436 case remoting.ClientSession.State.CLOSED:
437 // This class deregisters for state-change callbacks when the CONNECTED
438 // state is reached, so it only sees the CLOSED state in exceptional
439 // circumstances. For example, a CONNECTING -> CLOSED transition happens
440 // if the host closes the connection without an error message instead of
441 // accepting it. Since there's no way of knowing exactly what went wrong,
442 // we rely on server-side logs in this case and report a generic error
444 this.onError_(remoting.Error.UNEXPECTED);
447 case remoting.ClientSession.State.FAILED:
448 var error = this.clientSession_.getError();
449 console.error('Client plugin reported connection failed: ' + error);
451 error = remoting.Error.UNEXPECTED;
453 if (error == remoting.Error.HOST_IS_OFFLINE &&
454 this.refreshHostJidIfOffline_) {
455 remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
457 this.onError_(error);
462 console.error('Unexpected client plugin state: ' + newState);
463 // This should only happen if the web-app and client plugin get out of
464 // sync, and even then the version check should ensure compatibility.
465 this.onError_(remoting.Error.MISSING_PLUGIN);
470 * @param {boolean} success True if the host list was successfully refreshed;
471 * false if an error occurred.
474 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
476 var host = remoting.hostList.getHostForId(this.hostId_);
478 this.connectMe2MeInternal_(
479 host.hostId, host.jabberId, host.publicKey, host.hostName,
480 this.fetchPin_, this.fetchThirdPartyToken_,
481 this.clientPairingId_, this.clientPairedSecret_, false);
485 this.onError_(remoting.Error.HOST_IS_OFFLINE);
489 * @param {number} error An HTTP error code returned by the support-hosts
491 * @return {remoting.Error} The equivalent remoting.Error code.
494 remoting.SessionConnector.prototype.translateSupportHostsError =
497 case 0: return remoting.Error.NETWORK_FAILURE;
498 case 404: return remoting.Error.INVALID_ACCESS_CODE;
499 case 502: // No break
500 case 503: return remoting.Error.SERVICE_UNAVAILABLE;
501 default: return remoting.Error.UNEXPECTED;
506 * Normalize the access code entered by the user.
508 * @param {string} accessCode The access code, as entered by the user.
509 * @return {string} The normalized form of the code (whitespace removed).
511 remoting.SessionConnector.prototype.normalizeAccessCode_ =
512 function(accessCode) {
514 return accessCode.replace(/\s/g, '');