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