• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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'use strict';
6
7/** @suppress {duplicate} */
8var remoting = remoting || {};
9
10/** @constructor */
11remoting.HostController = function() {
12  this.hostDaemonFacade_ = this.createDaemonFacade_();
13};
14
15// Note that the values in the enums below are copied from
16// daemon_controller.h and must be kept in sync.
17/** @enum {number} */
18remoting.HostController.State = {
19  NOT_IMPLEMENTED: -1,
20  NOT_INSTALLED: 0,
21  INSTALLING: 1,
22  STOPPED: 2,
23  STARTING: 3,
24  STARTED: 4,
25  STOPPING: 5,
26  UNKNOWN: 6
27};
28
29/**
30 * @param {string} state The host controller state name.
31 * @return {remoting.HostController.State} The state enum value.
32 */
33remoting.HostController.State.fromString = function(state) {
34  if (!remoting.HostController.State.hasOwnProperty(state)) {
35    throw "Invalid HostController.State: " + state;
36  }
37  return remoting.HostController.State[state];
38}
39
40/** @enum {number} */
41remoting.HostController.AsyncResult = {
42  OK: 0,
43  FAILED: 1,
44  CANCELLED: 2,
45  FAILED_DIRECTORY: 3
46};
47
48/**
49 * @param {string} result The async result name.
50 * @return {remoting.HostController.AsyncResult} The result enum value.
51 */
52remoting.HostController.AsyncResult.fromString = function(result) {
53  if (!remoting.HostController.AsyncResult.hasOwnProperty(result)) {
54    throw "Invalid HostController.AsyncResult: " + result;
55  }
56  return remoting.HostController.AsyncResult[result];
57}
58
59/**
60 * @return {remoting.HostDaemonFacade}
61 * @private
62 */
63remoting.HostController.prototype.createDaemonFacade_ = function() {
64  /** @type {remoting.HostDaemonFacade} @private */
65  var hostDaemonFacade = new remoting.HostDaemonFacade();
66
67  /** @param {string} version */
68  var printVersion = function(version) {
69    if (version == '') {
70      console.log('Host not installed.');
71    } else {
72      console.log('Host version: ' + version);
73    }
74  };
75
76  hostDaemonFacade.getDaemonVersion(printVersion, function() {
77    console.log('Host version not available.');
78  });
79
80  return hostDaemonFacade;
81};
82
83/**
84 * Set of features for which hasFeature() can be used to test.
85 *
86 * @enum {string}
87 */
88remoting.HostController.Feature = {
89  PAIRING_REGISTRY: 'pairingRegistry',
90  OAUTH_CLIENT: 'oauthClient'
91};
92
93/**
94 * @param {remoting.HostController.Feature} feature The feature to test for.
95 * @param {function(boolean):void} callback
96 * @return {void}
97 */
98remoting.HostController.prototype.hasFeature = function(feature, callback) {
99  // TODO(rmsousa): This could synchronously return a boolean, provided it were
100  // only called after native messaging is completely initialized.
101  this.hostDaemonFacade_.hasFeature(feature, callback);
102};
103
104/**
105 * @param {function(boolean, boolean, boolean):void} onDone Callback to be
106 *     called when done.
107 * @param {function(remoting.Error):void} onError Callback to be called on
108 *     error.
109 */
110remoting.HostController.prototype.getConsent = function(onDone, onError) {
111  this.hostDaemonFacade_.getUsageStatsConsent(onDone, onError);
112};
113
114/**
115 * Registers and starts the host.
116 *
117 * @param {string} hostPin Host PIN.
118 * @param {boolean} consent The user's consent to crash dump reporting.
119 * @param {function():void} onDone Callback to be called when done.
120 * @param {function(remoting.Error):void} onError Callback to be called on
121 *     error.
122 * @return {void} Nothing.
123 */
124remoting.HostController.prototype.start = function(hostPin, consent, onDone,
125                                                   onError) {
126  /** @type {remoting.HostController} */
127  var that = this;
128
129  /** @return {string} */
130  function generateUuid() {
131    var random = new Uint16Array(8);
132    window.crypto.getRandomValues(random);
133    /** @type {Array.<string>} */
134    var e = new Array();
135    for (var i = 0; i < 8; i++) {
136      e[i] = (/** @type {number} */random[i] + 0x10000).
137          toString(16).substring(1);
138    }
139    return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
140        e[4] + '-' + e[5] + e[6] + e[7];
141  };
142
143  var newHostId = generateUuid();
144
145  /** @param {remoting.Error} error */
146  function onStartError(error) {
147    // Unregister the host if we failed to start it.
148    remoting.HostList.unregisterHostById(newHostId);
149    onError(error);
150  }
151
152  /**
153   * @param {string} hostName
154   * @param {string} publicKey
155   * @param {remoting.HostController.AsyncResult} result
156   */
157  function onStarted(hostName, publicKey, result) {
158    if (result == remoting.HostController.AsyncResult.OK) {
159      remoting.hostList.onLocalHostStarted(hostName, newHostId, publicKey);
160      onDone();
161    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
162      onStartError(remoting.Error.CANCELLED);
163    } else {
164      onStartError(remoting.Error.UNEXPECTED);
165    }
166  }
167
168  /**
169   * @param {string} hostName
170   * @param {string} publicKey
171   * @param {string} privateKey
172   * @param {string} xmppLogin
173   * @param {string} refreshToken
174   * @param {string} clientBaseJid
175   * @param {string} hostSecretHash
176   */
177  function startHostWithHash(hostName, publicKey, privateKey, xmppLogin,
178                             refreshToken, clientBaseJid, hostSecretHash) {
179    var hostConfig = {
180      xmpp_login: xmppLogin,
181      oauth_refresh_token: refreshToken,
182      host_id: newHostId,
183      host_name: hostName,
184      host_secret_hash: hostSecretHash,
185      private_key: privateKey
186    };
187    var hostOwner = clientBaseJid;
188    var hostOwnerEmail = remoting.identity.getCachedEmail();
189    if (hostOwner != xmppLogin) {
190      hostConfig['host_owner'] = hostOwner;
191      if (hostOwnerEmail != hostOwner) {
192        hostConfig['host_owner_email'] = hostOwnerEmail;
193      }
194    }
195    that.hostDaemonFacade_.startDaemon(
196        hostConfig, consent, onStarted.bind(null, hostName, publicKey),
197        onStartError);
198  }
199
200  /**
201   * @param {string} hostName
202   * @param {string} publicKey
203   * @param {string} privateKey
204   * @param {string} email
205   * @param {string} refreshToken
206   * @param {string} clientBaseJid
207   */
208  function onClientBaseJid(
209      hostName, publicKey, privateKey, email, refreshToken, clientBaseJid) {
210    that.hostDaemonFacade_.getPinHash(
211        newHostId, hostPin,
212        startHostWithHash.bind(null, hostName, publicKey, privateKey,
213                               email, refreshToken, clientBaseJid),
214        onError);
215  }
216
217  /**
218   * @param {string} hostName
219   * @param {string} publicKey
220   * @param {string} privateKey
221   * @param {string} email
222   * @param {string} refreshToken
223   */
224  function onServiceAccountCredentials(
225      hostName, publicKey, privateKey, email, refreshToken) {
226    that.getClientBaseJid_(
227        onClientBaseJid.bind(
228            null, hostName, publicKey, privateKey, email, refreshToken),
229        onStartError);
230  }
231
232  /**
233   * @param {string} hostName
234   * @param {string} publicKey
235   * @param {string} privateKey
236   * @param {XMLHttpRequest} xhr
237   */
238  function onRegistered(
239      hostName, publicKey, privateKey, xhr) {
240    var success = (xhr.status == 200);
241
242    if (success) {
243      var result = jsonParseSafe(xhr.responseText);
244      if ('data' in result && 'authorizationCode' in result['data']) {
245        that.hostDaemonFacade_.getCredentialsFromAuthCode(
246            result['data']['authorizationCode'],
247            onServiceAccountCredentials.bind(
248                null, hostName, publicKey, privateKey),
249            onError);
250      } else {
251        // No authorization code returned, use regular user credential flow.
252        that.hostDaemonFacade_.getPinHash(
253            newHostId, hostPin, startHostWithHash.bind(
254                null, hostName, publicKey, privateKey,
255                remoting.identity.getCachedEmail(),
256                remoting.oauth2.getRefreshToken()),
257          onError);
258      }
259    } else {
260      console.log('Failed to register the host. Status: ' + xhr.status +
261                  ' response: ' + xhr.responseText);
262      onError(remoting.Error.REGISTRATION_FAILED);
263    }
264  }
265
266  /**
267   * @param {string} hostName
268   * @param {string} privateKey
269   * @param {string} publicKey
270   * @param {string} hostClientId
271   * @param {string} oauthToken
272   */
273  function doRegisterHost(
274      hostName, privateKey, publicKey, hostClientId, oauthToken) {
275    var headers = {
276      'Authorization': 'OAuth ' + oauthToken,
277      'Content-type' : 'application/json; charset=UTF-8'
278    };
279
280    var newHostDetails = { data: {
281       hostId: newHostId,
282       hostName: hostName,
283       publicKey: publicKey
284    } };
285
286    var registerHostUrl =
287        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts';
288
289    if (hostClientId) {
290      registerHostUrl += '?' + remoting.xhr.urlencodeParamHash(
291          { hostClientId: hostClientId });
292    }
293
294    remoting.xhr.post(
295        registerHostUrl,
296        onRegistered.bind(null, hostName, publicKey, privateKey),
297        JSON.stringify(newHostDetails),
298        headers);
299  }
300
301  /**
302   * @param {string} hostName
303   * @param {string} privateKey
304   * @param {string} publicKey
305   * @param {string} hostClientId
306   */
307  function onHostClientId(
308      hostName, privateKey, publicKey, hostClientId) {
309    remoting.identity.callWithToken(
310        doRegisterHost.bind(
311            null, hostName, privateKey, publicKey, hostClientId), onError);
312  }
313
314  /**
315   * @param {string} hostName
316   * @param {string} privateKey
317   * @param {string} publicKey
318   * @param {boolean} hasFeature
319   */
320  function onHasFeatureOAuthClient(
321      hostName, privateKey, publicKey, hasFeature) {
322    if (hasFeature) {
323      that.hostDaemonFacade_.getHostClientId(
324          onHostClientId.bind(null, hostName, privateKey, publicKey), onError);
325    } else {
326      remoting.identity.callWithToken(
327          doRegisterHost.bind(
328              null, hostName, privateKey, publicKey, null), onError);
329    }
330  }
331
332  /**
333   * @param {string} hostName
334   * @param {string} privateKey
335   * @param {string} publicKey
336   */
337  function onKeyGenerated(hostName, privateKey, publicKey) {
338    that.hasFeature(
339        remoting.HostController.Feature.OAUTH_CLIENT,
340        onHasFeatureOAuthClient.bind(null, hostName, privateKey, publicKey));
341  }
342
343  /**
344   * @param {string} hostName
345   * @return {void} Nothing.
346   */
347  function startWithHostname(hostName) {
348    that.hostDaemonFacade_.generateKeyPair(onKeyGenerated.bind(null, hostName),
349                                         onError);
350  }
351
352  this.hostDaemonFacade_.getHostName(startWithHostname, onError);
353};
354
355/**
356 * Stop the daemon process.
357 * @param {function():void} onDone Callback to be called when done.
358 * @param {function(remoting.Error):void} onError Callback to be called on
359 *     error.
360 * @return {void} Nothing.
361 */
362remoting.HostController.prototype.stop = function(onDone, onError) {
363  /** @type {remoting.HostController} */
364  var that = this;
365
366  /** @param {string?} hostId The host id of the local host. */
367  function unregisterHost(hostId) {
368    if (hostId) {
369      remoting.HostList.unregisterHostById(hostId);
370    }
371    onDone();
372  }
373
374  /** @param {remoting.HostController.AsyncResult} result */
375  function onStopped(result) {
376    if (result == remoting.HostController.AsyncResult.OK) {
377      that.getLocalHostId(unregisterHost);
378    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
379      onError(remoting.Error.CANCELLED);
380    } else {
381      onError(remoting.Error.UNEXPECTED);
382    }
383  }
384
385  this.hostDaemonFacade_.stopDaemon(onStopped, onError);
386};
387
388/**
389 * Check the host configuration is valid (non-null, and contains both host_id
390 * and xmpp_login keys).
391 * @param {Object} config The host configuration.
392 * @return {boolean} True if it is valid.
393 */
394function isHostConfigValid_(config) {
395  return !!config && typeof config['host_id'] == 'string' &&
396      typeof config['xmpp_login'] == 'string';
397}
398
399/**
400 * @param {string} newPin The new PIN to set
401 * @param {function():void} onDone Callback to be called when done.
402 * @param {function(remoting.Error):void} onError Callback to be called on
403 *     error.
404 * @return {void} Nothing.
405 */
406remoting.HostController.prototype.updatePin = function(newPin, onDone,
407                                                       onError) {
408  /** @type {remoting.HostController} */
409  var that = this;
410
411  /** @param {remoting.HostController.AsyncResult} result */
412  function onConfigUpdated(result) {
413    if (result == remoting.HostController.AsyncResult.OK) {
414      onDone();
415    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
416      onError(remoting.Error.CANCELLED);
417    } else {
418      onError(remoting.Error.UNEXPECTED);
419    }
420  }
421
422  /** @param {string} pinHash */
423  function updateDaemonConfigWithHash(pinHash) {
424    var newConfig = {
425      host_secret_hash: pinHash
426    };
427    that.hostDaemonFacade_.updateDaemonConfig(newConfig, onConfigUpdated,
428                                            onError);
429  }
430
431  /** @param {Object} config */
432  function onConfig(config) {
433    if (!isHostConfigValid_(config)) {
434      onError(remoting.Error.UNEXPECTED);
435      return;
436    }
437    /** @type {string} */
438    var hostId = config['host_id'];
439    that.hostDaemonFacade_.getPinHash(
440        hostId, newPin, updateDaemonConfigWithHash, onError);
441  }
442
443  // TODO(sergeyu): When crbug.com/121518 is fixed: replace this call
444  // with an unprivileged version if that is necessary.
445  this.hostDaemonFacade_.getDaemonConfig(onConfig, onError);
446};
447
448/**
449 * Get the state of the local host.
450 *
451 * @param {function(remoting.HostController.State):void} onDone Completion
452 *     callback.
453 */
454remoting.HostController.prototype.getLocalHostState = function(onDone) {
455  /** @param {remoting.Error} error */
456  function onError(error) {
457    onDone((error == remoting.Error.MISSING_PLUGIN) ?
458               remoting.HostController.State.NOT_INSTALLED :
459               remoting.HostController.State.UNKNOWN);
460  }
461  this.hostDaemonFacade_.getDaemonState(onDone, onError);
462};
463
464/**
465 * Get the id of the local host, or null if it is not registered.
466 *
467 * @param {function(string?):void} onDone Completion callback.
468 */
469remoting.HostController.prototype.getLocalHostId = function(onDone) {
470  /** @type {remoting.HostController} */
471  var that = this;
472  /** @param {Object} config */
473  function onConfig(config) {
474    var hostId = null;
475    if (isHostConfigValid_(config)) {
476      hostId = /** @type {string} */ config['host_id'];
477    }
478    onDone(hostId);
479  };
480
481  this.hostDaemonFacade_.getDaemonConfig(onConfig, function(error) {
482    onDone(null);
483  });
484};
485
486/**
487 * Fetch the list of paired clients for this host.
488 *
489 * @param {function(Array.<remoting.PairedClient>):void} onDone
490 * @param {function(remoting.Error):void} onError
491 * @return {void}
492 */
493remoting.HostController.prototype.getPairedClients = function(onDone,
494                                                              onError) {
495  this.hostDaemonFacade_.getPairedClients(onDone, onError);
496};
497
498/**
499 * Delete a single paired client.
500 *
501 * @param {string} client The client id of the pairing to delete.
502 * @param {function():void} onDone Completion callback.
503 * @param {function(remoting.Error):void} onError Error callback.
504 * @return {void}
505 */
506remoting.HostController.prototype.deletePairedClient = function(
507    client, onDone, onError) {
508  this.hostDaemonFacade_.deletePairedClient(client, onDone, onError);
509};
510
511/**
512 * Delete all paired clients.
513 *
514 * @param {function():void} onDone Completion callback.
515 * @param {function(remoting.Error):void} onError Error callback.
516 * @return {void}
517 */
518remoting.HostController.prototype.clearPairedClients = function(
519    onDone, onError) {
520  this.hostDaemonFacade_.clearPairedClients(onDone, onError);
521};
522
523/**
524 * Gets the host owner's base JID, used by the host for client authorization.
525 * In most cases this is the same as the owner's email address, but for
526 * non-Gmail accounts, it may be different.
527 *
528 * @private
529 * @param {function(string): void} onSuccess
530 * @param {function(remoting.Error): void} onError
531 */
532remoting.HostController.prototype.getClientBaseJid_ = function(
533    onSuccess, onError) {
534  var signalStrategy = null;
535
536  var onState = function(state) {
537    switch (state) {
538      case remoting.SignalStrategy.State.CONNECTED:
539        var jid = signalStrategy.getJid().split('/')[0].toLowerCase();
540        base.dispose(signalStrategy);
541        signalStrategy = null;
542        onSuccess(jid);
543        break;
544
545      case remoting.SignalStrategy.State.FAILED:
546        var error = signalStrategy.getError();
547        base.dispose(signalStrategy);
548        signalStrategy = null;
549        onError(error);
550        break;
551    }
552  };
553
554  signalStrategy = remoting.SignalStrategy.create(onState);
555
556  /** @param {string} token */
557  function connectSignalingWithToken(token) {
558    remoting.identity.getEmail(
559        connectSignalingWithTokenAndEmail.bind(null, token), onError);
560  }
561
562  /**
563   * @param {string} token
564   * @param {string} email
565   */
566  function connectSignalingWithTokenAndEmail(token, email) {
567    signalStrategy.connect(
568        remoting.settings.XMPP_SERVER_ADDRESS, email, token);
569  }
570
571  remoting.identity.callWithToken(connectSignalingWithToken, onError);
572};
573
574/** @type {remoting.HostController} */
575remoting.hostController = null;
576