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 host daemon via Native Messaging.
8 */
9
10'use strict';
11
12/** @suppress {duplicate} */
13var remoting = remoting || {};
14
15/**
16 * @constructor
17 */
18remoting.HostDaemonFacade = function() {
19  /**
20   * @type {number}
21   * @private
22   */
23  this.nextId_ = 0;
24
25  /**
26   * @type {Object.<number, remoting.HostDaemonFacade.PendingReply>}
27   * @private
28   */
29  this.pendingReplies_ = {};
30
31  /** @type {?chrome.runtime.Port} @private */
32  this.port_ = null;
33
34  /** @type {string} @private */
35  this.version_ = '';
36
37  /** @type {Array.<remoting.HostController.Feature>} @private */
38  this.supportedFeatures_ = [];
39
40  /** @type {Array.<function(boolean):void>} @private */
41  this.afterInitializationTasks_ = [];
42
43  /** @private */
44  this.initializationFinished_ = false;
45
46  /** @type {remoting.Error} @private */
47  this.error_ = remoting.Error.NONE;
48
49  try {
50    this.port_ = chrome.runtime.connectNative(
51        'com.google.chrome.remote_desktop');
52    this.port_.onMessage.addListener(this.onIncomingMessage_.bind(this));
53    this.port_.onDisconnect.addListener(this.onDisconnect_.bind(this));
54    this.postMessage_({type: 'hello'},
55                      this.onInitialized_.bind(this, true),
56                      this.onInitialized_.bind(this, false));
57  } catch (err) {
58    console.log('Native Messaging initialization failed: ',
59                /** @type {*} */ (err));
60    this.onInitialized_(false);
61  }
62};
63
64/**
65 * Type used for entries of |pendingReplies_| list.
66 *
67 * @param {string} type Type of the originating request.
68 * @param {function(...):void} onDone Response callback. Parameters depend on
69 *     the request type.
70 * @param {function(remoting.Error):void} onError Callback to call on error.
71 * @constructor
72 */
73remoting.HostDaemonFacade.PendingReply = function(type, onDone, onError) {
74  this.type = type;
75  this.onDone = onDone;
76  this.onError = onError;
77};
78
79/**
80 * @param {boolean} success
81 * @return {void} Nothing.
82 * @private
83 */
84remoting.HostDaemonFacade.prototype.onInitialized_ = function(success) {
85  this.initializationFinished_ = true;
86  var afterInitializationTasks = this.afterInitializationTasks_;
87  this.afterInitializationTasks_ = [];
88  for (var id in afterInitializationTasks) {
89    afterInitializationTasks[/** @type {number} */(id)](success);
90  }
91};
92
93/**
94 * @param {remoting.HostController.Feature} feature The feature to test for.
95 * @param {function(boolean):void} onDone Callback to return result.
96 * @return {boolean} True if the implementation supports the named feature.
97 */
98remoting.HostDaemonFacade.prototype.hasFeature = function(feature, onDone) {
99  if (!this.port_) {
100    onDone(false);
101  } else if (this.initializationFinished_) {
102    onDone(this.supportedFeatures_.indexOf(feature) >= 0);
103  } else {
104    /** @type remoting.HostDaemonFacade */
105    var that = this;
106    this.afterInitializationTasks_.push(
107        /** @param {boolean} success */
108        function(success) {
109          onDone(that.supportedFeatures_.indexOf(feature) >= 0);
110        });
111  }
112};
113
114/**
115 * Attaches a new ID to the supplied message, and posts it to the Native
116 * Messaging port, adding |onDone| to the list of pending replies.
117 * |message| should have its 'type' field set, and any other fields set
118 * depending on the message type.
119 *
120 * @param {{type: string}} message The message to post.
121 * @param {function(...):void} onDone The callback, if any, to be triggered
122 *     on response.
123 * @param {function(remoting.Error):void} onError Callback to call on error.
124 * @return {void} Nothing.
125 * @private
126 */
127remoting.HostDaemonFacade.prototype.postMessage_ =
128    function(message, onDone, onError) {
129  if (!this.port_) {
130    onError(this.error_);
131    return;
132  }
133  var id = this.nextId_++;
134  message['id'] = id;
135  this.pendingReplies_[id] = new remoting.HostDaemonFacade.PendingReply(
136    message.type + 'Response', onDone, onError);
137  this.port_.postMessage(message);
138};
139
140/**
141 * Handler for incoming Native Messages.
142 *
143 * @param {Object} message The received message.
144 * @return {void} Nothing.
145 * @private
146 */
147remoting.HostDaemonFacade.prototype.onIncomingMessage_ = function(message) {
148  /** @type {number} */
149  var id = message['id'];
150  if (typeof(id) != 'number') {
151    console.error('NativeMessaging: missing or non-numeric id');
152    return;
153  }
154  var reply = this.pendingReplies_[id];
155  if (!reply) {
156    console.error('NativeMessaging: unexpected id: ', id);
157    return;
158  }
159  delete this.pendingReplies_[id];
160
161  try {
162    var type = getStringAttr(message, 'type');
163    if (type != reply.type) {
164      throw 'Expected reply type: ' + reply.type + ', got: ' + type;
165    }
166
167    this.handleIncomingMessage_(message, reply.onDone);
168  } catch (e) {
169    console.error('Error while processing native message' +
170                  /** @type {*} */ (e));
171    reply.onError(remoting.Error.UNEXPECTED);
172  }
173}
174
175/**
176 * Handler for incoming Native Messages.
177 *
178 * @param {Object} message The received message.
179 * @param {function(...):void} onDone Function to call when we're done
180 *     processing the message.
181 * @return {void} Nothing.
182 * @private
183 */
184remoting.HostDaemonFacade.prototype.handleIncomingMessage_ =
185    function(message, onDone) {
186  var type = getStringAttr(message, 'type');
187
188  switch (type) {
189    case 'helloResponse':
190      this.version_ = getStringAttr(message, 'version');
191      // Old versions of the native messaging host do not return this list.
192      // Those versions default to the empty list of supported features.
193      this.supportedFeatures_ = getArrayAttr(message, 'supportedFeatures', []);
194      onDone();
195      break;
196
197    case 'getHostNameResponse':
198      onDone(getStringAttr(message, 'hostname'));
199      break;
200
201    case 'getPinHashResponse':
202      onDone(getStringAttr(message, 'hash'));
203      break;
204
205    case 'generateKeyPairResponse':
206      var privateKey = getStringAttr(message, 'privateKey');
207      var publicKey = getStringAttr(message, 'publicKey');
208      onDone(privateKey, publicKey);
209      break;
210
211    case 'updateDaemonConfigResponse':
212      var result = remoting.HostController.AsyncResult.fromString(
213          getStringAttr(message, 'result'));
214      onDone(result);
215      break;
216
217    case 'getDaemonConfigResponse':
218      onDone(getObjectAttr(message, 'config'));
219      break;
220
221    case 'getUsageStatsConsentResponse':
222      var supported = getBooleanAttr(message, 'supported');
223      var allowed = getBooleanAttr(message, 'allowed');
224      var setByPolicy = getBooleanAttr(message, 'setByPolicy');
225      onDone(supported, allowed, setByPolicy);
226      break;
227
228    case 'startDaemonResponse':
229    case 'stopDaemonResponse':
230      var result = remoting.HostController.AsyncResult.fromString(
231          getStringAttr(message, 'result'));
232      onDone(result);
233      break;
234
235    case 'getDaemonStateResponse':
236      var state = remoting.HostController.State.fromString(
237        getStringAttr(message, 'state'));
238      onDone(state);
239      break;
240
241    case 'getPairedClientsResponse':
242      var pairedClients = remoting.PairedClient.convertToPairedClientArray(
243          message['pairedClients']);
244      if (pairedClients != null) {
245        onDone(pairedClients);
246      } else {
247        throw 'No paired clients!';
248      }
249      break;
250
251    case 'clearPairedClientsResponse':
252    case 'deletePairedClientResponse':
253      onDone(getBooleanAttr(message, 'result'));
254      break;
255
256    case 'getHostClientIdResponse':
257      onDone(getStringAttr(message, 'clientId'));
258      break;
259
260    case 'getCredentialsFromAuthCodeResponse':
261      var userEmail = getStringAttr(message, 'userEmail');
262      var refreshToken = getStringAttr(message, 'refreshToken');
263      if (userEmail && refreshToken) {
264        onDone(userEmail, refreshToken);
265      } else {
266        throw 'Missing userEmail or refreshToken';
267      }
268      break;
269
270    default:
271      throw 'Unexpected native message: ' + message;
272  }
273};
274
275/**
276 * @return {void} Nothing.
277 * @private
278 */
279remoting.HostDaemonFacade.prototype.onDisconnect_ = function() {
280  console.error('Native Message port disconnected');
281
282  this.port_ = null;
283
284  // If initialization hasn't finished then assume that the port was
285  // disconnected because Native Messaging host is not installed.
286  this.error_ = this.initializationFinished_ ? remoting.Error.UNEXPECTED :
287                                               remoting.Error.MISSING_PLUGIN;
288
289  // Notify the error-handlers of any requests that are still outstanding.
290  var pendingReplies = this.pendingReplies_;
291  this.pendingReplies_ = {};
292  for (var id in pendingReplies) {
293    pendingReplies[/** @type {number} */(id)].onError(this.error_);
294  }
295}
296
297/**
298 * Gets local hostname.
299 *
300 * @param {function(string):void} onDone Callback to return result.
301 * @param {function(remoting.Error):void} onError Callback to call on error.
302 * @return {void} Nothing.
303 */
304remoting.HostDaemonFacade.prototype.getHostName =
305    function(onDone, onError) {
306  this.postMessage_({type: 'getHostName'}, onDone, onError);
307};
308
309/**
310 * Calculates PIN hash value to be stored in the config, passing the resulting
311 * hash value base64-encoded to the callback.
312 *
313 * @param {string} hostId The host ID.
314 * @param {string} pin The PIN.
315 * @param {function(string):void} onDone Callback to return result.
316 * @param {function(remoting.Error):void} onError Callback to call on error.
317 * @return {void} Nothing.
318 */
319remoting.HostDaemonFacade.prototype.getPinHash =
320    function(hostId, pin, onDone, onError) {
321  this.postMessage_({
322      type: 'getPinHash',
323      hostId: hostId,
324      pin: pin
325  }, onDone, onError);
326};
327
328/**
329 * Generates new key pair to use for the host. The specified callback is called
330 * when the key is generated. The key is returned in format understood by the
331 * host (PublicKeyInfo structure encoded with ASN.1 DER, and then BASE64).
332 *
333 * @param {function(string, string):void} onDone Callback to return result.
334 * @param {function(remoting.Error):void} onError Callback to call on error.
335 * @return {void} Nothing.
336 */
337remoting.HostDaemonFacade.prototype.generateKeyPair =
338    function(onDone, onError) {
339  this.postMessage_({type: 'generateKeyPair'}, onDone, onError);
340};
341
342/**
343 * Updates host config with the values specified in |config|. All
344 * fields that are not specified in |config| remain
345 * unchanged. Following parameters cannot be changed using this
346 * function: host_id, xmpp_login. Error is returned if |config|
347 * includes these parameters. Changes take effect before the callback
348 * is called.
349 *
350 * @param {Object} config The new config parameters.
351 * @param {function(remoting.HostController.AsyncResult):void} onDone
352 *     Callback to be called when finished.
353 * @param {function(remoting.Error):void} onError Callback to call on error.
354 * @return {void} Nothing.
355 */
356remoting.HostDaemonFacade.prototype.updateDaemonConfig =
357    function(config, onDone, onError) {
358  this.postMessage_({
359      type: 'updateDaemonConfig',
360      config: config
361  }, onDone, onError);
362};
363
364/**
365 * Loads daemon config. The config is passed as a JSON formatted string to the
366 * callback.
367 *
368 * @param {function(Object):void} onDone Callback to return result.
369 * @param {function(remoting.Error):void} onError Callback to call on error.
370 * @return {void} Nothing.
371 */
372remoting.HostDaemonFacade.prototype.getDaemonConfig =
373    function(onDone, onError) {
374  this.postMessage_({type: 'getDaemonConfig'}, onDone, onError);
375};
376
377/**
378 * Retrieves daemon version. The version is passed to onDone as a dotted decimal
379 * string of the form major.minor.build.patch.
380 * @param {function(string):void} onDone Callback to be called to return result.
381 * @param {function(remoting.Error):void} onError Callback to call on error.
382 * @return {void}
383 */
384remoting.HostDaemonFacade.prototype.getDaemonVersion =
385    function(onDone, onError) {
386  if (!this.port_) {
387    onError(remoting.Error.UNEXPECTED);
388  } else if (this.initializationFinished_) {
389    onDone(this.version_);
390  } else {
391    /** @type remoting.HostDaemonFacade */
392    var that = this;
393    this.afterInitializationTasks_.push(
394        /** @param {boolean} success */
395        function(success) {
396          if (success) {
397            onDone(that.version_);
398          } else {
399            onError(that.error_);
400          }
401        });
402  }
403};
404
405/**
406 * Get the user's consent to crash reporting. The consent flags are passed to
407 * the callback as booleans: supported, allowed, set-by-policy.
408 *
409 * @param {function(boolean, boolean, boolean):void} onDone Callback to return
410 *     result.
411 * @param {function(remoting.Error):void} onError Callback to call on error.
412 * @return {void} Nothing.
413 */
414remoting.HostDaemonFacade.prototype.getUsageStatsConsent =
415    function(onDone, onError) {
416  this.postMessage_({type: 'getUsageStatsConsent'}, onDone, onError);
417};
418
419/**
420 * Starts the daemon process with the specified configuration.
421 *
422 * @param {Object} config Host configuration.
423 * @param {boolean} consent Consent to report crash dumps.
424 * @param {function(remoting.HostController.AsyncResult):void} onDone
425 *     Callback to return result.
426 * @param {function(remoting.Error):void} onError Callback to call on error.
427 * @return {void} Nothing.
428 */
429remoting.HostDaemonFacade.prototype.startDaemon =
430    function(config, consent, onDone, onError) {
431  this.postMessage_({
432      type: 'startDaemon',
433      config: config,
434      consent: consent
435  }, onDone, onError);
436};
437
438/**
439 * Stops the daemon process.
440 *
441 * @param {function(remoting.HostController.AsyncResult):void} onDone
442 *     Callback to return result.
443 * @param {function(remoting.Error):void} onError Callback to call on error.
444 * @return {void} Nothing.
445 */
446remoting.HostDaemonFacade.prototype.stopDaemon =
447    function(onDone, onError) {
448  this.postMessage_({type: 'stopDaemon'}, onDone, onError);
449};
450
451/**
452 * Gets the installed/running state of the Host process.
453 *
454 * @param {function(remoting.HostController.State):void} onDone Callback to
455*      return result.
456 * @param {function(remoting.Error):void} onError Callback to call on error.
457 * @return {void} Nothing.
458 */
459remoting.HostDaemonFacade.prototype.getDaemonState =
460    function(onDone, onError) {
461  this.postMessage_({type: 'getDaemonState'}, onDone, onError);
462}
463
464/**
465 * Retrieves the list of paired clients.
466 *
467 * @param {function(Array.<remoting.PairedClient>):void} onDone Callback to
468 *     return result.
469 * @param {function(remoting.Error):void} onError Callback to call on error.
470 */
471remoting.HostDaemonFacade.prototype.getPairedClients =
472    function(onDone, onError) {
473  this.postMessage_({type: 'getPairedClients'}, onDone, onError);
474}
475
476/**
477 * Clears all paired clients from the registry.
478 *
479 * @param {function(boolean):void} onDone Callback to be called when finished.
480 * @param {function(remoting.Error):void} onError Callback to call on error.
481 */
482remoting.HostDaemonFacade.prototype.clearPairedClients =
483    function(onDone, onError) {
484  this.postMessage_({type: 'clearPairedClients'}, onDone, onError);
485}
486
487/**
488 * Deletes a paired client referenced by client id.
489 *
490 * @param {string} client Client to delete.
491 * @param {function(boolean):void} onDone Callback to be called when finished.
492 * @param {function(remoting.Error):void} onError Callback to call on error.
493 */
494remoting.HostDaemonFacade.prototype.deletePairedClient =
495    function(client, onDone, onError) {
496  this.postMessage_({
497    type: 'deletePairedClient',
498    clientId: client
499  }, onDone, onError);
500}
501
502/**
503 * Gets the API keys to obtain/use service account credentials.
504 *
505 * @param {function(string):void} onDone Callback to return result.
506 * @param {function(remoting.Error):void} onError Callback to call on error.
507 * @return {void} Nothing.
508 */
509remoting.HostDaemonFacade.prototype.getHostClientId =
510    function(onDone, onError) {
511  this.postMessage_({type: 'getHostClientId'}, onDone, onError);
512};
513
514/**
515 *
516 * @param {string} authorizationCode OAuth authorization code.
517 * @param {function(string, string):void} onDone Callback to return result.
518 * @param {function(remoting.Error):void} onError Callback to call on error.
519 * @return {void} Nothing.
520 */
521remoting.HostDaemonFacade.prototype.getCredentialsFromAuthCode =
522    function(authorizationCode, onDone, onError) {
523  this.postMessage_({
524    type: 'getCredentialsFromAuthCode',
525    authorizationCode: authorizationCode
526  }, onDone, onError);
527};
528