Separate Simple Backend creation from initialization.
[chromium-blink-merge.git] / remoting / webapp / session_connector.js
blob649d129f84f50fdddf78418e4e002b6f6e973a7c
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 {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;
28   /**
29    * @type {function(remoting.ClientSession):void}
30    * @private
31    */
32   this.onOk_ = onOk;
34   /**
35    * @type {function(remoting.Error):void}
36    * @private
37    */
38   this.onError_ = onError;
40   /**
41    * @type {string}
42    * @private
43    */
44   this.clientJid_ = '';
46   /**
47    * @type {remoting.ClientSession.Mode}
48    * @private
49    */
50   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
52   /**
53    * A timer that polls for an updated access token.
54    *
55    * @type {number}
56    * @private
57    */
58   this.wcsAccessTokenRefreshTimer_ = 0;
60   // Initialize/declare per-connection state.
61   this.reset();
63   // Pre-load WCS to improve connection time.
64   remoting.identity.callWithToken(this.loadWcs_.bind(this), this.onError_);
67 /**
68  * Reset the per-connection state so that the object can be re-used for a
69  * second connection. Note the none of the shared WCS state is reset.
70  */
71 remoting.SessionConnector.prototype.reset = function() {
72   /**
73    * String used to identify the host to which to connect. For IT2Me, this is
74    * the first 7 digits of the access code; for Me2Me it is the host identifier.
75    *
76    * @type {string}
77    * @private
78    */
79   this.hostId_ = '';
81   /**
82    * String used to authenticate to the host on connection. For IT2Me, this is
83    * the access code; for Me2Me it is the PIN.
84    *
85    * @type {string}
86    * @private
87    */
88   this.passPhrase_ = '';
90   /**
91    * @type {string}
92    * @private
93    */
94   this.hostJid_ = '';
96   /**
97    * @type {string}
98    * @private
99    */
100   this.hostPublicKey_ = '';
102   /**
103    * @type {boolean}
104    * @private
105    */
106   this.refreshHostJidIfOffline_ = true;
108   /**
109    * @type {remoting.ClientSession}
110    * @private
111    */
112   this.clientSession_ = null;
114   /**
115    * @type {XMLHttpRequest}
116    * @private
117    */
118   this.pendingXhr_ = null;
120   /**
121    * Function to interactively obtain the PIN from the user.
122    * @type {function(function(string):void):void}
123    * @private
124    */
125   this.fetchPin_ = function(onPinFetched) {};
127   /**
128    * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
129    * this is the name of the host; for an IT2Me connection, it is the email
130    * address of the person sharing their computer.
131    *
132    * @type {string}
133    * @private
134    */
135   this.hostDisplayName_ = '';
139  * Initiate a Me2Me connection.
141  * @param {remoting.Host} host The Me2Me host to which to connect.
142  * @param {function(function(string):void):void} fetchPin Function to
143  *     interactively obtain the PIN from the user.
144  * @return {void} Nothing.
145  */
146 remoting.SessionConnector.prototype.connectMe2Me = function(host, fetchPin) {
147   // Cancel any existing connect operation.
148   this.cancel();
150   this.hostId_ = host.hostId;
151   this.hostJid_ = host.jabberId;
152   this.fetchPin_ = fetchPin;
153   this.hostDisplayName_ = host.hostName;
154   this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
155   this.createSessionIfReady_();
159  * Initiate an IT2Me connection.
161  * @param {string} accessCode The access code as entered by the user.
162  * @return {void} Nothing.
163  */
164 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
165   var kSupportIdLen = 7;
166   var kHostSecretLen = 5;
167   var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
169   // Cancel any existing connect operation.
170   this.cancel();
172   var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
173   if (normalizedAccessCode.length != kAccessCodeLen) {
174     this.onError_(remoting.Error.INVALID_ACCESS_CODE);
175     return;
176   }
178   this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
179   this.passPhrase_ = normalizedAccessCode;
180   this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
181   remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
182                                   this.onError_);
186  * Reconnect a closed connection.
188  * @return {void} Nothing.
189  */
190 remoting.SessionConnector.prototype.reconnect = function() {
191   if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
192     console.error('reconnect not supported for IT2Me.');
193     return;
194   }
195   this.createSessionIfReady_();
199  * Cancel a connection-in-progress.
200  */
201 remoting.SessionConnector.prototype.cancel = function() {
202   if (this.clientSession_) {
203     this.clientSession_.removePlugin();
204     this.clientSession_ = null;
205   }
206   if (this.pendingXhr_) {
207     this.pendingXhr_.abort();
208     this.pendingXhr_ = null;
209   }
210   this.reset();
214  * Get the connection mode (Me2Me or IT2Me)
216  * @return {remoting.ClientSession.Mode}
217  */
218 remoting.SessionConnector.prototype.getConnectionMode = function() {
219   return this.connectionMode_;
223  * Continue an IT2Me connection once an access token has been obtained.
225  * @param {string} token An OAuth2 access token.
226  * @return {void} Nothing.
227  * @private
228  */
229 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
230   // Resolve the host id to get the host JID.
231   this.pendingXhr_ = remoting.xhr.get(
232       remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
233           encodeURIComponent(this.hostId_),
234       this.onIT2MeHostInfo_.bind(this),
235       '',
236       { 'Authorization': 'OAuth ' + token });
240  * Continue an IT2Me connection once the host JID has been looked up.
242  * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
243  * @return {void} Nothing.
244  * @private
245  */
246 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
247   this.pendingXhr_ = null;
248   if (xhr.status == 200) {
249     var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
250         jsonParseSafe(xhr.responseText);
251     if (host && host.data && host.data.jabberId && host.data.publicKey) {
252       this.hostJid_ = host.data.jabberId;
253       this.hostPublicKey_ = host.data.publicKey;
254       this.hostDisplayName_ = this.hostJid_.split('/')[0];
255       this.createSessionIfReady_();
256       return;
257     } else {
258       console.error('Invalid "support-hosts" response from server.');
259     }
260   } else {
261     this.onError_(this.translateSupportHostsError(xhr.status));
262   }
266  * Load the WCS driver script.
268  * @param {string} token An OAuth2 access token.
269  * @return {void} Nothing.
270  * @private
271  */
272 remoting.SessionConnector.prototype.loadWcs_ = function(token) {
273   remoting.wcsSandbox.setOnReady(this.onWcsLoaded_.bind(this));
274   remoting.wcsSandbox.setOnError(this.onError_);
275   remoting.wcsSandbox.setAccessToken(token);
276   this.startAccessTokenRefreshTimer_();
280  * Continue an IT2Me or Me2Me connection once WCS has been loaded.
282  * @param {string} clientJid The full JID of the WCS client.
283  * @return {void} Nothing.
284  * @private
285  */
286 remoting.SessionConnector.prototype.onWcsLoaded_ = function(clientJid) {
287   this.clientJid_ = clientJid;
288   this.createSessionIfReady_();
292  * If both the client and host JIDs are available, create a session and connect.
294  * @return {void} Nothing.
295  * @private
296  */
297 remoting.SessionConnector.prototype.createSessionIfReady_ = function() {
298   if (!this.clientJid_ || !this.hostJid_) {
299     return;
300   }
302   var securityTypes = 'spake2_hmac,spake2_plain';
303   this.clientSession_ = new remoting.ClientSession(
304       this.hostJid_, this.clientJid_, this.hostPublicKey_, this.passPhrase_,
305       this.fetchPin_, securityTypes, this.hostId_, this.connectionMode_,
306       this.hostDisplayName_);
307   this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
308   this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
309   this.clientSession_.createPluginAndConnect(this.pluginParent_);
313  * Handle a change in the state of the client session prior to successful
314  * connection (after connection, this class no longer handles state change
315  * events). Errors that occur while connecting either trigger a reconnect
316  * or notify the onError handler.
318  * @param {number} oldState The previous state of the plugin.
319  * @param {number} newState The current state of the plugin.
320  * @return {void} Nothing.
321  * @private
322  */
323 remoting.SessionConnector.prototype.onStateChange_ =
324     function(oldState, newState) {
325   switch (newState) {
326     case remoting.ClientSession.State.CONNECTED:
327       // When the connection succeeds, deregister for state-change callbacks
328       // and pass the session to the onOk callback. It is expected that it
329       // will register a new state-change callback to handle disconnect
330       // or error conditions.
331       this.clientSession_.setOnStateChange(null);
332       this.onOk_(this.clientSession_);
333       break;
335     case remoting.ClientSession.State.CREATED:
336       console.log('Created plugin');
337       break;
339     case remoting.ClientSession.State.BAD_PLUGIN_VERSION:
340       this.onError_(remoting.Error.BAD_PLUGIN_VERSION);
341       break;
343     case remoting.ClientSession.State.CONNECTING:
344       console.log('Connecting as ' + remoting.identity.getCachedEmail());
345       break;
347     case remoting.ClientSession.State.INITIALIZING:
348       console.log('Initializing connection');
349       break;
351     case remoting.ClientSession.State.CLOSED:
352       // This class deregisters for state-change callbacks when the CONNECTED
353       // state is reached, so it only sees the CLOSED state in exceptional
354       // circumstances. For example, a CONNECTING -> CLOSED transition happens
355       // if the host closes the connection without an error message instead of
356       // accepting it. Since there's no way of knowing exactly what went wrong,
357       // we rely on server-side logs in this case and report a generic error
358       // message.
359       this.onError_(remoting.Error.UNEXPECTED);
360       break;
362     case remoting.ClientSession.State.FAILED:
363       var error = this.clientSession_.getError();
364       console.error('Client plugin reported connection failed: ' + error);
365       if (error == null) {
366         error = remoting.Error.UNEXPECTED;
367       }
368       if (error == remoting.Error.HOST_IS_OFFLINE &&
369           this.refreshHostJidIfOffline_) {
370         this.refreshHostJidIfOffline_ = false;
371         this.clientSession_.removePlugin();
372         this.clientSession_ = null;
373         remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
374       } else {
375         this.onError_(error);
376       }
377       break;
379     default:
380       console.error('Unexpected client plugin state: ' + newState);
381       // This should only happen if the web-app and client plugin get out of
382       // sync, and even then the version check should ensure compatibility.
383       this.onError_(remoting.Error.MISSING_PLUGIN);
384   }
388  * @param {boolean} success True if the host list was successfully refreshed;
389  *     false if an error occurred.
390  * @private
391  */
392 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
393   if (success) {
394     var host = remoting.hostList.getHostForId(this.hostId_);
395     if (host) {
396       this.connectMe2Me(host, this.fetchPin_);
397       return;
398     }
399   }
400   this.onError_(remoting.Error.HOST_IS_OFFLINE);
404  * Start a timer to periodically refresh the access token used by WCS. Access
405  * tokens have a limited lifespan, and since the WCS driver runs in a sandbox,
406  * it can't obtain a new one directly.
408  * @return {void} Nothing.
409  * @private
410  */
411 remoting.SessionConnector.prototype.startAccessTokenRefreshTimer_ = function() {
412   if (this.wcsAccessTokenRefreshTimer_ != 0) {
413     return;
414   }
416   /** @type {remoting.SessionConnector} */
417   var that = this;
418   var refreshAccessToken = function() {
419     remoting.identity.callWithToken(
420         remoting.wcsSandbox.setAccessToken.bind(remoting.wcsSandbox),
421         that.onError_);
422   };
423   /**
424    * A timer that polls for an updated access token.
425    * @type {number}
426    * @private
427    */
428   this.wcsAccessTokenRefreshTimer_ = setInterval(refreshAccessToken,
429                                                  60 * 1000);
433  * @param {number} error An HTTP error code returned by the support-hosts
434  *     endpoint.
435  * @return {remoting.Error} The equivalent remoting.Error code.
436  * @private
437  */
438 remoting.SessionConnector.prototype.translateSupportHostsError =
439     function(error) {
440   switch (error) {
441     case 0: return remoting.Error.NETWORK_FAILURE;
442     case 404: return remoting.Error.INVALID_ACCESS_CODE;
443     case 502: // No break
444     case 503: return remoting.Error.SERVICE_UNAVAILABLE;
445     default: return remoting.Error.UNEXPECTED;
446   }
450  * Normalize the access code entered by the user.
452  * @param {string} accessCode The access code, as entered by the user.
453  * @return {string} The normalized form of the code (whitespace removed).
454  */
455 remoting.SessionConnector.prototype.normalizeAccessCode_ =
456     function(accessCode) {
457   // Trim whitespace.
458   return accessCode.replace(/\s/g, '');