• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
57var 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 */
69remoting.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} */
106remoting.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} */
120remoting.It2MeHelpeeChannel.Features = {
121  REMOTE_ASSISTANCE: 'remoteAssistance'
122};
123
124remoting.It2MeHelpeeChannel.prototype.init = function() {
125  this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
126  this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
127};
128
129remoting.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 */
167remoting.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 */
204remoting.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 */
228remoting.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 */
241remoting.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 */
251remoting.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 */
282remoting.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 */
295remoting.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 */
318remoting.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 */
359remoting.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 * TODO(kelvinp): The existing implementation only works in the v2 app
378 * We need to implement token fetching for the v1 app using remoting.OAuth2
379 * before launch (crbug.com/405130).
380 *
381 * @return {Promise} Promise that resolves with the OAuth token as the value.
382 */
383remoting.It2MeHelpeeChannel.prototype.fetchOAuthToken_ = function() {
384  if (!base.isAppsV2()) {
385    throw new Error('fetchOAuthToken_ is not implemented in the v1 app.');
386  }
387
388  /**
389   * @param {function(*=):void} resolve
390   */
391  return new Promise(function(resolve){
392    chrome.identity.getAuthToken({ 'interactive': false }, resolve);
393  });
394};
395
396/**
397 * Connects to the It2Me Native Messaging Host and retrieves the access code
398 * in the |onHostStateChanged_| callback.
399 *
400 * @param {string} email
401 * @param {string} accessToken
402 * @private
403 */
404remoting.It2MeHelpeeChannel.prototype.connectToHost_ =
405    function(email, accessToken) {
406  base.debug.assert(this.host_.initialized());
407  this.host_.connect(
408    email,
409    'oauth2:' + accessToken,
410    this.onHostStateChanged_.bind(this),
411    base.doNothing, // Ignore |onNatPolicyChanged|.
412    console.log.bind(console), // Forward logDebugInfo to console.log.
413    remoting.settings.XMPP_SERVER_ADDRESS,
414    remoting.settings.XMPP_SERVER_USE_TLS,
415    remoting.settings.DIRECTORY_BOT_JID,
416    this.onHostConnectError_);
417};
418
419/**
420 * @param {remoting.Error} error
421 * @private
422 */
423remoting.It2MeHelpeeChannel.prototype.onHostConnectError_ = function(error) {
424  this.sendErrorResponse_(null, error);
425};
426
427/**
428 * @param {remoting.HostSession.State} state
429 * @private
430 */
431remoting.It2MeHelpeeChannel.prototype.onHostStateChanged_ = function(state) {
432  this.hostState_ = state;
433  var MessageTypes = remoting.It2MeHelpeeChannel.HangoutMessageTypes;
434  var HostState = remoting.HostSession.State;
435
436  switch (state) {
437    case HostState.RECEIVED_ACCESS_CODE:
438      var accessCode = this.host_.getAccessCode();
439      this.hangoutPort_.postMessage({
440        method: MessageTypes.CONNECT_RESPONSE,
441        accessCode: accessCode
442      });
443      break;
444    case HostState.CONNECTED:
445    case HostState.DISCONNECTED:
446      this.hangoutPort_.postMessage({
447        method: MessageTypes.HOST_STATE_CHANGED,
448        state: state
449      });
450      break;
451    case HostState.ERROR:
452      this.sendErrorResponse_(null, remoting.Error.UNEXPECTED);
453      break;
454    case HostState.INVALID_DOMAIN_ERROR:
455      this.sendErrorResponse_(null, remoting.Error.INVALID_HOST_DOMAIN);
456      break;
457    default:
458      // It is safe to ignore other state changes.
459  }
460};
461
462/**
463 * @param {?{method:string, data:Object.<string,*>}} incomingMessage
464 * @param {string|Error} error
465 * @private
466 */
467remoting.It2MeHelpeeChannel.prototype.sendErrorResponse_ =
468    function(incomingMessage, error) {
469  if (error instanceof Error) {
470    error = error.message;
471  }
472
473  console.error('Error responding to message method:' +
474                (incomingMessage ? incomingMessage.method : 'null') +
475                ' error:' + error);
476  this.hangoutPort_.postMessage({
477    method: remoting.It2MeHelpeeChannel.HangoutMessageTypes.ERROR,
478    message: error,
479    request: incomingMessage
480  });
481};
482