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
);
192 var error
= /** @type {Error} */ e
;
193 this.sendErrorResponse_(message
, error
.message
);
199 * Queries the |hostInstaller| for the installation status.
201 * @param {{method:string, data:Object.<string,*>}} message
204 remoting
.It2MeHelpeeChannel
.prototype.handleIsHostInstalled_
=
206 /** @type {remoting.It2MeHelpeeChannel} */
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
,
218 this.hostInstaller_
.isInstalled().then(
220 this.sendErrorResponse_
.bind(this, message
)
225 * @param {{method:string, data:Object.<string,*>}} message
228 remoting
.It2MeHelpeeChannel
.prototype.handleDownloadHost_ = function(message
) {
230 this.hostInstaller_
.download();
232 var error
= /** @type {Error} */ e
;
233 this.sendErrorResponse_(message
, error
.message
);
238 * Disconnect the session if the |hangoutPort| gets disconnected.
241 remoting
.It2MeHelpeeChannel
.prototype.onHangoutDisconnect_ = function() {
246 * Connects to the It2Me Native messaging Host and retrieves the access code.
248 * @param {{method:string, data:Object.<string,*>}} message
251 remoting
.It2MeHelpeeChannel
.prototype.handleConnect_
=
253 var email
= getStringAttr(message
, 'email');
256 throw new Error('Missing required parameter: email');
259 if (this.hostState_
!== remoting
.HostSession
.State
.UNKNOWN
) {
260 throw new Error('An existing connection is in progress.');
263 this.showConfirmDialog_().then(
264 this.initializeHost_
.bind(this)
266 this.fetchOAuthToken_
.bind(this)
268 this.connectToHost_
.bind(this, email
),
269 this.sendErrorResponse_
.bind(this, message
)
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.
278 * @return {Promise} A promise that resolves when the user clicks accept on the
282 remoting
.It2MeHelpeeChannel
.prototype.showConfirmDialog_ = function() {
284 * @param {function(*=):void} resolve
285 * @param {function(*=):void} reject
287 return new Promise(function(resolve
, reject
) {
288 // TODO(kelvinp): The existing implementation doesn't work in the v2 app as
289 // window.confirm is blacklisted. Implement the dialog using
290 // chrome.app.window instead (crbug.com/405139).
291 if (base
.isAppsV2()) {
293 // The unlocalized string will be replaced in (crbug.com/405139).
294 } else if (window
.confirm('Accept help?')) {
297 reject(new Error(remoting
.Error
.CANCELLED
));
303 * @return {Promise} A promise that resolves when the host is initialized.
306 remoting
.It2MeHelpeeChannel
.prototype.initializeHost_ = function() {
307 /** @type {remoting.It2MeHostFacade} */
308 var host
= this.host_
;
311 * @param {function(*=):void} resolve
312 * @param {function(*=):void} reject
314 return new Promise(function(resolve
, reject
) {
315 if (host
.initialized()) {
318 host
.initialize(resolve
, reject
);
324 * TODO(kelvinp): The existing implementation only works in the v2 app
325 * We need to implement token fetching for the v1 app using remoting.OAuth2
326 * before launch (crbug.com/405130).
328 * @return {Promise} Promise that resolves with the OAuth token as the value.
330 remoting
.It2MeHelpeeChannel
.prototype.fetchOAuthToken_ = function() {
331 if (!base
.isAppsV2()) {
332 throw new Error('fetchOAuthToken_ is not implemented in the v1 app.');
336 * @param {function(*=):void} resolve
338 return new Promise(function(resolve
){
339 chrome
.identity
.getAuthToken({ 'interactive': false }, resolve
);
344 * Connects to the It2Me Native Messaging Host and retrieves the access code
345 * in the |onHostStateChanged_| callback.
347 * @param {string} email
348 * @param {string} accessToken
351 remoting
.It2MeHelpeeChannel
.prototype.connectToHost_
=
352 function(email
, accessToken
) {
353 base
.debug
.assert(this.host_
.initialized());
356 'oauth2:' + accessToken
,
357 this.onHostStateChanged_
.bind(this),
358 base
.doNothing
, // Ignore |onNatPolicyChanged|.
359 console
.log
.bind(console
), // Forward logDebugInfo to console.log.
360 remoting
.settings
.XMPP_SERVER_ADDRESS
,
361 remoting
.settings
.XMPP_SERVER_USE_TLS
,
362 remoting
.settings
.DIRECTORY_BOT_JID
,
363 this.onHostConnectError_
);
367 * @param {remoting.Error} error
370 remoting
.It2MeHelpeeChannel
.prototype.onHostConnectError_ = function(error
) {
371 this.sendErrorResponse_(null, error
);
375 * @param {remoting.HostSession.State} state
378 remoting
.It2MeHelpeeChannel
.prototype.onHostStateChanged_ = function(state
) {
379 this.hostState_
= state
;
380 var MessageTypes
= remoting
.It2MeHelpeeChannel
.HangoutMessageTypes
;
381 var HostState
= remoting
.HostSession
.State
;
384 case HostState
.RECEIVED_ACCESS_CODE
:
385 var accessCode
= this.host_
.getAccessCode();
386 this.hangoutPort_
.postMessage({
387 method
: MessageTypes
.CONNECT_RESPONSE
,
388 accessCode
: accessCode
391 case HostState
.CONNECTED
:
392 case HostState
.DISCONNECTED
:
393 this.hangoutPort_
.postMessage({
394 method
: MessageTypes
.HOST_STATE_CHANGED
,
398 case HostState
.ERROR
:
399 this.sendErrorResponse_(null, remoting
.Error
.UNEXPECTED
);
401 case HostState
.INVALID_DOMAIN_ERROR
:
402 this.sendErrorResponse_(null, remoting
.Error
.INVALID_HOST_DOMAIN
);
405 // It is safe to ignore other state changes.
410 * @param {?{method:string, data:Object.<string,*>}} incomingMessage
411 * @param {string|Error} error
414 remoting
.It2MeHelpeeChannel
.prototype.sendErrorResponse_
=
415 function(incomingMessage
, error
) {
416 if (error
instanceof Error
) {
417 error
= error
.message
;
420 console
.error('Error responding to message method:' +
421 (incomingMessage
? incomingMessage
.method
: 'null') +
423 this.hangoutPort_
.postMessage({
424 method
: remoting
.It2MeHelpeeChannel
.HangoutMessageTypes
.ERROR
,
426 request
: incomingMessage