Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / session_connector_impl.js
blobbaf0e431a520a926a1531ba6c8ba092934cccac8
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 {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.
29  * @constructor
30  * @implements {remoting.SessionConnector}
31  */
32 remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError,
33                                          onExtensionMessage,
34                                          onConnectionFailed,
35                                          requiredCapabilities,
36                                          defaultRemapKeys) {
37   /**
38    * @type {HTMLElement}
39    * @private
40    */
41   this.clientContainer_ = clientContainer;
43   /**
44    * @type {function(remoting.ClientSession):void}
45    * @private
46    */
47   this.onConnected_ = onConnected;
49   /**
50    * @type {function(remoting.Error):void}
51    * @private
52    */
53   this.onError_ = onError;
55   /**
56    * @type {function(string, string):boolean}
57    * @private
58    */
59   this.onExtensionMessage_ = onExtensionMessage;
61   /**
62    * @type {function(remoting.Error):void}
63    * @private
64    */
65   this.onConnectionFailed_ = onConnectionFailed;
67   /**
68    * @type {Array<string>}
69    * @private
70    */
71   this.requiredCapabilities_ = requiredCapabilities;
73   /**
74    * @type {string}
75    * @private
76    */
77   this.defaultRemapKeys_ = defaultRemapKeys;
79   /**
80    * @type {string}
81    * @private
82    */
83   this.clientJid_ = '';
85   /**
86    * @type {remoting.DesktopConnectedView.Mode}
87    * @private
88    */
89   this.connectionMode_ = remoting.DesktopConnectedView.Mode.ME2ME;
91   /**
92    * @type {remoting.SignalStrategy}
93    * @private
94    */
95   this.signalStrategy_ = null;
97   /**
98    * @type {remoting.SmartReconnector}
99    * @private
100    */
101   this.reconnector_ = null;
103   /**
104    * @private
105    */
106   this.bound_ = {
107     onStateChange : this.onStateChange_.bind(this)
108   };
110   // Initialize/declare per-connection state.
111   this.reset();
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.
117  */
118 remoting.SessionConnectorImpl.prototype.reset = function() {
119   /**
120    * For paired connections, the client id of this device, issued by the host.
121    *
122    * @type {string}
123    * @private
124    */
125   this.clientPairingId_ = '';
127   /**
128    * For paired connections, the paired secret for this device, issued by the
129    * host.
130    *
131    * @type {string}
132    * @private
133    */
134   this.clientPairedSecret_ = '';
136   /**
137    * String used to authenticate to the host on connection. For IT2Me, this is
138    * the access code; for Me2Me it is the PIN.
139    *
140    * @type {string}
141    * @private
142    */
143   this.passPhrase_ = '';
145   /**
146    * @type {remoting.Host}
147    * @private
148    */
149   this.host_ = null;
151   /**
152    * @type {boolean}
153    * @private
154    */
155   this.logHostOfflineErrors_ = false;
157   /**
158    * @type {remoting.ClientSession}
159    * @private
160    */
161   this.clientSession_ = null;
163   /**
164    * @type {XMLHttpRequest}
165    * @private
166    */
167   this.pendingXhr_ = null;
169   /**
170    * Function to interactively obtain the PIN from the user.
171    * @type {function(boolean, function(string):void):void}
172    * @private
173    */
174   this.fetchPin_ = function(onPinFetched) {};
176   /**
177    * @type {function(string, string, string,
178    *                 function(string, string):void): void}
179    * @private
180    */
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.
203  */
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
217  * host is offline.
219  * @param {remoting.Host} host The Me2Me host to refresh.
220  * @return {void} Nothing.
221  */
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.
238  */
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.
251  */
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.
273  * @private
274  */
275 remoting.SessionConnectorImpl.prototype.connectMe2MeInternal_ =
276     function(host, fetchPin, fetchThirdPartyToken,
277              clientPairingId, clientPairedSecret) {
278   // Cancel any existing connect operation.
279   this.cancel();
281   this.host_ = host;
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.
294  */
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.
301   this.cancel();
303   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
304   if (normalizedAccessCode.length != kAccessCodeLen) {
305     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
306     return;
307   }
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.
321  */
322 remoting.SessionConnectorImpl.prototype.reconnect = function() {
323   if (this.connectionMode_ == remoting.DesktopConnectedView.Mode.IT2ME) {
324     console.error('reconnect not supported for IT2Me.');
325     return;
326   }
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.
335  */
336 remoting.SessionConnectorImpl.prototype.cancel = function() {
337   if (this.clientSession_) {
338     this.clientSession_.removePlugin();
339     this.clientSession_ = null;
340   }
341   if (this.pendingXhr_) {
342     this.pendingXhr_.abort();
343     this.pendingXhr_ = null;
344   }
345   this.reset();
349  * Get the connection mode (Me2Me or IT2Me)
351  * @return {remoting.DesktopConnectedView.Mode}
352  */
353 remoting.SessionConnectorImpl.prototype.getConnectionMode = function() {
354   return this.connectionMode_;
358  * Get host ID.
360  * @return {string}
361  */
362 remoting.SessionConnectorImpl.prototype.getHostId = function() {
363   return this.host_.hostId;
367  * @private
368  */
369 remoting.SessionConnectorImpl.prototype.connectSignaling_ = function() {
370   base.dispose(this.signalStrategy_);
371   this.signalStrategy_ = null;
373   /** @type {remoting.SessionConnectorImpl} */
374   var that = this;
376   /** @param {string} token */
377   function connectSignalingWithToken(token) {
378     remoting.identity.getUserInfo().then(
379         connectSignalingWithTokenAndUserInfo.bind(null, token),
380         remoting.Error.handler(that.onError_));
381   }
383   /**
384    * Success callback for when the email and fullName have been retrieved
385    * for this user.
386    * Note that the full name will be null unless the webapp has requested
387    * and been granted the userinfo.profile permission.
388    *
389    * @param {string} token
390    * @param {{email: string, name: string}} userInfo
391    */
392   function connectSignalingWithTokenAndUserInfo(token, userInfo) {
393     that.signalStrategy_.connect(
394         remoting.settings.XMPP_SERVER_FOR_CLIENT, userInfo.email, token);
395   }
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_));
407  * @private
408  * @param {remoting.SignalStrategy.State} state
409  */
410 remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) {
411   switch (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_();
416       }
417       break;
419     case remoting.SignalStrategy.State.FAILED:
420       this.onError_(this.signalStrategy_.getError());
421       break;
422   }
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.
431  * @private
432  */
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({
437     method: 'GET',
438     url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
439         encodeURIComponent(hostId),
440     onDone: this.onIT2MeHostInfo_.bind(this),
441     oauthToken: token
442   });
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.
451  * @private
452  */
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_();
466       return;
467     } else {
468       console.error('Invalid "support-hosts" response from server.');
469     }
470   } else {
471     this.onError_(this.translateSupportHostsError_(xhr.status));
472   }
476  * Creates ClientSession object.
477  */
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;
485   }
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.
510  * @private
511  */
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) {
525         this.reconnector_ =
526             new remoting.SmartReconnector(this, this.clientSession_);
527       }
528       this.onConnected_(this.clientSession_);
529       break;
531     case remoting.ClientSession.State.CREATED:
532       console.log('Created plugin');
533       break;
535     case remoting.ClientSession.State.CONNECTING:
536       console.log('Connecting as ' + remoting.identity.getCachedEmail());
537       break;
539     case remoting.ClientSession.State.INITIALIZING:
540       console.log('Initializing connection');
541       break;
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
550       // message.
551       this.onError_(remoting.Error.UNEXPECTED);
552       break;
554     case remoting.ClientSession.State.FAILED:
555       var error = this.clientSession_.getError();
556       console.error('Client plugin reported connection failed: ' + error);
557       if (error == null) {
558         error = remoting.Error.UNEXPECTED;
559       }
560       this.onConnectionFailed_(error);
561       break;
563     default:
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);
568   }
572  * @param {number} error An HTTP error code returned by the support-hosts
573  *     endpoint.
574  * @return {remoting.Error} The equivalent remoting.Error code.
575  * @private
576  */
577 remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ =
578     function(error) {
579   switch (error) {
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;
585   }
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).
593  * @private
594  */
595 remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ =
596     function(accessCode) {
597   // Trim whitespace.
598   return accessCode.replace(/\s/g, '');
603  * @constructor
604  * @implements {remoting.SessionConnectorFactory}
605  */
606 remoting.DefaultSessionConnectorFactory = function() {
610  * @param {HTMLElement} clientContainer Container element for the client view.
611  * @param {function(remoting.ClientSession):void} onConnected Callback on
612  *     success.
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;
616  *     false otherwise.
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}
624  */
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,
630                                            onConnectionFailed,
631                                            requiredCapabilities,
632                                            defaultRemapKeys);