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.
7 * Connect set-up state machine for Me2Me and IT2Me
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
16 * @param {HTMLElement} clientContainer Container element for the client view.
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;
24 remoting.SessionConnector = function(clientContainer, onOk, onError,
30 this.clientContainer_ = clientContainer;
33 * @type {function(remoting.ClientSession):void}
39 * @type {function(remoting.Error):void}
42 this.onError_ = onError;
45 * @type {function(string, string):boolean}
48 this.onExtensionMessage_ = onExtensionMessage;
57 * @type {remoting.ClientSession.Mode}
60 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
63 * @type {remoting.SmartReconnector}
66 this.reconnector_ = null;
72 onStateChange : this.onStateChange_.bind(this)
75 // Initialize/declare per-connection state.
80 * Reset the per-connection state so that the object can be re-used for a
81 * second connection. Note the none of the shared WCS state is reset.
83 remoting.SessionConnector.prototype.reset = function() {
85 * Set to true to indicate that the user requested pairing when entering
86 * their PIN for a Me2Me connection.
90 this.pairingRequested = false;
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.
102 * For paired connections, the client id of this device, issued by the host.
107 this.clientPairingId_ = '';
110 * For paired connections, the paired secret for this device, issued by the
116 this.clientPairedSecret_ = '';
119 * String used to authenticate to the host on connection. For IT2Me, this is
120 * the access code; for Me2Me it is the PIN.
125 this.passPhrase_ = '';
137 this.hostPublicKey_ = '';
143 this.refreshHostJidIfOffline_ = false;
146 * @type {remoting.ClientSession}
149 this.clientSession_ = null;
152 * @type {XMLHttpRequest}
155 this.pendingXhr_ = null;
158 * Function to interactively obtain the PIN from the user.
159 * @type {function(boolean, function(string):void):void}
162 this.fetchPin_ = function(onPinFetched) {};
165 * @type {function(string, string, string,
166 * function(string, string):void): void}
169 this.fetchThirdPartyToken_ = function(
170 tokenUrl, scope, onThirdPartyTokenFetched) {};
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.
180 this.hostDisplayName_ = '';
184 * Initiate a Me2Me connection.
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.
199 remoting.SessionConnector.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);
209 * Update the pairing info so that the reconnect function will work correctly.
211 * @param {string} clientId The paired client id.
212 * @param {string} sharedSecret The shared secret.
214 remoting.SessionConnector.prototype.updatePairingInfo =
215 function(clientId, sharedSecret) {
216 this.clientPairingId_ = clientId;
217 this.clientPairedSecret_ = sharedSecret;
221 * Initiate a Me2Me connection.
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.
242 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
243 function(hostId, hostJid, hostPublicKey, hostDisplayName,
244 fetchPin, fetchThirdPartyToken,
245 clientPairingId, clientPairedSecret,
246 refreshHostJidIfOffline) {
247 // Cancel any existing connect operation.
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 this.createSession_();
263 * Initiate an IT2Me connection.
265 * @param {string} accessCode The access code as entered by the user.
266 * @return {void} Nothing.
268 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) {
269 var kSupportIdLen = 7;
270 var kHostSecretLen = 5;
271 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
273 // Cancel any existing connect operation.
276 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
277 if (normalizedAccessCode.length != kAccessCodeLen) {
278 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
282 this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
283 this.passPhrase_ = normalizedAccessCode;
284 this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
285 remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
290 * Reconnect a closed connection.
292 * @return {void} Nothing.
294 remoting.SessionConnector.prototype.reconnect = function() {
295 if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
296 console.error('reconnect not supported for IT2Me.');
299 this.connectMe2MeInternal_(
300 this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
301 this.fetchPin_, this.fetchThirdPartyToken_,
302 this.clientPairingId_, this.clientPairedSecret_, true);
306 * Cancel a connection-in-progress.
308 remoting.SessionConnector.prototype.cancel = function() {
309 if (this.clientSession_) {
310 this.clientSession_.removePlugin();
311 this.clientSession_ = null;
313 if (this.pendingXhr_) {
314 this.pendingXhr_.abort();
315 this.pendingXhr_ = null;
321 * Get the connection mode (Me2Me or IT2Me)
323 * @return {remoting.ClientSession.Mode}
325 remoting.SessionConnector.prototype.getConnectionMode = function() {
326 return this.connectionMode_;
334 remoting.SessionConnector.prototype.getHostId = function() {
339 * Continue an IT2Me connection once an access token has been obtained.
341 * @param {string} token An OAuth2 access token.
342 * @return {void} Nothing.
345 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) {
346 // Resolve the host id to get the host JID.
347 this.pendingXhr_ = remoting.xhr.get(
348 remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
349 encodeURIComponent(this.hostId_),
350 this.onIT2MeHostInfo_.bind(this),
352 { 'Authorization': 'OAuth ' + token });
356 * Continue an IT2Me connection once the host JID has been looked up.
358 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
359 * @return {void} Nothing.
362 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
363 this.pendingXhr_ = null;
364 if (xhr.status == 200) {
365 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
366 jsonParseSafe(xhr.responseText);
367 if (host && host.data && host.data.jabberId && host.data.publicKey) {
368 this.hostJid_ = host.data.jabberId;
369 this.hostPublicKey_ = host.data.publicKey;
370 this.hostDisplayName_ = this.hostJid_.split('/')[0];
371 this.createSession_();
374 console.error('Invalid "support-hosts" response from server.');
377 this.onError_(this.translateSupportHostsError(xhr.status));
382 * Creates ClientSession object.
384 remoting.SessionConnector.prototype.createSession_ = function() {
385 // In some circumstances, the WCS <iframe> can get reloaded, which results
386 // in a new clientJid and a new callback. In this case, remove the old
387 // client plugin before instantiating a new one.
388 if (this.clientSession_) {
389 this.clientSession_.removePlugin();
390 this.clientSession_ = null;
393 var authenticationMethods =
394 'third_party,spake2_pair,spake2_hmac,spake2_plain';
395 this.clientSession_ = new remoting.ClientSession(
396 this.clientContainer_, this.hostDisplayName_, this.passPhrase_,
397 this.fetchPin_, this.fetchThirdPartyToken_, authenticationMethods,
398 this.hostId_, this.hostJid_, this.hostPublicKey_, this.connectionMode_,
399 this.clientPairingId_, this.clientPairedSecret_);
400 this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
401 this.clientSession_.addEventListener(
402 remoting.ClientSession.Events.stateChanged,
403 this.bound_.onStateChange);
404 this.clientSession_.createPluginAndConnect(this.onExtensionMessage_);
408 * Handle a change in the state of the client session prior to successful
409 * connection (after connection, this class no longer handles state change
410 * events). Errors that occur while connecting either trigger a reconnect
411 * or notify the onError handler.
413 * @param {remoting.ClientSession.StateEvent} event
414 * @return {void} Nothing.
417 remoting.SessionConnector.prototype.onStateChange_ = function(event) {
418 switch (event.current) {
419 case remoting.ClientSession.State.CONNECTED:
420 // When the connection succeeds, deregister for state-change callbacks
421 // and pass the session to the onOk callback. It is expected that it
422 // will register a new state-change callback to handle disconnect
423 // or error conditions.
424 this.clientSession_.removeEventListener(
425 remoting.ClientSession.Events.stateChanged,
426 this.bound_.onStateChange);
428 base.dispose(this.reconnector_);
429 if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) {
431 new remoting.SmartReconnector(this, this.clientSession_);
433 this.onOk_(this.clientSession_);
436 case remoting.ClientSession.State.CREATED:
437 console.log('Created plugin');
440 case remoting.ClientSession.State.CONNECTING:
441 console.log('Connecting as ' + remoting.identity.getCachedEmail());
444 case remoting.ClientSession.State.INITIALIZING:
445 console.log('Initializing connection');
448 case remoting.ClientSession.State.CLOSED:
449 // This class deregisters for state-change callbacks when the CONNECTED
450 // state is reached, so it only sees the CLOSED state in exceptional
451 // circumstances. For example, a CONNECTING -> CLOSED transition happens
452 // if the host closes the connection without an error message instead of
453 // accepting it. Since there's no way of knowing exactly what went wrong,
454 // we rely on server-side logs in this case and report a generic error
456 this.onError_(remoting.Error.UNEXPECTED);
459 case remoting.ClientSession.State.FAILED:
460 var error = this.clientSession_.getError();
461 console.error('Client plugin reported connection failed: ' + error);
463 error = remoting.Error.UNEXPECTED;
465 if (error == remoting.Error.HOST_IS_OFFLINE &&
466 this.refreshHostJidIfOffline_) {
467 // The plugin will be re-created when the host finished refreshing
468 remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
470 this.onError_(error);
475 console.error('Unexpected client plugin state: ' + event.current);
476 // This should only happen if the web-app and client plugin get out of
477 // sync, and even then the version check should ensure compatibility.
478 this.onError_(remoting.Error.MISSING_PLUGIN);
483 * @param {boolean} success True if the host list was successfully refreshed;
484 * false if an error occurred.
487 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
489 var host = remoting.hostList.getHostForId(this.hostId_);
491 this.connectMe2MeInternal_(
492 host.hostId, host.jabberId, host.publicKey, host.hostName,
493 this.fetchPin_, this.fetchThirdPartyToken_,
494 this.clientPairingId_, this.clientPairedSecret_, false);
498 this.onError_(remoting.Error.HOST_IS_OFFLINE);
502 * @param {number} error An HTTP error code returned by the support-hosts
504 * @return {remoting.Error} The equivalent remoting.Error code.
507 remoting.SessionConnector.prototype.translateSupportHostsError =
510 case 0: return remoting.Error.NETWORK_FAILURE;
511 case 404: return remoting.Error.INVALID_ACCESS_CODE;
512 case 502: // No break
513 case 503: return remoting.Error.SERVICE_UNAVAILABLE;
514 default: return remoting.Error.UNEXPECTED;
519 * Normalize the access code entered by the user.
521 * @param {string} accessCode The access code, as entered by the user.
522 * @return {string} The normalized form of the code (whitespace removed).
524 remoting.SessionConnector.prototype.normalizeAccessCode_ =
525 function(accessCode) {
527 return accessCode.replace(/\s/g, '');