Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / crd / js / session_connector_impl.js
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.
4
5 /**
6  * @fileoverview
7  * Connect set-up state machine for Me2Me and IT2Me
8  */
9
10 'use strict';
11
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
14
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  * @constructor
24  * @implements {remoting.SessionConnector}
25  */
26 remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError,
27                                      onExtensionMessage) {
28   /**
29    * @type {HTMLElement}
30    * @private
31    */
32   this.clientContainer_ = clientContainer;
33
34   /**
35    * @type {function(remoting.ClientSession):void}
36    * @private
37    */
38   this.onConnected_ = onConnected;
39
40   /**
41    * @type {function(remoting.Error):void}
42    * @private
43    */
44   this.onError_ = onError;
45
46   /**
47    * @type {function(string, string):boolean}
48    * @private
49    */
50   this.onExtensionMessage_ = onExtensionMessage;
51
52   /**
53    * @type {string}
54    * @private
55    */
56   this.clientJid_ = '';
57
58   /**
59    * @type {remoting.ClientSession.Mode}
60    * @private
61    */
62   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
63
64   /**
65    * @type {remoting.SignalStrategy}
66    * @private
67    */
68   this.signalStrategy_ = null;
69
70   /**
71    * @type {remoting.SmartReconnector}
72    * @private
73    */
74   this.reconnector_ = null;
75
76   /**
77    * @private
78    */
79   this.bound_ = {
80     onStateChange : this.onStateChange_.bind(this)
81   };
82
83   // Initialize/declare per-connection state.
84   this.reset();
85 };
86
87 /**
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.
90  */
91 remoting.SessionConnectorImpl.prototype.reset = function() {
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_ = '';
100
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_ = '';
108
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_ = '';
117
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_ = '';
126
127   /**
128    * @type {string}
129    * @private
130    */
131   this.hostJid_ = '';
132
133   /**
134    * @type {string}
135    * @private
136    */
137   this.hostPublicKey_ = '';
138
139   /**
140    * @type {boolean}
141    * @private
142    */
143   this.refreshHostJidIfOffline_ = false;
144
145   /**
146    * @type {remoting.ClientSession}
147    * @private
148    */
149   this.clientSession_ = null;
150
151   /**
152    * @type {XMLHttpRequest}
153    * @private
154    */
155   this.pendingXhr_ = null;
156
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) {};
163
164   /**
165    * @type {function(string, string, string,
166    *                 function(string, string):void): void}
167    * @private
168    */
169   this.fetchThirdPartyToken_ = function(
170       tokenUrl, scope, onThirdPartyTokenFetched) {};
171
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_ = '';
181 };
182
183 /**
184  * Initiate a Me2Me connection.
185  *
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.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);
206 };
207
208 /**
209  * Update the pairing info so that the reconnect function will work correctly.
210  *
211  * @param {string} clientId The paired client id.
212  * @param {string} sharedSecret The shared secret.
213  */
214 remoting.SessionConnectorImpl.prototype.updatePairingInfo =
215     function(clientId, sharedSecret) {
216   this.clientPairingId_ = clientId;
217   this.clientPairedSecret_ = sharedSecret;
218 };
219
220 /**
221  * Initiate a Me2Me connection.
222  *
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.SessionConnectorImpl.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();
249
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
260   this.connectSignaling_();
261 }
262
263 /**
264  * Initiate an IT2Me connection.
265  *
266  * @param {string} accessCode The access code as entered by the user.
267  * @return {void} Nothing.
268  */
269 remoting.SessionConnectorImpl.prototype.connectIT2Me = function(accessCode) {
270   var kSupportIdLen = 7;
271   var kHostSecretLen = 5;
272   var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
273
274   // Cancel any existing connect operation.
275   this.cancel();
276
277   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
278   if (normalizedAccessCode.length != kAccessCodeLen) {
279     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
280     return;
281   }
282
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),
287                                   this.onError_);
288 };
289
290 /**
291  * Reconnect a closed connection.
292  *
293  * @return {void} Nothing.
294  */
295 remoting.SessionConnectorImpl.prototype.reconnect = function() {
296   if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
297     console.error('reconnect not supported for IT2Me.');
298     return;
299   }
300   this.connectMe2MeInternal_(
301       this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
302       this.fetchPin_, this.fetchThirdPartyToken_,
303       this.clientPairingId_, this.clientPairedSecret_, true);
304 };
305
306 /**
307  * Cancel a connection-in-progress.
308  */
309 remoting.SessionConnectorImpl.prototype.cancel = function() {
310   if (this.clientSession_) {
311     this.clientSession_.removePlugin();
312     this.clientSession_ = null;
313   }
314   if (this.pendingXhr_) {
315     this.pendingXhr_.abort();
316     this.pendingXhr_ = null;
317   }
318   this.reset();
319 };
320
321 /**
322  * Get the connection mode (Me2Me or IT2Me)
323  *
324  * @return {remoting.ClientSession.Mode}
325  */
326 remoting.SessionConnectorImpl.prototype.getConnectionMode = function() {
327   return this.connectionMode_;
328 };
329
330 /**
331  * Get host ID.
332  *
333  * @return {string}
334  */
335 remoting.SessionConnectorImpl.prototype.getHostId = function() {
336   return this.hostId_;
337 };
338
339 /**
340  * @private
341  */
342 remoting.SessionConnectorImpl.prototype.connectSignaling_ = function() {
343   base.dispose(this.signalStrategy_);
344   this.signalStrategy_ = null;
345
346   /** @type {remoting.SessionConnectorImpl} */
347   var that = this;
348
349   /** @param {string} token */
350   function connectSignalingWithToken(token) {
351     remoting.identity.getEmail(
352         connectSignalingWithTokenAndEmail.bind(null, token), that.onError_);
353   }
354
355   /**
356    * @param {string} token
357    * @param {string} email
358    */
359   function connectSignalingWithTokenAndEmail(token, email) {
360     that.signalStrategy_.connect(
361         remoting.settings.XMPP_SERVER_ADDRESS, email, token);
362   }
363
364   this.signalStrategy_ =
365       remoting.SignalStrategy.create(this.onSignalingState_.bind(this));
366
367   remoting.identity.callWithToken(connectSignalingWithToken, this.onError_);
368 };
369
370 /**
371  * @private
372  * @param {remoting.SignalStrategy.State} state
373  */
374 remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) {
375   switch (state) {
376     case remoting.SignalStrategy.State.CONNECTED:
377       // Proceed only if the connection hasn't been canceled.
378       if (this.hostJid_) {
379         this.createSession_();
380       }
381       break;
382
383     case remoting.SignalStrategy.State.FAILED:
384       this.onError_(this.signalStrategy_.getError());
385       break;
386   }
387 };
388
389 /**
390  * Continue an IT2Me connection once an access token has been obtained.
391  *
392  * @param {string} token An OAuth2 access token.
393  * @return {void} Nothing.
394  * @private
395  */
396 remoting.SessionConnectorImpl.prototype.connectIT2MeWithToken_ =
397     function(token) {
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),
403       '',
404       { 'Authorization': 'OAuth ' + token });
405 };
406
407 /**
408  * Continue an IT2Me connection once the host JID has been looked up.
409  *
410  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
411  * @return {void} Nothing.
412  * @private
413  */
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         base.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_();
424       return;
425     } else {
426       console.error('Invalid "support-hosts" response from server.');
427     }
428   } else {
429     this.onError_(this.translateSupportHostsError_(xhr.status));
430   }
431 };
432
433 /**
434  * Creates ClientSession object.
435  */
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;
443   }
444
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_);
457 };
458
459 /**
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.
464  *
465  * @param  {remoting.ClientSession.StateEvent} event
466  * @return {void} Nothing.
467  * @private
468  */
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);
479
480       base.dispose(this.reconnector_);
481       if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) {
482         this.reconnector_ =
483             new remoting.SmartReconnector(this, this.clientSession_);
484       }
485       this.onConnected_(this.clientSession_);
486       break;
487
488     case remoting.ClientSession.State.CREATED:
489       console.log('Created plugin');
490       break;
491
492     case remoting.ClientSession.State.CONNECTING:
493       console.log('Connecting as ' + remoting.identity.getCachedEmail());
494       break;
495
496     case remoting.ClientSession.State.INITIALIZING:
497       console.log('Initializing connection');
498       break;
499
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
507       // message.
508       this.onError_(remoting.Error.UNEXPECTED);
509       break;
510
511     case remoting.ClientSession.State.FAILED:
512       var error = this.clientSession_.getError();
513       console.error('Client plugin reported connection failed: ' + error);
514       if (error == null) {
515         error = remoting.Error.UNEXPECTED;
516       }
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));
521       } else {
522         this.onError_(error);
523       }
524       break;
525
526     default:
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);
531   }
532 };
533
534 /**
535  * @param {boolean} success True if the host list was successfully refreshed;
536  *     false if an error occurred.
537  * @private
538  */
539 remoting.SessionConnectorImpl.prototype.onHostListRefresh_ = function(success) {
540   if (success) {
541     var host = remoting.hostList.getHostForId(this.hostId_);
542     if (host) {
543       this.connectMe2MeInternal_(
544           host.hostId, host.jabberId, host.publicKey, host.hostName,
545           this.fetchPin_, this.fetchThirdPartyToken_,
546           this.clientPairingId_, this.clientPairedSecret_, false);
547       return;
548     }
549   }
550   this.onError_(remoting.Error.HOST_IS_OFFLINE);
551 };
552
553 /**
554  * @param {number} error An HTTP error code returned by the support-hosts
555  *     endpoint.
556  * @return {remoting.Error} The equivalent remoting.Error code.
557  * @private
558  */
559 remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ =
560     function(error) {
561   switch (error) {
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;
567   }
568 };
569
570 /**
571  * Normalize the access code entered by the user.
572  *
573  * @param {string} accessCode The access code, as entered by the user.
574  * @return {string} The normalized form of the code (whitespace removed).
575  * @private
576  */
577 remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ =
578     function(accessCode) {
579   // Trim whitespace.
580   return accessCode.replace(/\s/g, '');
581 };
582
583
584 /**
585  * @constructor
586  * @implements {remoting.SessionConnectorFactory}
587  */
588 remoting.DefaultSessionConnectorFactory = function() {
589 };
590
591 /**
592  * @param {HTMLElement} clientContainer Container element for the client view.
593  * @param {function(remoting.ClientSession):void} onConnected Callback on
594  *     success.
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;
598  *     false otherwise.
599  */
600 remoting.DefaultSessionConnectorFactory.prototype.createConnector =
601     function(clientContainer, onConnected, onError, onExtensionMessage) {
602   return new remoting.SessionConnectorImpl(
603       clientContainer, onConnected, onError, onExtensionMessage);
604 };