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