Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / crd / js / it2me_helpee_channel.js
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(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  */
204 remoting.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  */
228 remoting.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  */
241 remoting.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  */
251 remoting.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  */
282 remoting.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  */
295 remoting.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  */
318 remoting.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  */
359 remoting.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  * @return {Promise} Promise that resolves with the OAuth token as the value.
378  */
379 remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
380   if (base.isAppsV2()) {
381     /**
382      * @param {function(*=):void} resolve
383      */
384     return new Promise(function(resolve){
385       // TODO(jamiewalch): Make this work with {interactive: true} as well.
386       chrome.identity.getAuthToken({ 'interactive': false }, resolve);
387     });
388   } else {
389     /**
390      * @param {function(*=):void} resolve
391      */
392     return new Promise(function(resolve) {
393       /** @type {remoting.OAuth2} */
394       var oauth2 = new remoting.OAuth2();
395       var onAuthenticated = function() {
396         oauth2.callWithToken(
397             resolve,
398             function() { throw new Error('Authentication failed.'); });
399       };
400       /** @param {remoting.Error} error */
401       var onError = function(error) {
402         if (error != remoting.Error.NOT_AUTHENTICATED) {
403           throw new Error('Unexpected error fetch auth token: ' + error);
404         }
405         oauth2.doAuthRedirect(onAuthenticated);
406       };
407       oauth2.callWithToken(resolve, onError);
408     });
409   }
410 };
411
412 /**
413  * Connects to the It2Me Native Messaging Host and retrieves the access code
414  * in the |onHostStateChanged_| callback.
415  *
416  * @param {string} email
417  * @param {string} accessToken
418  * @private
419  */
420 remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
421     function(email, accessToken) {
422   base.debug.assert(this.host_.initialized());
423   this.host_.connect(
424     email,
425     'oauth2:' + accessToken,
426     this.onHostStateChanged_.bind(this),
427     base.doNothing, // Ignore |onNatPolicyChanged|.
428     console.log.bind(console), // Forward logDebugInfo to console.log.
429     remoting.settings.XMPP_SERVER_ADDRESS,
430     remoting.settings.XMPP_SERVER_USE_TLS,
431     remoting.settings.DIRECTORY_BOT_JID,
432     this.onHostConnectError_);
433 };
434
435 /**
436  * @param {remoting.Error} error
437  * @private
438  */
439 remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
440   this.sendErrorResponse_(null, error);
441 };
442
443 /**
444  * @param {remoting.HostSession.State} state
445  * @private
446  */
447 remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
448   this.hostState_ = state;
449   var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
450   var HostState = remoting.HostSession.State;
451
452   switch (state) {
453     case HostState.RECEIVED_ACCESS_CODE:
454       var accessCode = this.host_.getAccessCode();
455       this.hangoutPort_.postMessage({
456         method: MessageTypes.CONNECT_RESPONSE,
457         accessCode: accessCode
458       });
459       break;
460     case HostState.CONNECTED:
461     case HostState.DISCONNECTED:
462       this.hangoutPort_.postMessage({
463         method: MessageTypes.HOST_STATE_CHANGED,
464         state: state
465       });
466       break;
467     case HostState.ERROR:
468       this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
469       break;
470     case HostState.INVALID_DOMAIN_ERROR:
471       this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
472       break;
473     default:
474       // It is safe to ignore other state changes.
475   }
476 };
477
478 /**
479  * @param {?{method:string, data:Object.<string,*>}} incomingMessage
480  * @param {string|Error} error
481  * @private
482  */
483 remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
484     function(incomingMessage, error) {
485   if (error instanceof Error) {
486     error = error.message;
487   }
488
489   console.error('Error responding to message method:' +
490                 (incomingMessage ? incomingMessage.method : 'null') +
491                 ' error:' + error);
492   this.hangoutPort_.postMessage({
493     method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
494     message: error,
495     request: incomingMessage
496   });
497 };