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} */ 57var 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 */ 69remoting.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} */ 106remoting.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} */ 120remoting.It2MeHelpeeChannel.Features = { 121 REMOTE_ASSISTANCE: 'remoteAssistance' 122}; 123 124remoting.It2MeHelpeeChannel.prototype.init = function() { 125 this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_); 126 this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_); 127}; 128 129remoting.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 */ 167remoting.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(e) { 192 var error = /** @type {Error} */ e; 193 this.sendErrorResponse_(message, error.message); 194 } 195 return false; 196}; 197 198/** 199 * Queries the |hostInstaller| for the installation status. 200 * 201 * @param {{method:string, data:Object.<string,*>}} message 202 * @private 203 */ 204remoting.It2MeHelpeeChannel.prototype.handleIsHostInstalled_ = 205 function(message) { 206 /** @type {remoting.It2MeHelpeeChannel} */ 207 var that = this; 208 209 /** @param {boolean} installed */ 210 function sendResponse(installed) { 211 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; 212 that.hangoutPort_.postMessage({ 213 method: MessageTypes.IS_HOST_INSTALLED_RESPONSE, 214 result: installed 215 }); 216 } 217 218 this.hostInstaller_.isInstalled().then( 219 sendResponse, 220 this.sendErrorResponse_.bind(this, message) 221 ); 222}; 223 224/** 225 * @param {{method:string, data:Object.<string,*>}} message 226 * @private 227 */ 228remoting.It2MeHelpeeChannel.prototype.handleDownloadHost_ = function(message) { 229 try { 230 this.hostInstaller_.download(); 231 } catch (e) { 232 var error = /** @type {Error} */ e; 233 this.sendErrorResponse_(message, error.message); 234 } 235}; 236 237/** 238 * Disconnect the session if the |hangoutPort| gets disconnected. 239 * @private 240 */ 241remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() { 242 this.dispose(); 243}; 244 245/** 246 * Connects to the It2Me Native messaging Host and retrieves the access code. 247 * 248 * @param {{method:string, data:Object.<string,*>}} message 249 * @private 250 */ 251remoting.It2MeHelpeeChannel.prototype.handleConnect_ = 252 function(message) { 253 var email = getStringAttr(message, 'email'); 254 255 if (!email) { 256 throw new Error('Missing required parameter: email'); 257 } 258 259 if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) { 260 throw new Error('An existing connection is in progress.'); 261 } 262 263 this.showConfirmDialog_().then( 264 this.initializeHost_.bind(this) 265 ).then( 266 this.fetchOAuthToken_.bind(this) 267 ).then( 268 this.connectToHost_.bind(this, email), 269 this.sendErrorResponse_.bind(this, message) 270 ); 271}; 272 273/** 274 * Prompts the user before starting the It2Me Native Messaging Host. This 275 * ensures that even if Hangouts is compromised, an attacker cannot start the 276 * host without explicit user confirmation. 277 * 278 * @return {Promise} A promise that resolves to a boolean value, indicating 279 * whether the user accepts the remote assistance or not. 280 * @private 281 */ 282remoting.It2MeHelpeeChannel.prototype.showConfirmDialog_ = function() { 283 if (base.isAppsV2()) { 284 return this.showConfirmDialogV2_(); 285 } else { 286 return this.showConfirmDialogV1_(); 287 } 288}; 289 290/** 291 * @return {Promise} A promise that resolves to a boolean value, indicating 292 * whether the user accepts the remote assistance or not. 293 * @private 294 */ 295remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV1_ = function() { 296 var messageHeader = l10n.getTranslationOrError( 297 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1'); 298 var message1 = l10n.getTranslationOrError( 299 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2'); 300 var message2 = l10n.getTranslationOrError( 301 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3'); 302 var message = base.escapeHTML(messageHeader) + '\n' + 303 '- ' + base.escapeHTML(message1) + '\n' + 304 '- ' + base.escapeHTML(message2) + '\n'; 305 306 if(window.confirm(message)) { 307 return Promise.resolve(); 308 } else { 309 return Promise.reject(new Error(remoting.Error.CANCELLED)); 310 } 311}; 312 313/** 314 * @return {Promise} A promise that resolves to a boolean value, indicating 315 * whether the user accepts the remote assistance or not. 316 * @private 317 */ 318remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV2_ = function() { 319 var messageHeader = l10n.getTranslationOrError( 320 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_1'); 321 var message1 = l10n.getTranslationOrError( 322 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_2'); 323 var message2 = l10n.getTranslationOrError( 324 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_MESSAGE_3'); 325 var message = '<div>' + base.escapeHTML(messageHeader) + '</div>' + 326 '<ul class="insetList">' + 327 '<li>' + base.escapeHTML(message1) + '</li>' + 328 '<li>' + base.escapeHTML(message2) + '</li>' + 329 '</ul>'; 330 /** 331 * @param {function(*=):void} resolve 332 * @param {function(*=):void} reject 333 */ 334 return new Promise(function(resolve, reject) { 335 /** @param {number} result */ 336 function confirmDialogCallback(result) { 337 if (result === 1) { 338 resolve(); 339 } else { 340 reject(new Error(remoting.Error.CANCELLED)); 341 } 342 } 343 remoting.MessageWindow.showConfirmWindow( 344 '', // Empty string to use the package name as the dialog title. 345 message, 346 l10n.getTranslationOrError( 347 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_ACCEPT'), 348 l10n.getTranslationOrError( 349 /*i18n-content*/'HANGOUTS_CONFIRM_DIALOG_DECLINE'), 350 confirmDialogCallback 351 ); 352 }); 353}; 354 355/** 356 * @return {Promise} A promise that resolves when the host is initialized. 357 * @private 358 */ 359remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() { 360 /** @type {remoting.It2MeHostFacade} */ 361 var host = this.host_; 362 363 /** 364 * @param {function(*=):void} resolve 365 * @param {function(*=):void} reject 366 */ 367 return new Promise(function(resolve, reject) { 368 if (host.initialized()) { 369 resolve(); 370 } else { 371 host.initialize(resolve, reject); 372 } 373 }); 374}; 375 376/** 377 * TODO(kelvinp): The existing implementation only works in the v2 app 378 * We need to implement token fetching for the v1 app using remoting.OAuth2 379 * before launch (crbug.com/405130). 380 * 381 * @return {Promise} Promise that resolves with the OAuth token as the value. 382 */ 383remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() { 384 if (!base.isAppsV2()) { 385 throw new Error('fetchOAuthToken_ is not implemented in the v1 app.'); 386 } 387 388 /** 389 * @param {function(*=):void} resolve 390 */ 391 return new Promise(function(resolve){ 392 chrome.identity.getAuthToken({ 'interactive': false }, resolve); 393 }); 394}; 395 396/** 397 * Connects to the It2Me Native Messaging Host and retrieves the access code 398 * in the |onHostStateChanged_| callback. 399 * 400 * @param {string} email 401 * @param {string} accessToken 402 * @private 403 */ 404remoting.It2MeHelpeeChannel.prototype.connectToHost_ = 405 function(email, accessToken) { 406 base.debug.assert(this.host_.initialized()); 407 this.host_.connect( 408 email, 409 'oauth2:' + accessToken, 410 this.onHostStateChanged_.bind(this), 411 base.doNothing, // Ignore |onNatPolicyChanged|. 412 console.log.bind(console), // Forward logDebugInfo to console.log. 413 remoting.settings.XMPP_SERVER_ADDRESS, 414 remoting.settings.XMPP_SERVER_USE_TLS, 415 remoting.settings.DIRECTORY_BOT_JID, 416 this.onHostConnectError_); 417}; 418 419/** 420 * @param {remoting.Error} error 421 * @private 422 */ 423remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) { 424 this.sendErrorResponse_(null, error); 425}; 426 427/** 428 * @param {remoting.HostSession.State} state 429 * @private 430 */ 431remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) { 432 this.hostState_ = state; 433 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes; 434 var HostState = remoting.HostSession.State; 435 436 switch (state) { 437 case HostState.RECEIVED_ACCESS_CODE: 438 var accessCode = this.host_.getAccessCode(); 439 this.hangoutPort_.postMessage({ 440 method: MessageTypes.CONNECT_RESPONSE, 441 accessCode: accessCode 442 }); 443 break; 444 case HostState.CONNECTED: 445 case HostState.DISCONNECTED: 446 this.hangoutPort_.postMessage({ 447 method: MessageTypes.HOST_STATE_CHANGED, 448 state: state 449 }); 450 break; 451 case HostState.ERROR: 452 this.sendErrorResponse_(null, remoting.Error.UNEXPECTED); 453 break; 454 case HostState.INVALID_DOMAIN_ERROR: 455 this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN); 456 break; 457 default: 458 // It is safe to ignore other state changes. 459 } 460}; 461 462/** 463 * @param {?{method:string, data:Object.<string,*>}} incomingMessage 464 * @param {string|Error} error 465 * @private 466 */ 467remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ = 468 function(incomingMessage, error) { 469 if (error instanceof Error) { 470 error = error.message; 471 } 472 473 console.error('Error responding to message method:' + 474 (incomingMessage ? incomingMessage.method : 'null') + 475 ' error:' + error); 476 this.hangoutPort_.postMessage({ 477 method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR, 478 message: error, 479 request: incomingMessage 480 }); 481}; 482