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 |