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  /** @return {remoting.HostPlugin} */
13  var createNpapiPlugin = function() {
14    var plugin = remoting.HostSession.createPlugin();
15    /** @type {HTMLElement} @private */
16    var container = document.getElementById('daemon-plugin-container');
17    container.appendChild(plugin);
18    return plugin;
19  }
20
21  /** @type {remoting.HostDispatcher} @private */
22  this.hostDispatcher_ = new remoting.HostDispatcher(createNpapiPlugin);
23
24  /** @param {string} version */
25  var printVersion = function(version) {
26    if (version == '') {
27      console.log('Host not installed.');
28    } else {
29      console.log('Host version: ' + version);
30    }
31  };
32
33  this.hostDispatcher_.getDaemonVersion(printVersion, function() {
34    console.log('Host version not available.');
35  });
36}
37
38// Note that the values in the enums below are copied from
39// daemon_controller.h and must be kept in sync.
40/** @enum {number} */
41remoting.HostController.State = {
42  NOT_IMPLEMENTED: -1,
43  NOT_INSTALLED: 0,
44  INSTALLING: 1,
45  STOPPED: 2,
46  STARTING: 3,
47  STARTED: 4,
48  STOPPING: 5,
49  UNKNOWN: 6
50};
51
52/** @enum {number} */
53remoting.HostController.AsyncResult = {
54  OK: 0,
55  FAILED: 1,
56  CANCELLED: 2,
57  FAILED_DIRECTORY: 3
58};
59
60/**
61 * Set of features for which hasFeature() can be used to test.
62 *
63 * @enum {string}
64 */
65remoting.HostController.Feature = {
66  PAIRING_REGISTRY: 'pairingRegistry'
67};
68
69/**
70 * @param {remoting.HostController.Feature} feature The feature to test for.
71 * @param {function(boolean):void} callback
72 * @return {void}
73 */
74remoting.HostController.prototype.hasFeature = function(feature, callback) {
75  // TODO(rmsousa): This could synchronously return a boolean, provided it were
76  // only called after the dispatcher is completely initialized.
77  this.hostDispatcher_.hasFeature(feature, callback);
78};
79
80/**
81 * @param {function(boolean, boolean, boolean):void} onDone Callback to be
82 *     called when done.
83 * @param {function(remoting.Error):void} onError Callback to be called on
84 *     error.
85 */
86remoting.HostController.prototype.getConsent = function(onDone, onError) {
87  this.hostDispatcher_.getUsageStatsConsent(onDone, onError);
88};
89
90/**
91 * Registers and starts the host.
92 *
93 * @param {string} hostPin Host PIN.
94 * @param {boolean} consent The user's consent to crash dump reporting.
95 * @param {function():void} onDone Callback to be called when done.
96 * @param {function(remoting.Error):void} onError Callback to be called on
97 *     error.
98 * @return {void} Nothing.
99 */
100remoting.HostController.prototype.start = function(hostPin, consent, onDone,
101                                                   onError) {
102  /** @type {remoting.HostController} */
103  var that = this;
104
105  /** @return {string} */
106  function generateUuid() {
107    var random = new Uint16Array(8);
108    window.crypto.getRandomValues(random);
109    /** @type {Array.<string>} */
110    var e = new Array();
111    for (var i = 0; i < 8; i++) {
112      e[i] = (/** @type {number} */random[i] + 0x10000).
113          toString(16).substring(1);
114    }
115    return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
116        e[4] + '-' + e[5] + e[6] + e[7];
117  };
118
119  var newHostId = generateUuid();
120
121  /** @param {remoting.Error} error */
122  function onStartError(error) {
123    // Unregister the host if we failed to start it.
124    remoting.HostList.unregisterHostById(newHostId);
125    onError(error);
126  }
127
128  /**
129   * @param {string} hostName
130   * @param {string} publicKey
131   * @param {remoting.HostController.AsyncResult} result
132   */
133  function onStarted(hostName, publicKey, result) {
134    if (result == remoting.HostController.AsyncResult.OK) {
135      remoting.hostList.onLocalHostStarted(hostName, newHostId, publicKey);
136      onDone();
137    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
138      onStartError(remoting.Error.CANCELLED);
139    } else {
140      onStartError(remoting.Error.UNEXPECTED);
141    }
142  }
143
144  /**
145   * @param {string} hostName
146   * @param {string} publicKey
147   * @param {string} privateKey
148   * @param {XMLHttpRequest} xhr
149   * @param {string} hostSecretHash
150   */
151  function startHostWithHash(hostName, publicKey, privateKey, xhr,
152                             hostSecretHash) {
153    var hostConfig = {
154        xmpp_login: remoting.identity.getCachedEmail(),
155        oauth_refresh_token: remoting.oauth2.exportRefreshToken(),
156        host_id: newHostId,
157        host_name: hostName,
158        host_secret_hash: hostSecretHash,
159        private_key: privateKey
160    };
161    that.hostDispatcher_.startDaemon(hostConfig, consent,
162                                     onStarted.bind(null, hostName, publicKey),
163                                     onStartError);
164  }
165
166  /**
167   * @param {string} hostName
168   * @param {string} publicKey
169   * @param {string} privateKey
170   * @param {XMLHttpRequest} xhr
171   */
172  function onRegistered(hostName, publicKey, privateKey, xhr) {
173    var success = (xhr.status == 200);
174
175    if (success) {
176      that.hostDispatcher_.getPinHash(newHostId, hostPin,
177          startHostWithHash.bind(null, hostName, publicKey, privateKey, xhr),
178          onError);
179    } else {
180      console.log('Failed to register the host. Status: ' + xhr.status +
181                  ' response: ' + xhr.responseText);
182      onError(remoting.Error.REGISTRATION_FAILED);
183    }
184  };
185
186  /**
187   * @param {string} hostName
188   * @param {string} privateKey
189   * @param {string} publicKey
190   * @param {string} oauthToken
191   */
192  function doRegisterHost(hostName, privateKey, publicKey, oauthToken) {
193    var headers = {
194      'Authorization': 'OAuth ' + oauthToken,
195      'Content-type' : 'application/json; charset=UTF-8'
196    };
197
198    var newHostDetails = { data: {
199       hostId: newHostId,
200       hostName: hostName,
201       publicKey: publicKey
202    } };
203    remoting.xhr.post(
204        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/',
205        onRegistered.bind(null, hostName, publicKey, privateKey),
206        JSON.stringify(newHostDetails),
207        headers);
208  };
209
210  /**
211   * @param {string} hostName
212   * @param {string} privateKey
213   * @param {string} publicKey
214   */
215  function onKeyGenerated(hostName, privateKey, publicKey) {
216    remoting.identity.callWithToken(
217        doRegisterHost.bind(null, hostName, privateKey, publicKey),
218        onError);
219  };
220
221  /**
222   * @param {string} hostName
223   * @return {void} Nothing.
224   */
225  function startWithHostname(hostName) {
226    that.hostDispatcher_.generateKeyPair(onKeyGenerated.bind(null, hostName),
227                                         onError);
228  }
229
230  this.hostDispatcher_.getHostName(startWithHostname, onError);
231};
232
233/**
234 * Stop the daemon process.
235 * @param {function():void} onDone Callback to be called when done.
236 * @param {function(remoting.Error):void} onError Callback to be called on
237 *     error.
238 * @return {void} Nothing.
239 */
240remoting.HostController.prototype.stop = function(onDone, onError) {
241  /** @type {remoting.HostController} */
242  var that = this;
243
244  /** @param {string?} hostId The host id of the local host. */
245  function unregisterHost(hostId) {
246    if (hostId) {
247      remoting.HostList.unregisterHostById(hostId);
248    }
249    onDone();
250  };
251
252  /** @param {remoting.HostController.AsyncResult} result */
253  function onStopped(result) {
254    if (result == remoting.HostController.AsyncResult.OK) {
255      that.getLocalHostId(unregisterHost);
256    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
257      onError(remoting.Error.CANCELLED);
258    } else {
259      onError(remoting.Error.UNEXPECTED);
260    }
261  }
262
263  this.hostDispatcher_.stopDaemon(onStopped, onError);
264};
265
266/**
267 * Check the host configuration is valid (non-null, and contains both host_id
268 * and xmpp_login keys).
269 * @param {Object} config The host configuration.
270 * @return {boolean} True if it is valid.
271 */
272function isHostConfigValid_(config) {
273  return !!config && typeof config['host_id'] == 'string' &&
274      typeof config['xmpp_login'] == 'string';
275}
276
277/**
278 * @param {string} newPin The new PIN to set
279 * @param {function():void} onDone Callback to be called when done.
280 * @param {function(remoting.Error):void} onError Callback to be called on
281 *     error.
282 * @return {void} Nothing.
283 */
284remoting.HostController.prototype.updatePin = function(newPin, onDone,
285                                                       onError) {
286  /** @type {remoting.HostController} */
287  var that = this;
288
289  /** @param {remoting.HostController.AsyncResult} result */
290  function onConfigUpdated(result) {
291    if (result == remoting.HostController.AsyncResult.OK) {
292      onDone();
293    } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
294      onError(remoting.Error.CANCELLED);
295    } else {
296      onError(remoting.Error.UNEXPECTED);
297    }
298  }
299
300  /** @param {string} pinHash */
301  function updateDaemonConfigWithHash(pinHash) {
302    var newConfig = {
303      host_secret_hash: pinHash
304    };
305    that.hostDispatcher_.updateDaemonConfig(newConfig, onConfigUpdated,
306                                            onError);
307  }
308
309  /** @param {Object} config */
310  function onConfig(config) {
311    if (!isHostConfigValid_(config)) {
312      onError(remoting.Error.UNEXPECTED);
313      return;
314    }
315    /** @type {string} */
316    var hostId = config['host_id'];
317    that.hostDispatcher_.getPinHash(hostId, newPin, updateDaemonConfigWithHash,
318                                    onError);
319  }
320
321  // TODO(sergeyu): When crbug.com/121518 is fixed: replace this call
322  // with an unprivileged version if that is necessary.
323  this.hostDispatcher_.getDaemonConfig(onConfig, onError);
324};
325
326/**
327 * Get the state of the local host.
328 *
329 * @param {function(remoting.HostController.State):void} onDone Completion
330 *     callback.
331 */
332remoting.HostController.prototype.getLocalHostState = function(onDone) {
333  this.hostDispatcher_.getDaemonState(onDone, function() {
334    onDone(remoting.HostController.State.NOT_IMPLEMENTED);
335  });
336};
337
338/**
339 * Get the id of the local host, or null if it is not registered.
340 *
341 * @param {function(string?):void} onDone Completion callback.
342 */
343remoting.HostController.prototype.getLocalHostId = function(onDone) {
344  /** @type {remoting.HostController} */
345  var that = this;
346  /** @param {Object} config */
347  function onConfig(config) {
348    var hostId = null;
349    if (isHostConfigValid_(config)) {
350      hostId = /** @type {string} */ config['host_id'];
351    }
352    onDone(hostId);
353  };
354
355  this.hostDispatcher_.getDaemonConfig(onConfig, function(error) {
356    onDone(null);
357  });
358};
359
360/**
361 * Fetch the list of paired clients for this host.
362 *
363 * @param {function(Array.<remoting.PairedClient>):void} onDone
364 * @param {function(remoting.Error):void} onError
365 * @return {void}
366 */
367remoting.HostController.prototype.getPairedClients = function(onDone,
368                                                              onError) {
369  this.hostDispatcher_.getPairedClients(onDone, onError);
370};
371
372/**
373 * Delete a single paired client.
374 *
375 * @param {string} client The client id of the pairing to delete.
376 * @param {function():void} onDone Completion callback.
377 * @param {function(remoting.Error):void} onError Error callback.
378 * @return {void}
379 */
380remoting.HostController.prototype.deletePairedClient = function(
381    client, onDone, onError) {
382  this.hostDispatcher_.deletePairedClient(client, onDone, onError);
383};
384
385/**
386 * Delete all paired clients.
387 *
388 * @param {function():void} onDone Completion callback.
389 * @param {function(remoting.Error):void} onError Error callback.
390 * @return {void}
391 */
392remoting.HostController.prototype.clearPairedClients = function(
393    onDone, onError) {
394  this.hostDispatcher_.clearPairedClients(onDone, onError);
395};
396
397/** @type {remoting.HostController} */
398remoting.hostController = null;
399