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 {HTMLElement} clientContainer Container element for the client view. 17 * @param {function(remoting.ClientSession):void} onConnected Callback on 18 * success. 19 * @param {function(remoting.Error):void} onError Callback on error. 20 * @param {function(string, string):boolean} onExtensionMessage The handler for 21 * protocol extension messages. Returns true if a message is recognized; 22 * false otherwise. 23 * @constructor 24 * @implements {remoting.SessionConnector} 25 */ 26remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError, 27 onExtensionMessage) { 28 /** 29 * @type {HTMLElement} 30 * @private 31 */ 32 this.clientContainer_ = clientContainer; 33 34 /** 35 * @type {function(remoting.ClientSession):void} 36 * @private 37 */ 38 this.onConnected_ = onConnected; 39 40 /** 41 * @type {function(remoting.Error):void} 42 * @private 43 */ 44 this.onError_ = onError; 45 46 /** 47 * @type {function(string, string):boolean} 48 * @private 49 */ 50 this.onExtensionMessage_ = onExtensionMessage; 51 52 /** 53 * @type {string} 54 * @private 55 */ 56 this.clientJid_ = ''; 57 58 /** 59 * @type {remoting.ClientSession.Mode} 60 * @private 61 */ 62 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME; 63 64 /** 65 * @type {remoting.SignalStrategy} 66 * @private 67 */ 68 this.signalStrategy_ = null; 69 70 /** 71 * @type {remoting.SmartReconnector} 72 * @private 73 */ 74 this.reconnector_ = null; 75 76 /** 77 * @private 78 */ 79 this.bound_ = { 80 onStateChange : this.onStateChange_.bind(this) 81 }; 82 83 // Initialize/declare per-connection state. 84 this.reset(); 85}; 86 87/** 88 * Reset the per-connection state so that the object can be re-used for a 89 * second connection. Note the none of the shared WCS state is reset. 90 */ 91remoting.SessionConnectorImpl.prototype.reset = function() { 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.SessionConnectorImpl.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.SessionConnectorImpl.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.SessionConnectorImpl.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 260 this.connectSignaling_(); 261} 262 263/** 264 * Initiate an IT2Me connection. 265 * 266 * @param {string} accessCode The access code as entered by the user. 267 * @return {void} Nothing. 268 */ 269remoting.SessionConnectorImpl.prototype.connectIT2Me = function(accessCode) { 270 var kSupportIdLen = 7; 271 var kHostSecretLen = 5; 272 var kAccessCodeLen = kSupportIdLen + kHostSecretLen; 273 274 // Cancel any existing connect operation. 275 this.cancel(); 276 277 var normalizedAccessCode = this.normalizeAccessCode_(accessCode); 278 if (normalizedAccessCode.length != kAccessCodeLen) { 279 this.onError_(remoting.Error.INVALID_ACCESS_CODE); 280 return; 281 } 282 283 this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen); 284 this.passPhrase_ = normalizedAccessCode; 285 this.connectionMode_ = remoting.ClientSession.Mode.IT2ME; 286 remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this), 287 this.onError_); 288}; 289 290/** 291 * Reconnect a closed connection. 292 * 293 * @return {void} Nothing. 294 */ 295remoting.SessionConnectorImpl.prototype.reconnect = function() { 296 if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) { 297 console.error('reconnect not supported for IT2Me.'); 298 return; 299 } 300 this.connectMe2MeInternal_( 301 this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_, 302 this.fetchPin_, this.fetchThirdPartyToken_, 303 this.clientPairingId_, this.clientPairedSecret_, true); 304}; 305 306/** 307 * Cancel a connection-in-progress. 308 */ 309remoting.SessionConnectorImpl.prototype.cancel = function() { 310 if (this.clientSession_) { 311 this.clientSession_.removePlugin(); 312 this.clientSession_ = null; 313 } 314 if (this.pendingXhr_) { 315 this.pendingXhr_.abort(); 316 this.pendingXhr_ = null; 317 } 318 this.reset(); 319}; 320 321/** 322 * Get the connection mode (Me2Me or IT2Me) 323 * 324 * @return {remoting.ClientSession.Mode} 325 */ 326remoting.SessionConnectorImpl.prototype.getConnectionMode = function() { 327 return this.connectionMode_; 328}; 329 330/** 331 * Get host ID. 332 * 333 * @return {string} 334 */ 335remoting.SessionConnectorImpl.prototype.getHostId = function() { 336 return this.hostId_; 337}; 338 339/** 340 * @private 341 */ 342remoting.SessionConnectorImpl.prototype.connectSignaling_ = function() { 343 base.dispose(this.signalStrategy_); 344 this.signalStrategy_ = null; 345 346 /** @type {remoting.SessionConnectorImpl} */ 347 var that = this; 348 349 /** @param {string} token */ 350 function connectSignalingWithToken(token) { 351 remoting.identity.getEmail( 352 connectSignalingWithTokenAndEmail.bind(null, token), that.onError_); 353 } 354 355 /** 356 * @param {string} token 357 * @param {string} email 358 */ 359 function connectSignalingWithTokenAndEmail(token, email) { 360 that.signalStrategy_.connect( 361 remoting.settings.XMPP_SERVER_ADDRESS, email, token); 362 } 363 364 this.signalStrategy_ = 365 remoting.SignalStrategy.create(this.onSignalingState_.bind(this)); 366 367 remoting.identity.callWithToken(connectSignalingWithToken, this.onError_); 368}; 369 370/** 371 * @private 372 * @param {remoting.SignalStrategy.State} state 373 */ 374remoting.SessionConnectorImpl.prototype.onSignalingState_ = function(state) { 375 switch (state) { 376 case remoting.SignalStrategy.State.CONNECTED: 377 // Proceed only if the connection hasn't been canceled. 378 if (this.hostJid_) { 379 this.createSession_(); 380 } 381 break; 382 383 case remoting.SignalStrategy.State.FAILED: 384 this.onError_(this.signalStrategy_.getError()); 385 break; 386 } 387}; 388 389/** 390 * Continue an IT2Me connection once an access token has been obtained. 391 * 392 * @param {string} token An OAuth2 access token. 393 * @return {void} Nothing. 394 * @private 395 */ 396remoting.SessionConnectorImpl.prototype.connectIT2MeWithToken_ = 397 function(token) { 398 // Resolve the host id to get the host JID. 399 this.pendingXhr_ = remoting.xhr.get( 400 remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + 401 encodeURIComponent(this.hostId_), 402 this.onIT2MeHostInfo_.bind(this), 403 '', 404 { 'Authorization': 'OAuth ' + token }); 405}; 406 407/** 408 * Continue an IT2Me connection once the host JID has been looked up. 409 * 410 * @param {XMLHttpRequest} xhr The server response to the support-hosts query. 411 * @return {void} Nothing. 412 * @private 413 */ 414remoting.SessionConnectorImpl.prototype.onIT2MeHostInfo_ = function(xhr) { 415 this.pendingXhr_ = null; 416 if (xhr.status == 200) { 417 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ 418 jsonParseSafe(xhr.responseText); 419 if (host && host.data && host.data.jabberId && host.data.publicKey) { 420 this.hostJid_ = host.data.jabberId; 421 this.hostPublicKey_ = host.data.publicKey; 422 this.hostDisplayName_ = this.hostJid_.split('/')[0]; 423 this.connectSignaling_(); 424 return; 425 } else { 426 console.error('Invalid "support-hosts" response from server.'); 427 } 428 } else { 429 this.onError_(this.translateSupportHostsError_(xhr.status)); 430 } 431}; 432 433/** 434 * Creates ClientSession object. 435 */ 436remoting.SessionConnectorImpl.prototype.createSession_ = function() { 437 // In some circumstances, the WCS <iframe> can get reloaded, which results 438 // in a new clientJid and a new callback. In this case, remove the old 439 // client plugin before instantiating a new one. 440 if (this.clientSession_) { 441 this.clientSession_.removePlugin(); 442 this.clientSession_ = null; 443 } 444 445 var authenticationMethods = 446 'third_party,spake2_pair,spake2_hmac,spake2_plain'; 447 this.clientSession_ = new remoting.ClientSession( 448 this.signalStrategy_, this.clientContainer_, this.hostDisplayName_, 449 this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_, 450 authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_, 451 this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_); 452 this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_); 453 this.clientSession_.addEventListener( 454 remoting.ClientSession.Events.stateChanged, 455 this.bound_.onStateChange); 456 this.clientSession_.createPluginAndConnect(this.onExtensionMessage_); 457}; 458 459/** 460 * Handle a change in the state of the client session prior to successful 461 * connection (after connection, this class no longer handles state change 462 * events). Errors that occur while connecting either trigger a reconnect 463 * or notify the onError handler. 464 * 465 * @param {remoting.ClientSession.StateEvent} event 466 * @return {void} Nothing. 467 * @private 468 */ 469remoting.SessionConnectorImpl.prototype.onStateChange_ = function(event) { 470 switch (event.current) { 471 case remoting.ClientSession.State.CONNECTED: 472 // When the connection succeeds, deregister for state-change callbacks 473 // and pass the session to the onConnected callback. It is expected that 474 // it will register a new state-change callback to handle disconnect 475 // or error conditions. 476 this.clientSession_.removeEventListener( 477 remoting.ClientSession.Events.stateChanged, 478 this.bound_.onStateChange); 479 480 base.dispose(this.reconnector_); 481 if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) { 482 this.reconnector_ = 483 new remoting.SmartReconnector(this, this.clientSession_); 484 } 485 this.onConnected_(this.clientSession_); 486 break; 487 488 case remoting.ClientSession.State.CREATED: 489 console.log('Created plugin'); 490 break; 491 492 case remoting.ClientSession.State.CONNECTING: 493 console.log('Connecting as ' + remoting.identity.getCachedEmail()); 494 break; 495 496 case remoting.ClientSession.State.INITIALIZING: 497 console.log('Initializing connection'); 498 break; 499 500 case remoting.ClientSession.State.CLOSED: 501 // This class deregisters for state-change callbacks when the CONNECTED 502 // state is reached, so it only sees the CLOSED state in exceptional 503 // circumstances. For example, a CONNECTING -> CLOSED transition happens 504 // if the host closes the connection without an error message instead of 505 // accepting it. Since there's no way of knowing exactly what went wrong, 506 // we rely on server-side logs in this case and report a generic error 507 // message. 508 this.onError_(remoting.Error.UNEXPECTED); 509 break; 510 511 case remoting.ClientSession.State.FAILED: 512 var error = this.clientSession_.getError(); 513 console.error('Client plugin reported connection failed: ' + error); 514 if (error == null) { 515 error = remoting.Error.UNEXPECTED; 516 } 517 if (error == remoting.Error.HOST_IS_OFFLINE && 518 this.refreshHostJidIfOffline_) { 519 // The plugin will be re-created when the host finished refreshing 520 remoting.hostList.refresh(this.onHostListRefresh_.bind(this)); 521 } else { 522 this.onError_(error); 523 } 524 break; 525 526 default: 527 console.error('Unexpected client plugin state: ' + event.current); 528 // This should only happen if the web-app and client plugin get out of 529 // sync, and even then the version check should ensure compatibility. 530 this.onError_(remoting.Error.MISSING_PLUGIN); 531 } 532}; 533 534/** 535 * @param {boolean} success True if the host list was successfully refreshed; 536 * false if an error occurred. 537 * @private 538 */ 539remoting.SessionConnectorImpl.prototype.onHostListRefresh_ = function(success) { 540 if (success) { 541 var host = remoting.hostList.getHostForId(this.hostId_); 542 if (host) { 543 this.connectMe2MeInternal_( 544 host.hostId, host.jabberId, host.publicKey, host.hostName, 545 this.fetchPin_, this.fetchThirdPartyToken_, 546 this.clientPairingId_, this.clientPairedSecret_, false); 547 return; 548 } 549 } 550 this.onError_(remoting.Error.HOST_IS_OFFLINE); 551}; 552 553/** 554 * @param {number} error An HTTP error code returned by the support-hosts 555 * endpoint. 556 * @return {remoting.Error} The equivalent remoting.Error code. 557 * @private 558 */ 559remoting.SessionConnectorImpl.prototype.translateSupportHostsError_ = 560 function(error) { 561 switch (error) { 562 case 0: return remoting.Error.NETWORK_FAILURE; 563 case 404: return remoting.Error.INVALID_ACCESS_CODE; 564 case 502: // No break 565 case 503: return remoting.Error.SERVICE_UNAVAILABLE; 566 default: return remoting.Error.UNEXPECTED; 567 } 568}; 569 570/** 571 * Normalize the access code entered by the user. 572 * 573 * @param {string} accessCode The access code, as entered by the user. 574 * @return {string} The normalized form of the code (whitespace removed). 575 * @private 576 */ 577remoting.SessionConnectorImpl.prototype.normalizeAccessCode_ = 578 function(accessCode) { 579 // Trim whitespace. 580 return accessCode.replace(/\s/g, ''); 581}; 582 583 584/** 585 * @constructor 586 * @implements {remoting.SessionConnectorFactory} 587 */ 588remoting.DefaultSessionConnectorFactory = function() { 589}; 590 591/** 592 * @param {HTMLElement} clientContainer Container element for the client view. 593 * @param {function(remoting.ClientSession):void} onConnected Callback on 594 * success. 595 * @param {function(remoting.Error):void} onError Callback on error. 596 * @param {function(string, string):boolean} onExtensionMessage The handler for 597 * protocol extension messages. Returns true if a message is recognized; 598 * false otherwise. 599 */ 600remoting.DefaultSessionConnectorFactory.prototype.createConnector = 601 function(clientContainer, onConnected, onError, onExtensionMessage) { 602 return new remoting.SessionConnectorImpl( 603 clientContainer, onConnected, onError, onExtensionMessage); 604}; 605