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