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 {HTMLElement} clientContainer Container element for the client view.
17 * @param {function(remoting.ClientSession):void} onConnected Callback on
19 * @param {function(remoting.Error):void} onError Callback on error.
20 * @param {function(string, string):boolean} onExtensionMessage The handler for
21 * protocol extension messages. Returns true if a message is recognized;
23 * @param {Array.<string>} requiredCapabilities Connector capabilities
24 * required by this application.
25 * @param {string} defaultRemapKeys The default set of key mappings for the
26 * client session to use.
28 * @implements {remoting.SessionConnector}
30 remoting
.SessionConnectorImpl = function(clientContainer
, onConnected
, onError
,
38 this.clientContainer_
= clientContainer
;
41 * @type {function(remoting.ClientSession):void}
44 this.onConnected_
= onConnected
;
47 * @type {function(remoting.Error):void}
50 this.onError_
= onError
;
53 * @type {function(string, string):boolean}
56 this.onExtensionMessage_
= onExtensionMessage
;
59 * @type {Array.<string>}
62 this.requiredCapabilities_
= requiredCapabilities
;
68 this.defaultRemapKeys_
= defaultRemapKeys
;
77 * @type {remoting.ClientSession.Mode}
80 this.connectionMode_
= remoting
.ClientSession
.Mode
.ME2ME
;
83 * @type {remoting.SignalStrategy}
86 this.signalStrategy_
= null;
89 * @type {remoting.SmartReconnector}
92 this.reconnector_
= null;
98 onStateChange
: this.onStateChange_
.bind(this)
101 // Initialize/declare per-connection state.
106 * Reset the per-connection state so that the object can be re-used for a
107 * second connection. Note the none of the shared WCS state is reset.
109 remoting
.SessionConnectorImpl
.prototype.reset = function() {
111 * String used to identify the host to which to connect. For IT2Me, this is
112 * the first 7 digits of the access code; for Me2Me it is the host identifier.
120 * For paired connections, the client id of this device, issued by the host.
125 this.clientPairingId_
= '';
128 * For paired connections, the paired secret for this device, issued by the
134 this.clientPairedSecret_
= '';
137 * String used to authenticate to the host on connection. For IT2Me, this is
138 * the access code; for Me2Me it is the PIN.
143 this.passPhrase_
= '';
155 this.hostPublicKey_
= '';
161 this.refreshHostJidIfOffline_
= false;
164 * @type {remoting.ClientSession}
167 this.clientSession_
= null;
170 * @type {XMLHttpRequest}
173 this.pendingXhr_
= null;
176 * Function to interactively obtain the PIN from the user.
177 * @type {function(boolean, function(string):void):void}
180 this.fetchPin_ = function(onPinFetched
) {};
183 * @type {function(string, string, string,
184 * function(string, string):void): void}
187 this.fetchThirdPartyToken_ = function(
188 tokenUrl
, scope
, onThirdPartyTokenFetched
) {};
191 * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
192 * this is the name of the host; for an IT2Me connection, it is the email
193 * address of the person sharing their computer.
198 this.hostDisplayName_
= '';
202 * Initiate a Me2Me connection.
204 * @param {remoting.Host} host The Me2Me host to which to connect.
205 * @param {function(boolean, function(string):void):void} fetchPin Function to
206 * interactively obtain the PIN from the user.
207 * @param {function(string, string, string,
208 * function(string, string): void): void}
209 * fetchThirdPartyToken Function to obtain a token from a third party
210 * authenticaiton server.
211 * @param {string} clientPairingId The client id issued by the host when
212 * this device was paired, if it is already paired.
213 * @param {string} clientPairedSecret The shared secret issued by the host when
214 * this device was paired, if it is already paired.
215 * @return {void} Nothing.
217 remoting
.SessionConnectorImpl
.prototype.connectMe2Me
=
218 function(host
, fetchPin
, fetchThirdPartyToken
,
219 clientPairingId
, clientPairedSecret
) {
220 this.connectMe2MeInternal_(
221 host
.hostId
, host
.jabberId
, host
.publicKey
, host
.hostName
,
222 fetchPin
, fetchThirdPartyToken
,
223 clientPairingId
, clientPairedSecret
, true);
227 * Update the pairing info so that the reconnect function will work correctly.
229 * @param {string} clientId The paired client id.
230 * @param {string} sharedSecret The shared secret.
232 remoting
.SessionConnectorImpl
.prototype.updatePairingInfo
=
233 function(clientId
, sharedSecret
) {
234 this.clientPairingId_
= clientId
;
235 this.clientPairedSecret_
= sharedSecret
;
239 * Initiate a Me2Me connection.
241 * @param {string} hostId ID of the Me2Me host.
242 * @param {string} hostJid XMPP JID of the host.
243 * @param {string} hostPublicKey Public Key of the host.
244 * @param {string} hostDisplayName Display name (friendly name) of the host.
245 * @param {function(boolean, function(string):void):void} fetchPin Function to
246 * interactively obtain the PIN from the user.
247 * @param {function(string, string, string,
248 * function(string, string): void): void}
249 * fetchThirdPartyToken Function to obtain a token from a third party
250 * authenticaiton server.
251 * @param {string} clientPairingId The client id issued by the host when
252 * this device was paired, if it is already paired.
253 * @param {string} clientPairedSecret The shared secret issued by the host when
254 * this device was paired, if it is already paired.
255 * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
256 * the connection if the current JID is offline.
257 * @return {void} Nothing.
260 remoting
.SessionConnectorImpl
.prototype.connectMe2MeInternal_
=
261 function(hostId
, hostJid
, hostPublicKey
, hostDisplayName
,
262 fetchPin
, fetchThirdPartyToken
,
263 clientPairingId
, clientPairedSecret
,
264 refreshHostJidIfOffline
) {
265 // Cancel any existing connect operation.
268 this.hostId_
= hostId
;
269 this.hostJid_
= hostJid
;
270 this.hostPublicKey_
= hostPublicKey
;
271 this.fetchPin_
= fetchPin
;
272 this.fetchThirdPartyToken_
= fetchThirdPartyToken
;
273 this.hostDisplayName_
= hostDisplayName
;
274 this.connectionMode_
= remoting
.ClientSession
.Mode
.ME2ME
;
275 this.refreshHostJidIfOffline_
= refreshHostJidIfOffline
;
276 this.updatePairingInfo(clientPairingId
, clientPairedSecret
);
278 this.connectSignaling_();
282 * Initiate an IT2Me connection.
284 * @param {string} accessCode The access code as entered by the user.
285 * @return {void} Nothing.
287 remoting
.SessionConnectorImpl
.prototype.connectIT2Me = function(accessCode
) {
288 var kSupportIdLen
= 7;
289 var kHostSecretLen
= 5;
290 var kAccessCodeLen
= kSupportIdLen
+ kHostSecretLen
;
292 // Cancel any existing connect operation.
295 var normalizedAccessCode
= this.normalizeAccessCode_(accessCode
);
296 if (normalizedAccessCode
.length
!= kAccessCodeLen
) {
297 this.onError_(remoting
.Error
.INVALID_ACCESS_CODE
);
301 this.hostId_
= normalizedAccessCode
.substring(0, kSupportIdLen
);
302 this.passPhrase_
= normalizedAccessCode
;
303 this.connectionMode_
= remoting
.ClientSession
.Mode
.IT2ME
;
304 remoting
.identity
.callWithToken(this.connectIT2MeWithToken_
.bind(this),
309 * Reconnect a closed connection.
311 * @return {void} Nothing.
313 remoting
.SessionConnectorImpl
.prototype.reconnect = function() {
314 if (this.connectionMode_
== remoting
.ClientSession
.Mode
.IT2ME
) {
315 console
.error('reconnect not supported for IT2Me.');
318 this.connectMe2MeInternal_(
319 this.hostId_
, this.hostJid_
, this.hostPublicKey_
, this.hostDisplayName_
,
320 this.fetchPin_
, this.fetchThirdPartyToken_
,
321 this.clientPairingId_
, this.clientPairedSecret_
, true);
325 * Cancel a connection-in-progress.
327 remoting
.SessionConnectorImpl
.prototype.cancel = function() {
328 if (this.clientSession_
) {
329 this.clientSession_
.removePlugin();
330 this.clientSession_
= null;
332 if (this.pendingXhr_
) {
333 this.pendingXhr_
.abort();
334 this.pendingXhr_
= null;
340 * Get the connection mode (Me2Me or IT2Me)
342 * @return {remoting.ClientSession.Mode}
344 remoting
.SessionConnectorImpl
.prototype.getConnectionMode = function() {
345 return this.connectionMode_
;
353 remoting
.SessionConnectorImpl
.prototype.getHostId = function() {
360 remoting
.SessionConnectorImpl
.prototype.connectSignaling_ = function() {
361 base
.dispose(this.signalStrategy_
);
362 this.signalStrategy_
= null;
364 /** @type {remoting.SessionConnectorImpl} */
367 /** @param {string} token */
368 function connectSignalingWithToken(token
) {
369 remoting
.identity
.getEmail(
370 connectSignalingWithTokenAndEmail
.bind(null, token
), that
.onError_
);
374 * @param {string} token
375 * @param {string} email
377 function connectSignalingWithTokenAndEmail(token
, email
) {
378 that
.signalStrategy_
.connect(
379 remoting
.settings
.XMPP_SERVER_ADDRESS
, email
, token
);
382 this.signalStrategy_
=
383 remoting
.SignalStrategy
.create(this.onSignalingState_
.bind(this));
385 remoting
.identity
.callWithToken(connectSignalingWithToken
, this.onError_
);
390 * @param {remoting.SignalStrategy.State} state
392 remoting
.SessionConnectorImpl
.prototype.onSignalingState_ = function(state
) {
394 case remoting
.SignalStrategy
.State
.CONNECTED
:
395 // Proceed only if the connection hasn't been canceled.
397 this.createSession_();
401 case remoting
.SignalStrategy
.State
.FAILED
:
402 this.onError_(this.signalStrategy_
.getError());
408 * Continue an IT2Me connection once an access token has been obtained.
410 * @param {string} token An OAuth2 access token.
411 * @return {void} Nothing.
414 remoting
.SessionConnectorImpl
.prototype.connectIT2MeWithToken_
=
416 // Resolve the host id to get the host JID.
417 this.pendingXhr_
= remoting
.xhr
.get(
418 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/support-hosts/' +
419 encodeURIComponent(this.hostId_
),
420 this.onIT2MeHostInfo_
.bind(this),
422 { 'Authorization': 'OAuth ' + token
});
426 * Continue an IT2Me connection once the host JID has been looked up.
428 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
429 * @return {void} Nothing.
432 remoting
.SessionConnectorImpl
.prototype.onIT2MeHostInfo_ = function(xhr
) {
433 this.pendingXhr_
= null;
434 if (xhr
.status
== 200) {
435 var host
= /** @type {{data: {jabberId: string, publicKey: string}}} */
436 base
.jsonParseSafe(xhr
.responseText
);
437 if (host
&& host
.data
&& host
.data
.jabberId
&& host
.data
.publicKey
) {
438 this.hostJid_
= host
.data
.jabberId
;
439 this.hostPublicKey_
= host
.data
.publicKey
;
440 this.hostDisplayName_
= this.hostJid_
.split('/')[0];
441 this.connectSignaling_();
444 console
.error('Invalid "support-hosts" response from server.');
447 this.onError_(this.translateSupportHostsError_(xhr
.status
));
452 * Creates ClientSession object.
454 remoting
.SessionConnectorImpl
.prototype.createSession_ = function() {
455 // In some circumstances, the WCS <iframe> can get reloaded, which results
456 // in a new clientJid and a new callback. In this case, remove the old
457 // client plugin before instantiating a new one.
458 if (this.clientSession_
) {
459 this.clientSession_
.removePlugin();
460 this.clientSession_
= null;
463 var authenticationMethods
=
464 'third_party,spake2_pair,spake2_hmac,spake2_plain';
465 this.clientSession_
= new remoting
.ClientSession(
466 this.signalStrategy_
, this.clientContainer_
, this.hostDisplayName_
,
467 this.passPhrase_
, this.fetchPin_
, this.fetchThirdPartyToken_
,
468 authenticationMethods
, this.hostId_
, this.hostJid_
, this.hostPublicKey_
,
469 this.connectionMode_
, this.clientPairingId_
, this.clientPairedSecret_
,
470 this.defaultRemapKeys_
);
471 this.clientSession_
.logHostOfflineErrors(!this.refreshHostJidIfOffline_
);
472 this.clientSession_
.addEventListener(
473 remoting
.ClientSession
.Events
.stateChanged
,
474 this.bound_
.onStateChange
);
475 this.clientSession_
.createPluginAndConnect(this.onExtensionMessage_
,
476 this.requiredCapabilities_
);
480 * Handle a change in the state of the client session prior to successful
481 * connection (after connection, this class no longer handles state change
482 * events). Errors that occur while connecting either trigger a reconnect
483 * or notify the onError handler.
485 * @param {remoting.ClientSession.StateEvent} event
486 * @return {void} Nothing.
489 remoting
.SessionConnectorImpl
.prototype.onStateChange_ = function(event
) {
490 switch (event
.current
) {
491 case remoting
.ClientSession
.State
.CONNECTED
:
492 // When the connection succeeds, deregister for state-change callbacks
493 // and pass the session to the onConnected callback. It is expected that
494 // it will register a new state-change callback to handle disconnect
495 // or error conditions.
496 this.clientSession_
.removeEventListener(
497 remoting
.ClientSession
.Events
.stateChanged
,
498 this.bound_
.onStateChange
);
500 base
.dispose(this.reconnector_
);
501 if (this.connectionMode_
!= remoting
.ClientSession
.Mode
.IT2ME
) {
503 new remoting
.SmartReconnector(this, this.clientSession_
);
505 this.onConnected_(this.clientSession_
);
508 case remoting
.ClientSession
.State
.CREATED
:
509 console
.log('Created plugin');
512 case remoting
.ClientSession
.State
.CONNECTING
:
513 console
.log('Connecting as ' + remoting
.identity
.getCachedEmail());
516 case remoting
.ClientSession
.State
.INITIALIZING
:
517 console
.log('Initializing connection');
520 case remoting
.ClientSession
.State
.CLOSED
:
521 // This class deregisters for state-change callbacks when the CONNECTED
522 // state is reached, so it only sees the CLOSED state in exceptional
523 // circumstances. For example, a CONNECTING -> CLOSED transition happens
524 // if the host closes the connection without an error message instead of
525 // accepting it. Since there's no way of knowing exactly what went wrong,
526 // we rely on server-side logs in this case and report a generic error
528 this.onError_(remoting
.Error
.UNEXPECTED
);
531 case remoting
.ClientSession
.State
.FAILED
:
532 var error
= this.clientSession_
.getError();
533 console
.error('Client plugin reported connection failed: ' + error
);
535 error
= remoting
.Error
.UNEXPECTED
;
537 if (error
== remoting
.Error
.HOST_IS_OFFLINE
&&
538 this.refreshHostJidIfOffline_
) {
539 // The plugin will be re-created when the host finished refreshing
540 remoting
.hostList
.refresh(this.onHostListRefresh_
.bind(this));
542 this.onError_(error
);
547 console
.error('Unexpected client plugin state: ' + event
.current
);
548 // This should only happen if the web-app and client plugin get out of
549 // sync, and even then the version check should ensure compatibility.
550 this.onError_(remoting
.Error
.MISSING_PLUGIN
);
555 * @param {boolean} success True if the host list was successfully refreshed;
556 * false if an error occurred.
559 remoting
.SessionConnectorImpl
.prototype.onHostListRefresh_ = function(success
) {
561 var host
= remoting
.hostList
.getHostForId(this.hostId_
);
563 this.connectMe2MeInternal_(
564 host
.hostId
, host
.jabberId
, host
.publicKey
, host
.hostName
,
565 this.fetchPin_
, this.fetchThirdPartyToken_
,
566 this.clientPairingId_
, this.clientPairedSecret_
, false);
570 this.onError_(remoting
.Error
.HOST_IS_OFFLINE
);
574 * @param {number} error An HTTP error code returned by the support-hosts
576 * @return {remoting.Error} The equivalent remoting.Error code.
579 remoting
.SessionConnectorImpl
.prototype.translateSupportHostsError_
=
582 case 0: return remoting
.Error
.NETWORK_FAILURE
;
583 case 404: return remoting
.Error
.INVALID_ACCESS_CODE
;
584 case 502: // No break
585 case 503: return remoting
.Error
.SERVICE_UNAVAILABLE
;
586 default: return remoting
.Error
.UNEXPECTED
;
591 * Normalize the access code entered by the user.
593 * @param {string} accessCode The access code, as entered by the user.
594 * @return {string} The normalized form of the code (whitespace removed).
597 remoting
.SessionConnectorImpl
.prototype.normalizeAccessCode_
=
598 function(accessCode
) {
600 return accessCode
.replace(/\s/g, '');
606 * @implements {remoting.SessionConnectorFactory}
608 remoting
.DefaultSessionConnectorFactory = function() {
612 * @param {HTMLElement} clientContainer Container element for the client view.
613 * @param {function(remoting.ClientSession):void} onConnected Callback on
615 * @param {function(remoting.Error):void} onError Callback on error.
616 * @param {function(string, string):boolean} onExtensionMessage The handler for
617 * protocol extension messages. Returns true if a message is recognized;
619 * @param {Array.<string>} requiredCapabilities Connector capabilities
620 * required by this application.
621 * @param {string} defaultRemapKeys The default set of key mappings to use
622 * in the client session.
624 remoting
.DefaultSessionConnectorFactory
.prototype.createConnector
=
625 function(clientContainer
, onConnected
, onError
, onExtensionMessage
,
626 requiredCapabilities
, defaultRemapKeys
) {
627 return new remoting
.SessionConnectorImpl(clientContainer
, onConnected
,
628 onError
, onExtensionMessage
,
629 requiredCapabilities
,