Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / 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   // Only use XMPP when TCP API is available and TLS support is enabled. That's
365   // not the case for V1 app (socket API is available only to platform apps)
366   // and for Chrome releases before 38.
367   if (chrome.socket && chrome.socket.secure) {
368     this.signalStrategy_ = /** @type {remoting.SignalStrategy} */
369         (new remoting.XmppConnection(this.onSignalingState_.bind(this)));
370   } else {
371     this.signalStrategy_ = /** @type {remoting.SignalStrategy} */
372         (new remoting.WcsAdapter(this.onSignalingState_.bind(this)));
373   }
374
375   remoting.identity.callWithToken(connectSignalingWithToken, this.onError_);
376 };
377
378 /**
379  * @private
380  * @param {remoting.SignalStrategy.State} state
381  */
382 remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) {
383   switch (state) {
384     case remoting.SignalStrategy.State.CONNECTED:
385       // Proceed only if the connection hasn't been canceled.
386       if (this.hostJid_) {
387         this.createSession_();
388       }
389       break;
390
391     case remoting.SignalStrategy.State.FAILED:
392       this.onError_(this.signalStrategy_.getError());
393       break;
394   }
395 };
396
397 /**
398  * Continue an IT2Me connection once an access token has been obtained.
399  *
400  * @param {string} token An OAuth2 access token.
401  * @return {void} Nothing.
402  * @private
403  */
404 remoting.SessionConnectorImpl.prototype.connectIT2MeWithToken_ =
405     function(token) {
406   // Resolve the host id to get the host JID.
407   this.pendingXhr_ = remoting.xhr.get(
408       remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
409           encodeURIComponent(this.hostId_),
410       this.onIT2MeHostInfo_.bind(this),
411       '',
412       { 'Authorization': 'OAuth ' + token });
413 };
414
415 /**
416  * Continue an IT2Me connection once the host JID has been looked up.
417  *
418  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
419  * @return {void} Nothing.
420  * @private
421  */
422 remoting.SessionConnectorImpl.prototype.onIT2MeHostInfo_ = function(xhr) {
423   this.pendingXhr_ = null;
424   if (xhr.status == 200) {
425     var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
426         jsonParseSafe(xhr.responseText);
427     if (host && host.data && host.data.jabberId && host.data.publicKey) {
428       this.hostJid_ = host.data.jabberId;
429       this.hostPublicKey_ = host.data.publicKey;
430       this.hostDisplayName_ = this.hostJid_.split('/')[0];
431       this.connectSignaling_();
432       return;
433     } else {
434       console.error('Invalid "support-hosts" response from server.');
435     }
436   } else {
437     this.onError_(this.translateSupportHostsError_(xhr.status));
438   }
439 };
440
441 /**
442  * Creates ClientSession object.
443  */
444 remoting.SessionConnectorImpl.prototype.createSession_ = function() {
445   // In some circumstances, the WCS <iframe> can get reloaded, which results
446   // in a new clientJid and a new callback. In this case, remove the old
447   // client plugin before instantiating a new one.
448   if (this.clientSession_) {
449     this.clientSession_.removePlugin();
450     this.clientSession_ = null;
451   }
452
453   var authenticationMethods =
454      'third_party,spake2_pair,spake2_hmac,spake2_plain';
455   this.clientSession_ = new remoting.ClientSession(
456       this.signalStrategy_, this.clientContainer_, this.hostDisplayName_,
457       this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_,
458       authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
459       this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
460   this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
461   this.clientSession_.addEventListener(
462       remoting.ClientSession.Events.stateChanged,
463       this.bound_.onStateChange);
464   this.clientSession_.createPluginAndConnect(this.onExtensionMessage_);
465 };
466
467 /**
468  * Handle a change in the state of the client session prior to successful
469  * connection (after connection, this class no longer handles state change
470  * events). Errors that occur while connecting either trigger a reconnect
471  * or notify the onError handler.
472  *
473  * @param  {remoting.ClientSession.StateEvent} event
474  * @return {void} Nothing.
475  * @private
476  */
477 remoting.SessionConnectorImpl.prototype.onStateChange_ = function(event) {
478   switch (event.current) {
479     case remoting.ClientSession.State.CONNECTED:
480       // When the connection succeeds, deregister for state-change callbacks
481       // and pass the session to the onConnected callback. It is expected that
482       // it will register a new state-change callback to handle disconnect
483       // or error conditions.
484       this.clientSession_.removeEventListener(
485           remoting.ClientSession.Events.stateChanged,
486           this.bound_.onStateChange);
487
488       base.dispose(this.reconnector_);
489       if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) {
490         this.reconnector_ =
491             new remoting.SmartReconnector(this, this.clientSession_);
492       }
493       this.onConnected_(this.clientSession_);
494       break;
495
496     case remoting.ClientSession.State.CREATED:
497       console.log('Created plugin');
498       break;
499
500     case remoting.ClientSession.State.CONNECTING:
501       console.log('Connecting as ' + remoting.identity.getCachedEmail());
502       break;
503
504     case remoting.ClientSession.State.INITIALIZING:
505       console.log('Initializing connection');
506       break;
507
508     case remoting.ClientSession.State.CLOSED:
509       // This class deregisters for state-change callbacks when the CONNECTED
510       // state is reached, so it only sees the CLOSED state in exceptional
511       // circumstances. For example, a CONNECTING -> CLOSED transition happens
512       // if the host closes the connection without an error message instead of
513       // accepting it. Since there's no way of knowing exactly what went wrong,
514       // we rely on server-side logs in this case and report a generic error
515       // message.
516       this.onError_(remoting.Error.UNEXPECTED);
517       break;
518
519     case remoting.ClientSession.State.FAILED:
520       var error = this.clientSession_.getError();
521       console.error('Client plugin reported connection failed: ' + error);
522       if (error == null) {
523         error = remoting.Error.UNEXPECTED;
524       }
525       if (error == remoting.Error.HOST_IS_OFFLINE &&
526           this.refreshHostJidIfOffline_) {
527         // The plugin will be re-created when the host finished refreshing
528         remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
529       } else {
530         this.onError_(error);
531       }
532       break;
533
534     default:
535       console.error('Unexpected client plugin state: ' + event.current);
536       // This should only happen if the web-app and client plugin get out of
537       // sync, and even then the version check should ensure compatibility.
538       this.onError_(remoting.Error.MISSING_PLUGIN);
539   }
540 };
541
542 /**
543  * @param {boolean} success True if the host list was successfully refreshed;
544  *     false if an error occurred.
545  * @private
546  */
547 remoting.SessionConnectorImpl.prototype.onHostListRefresh_ = function(success) {
548   if (success) {
549     var host = remoting.hostList.getHostForId(this.hostId_);
550     if (host) {
551       this.connectMe2MeInternal_(
552           host.hostId, host.jabberId, host.publicKey, host.hostName,
553           this.fetchPin_, this.fetchThirdPartyToken_,
554           this.clientPairingId_, this.clientPairedSecret_, false);
555       return;
556     }
557   }
558   this.onError_(remoting.Error.HOST_IS_OFFLINE);
559 };
560
561 /**
562  * @param {number} error An HTTP error code returned by the support-hosts
563  *     endpoint.
564  * @return {remoting.Error} The equivalent remoting.Error code.
565  * @private
566  */
567 remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ =
568     function(error) {
569   switch (error) {
570     case 0: return remoting.Error.NETWORK_FAILURE;
571     case 404: return remoting.Error.INVALID_ACCESS_CODE;
572     case 502: // No break
573     case 503: return remoting.Error.SERVICE_UNAVAILABLE;
574     default: return remoting.Error.UNEXPECTED;
575   }
576 };
577
578 /**
579  * Normalize the access code entered by the user.
580  *
581  * @param {string} accessCode The access code, as entered by the user.
582  * @return {string} The normalized form of the code (whitespace removed).
583  * @private
584  */
585 remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ =
586     function(accessCode) {
587   // Trim whitespace.
588   return accessCode.replace(/\s/g, '');
589 };
590
591
592 /**
593  * @constructor
594  * @implements {remoting.SessionConnectorFactory}
595  */
596 remoting.DefaultSessionConnectorFactory = function() {
597 };
598
599 /**
600  * @param {HTMLElement} clientContainer Container element for the client view.
601  * @param {function(remoting.ClientSession):void} onConnected Callback on
602  *     success.
603  * @param {function(remoting.Error):void} onError Callback on error.
604  * @param {function(string, string):boolean} onExtensionMessage The handler for
605  *     protocol extension messages. Returns true if a message is recognized;
606  *     false otherwise.
607  */
608 remoting.DefaultSessionConnectorFactory.prototype.createConnector =
609     function(clientContainer, onConnected, onError, onExtensionMessage) {
610   return new remoting.SessionConnectorImpl(
611       clientContainer, onConnected, onError, onExtensionMessage);
612 };