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 {function(remoting.Error):void} onConnectionFailed Callback for when
24 * the connection fails.
25 * @param {Array<string>} requiredCapabilities Connector capabilities
26 * required by this application.
27 * @param {string} defaultRemapKeys The default set of key mappings for the
28 * client session to use.
30 * @implements {remoting.SessionConnector}
32 remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError,
41 this.clientContainer_ = clientContainer;
44 * @type {function(remoting.ClientSession):void}
47 this.onConnected_ = onConnected;
50 * @type {function(remoting.Error):void}
53 this.onError_ = onError;
56 * @type {function(string, string):boolean}
59 this.onExtensionMessage_ = onExtensionMessage;
62 * @type {function(remoting.Error):void}
65 this.onConnectionFailed_ = onConnectionFailed;
68 * @type {Array<string>}
71 this.requiredCapabilities_ = requiredCapabilities;
77 this.defaultRemapKeys_ = defaultRemapKeys;
86 * @type {remoting.DesktopConnectedView.Mode}
89 this.connectionMode_ = remoting.DesktopConnectedView.Mode.ME2ME;
92 * @type {remoting.SignalStrategy}
95 this.signalStrategy_ = null;
98 * @type {remoting.SmartReconnector}
101 this.reconnector_ = null;
107 onStateChange : this.onStateChange_.bind(this)
110 // Initialize/declare per-connection state.
115 * Reset the per-connection state so that the object can be re-used for a
116 * second connection. Note the none of the shared WCS state is reset.
118 remoting.SessionConnectorImpl.prototype.reset = function() {
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_ = '';
146 * @type {remoting.Host}
155 this.logHostOfflineErrors_ = false;
158 * @type {remoting.ClientSession}
161 this.clientSession_ = null;
164 * @type {XMLHttpRequest}
167 this.pendingXhr_ = null;
170 * Function to interactively obtain the PIN from the user.
171 * @type {function(boolean, function(string):void):void}
174 this.fetchPin_ = function(onPinFetched) {};
177 * @type {function(string, string, string,
178 * function(string, string):void): void}
181 this.fetchThirdPartyToken_ = function(
182 tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) {};
186 * Initiate a Me2Me connection.
188 * This doesn't report host-offline errors because the connection will
189 * be retried and retryConnectMe2Me is responsible for reporting these errors.
191 * @param {remoting.Host} host The Me2Me host to which to connect.
192 * @param {function(boolean, function(string):void):void} fetchPin Function to
193 * interactively obtain the PIN from the user.
194 * @param {function(string, string, string,
195 * function(string, string): void): void}
196 * fetchThirdPartyToken Function to obtain a token from a third party
197 * authentication server.
198 * @param {string} clientPairingId The client id issued by the host when
199 * this device was paired, if it is already paired.
200 * @param {string} clientPairedSecret The shared secret issued by the host when
201 * this device was paired, if it is already paired.
202 * @return {void} Nothing.
204 remoting.SessionConnectorImpl.prototype.connectMe2Me =
205 function(host, fetchPin, fetchThirdPartyToken,
206 clientPairingId, clientPairedSecret) {
207 this.connectionMode_ = remoting.DesktopConnectedView.Mode.ME2ME;
208 this.logHostOfflineErrors_ = false;
209 this.connectMe2MeInternal_(host, fetchPin, fetchThirdPartyToken,
210 clientPairingId, clientPairedSecret);
214 * Retry connecting to a Me2Me host after a connection failure.
216 * This is the same as connectMe2Me except that is will log errors if the
219 * @param {remoting.Host} host The Me2Me host to refresh.
220 * @return {void} Nothing.
222 remoting.SessionConnectorImpl.prototype.retryConnectMe2Me = function(host) {
223 this.connectionMode_ = remoting.DesktopConnectedView.Mode.ME2ME;
224 this.logHostOfflineErrors_ = true;
225 this.connectMe2MeInternal_(host, this.fetchPin_, this.fetchThirdPartyToken_,
226 this.clientPairingId_, this.clientPairedSecret_);
230 * Initiate a Me2App connection.
232 * @param {remoting.Host} host The Me2Me host to which to connect.
233 * @param {function(string, string, string,
234 * function(string, string): void): void}
235 * fetchThirdPartyToken Function to obtain a token from a third party
236 * authenticaiton server.
237 * @return {void} Nothing.
239 remoting.SessionConnectorImpl.prototype.connectMe2App =
240 function(host, fetchThirdPartyToken) {
241 this.connectionMode_ = remoting.DesktopConnectedView.Mode.APP_REMOTING;
242 this.logHostOfflineErrors_ = true;
243 this.connectMe2MeInternal_(host, function() {}, fetchThirdPartyToken, '', '');
247 * Update the pairing info so that the reconnect function will work correctly.
249 * @param {string} clientId The paired client id.
250 * @param {string} sharedSecret The shared secret.
252 remoting.SessionConnectorImpl.prototype.updatePairingInfo =
253 function(clientId, sharedSecret) {
254 this.clientPairingId_ = clientId;
255 this.clientPairedSecret_ = sharedSecret;
259 * Initiate a Me2Me connection.
261 * @param {remoting.Host} host the Host to connect to.
262 * @param {function(boolean, function(string):void):void} fetchPin Function to
263 * interactively obtain the PIN from the user.
264 * @param {function(string, string, string,
265 * function(string, string): void): void}
266 * fetchThirdPartyToken Function to obtain a token from a third party
267 * authentication server.
268 * @param {string} clientPairingId The client id issued by the host when
269 * this device was paired, if it is already paired.
270 * @param {string} clientPairedSecret The shared secret issued by the host when
271 * this device was paired, if it is already paired.
272 * @return {void} Nothing.
275 remoting.SessionConnectorImpl.prototype.connectMe2MeInternal_ =
276 function(host, fetchPin, fetchThirdPartyToken,
277 clientPairingId, clientPairedSecret) {
278 // Cancel any existing connect operation.
282 this.fetchPin_ = fetchPin;
283 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
284 this.updatePairingInfo(clientPairingId, clientPairedSecret);
286 this.connectSignaling_();
290 * Initiate an IT2Me connection.
292 * @param {string} accessCode The access code as entered by the user.
293 * @return {void} Nothing.
295 remoting.SessionConnectorImpl.prototype.connectIT2Me = function(accessCode) {
296 var kSupportIdLen = 7;
297 var kHostSecretLen = 5;
298 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
300 // Cancel any existing connect operation.
303 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
304 if (normalizedAccessCode.length != kAccessCodeLen) {
305 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
309 var hostId = normalizedAccessCode.substring(0, kSupportIdLen);
310 this.passPhrase_ = normalizedAccessCode;
311 this.connectionMode_ = remoting.DesktopConnectedView.Mode.IT2ME;
312 remoting.identity.getToken().then(
313 this.connectIT2MeWithToken_.bind(this, hostId),
314 remoting.Error.handler(this.onError_));
318 * Reconnect a closed connection.
320 * @return {void} Nothing.
322 remoting.SessionConnectorImpl.prototype.reconnect = function() {
323 if (this.connectionMode_ == remoting.DesktopConnectedView.Mode.IT2ME) {
324 console.error('reconnect not supported for IT2Me.');
327 this.logHostOfflineErrors_ = false;
328 this.connectMe2MeInternal_(this.host_, this.fetchPin_,
329 this.fetchThirdPartyToken_, this.clientPairingId_,
330 this.clientPairedSecret_);
334 * Cancel a connection-in-progress.
336 remoting.SessionConnectorImpl.prototype.cancel = function() {
337 if (this.clientSession_) {
338 this.clientSession_.removePlugin();
339 this.clientSession_ = null;
341 if (this.pendingXhr_) {
342 this.pendingXhr_.abort();
343 this.pendingXhr_ = null;
349 * Get the connection mode (Me2Me or IT2Me)
351 * @return {remoting.DesktopConnectedView.Mode}
353 remoting.SessionConnectorImpl.prototype.getConnectionMode = function() {
354 return this.connectionMode_;
362 remoting.SessionConnectorImpl.prototype.getHostId = function() {
363 return this.host_.hostId;
369 remoting.SessionConnectorImpl.prototype.connectSignaling_ = function() {
370 base.dispose(this.signalStrategy_);
371 this.signalStrategy_ = null;
373 /** @type {remoting.SessionConnectorImpl} */
376 /** @param {string} token */
377 function connectSignalingWithToken(token) {
378 remoting.identity.getUserInfo().then(
379 connectSignalingWithTokenAndUserInfo.bind(null, token),
380 remoting.Error.handler(that.onError_));
384 * Success callback for when the email and fullName have been retrieved
386 * Note that the full name will be null unless the webapp has requested
387 * and been granted the userinfo.profile permission.
389 * @param {string} token
390 * @param {{email: string, name: string}} userInfo
392 function connectSignalingWithTokenAndUserInfo(token, userInfo) {
393 that.signalStrategy_.connect(
394 remoting.settings.XMPP_SERVER_FOR_CLIENT, userInfo.email, token);
397 this.signalStrategy_ = remoting.SignalStrategy.create();
398 this.signalStrategy_.setStateChangedCallback(
399 this.onSignalingState_.bind(this));
401 remoting.identity.getToken().then(
402 connectSignalingWithToken,
403 remoting.Error.handler(this.onError_));
408 * @param {remoting.SignalStrategy.State} state
410 remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) {
412 case remoting.SignalStrategy.State.CONNECTED:
413 // Proceed only if the connection hasn't been canceled.
414 if (this.host_.jabberId) {
415 this.createSession_();
419 case remoting.SignalStrategy.State.FAILED:
420 this.onError_(this.signalStrategy_.getError());
426 * Continue an IT2Me connection once an access token has been obtained.
428 * @param {string} hostId
429 * @param {string} token An OAuth2 access token.
430 * @return {void} Nothing.
433 remoting.SessionConnectorImpl.prototype.connectIT2MeWithToken_ =
434 function(hostId, token) {
435 // Resolve the host id to get the host JID.
436 this.pendingXhr_ = remoting.xhr.start({
438 url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
439 encodeURIComponent(hostId),
440 onDone: this.onIT2MeHostInfo_.bind(this),
446 * Continue an IT2Me connection once the host JID has been looked up.
448 * @param {string} hostId
449 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
450 * @return {void} Nothing.
453 remoting.SessionConnectorImpl.prototype.onIT2MeHostInfo_ =
454 function(hostId, xhr) {
455 this.pendingXhr_ = null;
456 if (xhr.status == 200) {
457 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
458 (base.jsonParseSafe(xhr.responseText));
459 if (host && host.data && host.data.jabberId && host.data.publicKey) {
460 this.host_ = new remoting.Host();
461 this.host_.hostId = hostId;
462 this.host_.jabberId = host.data.jabberId;
463 this.host_.publicKey = host.data.publicKey;
464 this.host_.hostName = host.data.jabberId.split('/')[0];
465 this.connectSignaling_();
468 console.error('Invalid "support-hosts" response from server.');
471 this.onError_(this.translateSupportHostsError_(xhr.status));
476 * Creates ClientSession object.
478 remoting.SessionConnectorImpl.prototype.createSession_ = function() {
479 // In some circumstances, the WCS <iframe> can get reloaded, which results
480 // in a new clientJid and a new callback. In this case, remove the old
481 // client plugin before instantiating a new one.
482 if (this.clientSession_) {
483 this.clientSession_.removePlugin();
484 this.clientSession_ = null;
487 var authenticationMethods =
488 'third_party,spake2_pair,spake2_hmac,spake2_plain';
489 this.clientSession_ = new remoting.ClientSession(
490 this.host_, this.signalStrategy_, this.clientContainer_, this.passPhrase_,
491 this.fetchPin_, this.fetchThirdPartyToken_, authenticationMethods,
492 this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_,
493 this.defaultRemapKeys_);
494 this.clientSession_.logHostOfflineErrors(this.logHostOfflineErrors_);
495 this.clientSession_.addEventListener(
496 remoting.ClientSession.Events.stateChanged,
497 this.bound_.onStateChange);
498 this.clientSession_.createPluginAndConnect(this.onExtensionMessage_,
499 this.requiredCapabilities_);
503 * Handle a change in the state of the client session prior to successful
504 * connection (after connection, this class no longer handles state change
505 * events). Errors that occur while connecting either trigger a reconnect
506 * or notify the onError handler.
508 * @param {remoting.ClientSession.StateEvent=} event
509 * @return {void} Nothing.
512 remoting.SessionConnectorImpl.prototype.onStateChange_ = function(event) {
513 switch (event.current) {
514 case remoting.ClientSession.State.CONNECTED:
515 // When the connection succeeds, deregister for state-change callbacks
516 // and pass the session to the onConnected callback. It is expected that
517 // it will register a new state-change callback to handle disconnect
518 // or error conditions.
519 this.clientSession_.removeEventListener(
520 remoting.ClientSession.Events.stateChanged,
521 this.bound_.onStateChange);
523 base.dispose(this.reconnector_);
524 if (this.connectionMode_ != remoting.DesktopConnectedView.Mode.IT2ME) {
526 new remoting.SmartReconnector(this, this.clientSession_);
528 this.onConnected_(this.clientSession_);
531 case remoting.ClientSession.State.CREATED:
532 console.log('Created plugin');
535 case remoting.ClientSession.State.CONNECTING:
536 console.log('Connecting as ' + remoting.identity.getCachedEmail());
539 case remoting.ClientSession.State.INITIALIZING:
540 console.log('Initializing connection');
543 case remoting.ClientSession.State.CLOSED:
544 // This class deregisters for state-change callbacks when the CONNECTED
545 // state is reached, so it only sees the CLOSED state in exceptional
546 // circumstances. For example, a CONNECTING -> CLOSED transition happens
547 // if the host closes the connection without an error message instead of
548 // accepting it. Since there's no way of knowing exactly what went wrong,
549 // we rely on server-side logs in this case and report a generic error
551 this.onError_(remoting.Error.UNEXPECTED);
554 case remoting.ClientSession.State.FAILED:
555 var error = this.clientSession_.getError();
556 console.error('Client plugin reported connection failed: ' + error);
558 error = remoting.Error.UNEXPECTED;
560 this.onConnectionFailed_(error);
564 console.error('Unexpected client plugin state: ' + event.current);
565 // This should only happen if the web-app and client plugin get out of
566 // sync, and even then the version check should ensure compatibility.
567 this.onError_(remoting.Error.MISSING_PLUGIN);
572 * @param {number} error An HTTP error code returned by the support-hosts
574 * @return {remoting.Error} The equivalent remoting.Error code.
577 remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ =
580 case 0: return remoting.Error.NETWORK_FAILURE;
581 case 404: return remoting.Error.INVALID_ACCESS_CODE;
582 case 502: // No break
583 case 503: return remoting.Error.SERVICE_UNAVAILABLE;
584 default: return remoting.Error.UNEXPECTED;
589 * Normalize the access code entered by the user.
591 * @param {string} accessCode The access code, as entered by the user.
592 * @return {string} The normalized form of the code (whitespace removed).
595 remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ =
596 function(accessCode) {
598 return accessCode.replace(/\s/g, '');
604 * @implements {remoting.SessionConnectorFactory}
606 remoting.DefaultSessionConnectorFactory = function() {
610 * @param {HTMLElement} clientContainer Container element for the client view.
611 * @param {function(remoting.ClientSession):void} onConnected Callback on
613 * @param {function(remoting.Error):void} onError Callback on error.
614 * @param {function(string, string):boolean} onExtensionMessage The handler for
615 * protocol extension messages. Returns true if a message is recognized;
617 * @param {function(remoting.Error):void} onConnectionFailed Callback for when
618 * the connection fails.
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.
623 * @return {remoting.SessionConnector}
625 remoting.DefaultSessionConnectorFactory.prototype.createConnector =
626 function(clientContainer, onConnected, onError, onExtensionMessage,
627 onConnectionFailed, requiredCapabilities, defaultRemapKeys) {
628 return new remoting.SessionConnectorImpl(clientContainer, onConnected,
629 onError, onExtensionMessage,
631 requiredCapabilities,