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