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 * Class to communicate with the It2me Host component via Native Messaging.
8 */
9
10'use strict';
11
12/** @suppress {duplicate} */
13var remoting = remoting || {};
14
15/**
16 * @constructor
17 */
18remoting.It2MeHostFacade = function() {
19  /**
20   * @type {number}
21   * @private
22   */
23  this.nextId_ = 0;
24
25  /**
26   * @type {?chrome.runtime.Port}
27   * @private
28   */
29  this.port_ = null;
30
31  /**
32   * @type {string}
33   * @private
34   */
35  this.accessCode_ = '';
36
37  /**
38   * @type {number}
39   * @private
40   */
41  this.accessCodeLifetime_ = 0;
42
43  /**
44   * @type {string}
45   * @private
46   */
47  this.clientId_ = '';
48
49  /**
50   * @type {boolean}
51   * @private
52   */
53  this.initialized_ = false;
54
55  /**
56   * @type {?function():void}
57 * @private
58   */
59  this.onInitialized_ = function() {};
60
61  /**
62   * Called if Native Messaging host has failed to start.
63   * @private
64   * */
65  this.onInitializationFailed_ = function() {};
66
67  /**
68   * Called if the It2Me Native Messaging host sends a malformed message:
69   * missing required attributes, attributes with incorrect types, etc.
70   * @param {remoting.Error} error
71   * @private
72   */
73  this.onError_ = function(error) {};
74
75  /**
76   * @type {?function(remoting.HostSession.State):void}
77   * @private
78   */
79  this.onStateChanged_ = function() {};
80
81  /**
82   * @type {?function(boolean):void}
83   * @private
84   */
85  this.onNatPolicyChanged_ = function() {};
86};
87
88/**
89 * Sets up connection to the Native Messaging host process and exchanges
90 * 'hello' messages. If Native Messaging is not supported or if the it2me
91 * native messaging host is not installed, onInitializationFailed is invoked.
92 * Otherwise, onInitialized is invoked.
93 *
94 * @param {function():void} onInitialized Called after successful
95 *     initialization.
96 * @param {function():void} onInitializationFailed Called if cannot connect to
97 *     the native messaging host.
98 * @return {void}
99 */
100remoting.It2MeHostFacade.prototype.initialize =
101    function(onInitialized, onInitializationFailed) {
102  this.onInitialized_ = onInitialized;
103  this.onInitializationFailed_ = onInitializationFailed;
104
105  try {
106    this.port_ = chrome.runtime.connectNative(
107        'com.google.chrome.remote_assistance');
108    this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this));
109    this.port_.onDisconnect.addListener(this.onHostDisconnect_.bind(this));
110    this.port_.postMessage({type: 'hello'});
111  } catch (err) {
112    console.log('Native Messaging initialization failed: ',
113                /** @type {*} */ (err));
114    onInitializationFailed();
115    return;
116  }
117};
118
119/**
120 * @param {string} email The user's email address.
121 * @param {string} authServiceWithToken Concatenation of the auth service
122 *     (e.g. oauth2) and the access token.
123 * @param {function(remoting.HostSession.State):void} onStateChanged Callback to
124 *     invoke when the host state changes.
125 * @param {function(boolean):void} onNatPolicyChanged Callback to invoke when
126 *     the nat traversal policy changes.
127 * @param {function(string):void} logDebugInfo Callback allowing the plugin
128 *     to log messages to the debug log.
129 * @param {string} xmppServerAddress XMPP server host name (or IP address) and
130 *     port.
131 * @param {boolean} xmppServerUseTls Whether to use TLS on connections to the
132 *     XMPP server
133 * @param {string} directoryBotJid XMPP JID for the remoting directory server
134 *     bot.
135 * @param {function(remoting.Error):void} onError Callback to invoke in case of
136 *     an error.
137 * @return {void}
138 */
139remoting.It2MeHostFacade.prototype.connect =
140    function(email, authServiceWithToken, onStateChanged, onNatPolicyChanged,
141             logDebugInfo, xmppServerAddress, xmppServerUseTls, directoryBotJid,
142             onError) {
143  if (!this.port_) {
144    console.error(
145        'remoting.It2MeHostFacade.connect() without initialization.');
146    onError(remoting.Error.UNEXPECTED);
147    return;
148  }
149
150  this.onStateChanged_ = onStateChanged;
151  this.onNatPolicyChanged_ = onNatPolicyChanged;
152  this.onError_ = onError;
153  this.port_.postMessage({
154    type: 'connect',
155    userName: email,
156    authServiceWithToken: authServiceWithToken,
157    xmppServerAddress: xmppServerAddress,
158    xmppServerUseTls: xmppServerUseTls,
159    directoryBotJid: directoryBotJid
160  });
161};
162
163/**
164 * Unhooks the |onStateChanged|, |onError|, |onNatPolicyChanged| and
165 * |onInitalized| callbacks.  This is called when the client shuts down so that
166 * the callbacks will not be invoked on a disposed client.
167 *
168 * @return {void}
169 */
170remoting.It2MeHostFacade.prototype.unhookCallbacks = function() {
171  this.onStateChanged_ = null;
172  this.onNatPolicyChanged_ = null;
173  this.onError_ = null;
174  this.onInitialized_ = null;
175};
176
177/**
178 * @return {void}
179 */
180remoting.It2MeHostFacade.prototype.disconnect = function() {
181  if (this.port_)
182    this.port_.postMessage({type: 'disconnect'});
183};
184
185/**
186 * @return {boolean}
187 */
188remoting.It2MeHostFacade.prototype.initialized = function() {
189  return this.initialized_;
190};
191
192/**
193 * @return {string}
194 */
195remoting.It2MeHostFacade.prototype.getAccessCode = function() {
196  return this.accessCode_;
197};
198
199/**
200 * @return {number}
201 */
202remoting.It2MeHostFacade.prototype.getAccessCodeLifetime = function() {
203  return this.accessCodeLifetime_;
204};
205
206/**
207 * @return {string}
208 */
209remoting.It2MeHostFacade.prototype.getClient = function() {
210  return this.clientId_;
211};
212
213/**
214 * Handler for incoming messages.
215 *
216 * @param {Object} message The received message.
217 * @return {void}
218 * @private
219 */
220remoting.It2MeHostFacade.prototype.onIncomingMessage_ =
221    function(message) {
222  var type = getStringAttr(message, 'type');
223
224  switch (type) {
225    case 'helloResponse':
226      var version = getStringAttr(message, 'version');
227      console.log('Host version: ', version);
228      this.initialized_ = true;
229      // A "hello" request is sent immediately after the native messaging host
230      // is started. We can proceed to the next task once we receive the
231      // "helloReponse".
232      if (this.onInitialized_) {
233        this.onInitialized_();
234      }
235      break;
236
237    case 'connectResponse':
238      console.log('connectResponse received');
239      // Response to the "connect" request. No action is needed until we
240      // receive the corresponding "hostStateChanged" message.
241      break;
242
243    case 'disconnectResponse':
244      console.log('disconnectResponse received');
245      // Response to the "disconnect" request. No action is needed until we
246      // receive the corresponding "hostStateChanged" message.
247      break;
248
249    case 'hostStateChanged':
250      var stateString = getStringAttr(message, 'state');
251      console.log('hostStateChanged received: ', stateString);
252      var state = remoting.HostSession.State.fromString(stateString);
253
254      switch (state) {
255        case remoting.HostSession.State.RECEIVED_ACCESS_CODE:
256          var accessCode = getStringAttr(message, 'accessCode');
257          var accessCodeLifetime = getNumberAttr(message, 'accessCodeLifetime');
258          this.onReceivedAccessCode_(accessCode, accessCodeLifetime);
259          break;
260
261        case remoting.HostSession.State.CONNECTED:
262          var client = getStringAttr(message, 'client');
263          this.onConnected_(client);
264          break;
265      }
266      if (this.onStateChanged_) {
267        this.onStateChanged_(state);
268      }
269      break;
270
271    case 'natPolicyChanged':
272      if (this.onNatPolicyChanged_) {
273        var natTraversalEnabled =
274            getBooleanAttr(message, 'natTraversalEnabled');
275        this.onNatPolicyChanged_(natTraversalEnabled);
276      }
277      break;
278
279    case 'error':
280      console.error(getStringAttr(message, 'description'));
281      if (this.onError_) {
282        this.onError_(remoting.Error.UNEXPECTED);
283      }
284      break;
285
286    default:
287      throw 'Unexpected native message: ' + message;
288  }
289};
290
291/**
292 * @param {string} accessCode
293 * @param {number} accessCodeLifetime
294 * @return {void}
295 * @private
296 */
297remoting.It2MeHostFacade.prototype.onReceivedAccessCode_ =
298    function(accessCode, accessCodeLifetime) {
299  this.accessCode_ = accessCode;
300  this.accessCodeLifetime_ = accessCodeLifetime;
301};
302
303/**
304 * @param {string} clientId
305 * @return {void}
306 * @private
307 */
308remoting.It2MeHostFacade.prototype.onConnected_ = function(clientId) {
309  this.clientId_ = clientId;
310};
311
312/**
313 * @return {void}
314 * @private
315 */
316remoting.It2MeHostFacade.prototype.onHostDisconnect_ = function() {
317  if (!this.initialized_) {
318    // If the host is disconnected before it is initialized, it probably means
319    // the host is not propertly installed (or not installed at all).
320    // E.g., if the host manifest is not present we get "Specified native
321    // messaging host not found" error. If the host manifest is present but
322    // the host binary cannot be found we get the "Native host has exited"
323    // error.
324    console.log('Native Messaging initialization failed: ' +
325                chrome.runtime.lastError.message);
326    this.onInitializationFailed_();
327  } else {
328    console.error('Native Messaging port disconnected.');
329    this.port_ = null;
330    this.onError_(remoting.Error.UNEXPECTED);
331  }
332}
333