- add sources.
[platform/framework/web/crosswalk.git] / src / remoting / webapp / session_connector.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 {Element} pluginParent The node under which to add the client plugin.
17  * @param {function(remoting.ClientSession):void} onOk Callback on success.
18  * @param {function(remoting.Error):void} onError Callback on error.
19  * @constructor
20  */
21 remoting.SessionConnector = function(pluginParent, onOk, onError) {
22   /**
23    * @type {Element}
24    * @private
25    */
26   this.pluginParent_ = pluginParent;
27
28   /**
29    * @type {function(remoting.ClientSession):void}
30    * @private
31    */
32   this.onOk_ = onOk;
33
34   /**
35    * @type {function(remoting.Error):void}
36    * @private
37    */
38   this.onError_ = onError;
39
40   /**
41    * @type {string}
42    * @private
43    */
44   this.clientJid_ = '';
45
46   /**
47    * @type {remoting.ClientSession.Mode}
48    * @private
49    */
50   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
51
52   // Initialize/declare per-connection state.
53   this.reset();
54 };
55
56 /**
57  * Reset the per-connection state so that the object can be re-used for a
58  * second connection. Note the none of the shared WCS state is reset.
59  */
60 remoting.SessionConnector.prototype.reset = function() {
61   /**
62    * Set to true to indicate that the user requested pairing when entering
63    * their PIN for a Me2Me connection.
64    *
65    * @type {boolean}
66    */
67   this.pairingRequested = false;
68
69   /**
70    * String used to identify the host to which to connect. For IT2Me, this is
71    * the first 7 digits of the access code; for Me2Me it is the host identifier.
72    *
73    * @type {string}
74    * @private
75    */
76   this.hostId_ = '';
77
78   /**
79    * For paired connections, the client id of this device, issued by the host.
80    *
81    * @type {string}
82    * @private
83    */
84   this.clientPairingId_ = '';
85
86   /**
87    * For paired connections, the paired secret for this device, issued by the
88    * host.
89    *
90    * @type {string}
91    * @private
92    */
93   this.clientPairedSecret_ = '';
94
95   /**
96    * String used to authenticate to the host on connection. For IT2Me, this is
97    * the access code; for Me2Me it is the PIN.
98    *
99    * @type {string}
100    * @private
101    */
102   this.passPhrase_ = '';
103
104   /**
105    * @type {string}
106    * @private
107    */
108   this.hostJid_ = '';
109
110   /**
111    * @type {string}
112    * @private
113    */
114   this.hostPublicKey_ = '';
115
116   /**
117    * @type {boolean}
118    * @private
119    */
120   this.refreshHostJidIfOffline_ = false;
121
122   /**
123    * @type {remoting.ClientSession}
124    * @private
125    */
126   this.clientSession_ = null;
127
128   /**
129    * @type {XMLHttpRequest}
130    * @private
131    */
132   this.pendingXhr_ = null;
133
134   /**
135    * Function to interactively obtain the PIN from the user.
136    * @type {function(boolean, function(string):void):void}
137    * @private
138    */
139   this.fetchPin_ = function(onPinFetched) {};
140
141   /**
142    * @type {function(string, string, string,
143    *                 function(string, string):void): void}
144    * @private
145    */
146   this.fetchThirdPartyToken_ = function(
147       tokenUrl, scope, onThirdPartyTokenFetched) {};
148
149   /**
150    * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
151    * this is the name of the host; for an IT2Me connection, it is the email
152    * address of the person sharing their computer.
153    *
154    * @type {string}
155    * @private
156    */
157   this.hostDisplayName_ = '';
158 };
159
160 /**
161  * Initiate a Me2Me connection.
162  *
163  * @param {remoting.Host} host The Me2Me host to which to connect.
164  * @param {function(boolean, function(string):void):void} fetchPin Function to
165  *     interactively obtain the PIN from the user.
166  * @param {function(string, string, string,
167  *                  function(string, string): void): void}
168  *     fetchThirdPartyToken Function to obtain a token from a third party
169  *     authenticaiton server.
170  * @param {string} clientPairingId The client id issued by the host when
171  *     this device was paired, if it is already paired.
172  * @param {string} clientPairedSecret The shared secret issued by the host when
173  *     this device was paired, if it is already paired.
174  * @return {void} Nothing.
175  */
176 remoting.SessionConnector.prototype.connectMe2Me =
177     function(host, fetchPin, fetchThirdPartyToken,
178              clientPairingId, clientPairedSecret) {
179   this.connectMe2MeInternal_(
180       host.hostId, host.jabberId, host.publicKey, host.hostName,
181       fetchPin, fetchThirdPartyToken,
182       clientPairingId, clientPairedSecret, true);
183 };
184
185 /**
186  * Update the pairing info so that the reconnect function will work correctly.
187  *
188  * @param {string} clientId The paired client id.
189  * @param {string} sharedSecret The shared secret.
190  */
191 remoting.SessionConnector.prototype.updatePairingInfo =
192     function(clientId, sharedSecret) {
193   this.clientPairingId_ = clientId;
194   this.clientPairedSecret_ = sharedSecret;
195 };
196
197 /**
198  * Initiate a Me2Me connection.
199  *
200  * @param {string} hostId ID of the Me2Me host.
201  * @param {string} hostJid XMPP JID of the host.
202  * @param {string} hostPublicKey Public Key of the host.
203  * @param {string} hostDisplayName Display name (friendly name) of the host.
204  * @param {function(boolean, function(string):void):void} fetchPin Function to
205  *     interactively obtain the PIN from the user.
206  * @param {function(string, string, string,
207  *                  function(string, string): void): void}
208  *     fetchThirdPartyToken Function to obtain a token from a third party
209  *     authenticaiton server.
210  * @param {string} clientPairingId The client id issued by the host when
211  *     this device was paired, if it is already paired.
212  * @param {string} clientPairedSecret The shared secret issued by the host when
213  *     this device was paired, if it is already paired.
214  * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
215  *     the connection if the current JID is offline.
216  * @return {void} Nothing.
217  * @private
218  */
219 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
220     function(hostId, hostJid, hostPublicKey, hostDisplayName,
221              fetchPin, fetchThirdPartyToken,
222              clientPairingId, clientPairedSecret,
223              refreshHostJidIfOffline) {
224   // Cancel any existing connect operation.
225   this.cancel();
226
227   this.hostId_ = hostId;
228   this.hostJid_ = hostJid;
229   this.hostPublicKey_ = hostPublicKey;
230   this.fetchPin_ = fetchPin;
231   this.fetchThirdPartyToken_ = fetchThirdPartyToken;
232   this.hostDisplayName_ = hostDisplayName;
233   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
234   this.refreshHostJidIfOffline_ = refreshHostJidIfOffline;
235   this.updatePairingInfo(clientPairingId, clientPairedSecret);
236   this.createSession_();
237 };
238
239 /**
240  * Initiate an IT2Me connection.
241  *
242  * @param {string} accessCode The access code as entered by the user.
243  * @return {void} Nothing.
244  */
245 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
246   var kSupportIdLen = 7;
247   var kHostSecretLen = 5;
248   var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
249
250   // Cancel any existing connect operation.
251   this.cancel();
252
253   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
254   if (normalizedAccessCode.length != kAccessCodeLen) {
255     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
256     return;
257   }
258
259   this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
260   this.passPhrase_ = normalizedAccessCode;
261   this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
262   remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
263                                   this.onError_);
264 };
265
266 /**
267  * Reconnect a closed connection.
268  *
269  * @return {void} Nothing.
270  */
271 remoting.SessionConnector.prototype.reconnect = function() {
272   if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
273     console.error('reconnect not supported for IT2Me.');
274     return;
275   }
276   this.connectMe2MeInternal_(
277       this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
278       this.fetchPin_, this.fetchThirdPartyToken_,
279       this.clientPairingId_, this.clientPairedSecret_, true);
280 };
281
282 /**
283  * Cancel a connection-in-progress.
284  */
285 remoting.SessionConnector.prototype.cancel = function() {
286   if (this.clientSession_) {
287     this.clientSession_.removePlugin();
288     this.clientSession_ = null;
289   }
290   if (this.pendingXhr_) {
291     this.pendingXhr_.abort();
292     this.pendingXhr_ = null;
293   }
294   this.reset();
295 };
296
297 /**
298  * Get the connection mode (Me2Me or IT2Me)
299  *
300  * @return {remoting.ClientSession.Mode}
301  */
302 remoting.SessionConnector.prototype.getConnectionMode = function() {
303   return this.connectionMode_;
304 };
305
306 /**
307  * Get host ID.
308  *
309  * @return {string}
310  */
311 remoting.SessionConnector.prototype.getHostId = function() {
312   return this.hostId_;
313 };
314
315 /**
316  * Get host display name.
317  *
318  * @return {string}
319  */
320 remoting.SessionConnector.prototype.getHostDisplayName = function() {
321   return this.hostDisplayName_;
322 };
323
324 /**
325  * Continue an IT2Me connection once an access token has been obtained.
326  *
327  * @param {string} token An OAuth2 access token.
328  * @return {void} Nothing.
329  * @private
330  */
331 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
332   // Resolve the host id to get the host JID.
333   this.pendingXhr_ = remoting.xhr.get(
334       remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
335           encodeURIComponent(this.hostId_),
336       this.onIT2MeHostInfo_.bind(this),
337       '',
338       { 'Authorization': 'OAuth ' + token });
339 };
340
341 /**
342  * Continue an IT2Me connection once the host JID has been looked up.
343  *
344  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
345  * @return {void} Nothing.
346  * @private
347  */
348 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
349   this.pendingXhr_ = null;
350   if (xhr.status == 200) {
351     var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
352         jsonParseSafe(xhr.responseText);
353     if (host && host.data && host.data.jabberId && host.data.publicKey) {
354       this.hostJid_ = host.data.jabberId;
355       this.hostPublicKey_ = host.data.publicKey;
356       this.hostDisplayName_ = this.hostJid_.split('/')[0];
357       this.createSession_();
358       return;
359     } else {
360       console.error('Invalid "support-hosts" response from server.');
361     }
362   } else {
363     this.onError_(this.translateSupportHostsError(xhr.status));
364   }
365 };
366
367 /**
368  * Creates ClientSession object.
369  */
370 remoting.SessionConnector.prototype.createSession_ = function() {
371   // In some circumstances, the WCS <iframe> can get reloaded, which results
372   // in a new clientJid and a new callback. In this case, remove the old
373   // client plugin before instantiating a new one.
374   if (this.clientSession_) {
375     this.clientSession_.removePlugin();
376     this.clientSession_ = null;
377   }
378
379   var authenticationMethods =
380      'third_party,spake2_pair,spake2_hmac,spake2_plain';
381   this.clientSession_ = new remoting.ClientSession(
382       this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_,
383       authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
384       this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
385   this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
386   this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
387   this.clientSession_.createPluginAndConnect(this.pluginParent_);
388 };
389
390 /**
391  * Handle a change in the state of the client session prior to successful
392  * connection (after connection, this class no longer handles state change
393  * events). Errors that occur while connecting either trigger a reconnect
394  * or notify the onError handler.
395  *
396  * @param {number} oldState The previous state of the plugin.
397  * @param {number} newState The current state of the plugin.
398  * @return {void} Nothing.
399  * @private
400  */
401 remoting.SessionConnector.prototype.onStateChange_ =
402     function(oldState, newState) {
403   switch (newState) {
404     case remoting.ClientSession.State.CONNECTED:
405       // When the connection succeeds, deregister for state-change callbacks
406       // and pass the session to the onOk callback. It is expected that it
407       // will register a new state-change callback to handle disconnect
408       // or error conditions.
409       this.clientSession_.setOnStateChange(null);
410       this.onOk_(this.clientSession_);
411       break;
412
413     case remoting.ClientSession.State.CREATED:
414       console.log('Created plugin');
415       break;
416
417     case remoting.ClientSession.State.CONNECTING:
418       console.log('Connecting as ' + remoting.identity.getCachedEmail());
419       break;
420
421     case remoting.ClientSession.State.INITIALIZING:
422       console.log('Initializing connection');
423       break;
424
425     case remoting.ClientSession.State.CLOSED:
426       // This class deregisters for state-change callbacks when the CONNECTED
427       // state is reached, so it only sees the CLOSED state in exceptional
428       // circumstances. For example, a CONNECTING -> CLOSED transition happens
429       // if the host closes the connection without an error message instead of
430       // accepting it. Since there's no way of knowing exactly what went wrong,
431       // we rely on server-side logs in this case and report a generic error
432       // message.
433       this.onError_(remoting.Error.UNEXPECTED);
434       break;
435
436     case remoting.ClientSession.State.FAILED:
437       var error = this.clientSession_.getError();
438       console.error('Client plugin reported connection failed: ' + error);
439       if (error == null) {
440         error = remoting.Error.UNEXPECTED;
441       }
442       if (error == remoting.Error.HOST_IS_OFFLINE &&
443           this.refreshHostJidIfOffline_) {
444         remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
445       } else {
446         this.onError_(error);
447       }
448       break;
449
450     default:
451       console.error('Unexpected client plugin state: ' + newState);
452       // This should only happen if the web-app and client plugin get out of
453       // sync, and even then the version check should ensure compatibility.
454       this.onError_(remoting.Error.MISSING_PLUGIN);
455   }
456 };
457
458 /**
459  * @param {boolean} success True if the host list was successfully refreshed;
460  *     false if an error occurred.
461  * @private
462  */
463 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
464   if (success) {
465     var host = remoting.hostList.getHostForId(this.hostId_);
466     if (host) {
467       this.connectMe2MeInternal_(
468           host.hostId, host.jabberId, host.publicKey, host.hostName,
469           this.fetchPin_, this.fetchThirdPartyToken_,
470           this.clientPairingId_, this.clientPairedSecret_, false);
471       return;
472     }
473   }
474   this.onError_(remoting.Error.HOST_IS_OFFLINE);
475 };
476
477 /**
478  * @param {number} error An HTTP error code returned by the support-hosts
479  *     endpoint.
480  * @return {remoting.Error} The equivalent remoting.Error code.
481  * @private
482  */
483 remoting.SessionConnector.prototype.translateSupportHostsError =
484     function(error) {
485   switch (error) {
486     case 0: return remoting.Error.NETWORK_FAILURE;
487     case 404: return remoting.Error.INVALID_ACCESS_CODE;
488     case 502: // No break
489     case 503: return remoting.Error.SERVICE_UNAVAILABLE;
490     default: return remoting.Error.UNEXPECTED;
491   }
492 };
493
494 /**
495  * Normalize the access code entered by the user.
496  *
497  * @param {string} accessCode The access code, as entered by the user.
498  * @return {string} The normalized form of the code (whitespace removed).
499  */
500 remoting.SessionConnector.prototype.normalizeAccessCode_ =
501     function(accessCode) {
502   // Trim whitespace.
503   return accessCode.replace(/\s/g, '');
504 };