| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 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 * | |
| 8 * It2MeHelpeeChannel relays messages between the Hangouts web page (Hangouts) | |
| 9 * and the It2Me Native Messaging Host (It2MeHost) for the helpee (the Hangouts | |
| 10 * participant who is receiving remoting assistance). | |
| 11 * | |
| 12 * It runs in the background page. It contains a chrome.runtime.Port object, | |
| 13 * representing a connection to Hangouts, and a remoting.It2MeHostFacade object, | |
| 14 * representing a connection to the IT2Me Native Messaging Host. | |
| 15 * | |
| 16 * Hangouts It2MeHelpeeChannel It2MeHost | |
| 17 * |---------runtime.connect()-------->| | | |
| 18 * |-----------hello message---------->| | | |
| 19 * |<-----helloResponse message------->| | | |
| 20 * |----------connect message--------->| | | |
| 21 * | |-----showConfirmDialog()----->| | |
| 22 * | |----------connect()---------->| | |
| 23 * | |<-------hostStateChanged------| | |
| 24 * | | (RECEIVED_ACCESS_CODE) | | |
| 25 * |<---connect response (access code)-| | | |
| 26 * | | | | |
| 27 * | |
| 28 * Hangouts will send the access code to the web app on the helper side. | |
| 29 * The helper will then connect to the It2MeHost using the access code. | |
| 30 * | |
| 31 * Hangouts It2MeHelpeeChannel It2MeHost | |
| 32 * | |<-------hostStateChanged------| | |
| 33 * | | (CONNECTED) | | |
| 34 * |<-- hostStateChanged(CONNECTED)----| | | |
| 35 * |-------disconnect message--------->| | | |
| 36 * |<--hostStateChanged(DISCONNECTED)--| | | |
| 37 * | |
| 38 * | |
| 39 * It also handles host downloads and install status queries: | |
| 40 * | |
| 41 * Hangouts It2MeHelpeeChannel | |
| 42 * |------isHostInstalled message----->| | |
| 43 * |<-isHostInstalled response(false)--| | |
| 44 * | | | |
| 45 * |--------downloadHost message------>| | |
| 46 * | | | |
| 47 * |------isHostInstalled message----->| | |
| 48 * |<-isHostInstalled response(false)--| | |
| 49 * | | | |
| 50 * |------isHostInstalled message----->| | |
| 51 * |<-isHostInstalled response(true)---| | |
| 52 */ | |
| 53 | |
| 54 'use strict'; | |
| 55 | |
| 56 /** @suppress {duplicate} */ | |
| 57 var remoting = remoting || {}; | |
| 58 | |
| 59 /** | |
| 60 * @param {chrome.runtime.Port} hangoutPort | |
| 61 * @param {remoting.It2MeHostFacade} host | |
| 62 * @param {remoting.HostInstaller} hostInstaller | |
| 63 * @param {function()} onDisposedCallback Callback to notify the client when | |
| 64 * the connection is torn down. | |
| 65 * | |
| 66 * @constructor | |
| 67 * @implements {base.Disposable} | |
| 68 */ | |
| 69 remoting.It2MeHelpeeChannel = | |
| 70 function(hangoutPort, host, hostInstaller, onDisposedCallback) { | |
| 71 /** | |
| 72 * @type {chrome.runtime.Port} | |
| 73 * @private | |
| 74 */ | |
| 75 this.hangoutPort_ = hangoutPort; | |
| 76 | |
| 77 /** | |
| 78 * @type {remoting.It2MeHostFacade} | |
| 79 * @private | |
| 80 */ | |
| 81 this.host_ = host; | |
| 82 | |
| 83 /** | |
| 84 * @type {?remoting.HostInstaller} | |
| 85 * @private | |
| 86 */ | |
| 87 this.hostInstaller_ = hostInstaller; | |
| 88 | |
| 89 /** | |
| 90 * @type {remoting.HostSession.State} | |
| 91 * @private | |
| 92 */ | |
| 93 this.hostState_ = remoting.HostSession.State.UNKNOWN; | |
| 94 | |
| 95 /** | |
| 96 * @type {?function()} | |
| 97 * @private | |
| 98 */ | |
| 99 this.onDisposedCallback_ = onDisposedCallback; | |
| 100 | |
| 101 this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this); | |
| 102 this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this); | |
| 103 }; | |
| 104 | |
| 105 /** @enum {string} */ | |
| 106 remoting.It2MeHelpeeChannel.HangoutMessageTypes = { | |
| 107 CONNECT: 'connect', | |
| 108 CONNECT_RESPONSE: 'connectResponse', | |
| 109 DISCONNECT: 'disconnect', | |
| 110 DOWNLOAD_HOST: 'downloadHost', | |
| 111 ERROR: 'error', | |
| 112 HELLO: 'hello', | |
| 113 HELLO_RESPONSE: 'helloResponse', | |
| 114 HOST_STATE_CHANGED: 'hostStateChanged', | |
| 115 IS_HOST_INSTALLED: 'isHostInstalled', | |
| 116 IS_HOST_INSTALLED_RESPONSE: 'isHostInstalledResponse' | |
| 117 }; | |
| 118 | |
| 119 /** @enum {string} */ | |
| 120 remoting.It2MeHelpeeChannel.Features = { | |
| 121 REMOTE_ASSISTANCE: 'remoteAssistance' | |
| 122 }; | |
| 123 | |
| 124 remoting.It2MeHelpeeChannel.prototype.init = function() { | |
| 125 this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_); | |
| 126 this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_); | |
| 127 }; | |
| 128 | |
| 129 remoting.It2MeHelpeeChannel.prototype.dispose = function() { | |
| 130 if (this.host_ !== null) { | |
| 131 this.host_.unhookCallbacks(); | |
| 132 this.host_.disconnect(); | |
| 133 this.host_ = null; | |
| 134 } | |
| 135 | |
| 136 if (this.hangoutPort_ !== null) { | |
| 137 this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_); | |
| 138 this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_); | |
| 139 this.hostState_ = remoting.HostSession.State.DISCONNECTED; | |
| 140 | |
| 141 try { | |
| 142 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; | |
| 143 this.hangoutPort_.postMessage({ | |
| 144 method: MessageTypes.HOST_STATE_CHANGED, | |
| 145 state: this.hostState_ | |
| 146 }); | |
| 147 } catch (e) { | |
| 148 // |postMessage| throws if |this.hangoutPort_| is disconnected | |
| 149 // It is safe to ignore the exception. | |
| 150 } | |
| 151 this.hangoutPort_.disconnect(); | |
| 152 this.hangoutPort_ = null; | |
| 153 } | |
| 154 | |
| 155 if (this.onDisposedCallback_ !== null) { | |
| 156 this.onDisposedCallback_(); | |
| 157 this.onDisposedCallback_ = null; | |
| 158 } | |
| 159 }; | |
| 160 | |
| 161 /** | |
| 162 * Message Handler for incoming runtime messages from Hangouts. | |
| 163 * | |
| 164 * @param {{method:string, data:Object<string,*>}} message | |
| 165 * @private | |
| 166 */ | |
| 167 remoting.It2MeHelpeeChannel.prototype.onHangoutMessage_ = function(message) { | |
| 168 try { | |
| 169 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; | |
| 170 switch (message.method) { | |
| 171 case MessageTypes.HELLO: | |
| 172 this.hangoutPort_.postMessage({ | |
| 173 method: MessageTypes.HELLO_RESPONSE, | |
| 174 supportedFeatures: base.values(remoting.It2MeHelpeeChannel.Features) | |
| 175 }); | |
| 176 return true; | |
| 177 case MessageTypes.IS_HOST_INSTALLED: | |
| 178 this.handleIsHostInstalled_(message); | |
| 179 return true; | |
| 180 case MessageTypes.DOWNLOAD_HOST: | |
| 181 this.handleDownloadHost_(message); | |
| 182 return true; | |
| 183 case MessageTypes.CONNECT: | |
| 184 this.handleConnect_(message); | |
| 185 return true; | |
| 186 case MessageTypes.DISCONNECT: | |
| 187 this.dispose(); | |
| 188 return true; | |
| 189 } | |
| 190 throw new Error('Unsupported message method=' + message.method); | |
| 191 } catch(/** @type {Error} */ error) { | |
| 192 this.sendErrorResponse_(message, error.message); | |
| 193 } | |
| 194 return false; | |
| 195 }; | |
| 196 | |
| 197 /** | |
| 198 * Queries the |hostInstaller| for the installation status. | |
| 199 * | |
| 200 * @param {{method:string, data:Object<string,*>}} message | |
| 201 * @private | |
| 202 */ | |
| 203 remoting.It2MeHelpeeChannel.prototype.handleIsHostInstalled_ = | |
| 204 function(message) { | |
| 205 /** @type {remoting.It2MeHelpeeChannel} */ | |
| 206 var that = this; | |
| 207 | |
| 208 /** @param {boolean} installed */ | |
| 209 function sendResponse(installed) { | |
| 210 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; | |
| 211 that.hangoutPort_.postMessage({ | |
| 212 method: MessageTypes.IS_HOST_INSTALLED_RESPONSE, | |
| 213 result: installed | |
| 214 }); | |
| 215 } | |
| 216 | |
| 217 remoting.HostInstaller.isInstalled().then( | |
| 218 sendResponse, | |
| 219 /** @type {function(*):void} */(this.sendErrorResponse_.bind(this, message)) | |
| 220 ); | |
| 221 }; | |
| 222 | |
| 223 /** | |
| 224 * @param {{method:string, data:Object<string,*>}} message | |
| 225 * @private | |
| 226 */ | |
| 227 remoting.It2MeHelpeeChannel.prototype.handleDownloadHost_ = function(message) { | |
| 228 try { | |
| 229 this.hostInstaller_.download(); | |
| 230 } catch (/** @type {*} */ e) { | |
| 231 var error = /** @type {Error} */ (e); | |
| 232 this.sendErrorResponse_(message, error.message); | |
| 233 } | |
| 234 }; | |
| 235 | |
| 236 /** | |
| 237 * Disconnect the session if the |hangoutPort| gets disconnected. | |
| 238 * @private | |
| 239 */ | |
| 240 remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() { | |
| 241 this.dispose(); | |
| 242 }; | |
| 243 | |
| 244 /** | |
| 245 * Connects to the It2Me Native messaging Host and retrieves the access code. | |
| 246 * | |
| 247 * @param {{method:string, data:Object<string,*>}} message | |
| 248 * @private | |
| 249 */ | |
| 250 remoting.It2MeHelpeeChannel.prototype.handleConnect_ = | |
| 251 function(message) { | |
| 252 var bounds = | |
| 253 /** @type {Bounds} */ (getObjectAttr(message, 'hangoutBounds', null)); | |
| 254 | |
| 255 if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) { | |
| 256 throw new Error('An existing connection is in progress.'); | |
| 257 } | |
| 258 | |
| 259 var that = this; | |
| 260 this.showConfirmDialog_(bounds) | |
| 261 .then(this.initializeHost_.bind(this)) | |
| 262 .then(this.fetchOAuthToken_.bind(this)) | |
| 263 .then(this.fetchEmail_.bind(this)) | |
| 264 /** @param {{email:string, token:string}|Promise} result */ | |
| 265 .then(function(result) { | |
| 266 that.connectToHost_(result.email, result.token); | |
| 267 /** @param {*} reason */ | |
| 268 }).catch(function(reason) { | |
| 269 that.sendErrorResponse_(message, /** @type {Error} */ (reason)); | |
| 270 that.dispose(); | |
| 271 }); | |
| 272 }; | |
| 273 | |
| 274 /** | |
| 275 * Prompts the user before starting the It2Me Native Messaging Host. This | |
| 276 * ensures that even if Hangouts is compromised, an attacker cannot start the | |
| 277 * host without explicit user confirmation. | |
| 278 * | |
| 279 * @param {Bounds} bounds Bounds of the hangout window | |
| 280 * @return {Promise} A promise that will resolve if the user accepts remote | |
| 281 * assistance or reject otherwise. | |
| 282 * @private | |
| 283 */ | |
| 284 remoting.It2MeHelpeeChannel.prototype.showConfirmDialog_ = function(bounds) { | |
| 285 if (base.isAppsV2()) { | |
| 286 return this.showConfirmDialogV2_(bounds); | |
| 287 } else { | |
| 288 return this.showConfirmDialogV1_(); | |
| 289 } | |
| 290 }; | |
| 291 | |
| 292 /** | |
| 293 * @return {Promise} A promise that will resolve if the user accepts remote | |
| 294 * assistance or reject otherwise. | |
| 295 * @private | |
| 296 */ | |
| 297 remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV1_ = function() { | |
| 298 var messageHeader = l10n.getTranslationOrError( | |
| 299 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1'); | |
| 300 var message1 = l10n.getTranslationOrError( | |
| 301 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2'); | |
| 302 var message2 = l10n.getTranslationOrError( | |
| 303 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3'); | |
| 304 var message = base.escapeHTML(messageHeader) + '\n' + | |
| 305 '- ' + base.escapeHTML(message1) + '\n' + | |
| 306 '- ' + base.escapeHTML(message2) + '\n'; | |
| 307 | |
| 308 if(window.confirm(message)) { | |
| 309 return Promise.resolve(); | |
| 310 } else { | |
| 311 return Promise.reject(new Error(remoting.Error.CANCELLED)); | |
| 312 } | |
| 313 }; | |
| 314 | |
| 315 /** | |
| 316 * @param {Bounds} bounds the bounds of the Hangouts Window. If set, the | |
| 317 * confirm dialog will be centered within |bounds|. | |
| 318 * @return {Promise} A promise that will resolve if the user accepts remote | |
| 319 * assistance or reject otherwise. | |
| 320 * @private | |
| 321 */ | |
| 322 remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV2_ = function(bounds) { | |
| 323 var getToken = | |
| 324 base.Promise.as(chrome.identity.getAuthToken, [{interactive: false}]); | |
| 325 | |
| 326 return getToken; | |
| 327 }; | |
| 328 | |
| 329 /** | |
| 330 * @return {Promise} A promise that resolves when the host is initialized. | |
| 331 * @private | |
| 332 */ | |
| 333 remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() { | |
| 334 /** @type {remoting.It2MeHostFacade} */ | |
| 335 var host = this.host_; | |
| 336 | |
| 337 /** | |
| 338 * @param {function(*=):void} resolve | |
| 339 * @param {function(*=):void} reject | |
| 340 */ | |
| 341 return new Promise(function(resolve, reject) { | |
| 342 if (host.initialized()) { | |
| 343 resolve(true); | |
| 344 } else { | |
| 345 host.initialize(/** @type {function(*=):void} */ (resolve), | |
| 346 /** @type {function(*=):void} */ (reject)); | |
| 347 } | |
| 348 }); | |
| 349 }; | |
| 350 | |
| 351 /** | |
| 352 * @return {!Promise<string>} Promise that resolves with the OAuth token as the | |
| 353 * value. | |
| 354 */ | |
| 355 remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() { | |
| 356 if (base.isAppsV2()) { | |
| 357 return remoting.identity.getToken(); | |
| 358 } else { | |
| 359 var onError = function(/** * */ error) { | |
| 360 if (error === remoting.Error.NOT_AUTHENTICATED) { | |
| 361 return new Promise(function(resolve, reject) { | |
| 362 remoting.oauth2.doAuthRedirect(function() { | |
| 363 remoting.identity.getToken().then(resolve); | |
| 364 }); | |
| 365 }); | |
| 366 } | |
| 367 throw Error(remoting.Error.NOT_AUTHENTICATED); | |
| 368 }; | |
| 369 return /** @type {!Promise<string>} */ ( | |
| 370 remoting.identity.getToken().catch(onError)); | |
| 371 } | |
| 372 }; | |
| 373 | |
| 374 /** | |
| 375 * @param {string|Promise} token | |
| 376 * @return {Promise} Promise that resolves with the access token and the email | |
| 377 * of the user. | |
| 378 */ | |
| 379 remoting.It2MeHelpeeChannel.prototype.fetchEmail_ = function(token) { | |
| 380 /** @param {string} email */ | |
| 381 function onEmail (email) { | |
| 382 return { email: email, token: token }; | |
| 383 } | |
| 384 return remoting.identity.getEmail().then(onEmail); | |
| 385 }; | |
| 386 | |
| 387 /** | |
| 388 * Connects to the It2Me Native Messaging Host and retrieves the access code | |
| 389 * in the |onHostStateChanged_| callback. | |
| 390 * | |
| 391 * @param {string} email | |
| 392 * @param {string} accessToken | |
| 393 * @private | |
| 394 */ | |
| 395 remoting.It2MeHelpeeChannel.prototype.connectToHost_ = | |
| 396 function(email, accessToken) { | |
| 397 base.debug.assert(this.host_.initialized()); | |
| 398 this.host_.connect( | |
| 399 email, | |
| 400 'oauth2:' + accessToken, | |
| 401 this.onHostStateChanged_.bind(this), | |
| 402 base.doNothing, // Ignore |onNatPolicyChanged|. | |
| 403 console.log.bind(console), // Forward logDebugInfo to console.log. | |
| 404 remoting.settings.XMPP_SERVER_FOR_IT2ME_HOST, | |
| 405 remoting.settings.XMPP_SERVER_USE_TLS, | |
| 406 remoting.settings.DIRECTORY_BOT_JID, | |
| 407 this.onHostConnectError_); | |
| 408 }; | |
| 409 | |
| 410 /** | |
| 411 * @param {remoting.Error} error | |
| 412 * @private | |
| 413 */ | |
| 414 remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) { | |
| 415 this.sendErrorResponse_(null, error); | |
| 416 }; | |
| 417 | |
| 418 /** | |
| 419 * @param {remoting.HostSession.State} state | |
| 420 * @private | |
| 421 */ | |
| 422 remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) { | |
| 423 this.hostState_ = state; | |
| 424 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; | |
| 425 var HostState = remoting.HostSession.State; | |
| 426 | |
| 427 switch (state) { | |
| 428 case HostState.RECEIVED_ACCESS_CODE: | |
| 429 var accessCode = this.host_.getAccessCode(); | |
| 430 this.hangoutPort_.postMessage({ | |
| 431 method: MessageTypes.CONNECT_RESPONSE, | |
| 432 accessCode: accessCode | |
| 433 }); | |
| 434 break; | |
| 435 case HostState.CONNECTED: | |
| 436 case HostState.DISCONNECTED: | |
| 437 this.hangoutPort_.postMessage({ | |
| 438 method: MessageTypes.HOST_STATE_CHANGED, | |
| 439 state: state | |
| 440 }); | |
| 441 break; | |
| 442 case HostState.ERROR: | |
| 443 this.sendErrorResponse_(null, remoting.Error.UNEXPECTED); | |
| 444 break; | |
| 445 case HostState.INVALID_DOMAIN_ERROR: | |
| 446 this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN); | |
| 447 break; | |
| 448 default: | |
| 449 // It is safe to ignore other state changes. | |
| 450 } | |
| 451 }; | |
| 452 | |
| 453 /** | |
| 454 * @param {?{method:string, data:Object<string,*>}} incomingMessage | |
| 455 * @param {string|Error} error | |
| 456 * @private | |
| 457 */ | |
| 458 remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ = | |
| 459 function(incomingMessage, error) { | |
| 460 if (error instanceof Error) { | |
| 461 error = error.message; | |
| 462 } | |
| 463 | |
| 464 console.error('Error responding to message method:' + | |
| 465 (incomingMessage ? incomingMessage.method : 'null') + | |
| 466 ' error:' + error); | |
| 467 this.hangoutPort_.postMessage({ | |
| 468 method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR, | |
| 469 message: error, | |
| 470 request: incomingMessage | |
| 471 }); | |
| 472 }; | |
| OLD | NEW |