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;
24 * @implements {remoting.SessionConnector}
26 remoting
.SessionConnectorImpl = function(clientContainer
, onConnected
, onError
,
32 this.clientContainer_
= clientContainer
;
35 * @type {function(remoting.ClientSession):void}
38 this.onConnected_
= onConnected
;
41 * @type {function(remoting.Error):void}
44 this.onError_
= onError
;
47 * @type {function(string, string):boolean}
50 this.onExtensionMessage_
= onExtensionMessage
;
59 * @type {remoting.ClientSession.Mode}
62 this.connectionMode_
= remoting
.ClientSession
.Mode
.ME2ME
;
65 * @type {remoting.SignalStrategy}
68 this.signalStrategy_
= null;
71 * @type {remoting.SmartReconnector}
74 this.reconnector_
= null;
80 onStateChange
: this.onStateChange_
.bind(this)
83 // Initialize/declare per-connection state.
88 * Reset the per-connection state so that the object can be re-used for a
89 * second connection. Note the none of the shared WCS state is reset.
91 remoting
.SessionConnectorImpl
.prototype.reset = function() {
93 * String used to identify the host to which to connect. For IT2Me, this is
94 * the first 7 digits of the access code; for Me2Me it is the host identifier.
102 * For paired connections, the client id of this device, issued by the host.
107 this.clientPairingId_
= '';
110 * For paired connections, the paired secret for this device, issued by the
116 this.clientPairedSecret_
= '';
119 * String used to authenticate to the host on connection. For IT2Me, this is
120 * the access code; for Me2Me it is the PIN.
125 this.passPhrase_
= '';
137 this.hostPublicKey_
= '';
143 this.refreshHostJidIfOffline_
= false;
146 * @type {remoting.ClientSession}
149 this.clientSession_
= null;
152 * @type {XMLHttpRequest}
155 this.pendingXhr_
= null;
158 * Function to interactively obtain the PIN from the user.
159 * @type {function(boolean, function(string):void):void}
162 this.fetchPin_ = function(onPinFetched
) {};
165 * @type {function(string, string, string,
166 * function(string, string):void): void}
169 this.fetchThirdPartyToken_ = function(
170 tokenUrl
, scope
, onThirdPartyTokenFetched
) {};
173 * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
174 * this is the name of the host; for an IT2Me connection, it is the email
175 * address of the person sharing their computer.
180 this.hostDisplayName_
= '';
184 * Initiate a Me2Me connection.
186 * @param {remoting.Host} host The Me2Me host to which to connect.
187 * @param {function(boolean, function(string):void):void} fetchPin Function to
188 * interactively obtain the PIN from the user.
189 * @param {function(string, string, string,
190 * function(string, string): void): void}
191 * fetchThirdPartyToken Function to obtain a token from a third party
192 * authenticaiton server.
193 * @param {string} clientPairingId The client id issued by the host when
194 * this device was paired, if it is already paired.
195 * @param {string} clientPairedSecret The shared secret issued by the host when
196 * this device was paired, if it is already paired.
197 * @return {void} Nothing.
199 remoting
.SessionConnectorImpl
.prototype.connectMe2Me
=
200 function(host
, fetchPin
, fetchThirdPartyToken
,
201 clientPairingId
, clientPairedSecret
) {
202 this.connectMe2MeInternal_(
203 host
.hostId
, host
.jabberId
, host
.publicKey
, host
.hostName
,
204 fetchPin
, fetchThirdPartyToken
,
205 clientPairingId
, clientPairedSecret
, true);
209 * Update the pairing info so that the reconnect function will work correctly.
211 * @param {string} clientId The paired client id.
212 * @param {string} sharedSecret The shared secret.
214 remoting
.SessionConnectorImpl
.prototype.updatePairingInfo
=
215 function(clientId
, sharedSecret
) {
216 this.clientPairingId_
= clientId
;
217 this.clientPairedSecret_
= sharedSecret
;
221 * Initiate a Me2Me connection.
223 * @param {string} hostId ID of the Me2Me host.
224 * @param {string} hostJid XMPP JID of the host.
225 * @param {string} hostPublicKey Public Key of the host.
226 * @param {string} hostDisplayName Display name (friendly name) of the host.
227 * @param {function(boolean, function(string):void):void} fetchPin Function to
228 * interactively obtain the PIN from the user.
229 * @param {function(string, string, string,
230 * function(string, string): void): void}
231 * fetchThirdPartyToken Function to obtain a token from a third party
232 * authenticaiton server.
233 * @param {string} clientPairingId The client id issued by the host when
234 * this device was paired, if it is already paired.
235 * @param {string} clientPairedSecret The shared secret issued by the host when
236 * this device was paired, if it is already paired.
237 * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
238 * the connection if the current JID is offline.
239 * @return {void} Nothing.
242 remoting
.SessionConnectorImpl
.prototype.connectMe2MeInternal_
=
243 function(hostId
, hostJid
, hostPublicKey
, hostDisplayName
,
244 fetchPin
, fetchThirdPartyToken
,
245 clientPairingId
, clientPairedSecret
,
246 refreshHostJidIfOffline
) {
247 // Cancel any existing connect operation.
250 this.hostId_
= hostId
;
251 this.hostJid_
= hostJid
;
252 this.hostPublicKey_
= hostPublicKey
;
253 this.fetchPin_
= fetchPin
;
254 this.fetchThirdPartyToken_
= fetchThirdPartyToken
;
255 this.hostDisplayName_
= hostDisplayName
;
256 this.connectionMode_
= remoting
.ClientSession
.Mode
.ME2ME
;
257 this.refreshHostJidIfOffline_
= refreshHostJidIfOffline
;
258 this.updatePairingInfo(clientPairingId
, clientPairedSecret
);
260 this.connectSignaling_();
264 * Initiate an IT2Me connection.
266 * @param {string} accessCode The access code as entered by the user.
267 * @return {void} Nothing.
269 remoting
.SessionConnectorImpl
.prototype.connectIT2Me = function(accessCode
) {
270 var kSupportIdLen
= 7;
271 var kHostSecretLen
= 5;
272 var kAccessCodeLen
= kSupportIdLen
+ kHostSecretLen
;
274 // Cancel any existing connect operation.
277 var normalizedAccessCode
= this.normalizeAccessCode_(accessCode
);
278 if (normalizedAccessCode
.length
!= kAccessCodeLen
) {
279 this.onError_(remoting
.Error
.INVALID_ACCESS_CODE
);
283 this.hostId_
= normalizedAccessCode
.substring(0, kSupportIdLen
);
284 this.passPhrase_
= normalizedAccessCode
;
285 this.connectionMode_
= remoting
.ClientSession
.Mode
.IT2ME
;
286 remoting
.identity
.callWithToken(this.connectIT2MeWithToken_
.bind(this),
291 * Reconnect a closed connection.
293 * @return {void} Nothing.
295 remoting
.SessionConnectorImpl
.prototype.reconnect = function() {
296 if (this.connectionMode_
== remoting
.ClientSession
.Mode
.IT2ME
) {
297 console
.error('reconnect not supported for IT2Me.');
300 this.connectMe2MeInternal_(
301 this.hostId_
, this.hostJid_
, this.hostPublicKey_
, this.hostDisplayName_
,
302 this.fetchPin_
, this.fetchThirdPartyToken_
,
303 this.clientPairingId_
, this.clientPairedSecret_
, true);
307 * Cancel a connection-in-progress.
309 remoting
.SessionConnectorImpl
.prototype.cancel = function() {
310 if (this.clientSession_
) {
311 this.clientSession_
.removePlugin();
312 this.clientSession_
= null;
314 if (this.pendingXhr_
) {
315 this.pendingXhr_
.abort();
316 this.pendingXhr_
= null;
322 * Get the connection mode (Me2Me or IT2Me)
324 * @return {remoting.ClientSession.Mode}
326 remoting
.SessionConnectorImpl
.prototype.getConnectionMode = function() {
327 return this.connectionMode_
;
335 remoting
.SessionConnectorImpl
.prototype.getHostId = function() {
342 remoting
.SessionConnectorImpl
.prototype.connectSignaling_ = function() {
343 base
.dispose(this.signalStrategy_
);
344 this.signalStrategy_
= null;
346 /** @type {remoting.SessionConnectorImpl} */
349 /** @param {string} token */
350 function connectSignalingWithToken(token
) {
351 remoting
.identity
.getEmail(
352 connectSignalingWithTokenAndEmail
.bind(null, token
), that
.onError_
);
356 * @param {string} token
357 * @param {string} email
359 function connectSignalingWithTokenAndEmail(token
, email
) {
360 that
.signalStrategy_
.connect(
361 remoting
.settings
.XMPP_SERVER_ADDRESS
, email
, token
);
364 this.signalStrategy_
=
365 remoting
.SignalStrategy
.create(this.onSignalingState_
.bind(this));
367 remoting
.identity
.callWithToken(connectSignalingWithToken
, this.onError_
);
372 * @param {remoting.SignalStrategy.State} state
374 remoting
.SessionConnectorImpl
.prototype.onSignalingState_ = function(state
) {
376 case remoting
.SignalStrategy
.State
.CONNECTED
:
377 // Proceed only if the connection hasn't been canceled.
379 this.createSession_();
383 case remoting
.SignalStrategy
.State
.FAILED
:
384 this.onError_(this.signalStrategy_
.getError());
390 * Continue an IT2Me connection once an access token has been obtained.
392 * @param {string} token An OAuth2 access token.
393 * @return {void} Nothing.
396 remoting
.SessionConnectorImpl
.prototype.connectIT2MeWithToken_
=
398 // Resolve the host id to get the host JID.
399 this.pendingXhr_
= remoting
.xhr
.get(
400 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/support-hosts/' +
401 encodeURIComponent(this.hostId_
),
402 this.onIT2MeHostInfo_
.bind(this),
404 { 'Authorization': 'OAuth ' + token
});
408 * Continue an IT2Me connection once the host JID has been looked up.
410 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
411 * @return {void} Nothing.
414 remoting
.SessionConnectorImpl
.prototype.onIT2MeHostInfo_ = function(xhr
) {
415 this.pendingXhr_
= null;
416 if (xhr
.status
== 200) {
417 var host
= /** @type {{data: {jabberId: string, publicKey: string}}} */
418 jsonParseSafe(xhr
.responseText
);
419 if (host
&& host
.data
&& host
.data
.jabberId
&& host
.data
.publicKey
) {
420 this.hostJid_
= host
.data
.jabberId
;
421 this.hostPublicKey_
= host
.data
.publicKey
;
422 this.hostDisplayName_
= this.hostJid_
.split('/')[0];
423 this.connectSignaling_();
426 console
.error('Invalid "support-hosts" response from server.');
429 this.onError_(this.translateSupportHostsError_(xhr
.status
));
434 * Creates ClientSession object.
436 remoting
.SessionConnectorImpl
.prototype.createSession_ = function() {
437 // In some circumstances, the WCS <iframe> can get reloaded, which results
438 // in a new clientJid and a new callback. In this case, remove the old
439 // client plugin before instantiating a new one.
440 if (this.clientSession_
) {
441 this.clientSession_
.removePlugin();
442 this.clientSession_
= null;
445 var authenticationMethods
=
446 'third_party,spake2_pair,spake2_hmac,spake2_plain';
447 this.clientSession_
= new remoting
.ClientSession(
448 this.signalStrategy_
, this.clientContainer_
, this.hostDisplayName_
,
449 this.passPhrase_
, this.fetchPin_
, this.fetchThirdPartyToken_
,
450 authenticationMethods
, this.hostId_
, this.hostJid_
, this.hostPublicKey_
,
451 this.connectionMode_
, this.clientPairingId_
, this.clientPairedSecret_
);
452 this.clientSession_
.logHostOfflineErrors(!this.refreshHostJidIfOffline_
);
453 this.clientSession_
.addEventListener(
454 remoting
.ClientSession
.Events
.stateChanged
,
455 this.bound_
.onStateChange
);
456 this.clientSession_
.createPluginAndConnect(this.onExtensionMessage_
);
460 * Handle a change in the state of the client session prior to successful
461 * connection (after connection, this class no longer handles state change
462 * events). Errors that occur while connecting either trigger a reconnect
463 * or notify the onError handler.
465 * @param {remoting.ClientSession.StateEvent} event
466 * @return {void} Nothing.
469 remoting
.SessionConnectorImpl
.prototype.onStateChange_ = function(event
) {
470 switch (event
.current
) {
471 case remoting
.ClientSession
.State
.CONNECTED
:
472 // When the connection succeeds, deregister for state-change callbacks
473 // and pass the session to the onConnected callback. It is expected that
474 // it will register a new state-change callback to handle disconnect
475 // or error conditions.
476 this.clientSession_
.removeEventListener(
477 remoting
.ClientSession
.Events
.stateChanged
,
478 this.bound_
.onStateChange
);
480 base
.dispose(this.reconnector_
);
481 if (this.connectionMode_
!= remoting
.ClientSession
.Mode
.IT2ME
) {
483 new remoting
.SmartReconnector(this, this.clientSession_
);
485 this.onConnected_(this.clientSession_
);
488 case remoting
.ClientSession
.State
.CREATED
:
489 console
.log('Created plugin');
492 case remoting
.ClientSession
.State
.CONNECTING
:
493 console
.log('Connecting as ' + remoting
.identity
.getCachedEmail());
496 case remoting
.ClientSession
.State
.INITIALIZING
:
497 console
.log('Initializing connection');
500 case remoting
.ClientSession
.State
.CLOSED
:
501 // This class deregisters for state-change callbacks when the CONNECTED
502 // state is reached, so it only sees the CLOSED state in exceptional
503 // circumstances. For example, a CONNECTING -> CLOSED transition happens
504 // if the host closes the connection without an error message instead of
505 // accepting it. Since there's no way of knowing exactly what went wrong,
506 // we rely on server-side logs in this case and report a generic error
508 this.onError_(remoting
.Error
.UNEXPECTED
);
511 case remoting
.ClientSession
.State
.FAILED
:
512 var error
= this.clientSession_
.getError();
513 console
.error('Client plugin reported connection failed: ' + error
);
515 error
= remoting
.Error
.UNEXPECTED
;
517 if (error
== remoting
.Error
.HOST_IS_OFFLINE
&&
518 this.refreshHostJidIfOffline_
) {
519 // The plugin will be re-created when the host finished refreshing
520 remoting
.hostList
.refresh(this.onHostListRefresh_
.bind(this));
522 this.onError_(error
);
527 console
.error('Unexpected client plugin state: ' + event
.current
);
528 // This should only happen if the web-app and client plugin get out of
529 // sync, and even then the version check should ensure compatibility.
530 this.onError_(remoting
.Error
.MISSING_PLUGIN
);
535 * @param {boolean} success True if the host list was successfully refreshed;
536 * false if an error occurred.
539 remoting
.SessionConnectorImpl
.prototype.onHostListRefresh_ = function(success
) {
541 var host
= remoting
.hostList
.getHostForId(this.hostId_
);
543 this.connectMe2MeInternal_(
544 host
.hostId
, host
.jabberId
, host
.publicKey
, host
.hostName
,
545 this.fetchPin_
, this.fetchThirdPartyToken_
,
546 this.clientPairingId_
, this.clientPairedSecret_
, false);
550 this.onError_(remoting
.Error
.HOST_IS_OFFLINE
);
554 * @param {number} error An HTTP error code returned by the support-hosts
556 * @return {remoting.Error} The equivalent remoting.Error code.
559 remoting
.SessionConnectorImpl
.prototype.translateSupportHostsError_
=
562 case 0: return remoting
.Error
.NETWORK_FAILURE
;
563 case 404: return remoting
.Error
.INVALID_ACCESS_CODE
;
564 case 502: // No break
565 case 503: return remoting
.Error
.SERVICE_UNAVAILABLE
;
566 default: return remoting
.Error
.UNEXPECTED
;
571 * Normalize the access code entered by the user.
573 * @param {string} accessCode The access code, as entered by the user.
574 * @return {string} The normalized form of the code (whitespace removed).
577 remoting
.SessionConnectorImpl
.prototype.normalizeAccessCode_
=
578 function(accessCode
) {
580 return accessCode
.replace(/\s/g, '');
586 * @implements {remoting.SessionConnectorFactory}
588 remoting
.DefaultSessionConnectorFactory = function() {
592 * @param {HTMLElement} clientContainer Container element for the client view.
593 * @param {function(remoting.ClientSession):void} onConnected Callback on
595 * @param {function(remoting.Error):void} onError Callback on error.
596 * @param {function(string, string):boolean} onExtensionMessage The handler for
597 * protocol extension messages. Returns true if a message is recognized;
600 remoting
.DefaultSessionConnectorFactory
.prototype.createConnector
=
601 function(clientContainer
, onConnected
, onError
, onExtensionMessage
) {
602 return new remoting
.SessionConnectorImpl(
603 clientContainer
, onConnected
, onError
, onExtensionMessage
);