• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
13var 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 */
21remoting.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 */
60remoting.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 */
176remoting.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 */
191remoting.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 */
219remoting.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 */
245remoting.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 */
271remoting.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 */
285remoting.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 */
302remoting.SessionConnector.prototype.getConnectionMode = function() {
303  return this.connectionMode_;
304};
305
306/**
307 * Get host ID.
308 *
309 * @return {string}
310 */
311remoting.SessionConnector.prototype.getHostId = function() {
312  return this.hostId_;
313};
314
315/**
316 * Get host display name.
317 *
318 * @return {string}
319 */
320remoting.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 */
331remoting.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 */
348remoting.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 */
370remoting.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 */
401remoting.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 */
463remoting.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 */
483remoting.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 */
500remoting.SessionConnector.prototype.normalizeAccessCode_ =
501    function(accessCode) {
502  // Trim whitespace.
503  return accessCode.replace(/\s/g, '');
504};
505