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.
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).
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.
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)-| |
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.
31 * Hangouts It2MeHelpeeChannel It2MeHost
32 * | |<-------hostStateChanged------|
34 * |<-- hostStateChanged(CONNECTED)----| |
35 * |-------disconnect message--------->| |
36 * |<--hostStateChanged(DISCONNECTED)--| |
39 * It also handles host downloads and install status queries:
41 * Hangouts It2MeHelpeeChannel
42 * |------isHostInstalled message----->|
43 * |<-isHostInstalled response(false)--|
45 * |--------downloadHost message------>|
47 * |------isHostInstalled message----->|
48 * |<-isHostInstalled response(false)--|
50 * |------isHostInstalled message----->|
51 * |<-isHostInstalled response(true)---|
56 /** @suppress {duplicate} */
57 var remoting = remoting || {};
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.
67 * @implements {base.Disposable}
69 remoting.It2MeHelpeeChannel =
70 function(hangoutPort, host, hostInstaller, onDisposedCallback) {
72 * @type {chrome.runtime.Port}
75 this.hangoutPort_ = hangoutPort;
78 * @type {remoting.It2MeHostFacade}
84 * @type {?remoting.HostInstaller}
87 this.hostInstaller_ = hostInstaller;
90 * @type {remoting.HostSession.State}
93 this.hostState_ = remoting.HostSession.State.UNKNOWN;
99 this.onDisposedCallback_ = onDisposedCallback;
101 this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this);
102 this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this);
105 /** @enum {string} */
106 remoting.It2MeHelpeeChannel.HangoutMessageTypes = {
108 CONNECT_RESPONSE: 'connectResponse',
109 DISCONNECT: 'disconnect',
110 DOWNLOAD_HOST: 'downloadHost',
113 HELLO_RESPONSE: 'helloResponse',
114 HOST_STATE_CHANGED: 'hostStateChanged',
115 IS_HOST_INSTALLED: 'isHostInstalled',
116 IS_HOST_INSTALLED_RESPONSE: 'isHostInstalledResponse'
119 /** @enum {string} */
120 remoting.It2MeHelpeeChannel.Features = {
121 REMOTE_ASSISTANCE: 'remoteAssistance'
124 remoting.It2MeHelpeeChannel.prototype.init = function() {
125 this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
126 this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
129 remoting.It2MeHelpeeChannel.prototype.dispose = function() {
130 if (this.host_ !== null) {
131 this.host_.unhookCallbacks();
132 this.host_.disconnect();
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;
142 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
143 this.hangoutPort_.postMessage({
144 method: MessageTypes.HOST_STATE_CHANGED,
145 state: this.hostState_
148 // |postMessage| throws if |this.hangoutPort_| is disconnected
149 // It is safe to ignore the exception.
151 this.hangoutPort_.disconnect();
152 this.hangoutPort_ = null;
155 if (this.onDisposedCallback_ !== null) {
156 this.onDisposedCallback_();
157 this.onDisposedCallback_ = null;
162 * Message Handler for incoming runtime messages from Hangouts.
164 * @param {{method:string, data:Object<string,*>}} message
167 remoting.It2MeHelpeeChannel.prototype.onHangoutMessage_ = function(message) {
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)
177 case MessageTypes.IS_HOST_INSTALLED:
178 this.handleIsHostInstalled_(message);
180 case MessageTypes.DOWNLOAD_HOST:
181 this.handleDownloadHost_(message);
183 case MessageTypes.CONNECT:
184 this.handleConnect_(message);
186 case MessageTypes.DISCONNECT:
190 throw new Error('Unsupported message method=' + message.method);
191 } catch(/** @type {Error} */ error) {
192 this.sendErrorResponse_(message, error.message);
198 * Queries the |hostInstaller| for the installation status.
200 * @param {{method:string, data:Object<string,*>}} message
203 remoting.It2MeHelpeeChannel.prototype.handleIsHostInstalled_ =
205 /** @type {remoting.It2MeHelpeeChannel} */
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,
217 remoting.HostInstaller.isInstalled().then(
219 /** @type {function(*):void} */(this.sendErrorResponse_.bind(this, message))
224 * @param {{method:string, data:Object<string,*>}} message
227 remoting.It2MeHelpeeChannel.prototype.handleDownloadHost_ = function(message) {
229 this.hostInstaller_.download();
230 } catch (/** @type {*} */ e) {
231 var error = /** @type {Error} */ (e);
232 this.sendErrorResponse_(message, error.message);
237 * Disconnect the session if the |hangoutPort| gets disconnected.
240 remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() {
245 * Connects to the It2Me Native messaging Host and retrieves the access code.
247 * @param {{method:string, data:Object<string,*>}} message
250 remoting.It2MeHelpeeChannel.prototype.handleConnect_ =
253 /** @type {Bounds} */ (getObjectAttr(message, 'hangoutBounds', null));
255 if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) {
256 throw new Error('An existing connection is in progress.');
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));
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.
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.
284 remoting.It2MeHelpeeChannel.prototype.showConfirmDialog_ = function(bounds) {
285 if (base.isAppsV2()) {
286 return this.showConfirmDialogV2_(bounds);
288 return this.showConfirmDialogV1_();
293 * @return {Promise} A promise that will resolve if the user accepts remote
294 * assistance or reject otherwise.
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';
308 if(window.confirm(message)) {
309 return Promise.resolve();
311 return Promise.reject(new Error(remoting.Error.CANCELLED));
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.
322 remoting.It2MeHelpeeChannel.prototype.showConfirmDialogV2_ = function(bounds) {
324 base.Promise.as(chrome.identity.getAuthToken, [{interactive: false}]);
326 return getToken.then(
327 /** @param {string} token */
329 return remoting.HangoutConsentDialog.getInstance().show(Boolean(token),
335 * @return {Promise} A promise that resolves when the host is initialized.
338 remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() {
339 /** @type {remoting.It2MeHostFacade} */
340 var host = this.host_;
343 * @param {function(*=):void} resolve
344 * @param {function(*=):void} reject
346 return new Promise(function(resolve, reject) {
347 if (host.initialized()) {
350 host.initialize(/** @type {function(*=):void} */ (resolve),
351 /** @type {function(*=):void} */ (reject));
357 * @return {!Promise<string>} Promise that resolves with the OAuth token as the
360 remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
361 if (base.isAppsV2()) {
362 return remoting.identity.getToken();
364 var onError = function(/** * */ error) {
365 if (error === remoting.Error.NOT_AUTHENTICATED) {
366 return new Promise(function(resolve, reject) {
367 remoting.oauth2.doAuthRedirect(function() {
368 remoting.identity.getToken().then(resolve);
372 throw Error(remoting.Error.NOT_AUTHENTICATED);
374 return /** @type {!Promise<string>} */ (
375 remoting.identity.getToken().catch(onError));
380 * @param {string|Promise} token
381 * @return {Promise} Promise that resolves with the access token and the email
384 remoting.It2MeHelpeeChannel.prototype.fetchEmail_ = function(token) {
385 /** @param {string} email */
386 function onEmail (email) {
387 return { email: email, token: token };
389 return remoting.identity.getEmail().then(onEmail);
393 * Connects to the It2Me Native Messaging Host and retrieves the access code
394 * in the |onHostStateChanged_| callback.
396 * @param {string} email
397 * @param {string} accessToken
400 remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
401 function(email, accessToken) {
402 base.debug.assert(this.host_.initialized());
405 'oauth2:' + accessToken,
406 this.onHostStateChanged_.bind(this),
407 base.doNothing, // Ignore |onNatPolicyChanged|.
408 console.log.bind(console), // Forward logDebugInfo to console.log.
409 remoting.settings.XMPP_SERVER_FOR_IT2ME_HOST,
410 remoting.settings.XMPP_SERVER_USE_TLS,
411 remoting.settings.DIRECTORY_BOT_JID,
412 this.onHostConnectError_);
416 * @param {remoting.Error} error
419 remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
420 this.sendErrorResponse_(null, error);
424 * @param {remoting.HostSession.State} state
427 remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
428 this.hostState_ = state;
429 var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
430 var HostState = remoting.HostSession.State;
433 case HostState.RECEIVED_ACCESS_CODE:
434 var accessCode = this.host_.getAccessCode();
435 this.hangoutPort_.postMessage({
436 method: MessageTypes.CONNECT_RESPONSE,
437 accessCode: accessCode
440 case HostState.CONNECTED:
441 case HostState.DISCONNECTED:
442 this.hangoutPort_.postMessage({
443 method: MessageTypes.HOST_STATE_CHANGED,
447 case HostState.ERROR:
448 this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
450 case HostState.INVALID_DOMAIN_ERROR:
451 this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
454 // It is safe to ignore other state changes.
459 * @param {?{method:string, data:Object<string,*>}} incomingMessage
460 * @param {string|Error} error
463 remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
464 function(incomingMessage, error) {
465 if (error instanceof Error) {
466 error = error.message;
469 console.error('Error responding to message method:' +
470 (incomingMessage ? incomingMessage.method : 'null') +
472 this.hangoutPort_.postMessage({
473 method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
475 request: incomingMessage