GN + Android: extract android_standalone_library rule.
[chromium-blink-merge.git] / remoting / webapp / crd / js / session_connector_impl.js
blobf0862bc23f68d5ad898415b3a7127262e229c2d6
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.
5 /**
6 * @fileoverview
7 * Connect set-up state machine for Me2Me and IT2Me
8 */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
15 /**
16 * @param {HTMLElement} clientContainer Container element for the client view.
17 * @param {function(remoting.ClientSession):void} onConnected Callback on
18 * success.
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;
22 * false otherwise.
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.
27 * @constructor
28 * @implements {remoting.SessionConnector}
30 remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError,
31 onExtensionMessage,
32 requiredCapabilities,
33 defaultRemapKeys) {
34 /**
35 * @type {HTMLElement}
36 * @private
38 this.clientContainer_ = clientContainer;
40 /**
41 * @type {function(remoting.ClientSession):void}
42 * @private
44 this.onConnected_ = onConnected;
46 /**
47 * @type {function(remoting.Error):void}
48 * @private
50 this.onError_ = onError;
52 /**
53 * @type {function(string, string):boolean}
54 * @private
56 this.onExtensionMessage_ = onExtensionMessage;
58 /**
59 * @type {Array.<string>}
60 * @private
62 this.requiredCapabilities_ = requiredCapabilities;
64 /**
65 * @type {string}
66 * @private
68 this.defaultRemapKeys_ = defaultRemapKeys;
70 /**
71 * @type {string}
72 * @private
74 this.clientJid_ = '';
76 /**
77 * @type {remoting.ClientSession.Mode}
78 * @private
80 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
82 /**
83 * @type {remoting.SignalStrategy}
84 * @private
86 this.signalStrategy_ = null;
88 /**
89 * @type {remoting.SmartReconnector}
90 * @private
92 this.reconnector_ = null;
94 /**
95 * @private
97 this.bound_ = {
98 onStateChange : this.onStateChange_.bind(this)
101 // Initialize/declare per-connection state.
102 this.reset();
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.
114 * @type {string}
115 * @private
117 this.hostId_ = '';
120 * For paired connections, the client id of this device, issued by the host.
122 * @type {string}
123 * @private
125 this.clientPairingId_ = '';
128 * For paired connections, the paired secret for this device, issued by the
129 * host.
131 * @type {string}
132 * @private
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.
140 * @type {string}
141 * @private
143 this.passPhrase_ = '';
146 * @type {string}
147 * @private
149 this.hostJid_ = '';
152 * @type {string}
153 * @private
155 this.hostPublicKey_ = '';
158 * @type {boolean}
159 * @private
161 this.refreshHostJidIfOffline_ = false;
164 * @type {remoting.ClientSession}
165 * @private
167 this.clientSession_ = null;
170 * @type {XMLHttpRequest}
171 * @private
173 this.pendingXhr_ = null;
176 * Function to interactively obtain the PIN from the user.
177 * @type {function(boolean, function(string):void):void}
178 * @private
180 this.fetchPin_ = function(onPinFetched) {};
183 * @type {function(string, string, string,
184 * function(string, string):void): void}
185 * @private
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.
195 * @type {string}
196 * @private
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.
258 * @private
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.
266 this.cancel();
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.
293 this.cancel();
295 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
296 if (normalizedAccessCode.length != kAccessCodeLen) {
297 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
298 return;
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),
305 this.onError_);
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.');
316 return;
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;
336 this.reset();
340 * Get the connection mode (Me2Me or IT2Me)
342 * @return {remoting.ClientSession.Mode}
344 remoting.SessionConnectorImpl.prototype.getConnectionMode = function() {
345 return this.connectionMode_;
349 * Get host ID.
351 * @return {string}
353 remoting.SessionConnectorImpl.prototype.getHostId = function() {
354 return this.hostId_;
358 * @private
360 remoting.SessionConnectorImpl.prototype.connectSignaling_ = function() {
361 base.dispose(this.signalStrategy_);
362 this.signalStrategy_ = null;
364 /** @type {remoting.SessionConnectorImpl} */
365 var that = this;
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_);
389 * @private
390 * @param {remoting.SignalStrategy.State} state
392 remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) {
393 switch (state) {
394 case remoting.SignalStrategy.State.CONNECTED:
395 // Proceed only if the connection hasn't been canceled.
396 if (this.hostJid_) {
397 this.createSession_();
399 break;
401 case remoting.SignalStrategy.State.FAILED:
402 this.onError_(this.signalStrategy_.getError());
403 break;
408 * Continue an IT2Me connection once an access token has been obtained.
410 * @param {string} token An OAuth2 access token.
411 * @return {void} Nothing.
412 * @private
414 remoting.SessionConnectorImpl.prototype.connectIT2MeWithToken_ =
415 function(token) {
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.
430 * @private
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_();
442 return;
443 } else {
444 console.error('Invalid "support-hosts" response from server.');
446 } else {
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.
487 * @private
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) {
502 this.reconnector_ =
503 new remoting.SmartReconnector(this, this.clientSession_);
505 this.onConnected_(this.clientSession_);
506 break;
508 case remoting.ClientSession.State.CREATED:
509 console.log('Created plugin');
510 break;
512 case remoting.ClientSession.State.CONNECTING:
513 console.log('Connecting as ' + remoting.identity.getCachedEmail());
514 break;
516 case remoting.ClientSession.State.INITIALIZING:
517 console.log('Initializing connection');
518 break;
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
527 // message.
528 this.onError_(remoting.Error.UNEXPECTED);
529 break;
531 case remoting.ClientSession.State.FAILED:
532 var error = this.clientSession_.getError();
533 console.error('Client plugin reported connection failed: ' + error);
534 if (error == null) {
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));
541 } else {
542 this.onError_(error);
544 break;
546 default:
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.
557 * @private
559 remoting.SessionConnectorImpl.prototype.onHostListRefresh_ = function(success) {
560 if (success) {
561 var host = remoting.hostList.getHostForId(this.hostId_);
562 if (host) {
563 this.connectMe2MeInternal_(
564 host.hostId, host.jabberId, host.publicKey, host.hostName,
565 this.fetchPin_, this.fetchThirdPartyToken_,
566 this.clientPairingId_, this.clientPairedSecret_, false);
567 return;
570 this.onError_(remoting.Error.HOST_IS_OFFLINE);
574 * @param {number} error An HTTP error code returned by the support-hosts
575 * endpoint.
576 * @return {remoting.Error} The equivalent remoting.Error code.
577 * @private
579 remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ =
580 function(error) {
581 switch (error) {
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).
595 * @private
597 remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ =
598 function(accessCode) {
599 // Trim whitespace.
600 return accessCode.replace(/\s/g, '');
605 * @constructor
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
614 * success.
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;
618 * false otherwise.
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,
630 defaultRemapKeys);