Pipeline: Handle the case where Seek() is called after error happened.
[chromium-blink-merge.git] / remoting / webapp / session_connector.js
blob0dd52cd97f9bda2519175c956bcf476fd9b03b98
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} onOk Callback on success.
18  * @param {function(remoting.Error):void} onError Callback on error.
19  * @param {function(string, string):boolean} onExtensionMessage The handler for
20  *     protocol extension messages. Returns true if a message is recognized;
21  *     false otherwise.
22  * @constructor
23  */
24 remoting.SessionConnector = function(clientContainer, onOk, onError,
25                                      onExtensionMessage) {
26   /**
27    * @type {HTMLElement}
28    * @private
29    */
30   this.clientContainer_ = clientContainer;
32   /**
33    * @type {function(remoting.ClientSession):void}
34    * @private
35    */
36   this.onOk_ = onOk;
38   /**
39    * @type {function(remoting.Error):void}
40    * @private
41    */
42   this.onError_ = onError;
44   /**
45    * @type {function(string, string):boolean}
46    * @private
47    */
48   this.onExtensionMessage_ = onExtensionMessage;
50   /**
51    * @type {string}
52    * @private
53    */
54   this.clientJid_ = '';
56   /**
57    * @type {remoting.ClientSession.Mode}
58    * @private
59    */
60   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
62   /**
63    * @type {remoting.SmartReconnector}
64    * @private
65    */
66   this.reconnector_ = null;
68   /**
69    * @private
70    */
71   this.bound_ = {
72     onStateChange : this.onStateChange_.bind(this)
73   };
75   // Initialize/declare per-connection state.
76   this.reset();
79 /**
80  * Reset the per-connection state so that the object can be re-used for a
81  * second connection. Note the none of the shared WCS state is reset.
82  */
83 remoting.SessionConnector.prototype.reset = function() {
84   /**
85    * Set to true to indicate that the user requested pairing when entering
86    * their PIN for a Me2Me connection.
87    *
88    * @type {boolean}
89    */
90   this.pairingRequested = false;
92   /**
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.
95    *
96    * @type {string}
97    * @private
98    */
99   this.hostId_ = '';
101   /**
102    * For paired connections, the client id of this device, issued by the host.
103    *
104    * @type {string}
105    * @private
106    */
107   this.clientPairingId_ = '';
109   /**
110    * For paired connections, the paired secret for this device, issued by the
111    * host.
112    *
113    * @type {string}
114    * @private
115    */
116   this.clientPairedSecret_ = '';
118   /**
119    * String used to authenticate to the host on connection. For IT2Me, this is
120    * the access code; for Me2Me it is the PIN.
121    *
122    * @type {string}
123    * @private
124    */
125   this.passPhrase_ = '';
127   /**
128    * @type {string}
129    * @private
130    */
131   this.hostJid_ = '';
133   /**
134    * @type {string}
135    * @private
136    */
137   this.hostPublicKey_ = '';
139   /**
140    * @type {boolean}
141    * @private
142    */
143   this.refreshHostJidIfOffline_ = false;
145   /**
146    * @type {remoting.ClientSession}
147    * @private
148    */
149   this.clientSession_ = null;
151   /**
152    * @type {XMLHttpRequest}
153    * @private
154    */
155   this.pendingXhr_ = null;
157   /**
158    * Function to interactively obtain the PIN from the user.
159    * @type {function(boolean, function(string):void):void}
160    * @private
161    */
162   this.fetchPin_ = function(onPinFetched) {};
164   /**
165    * @type {function(string, string, string,
166    *                 function(string, string):void): void}
167    * @private
168    */
169   this.fetchThirdPartyToken_ = function(
170       tokenUrl, scope, onThirdPartyTokenFetched) {};
172   /**
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.
176    *
177    * @type {string}
178    * @private
179    */
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.
198  */
199 remoting.SessionConnector.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.
213  */
214 remoting.SessionConnector.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.
240  * @private
241  */
242 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
243     function(hostId, hostJid, hostPublicKey, hostDisplayName,
244              fetchPin, fetchThirdPartyToken,
245              clientPairingId, clientPairedSecret,
246              refreshHostJidIfOffline) {
247   // Cancel any existing connect operation.
248   this.cancel();
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);
259   this.createSession_();
263  * Initiate an IT2Me connection.
265  * @param {string} accessCode The access code as entered by the user.
266  * @return {void} Nothing.
267  */
268 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
269   var kSupportIdLen = 7;
270   var kHostSecretLen = 5;
271   var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
273   // Cancel any existing connect operation.
274   this.cancel();
276   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
277   if (normalizedAccessCode.length != kAccessCodeLen) {
278     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
279     return;
280   }
282   this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
283   this.passPhrase_ = normalizedAccessCode;
284   this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
285   remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
286                                   this.onError_);
290  * Reconnect a closed connection.
292  * @return {void} Nothing.
293  */
294 remoting.SessionConnector.prototype.reconnect = function() {
295   if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
296     console.error('reconnect not supported for IT2Me.');
297     return;
298   }
299   this.connectMe2MeInternal_(
300       this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
301       this.fetchPin_, this.fetchThirdPartyToken_,
302       this.clientPairingId_, this.clientPairedSecret_, true);
306  * Cancel a connection-in-progress.
307  */
308 remoting.SessionConnector.prototype.cancel = function() {
309   if (this.clientSession_) {
310     this.clientSession_.removePlugin();
311     this.clientSession_ = null;
312   }
313   if (this.pendingXhr_) {
314     this.pendingXhr_.abort();
315     this.pendingXhr_ = null;
316   }
317   this.reset();
321  * Get the connection mode (Me2Me or IT2Me)
323  * @return {remoting.ClientSession.Mode}
324  */
325 remoting.SessionConnector.prototype.getConnectionMode = function() {
326   return this.connectionMode_;
330  * Get host ID.
332  * @return {string}
333  */
334 remoting.SessionConnector.prototype.getHostId = function() {
335   return this.hostId_;
339  * Continue an IT2Me connection once an access token has been obtained.
341  * @param {string} token An OAuth2 access token.
342  * @return {void} Nothing.
343  * @private
344  */
345 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
346   // Resolve the host id to get the host JID.
347   this.pendingXhr_ = remoting.xhr.get(
348       remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
349           encodeURIComponent(this.hostId_),
350       this.onIT2MeHostInfo_.bind(this),
351       '',
352       { 'Authorization': 'OAuth ' + token });
356  * Continue an IT2Me connection once the host JID has been looked up.
358  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
359  * @return {void} Nothing.
360  * @private
361  */
362 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
363   this.pendingXhr_ = null;
364   if (xhr.status == 200) {
365     var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
366         jsonParseSafe(xhr.responseText);
367     if (host && host.data && host.data.jabberId && host.data.publicKey) {
368       this.hostJid_ = host.data.jabberId;
369       this.hostPublicKey_ = host.data.publicKey;
370       this.hostDisplayName_ = this.hostJid_.split('/')[0];
371       this.createSession_();
372       return;
373     } else {
374       console.error('Invalid "support-hosts" response from server.');
375     }
376   } else {
377     this.onError_(this.translateSupportHostsError(xhr.status));
378   }
382  * Creates ClientSession object.
383  */
384 remoting.SessionConnector.prototype.createSession_ = function() {
385   // In some circumstances, the WCS <iframe> can get reloaded, which results
386   // in a new clientJid and a new callback. In this case, remove the old
387   // client plugin before instantiating a new one.
388   if (this.clientSession_) {
389     this.clientSession_.removePlugin();
390     this.clientSession_ = null;
391   }
393   var authenticationMethods =
394      'third_party,spake2_pair,spake2_hmac,spake2_plain';
395   this.clientSession_ = new remoting.ClientSession(
396       this.clientContainer_, this.hostDisplayName_, this.passPhrase_,
397       this.fetchPin_, this.fetchThirdPartyToken_, authenticationMethods,
398       this.hostId_, this.hostJid_, this.hostPublicKey_, this.connectionMode_,
399       this.clientPairingId_, this.clientPairedSecret_);
400   this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
401   this.clientSession_.addEventListener(
402       remoting.ClientSession.Events.stateChanged,
403       this.bound_.onStateChange);
404   this.clientSession_.createPluginAndConnect(this.onExtensionMessage_);
408  * Handle a change in the state of the client session prior to successful
409  * connection (after connection, this class no longer handles state change
410  * events). Errors that occur while connecting either trigger a reconnect
411  * or notify the onError handler.
413  * @param  {remoting.ClientSession.StateEvent} event
414  * @return {void} Nothing.
415  * @private
416  */
417 remoting.SessionConnector.prototype.onStateChange_ = function(event) {
418   switch (event.current) {
419     case remoting.ClientSession.State.CONNECTED:
420       // When the connection succeeds, deregister for state-change callbacks
421       // and pass the session to the onOk callback. It is expected that it
422       // will register a new state-change callback to handle disconnect
423       // or error conditions.
424       this.clientSession_.removeEventListener(
425           remoting.ClientSession.Events.stateChanged,
426           this.bound_.onStateChange);
428       base.dispose(this.reconnector_);
429       if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) {
430         this.reconnector_ =
431             new remoting.SmartReconnector(this, this.clientSession_);
432       }
433       this.onOk_(this.clientSession_);
434       break;
436     case remoting.ClientSession.State.CREATED:
437       console.log('Created plugin');
438       break;
440     case remoting.ClientSession.State.CONNECTING:
441       console.log('Connecting as ' + remoting.identity.getCachedEmail());
442       break;
444     case remoting.ClientSession.State.INITIALIZING:
445       console.log('Initializing connection');
446       break;
448     case remoting.ClientSession.State.CLOSED:
449       // This class deregisters for state-change callbacks when the CONNECTED
450       // state is reached, so it only sees the CLOSED state in exceptional
451       // circumstances. For example, a CONNECTING -> CLOSED transition happens
452       // if the host closes the connection without an error message instead of
453       // accepting it. Since there's no way of knowing exactly what went wrong,
454       // we rely on server-side logs in this case and report a generic error
455       // message.
456       this.onError_(remoting.Error.UNEXPECTED);
457       break;
459     case remoting.ClientSession.State.FAILED:
460       var error = this.clientSession_.getError();
461       console.error('Client plugin reported connection failed: ' + error);
462       if (error == null) {
463         error = remoting.Error.UNEXPECTED;
464       }
465       if (error == remoting.Error.HOST_IS_OFFLINE &&
466           this.refreshHostJidIfOffline_) {
467         // The plugin will be re-created when the host finished refreshing
468         remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
469       } else {
470         this.onError_(error);
471       }
472       break;
474     default:
475       console.error('Unexpected client plugin state: ' + event.current);
476       // This should only happen if the web-app and client plugin get out of
477       // sync, and even then the version check should ensure compatibility.
478       this.onError_(remoting.Error.MISSING_PLUGIN);
479   }
483  * @param {boolean} success True if the host list was successfully refreshed;
484  *     false if an error occurred.
485  * @private
486  */
487 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
488   if (success) {
489     var host = remoting.hostList.getHostForId(this.hostId_);
490     if (host) {
491       this.connectMe2MeInternal_(
492           host.hostId, host.jabberId, host.publicKey, host.hostName,
493           this.fetchPin_, this.fetchThirdPartyToken_,
494           this.clientPairingId_, this.clientPairedSecret_, false);
495       return;
496     }
497   }
498   this.onError_(remoting.Error.HOST_IS_OFFLINE);
502  * @param {number} error An HTTP error code returned by the support-hosts
503  *     endpoint.
504  * @return {remoting.Error} The equivalent remoting.Error code.
505  * @private
506  */
507 remoting.SessionConnector.prototype.translateSupportHostsError =
508     function(error) {
509   switch (error) {
510     case 0: return remoting.Error.NETWORK_FAILURE;
511     case 404: return remoting.Error.INVALID_ACCESS_CODE;
512     case 502: // No break
513     case 503: return remoting.Error.SERVICE_UNAVAILABLE;
514     default: return remoting.Error.UNEXPECTED;
515   }
519  * Normalize the access code entered by the user.
521  * @param {string} accessCode The access code, as entered by the user.
522  * @return {string} The normalized form of the code (whitespace removed).
523  */
524 remoting.SessionConnector.prototype.normalizeAccessCode_ =
525     function(accessCode) {
526   // Trim whitespace.
527   return accessCode.replace(/\s/g, '');