Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / it2me_helpee_channel.js
blob79c016b3dd7fc3375f27428e2213b78e3b1c0293
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.
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  */
54 'use strict';
56 /** @suppress {duplicate} */
57 var remoting = remoting || {};
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;
77   /**
78    * @type {remoting.It2MeHostFacade}
79    * @private
80    */
81   this.host_ = host;
83   /**
84    * @type {?remoting.HostInstaller}
85    * @private
86    */
87   this.hostInstaller_ = hostInstaller;
89   /**
90    * @type {remoting.HostSession.State}
91    * @private
92    */
93   this.hostState_ = remoting.HostSession.State.UNKNOWN;
95   /**
96    * @type {?function()}
97    * @private
98    */
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 = {
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'
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();
133     this.host_ = null;
134   }
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;
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   }
155   if (this.onDisposedCallback_ !== null) {
156     this.onDisposedCallback_();
157     this.onDisposedCallback_ = null;
158   }
162  * Message Handler for incoming runtime messages from Hangouts.
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;
198  * Queries the |hostInstaller| for the installation status.
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;
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   }
217   remoting.HostInstaller.isInstalled().then(
218     sendResponse,
219     /** @type {function(*):void} */(this.sendErrorResponse_.bind(this, message))
220   );
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   }
237  * Disconnect the session if the |hangoutPort| gets disconnected.
238  * @private
239  */
240 remoting.It2MeHelpeeChannel.prototype.onHangoutDisconnect_ = function() {
241   this.dispose();
245  * Connects to the It2Me Native messaging Host and retrieves the access code.
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));
255   if (this.hostState_ !== remoting.HostSession.State.UNKNOWN) {
256     throw new Error('An existing connection is in progress.');
257   }
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       });
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.
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   }
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';
308   if(window.confirm(message)) {
309     return Promise.resolve();
310   } else {
311     return Promise.reject(new Error(remoting.Error.CANCELLED));
312   }
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}]);
326   return getToken.then(
327     /** @param {string} token */
328     function(token) {
329       return remoting.HangoutConsentDialog.getInstance().show(Boolean(token),
330                                                               bounds);
331     });
335  * @return {Promise} A promise that resolves when the host is initialized.
336  * @private
337  */
338 remoting.It2MeHelpeeChannel.prototype.initializeHost_ = function() {
339   /** @type {remoting.It2MeHostFacade} */
340   var host = this.host_;
342   /**
343    * @param {function(*=):void} resolve
344    * @param {function(*=):void} reject
345    */
346   return new Promise(function(resolve, reject) {
347     if (host.initialized()) {
348       resolve(true);
349     } else {
350       host.initialize(/** @type {function(*=):void} */ (resolve),
351                       /** @type {function(*=):void} */ (reject));
352     }
353   });
357  * @return {!Promise<string>} Promise that resolves with the OAuth token as the
358  * value.
359  */
360 remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
361   if (base.isAppsV2()) {
362     return remoting.identity.getToken();
363   } else {
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);
369             });
370           });
371         }
372         throw Error(remoting.Error.NOT_AUTHENTICATED);
373       };
374     return /** @type {!Promise<string>} */ (
375         remoting.identity.getToken().catch(onError));
376   }
380  * @param {string|Promise} token
381  * @return {Promise} Promise that resolves with the access token and the email
382  *   of the user.
383  */
384 remoting.It2MeHelpeeChannel.prototype.fetchEmail_ = function(token) {
385   /** @param {string} email */
386   function onEmail (email) {
387     return { email: email, token: token };
388   }
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
398  * @private
399  */
400 remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
401     function(email, accessToken) {
402   base.debug.assert(this.host_.initialized());
403   this.host_.connect(
404     email,
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
417  * @private
418  */
419 remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
420   this.sendErrorResponse_(null, error);
424  * @param {remoting.HostSession.State} state
425  * @private
426  */
427 remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
428   this.hostState_ = state;
429   var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
430   var HostState = remoting.HostSession.State;
432   switch (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
438       });
439       break;
440     case HostState.CONNECTED:
441     case HostState.DISCONNECTED:
442       this.hangoutPort_.postMessage({
443         method: MessageTypes.HOST_STATE_CHANGED,
444         state: state
445       });
446       break;
447     case HostState.ERROR:
448       this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
449       break;
450     case HostState.INVALID_DOMAIN_ERROR:
451       this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
452       break;
453     default:
454       // It is safe to ignore other state changes.
455   }
459  * @param {?{method:string, data:Object<string,*>}} incomingMessage
460  * @param {string|Error} error
461  * @private
462  */
463 remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
464     function(incomingMessage, error) {
465   if (error instanceof Error) {
466     error = error.message;
467   }
469   console.error('Error responding to message method:' +
470                 (incomingMessage ? incomingMessage.method : 'null') +
471                 ' error:' + error);
472   this.hangoutPort_.postMessage({
473     method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
474     message: error,
475     request: incomingMessage
476   });