Remove base.debug.assert.
[chromium-blink-merge.git] / remoting / webapp / crd / js / host_controller.js
blob03b49bc4c8c908936596ede421030a0df9c54c64
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.
5 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /** @constructor */
11 remoting.HostController = function() {
12   /** @type {remoting.HostDaemonFacade} @private */
13   this.hostDaemonFacade_ = new remoting.HostDaemonFacade();
15   /** @param {string} version */
16   var printVersion = function(version) {
17     if (version == '') {
18       console.log('Host not installed.');
19     } else {
20       console.log('Host version: ' + version);
21     }
22   };
24   this.getLocalHostVersion()
25       .then(printVersion)
26       .catch(function() {
27         console.log('Host version not available.');
28       });
31 // The values in the enums below are duplicated in daemon_controller.h except
32 // for NOT_INSTALLED.
33 /** @enum {number} */
34 remoting.HostController.State = {
35   NOT_IMPLEMENTED: 0,
36   NOT_INSTALLED: 1,
37   STOPPED: 2,
38   STARTING: 3,
39   STARTED: 4,
40   STOPPING: 5,
41   UNKNOWN: 6
44 /**
45  * @param {string} state The host controller state name.
46  * @return {remoting.HostController.State} The state enum value.
47  */
48 remoting.HostController.State.fromString = function(state) {
49   if (!remoting.HostController.State.hasOwnProperty(state)) {
50     throw "Invalid HostController.State: " + state;
51   }
52   return remoting.HostController.State[state];
55 /** @enum {number} */
56 remoting.HostController.AsyncResult = {
57   OK: 0,
58   FAILED: 1,
59   CANCELLED: 2,
60   FAILED_DIRECTORY: 3
63 /**
64  * @param {string} result The async result name.
65  * @return {remoting.HostController.AsyncResult} The result enum value.
66  */
67 remoting.HostController.AsyncResult.fromString = function(result) {
68   if (!remoting.HostController.AsyncResult.hasOwnProperty(result)) {
69     throw "Invalid HostController.AsyncResult: " + result;
70   }
71   return remoting.HostController.AsyncResult[result];
74 /**
75  * Set of features for which hasFeature() can be used to test.
76  *
77  * @enum {string}
78  */
79 remoting.HostController.Feature = {
80   PAIRING_REGISTRY: 'pairingRegistry',
81   OAUTH_CLIENT: 'oauthClient'
84 /**
85  * Information relating to user consent to collect usage stats.  The
86  * fields are:
87  *
88  *   supported: True if crash dump reporting is supported by the host.
89  *
90  *   allowed: True if crash dump reporting is allowed.
91  *
92  *   setByPolicy: True if crash dump reporting is controlled by policy.
93  *
94  * @typedef {{
95  *   supported:boolean,
96  *   allowed:boolean,
97  *   setByPolicy:boolean
98  * }}
99  */
100 remoting.UsageStatsConsent;
103  * @typedef {{
104  *   userEmail:string,
105  *   refreshToken:string
106  * }}
107  */
108 remoting.XmppCredentials;
111  * @typedef {{
112  *   privateKey:string,
113  *   publicKey:string
114  * }}
115  */
116 remoting.KeyPair;
119  * @param {remoting.HostController.Feature} feature The feature to test for.
120  * @return {!Promise<boolean>} A promise that always resolves.
121  */
122 remoting.HostController.prototype.hasFeature = function(feature) {
123   // TODO(rmsousa): This could synchronously return a boolean, provided it were
124   // only called after native messaging is completely initialized.
125   return this.hostDaemonFacade_.hasFeature(feature);
129  * @return {!Promise<remoting.UsageStatsConsent>}
130  */
131 remoting.HostController.prototype.getConsent = function() {
132   return this.hostDaemonFacade_.getUsageStatsConsent();
136  * Registers and starts the host.
138  * @param {string} hostPin Host PIN.
139  * @param {boolean} consent The user's consent to crash dump reporting.
140  * @return {!Promise<void>} A promise resolved once the host is started.
141  */
142 remoting.HostController.prototype.start = function(hostPin, consent) {
143   /** @type {remoting.HostController} */
144   var that = this;
146   // Start a bunch of requests with no side-effects.
147   var hostNamePromise = this.hostDaemonFacade_.getHostName();
148   var hasOauthPromise =
149       this.hasFeature(remoting.HostController.Feature.OAUTH_CLIENT);
150   var keyPairPromise = this.hostDaemonFacade_.generateKeyPair();
151   var hostClientIdPromise = hasOauthPromise.then(function(hasOauth) {
152     if (hasOauth) {
153       return that.hostDaemonFacade_.getHostClientId();
154     } else {
155       return null;
156     }
157   });
158   var hostOwnerPromise = this.getClientBaseJid_();
160   // Register the host and extract an auth code from the host response
161   // and, optionally an email address for the robot account.
162   /** @type {!Promise<remoting.HostListApi.RegisterResult>} */
163   var registerResultPromise = Promise.all([
164     hostClientIdPromise,
165     hostNamePromise,
166     keyPairPromise
167   ]).then(function(/** Array */ a) {
168     var hostClientId = /** @type {string} */ (a[0]);
169     var hostName = /** @type {string} */ (a[1]);
170     var keyPair = /** @type {remoting.KeyPair} */ (a[2]);
172     return remoting.HostListApi.getInstance().register(
173         hostName, keyPair.publicKey, hostClientId);
174   });
176   // For convenience, make the host ID available as a separate promise.
177   /** @type {!Promise<string>} */
178   var hostIdPromise = registerResultPromise.then(function(registerResult) {
179     return registerResult.hostId;
180   });
182   // Get the PIN hash based on the host ID.
183   /** @type {!Promise<string>} */
184   var pinHashPromise = hostIdPromise.then(function(hostId) {
185     return that.hostDaemonFacade_.getPinHash(hostId, hostPin);
186   });
188   // Get XMPP creditials.
189   var xmppCredsPromise = registerResultPromise.then(function(registerResult) {
190     console.assert(registerResult.authCode != '', '|authCode| is empty.');
191     if (registerResult.email) {
192       // Use auth code and email supplied by GCD.
193       return that.hostDaemonFacade_.getRefreshTokenFromAuthCode(
194           registerResult.authCode).then(function(token) {
195             return {
196               userEmail: registerResult.email,
197               refreshToken: token
198             };
199           });
200     } else {
201       // Use auth code supplied by Chromoting registry.
202       return that.hostDaemonFacade_.getCredentialsFromAuthCode(
203           registerResult.authCode);
204     }
205   });
207   // Build the host configuration.
208   /** @type {!Promise<!Object>} */
209   var hostConfigPromise = Promise.all([
210     hostNamePromise,
211     pinHashPromise,
212     xmppCredsPromise,
213     keyPairPromise,
214     hostOwnerPromise,
215     remoting.identity.getEmail(),
216     registerResultPromise
217   ]).then(function(/** Array */ a) {
218     var hostName = /** @type {string} */ (a[0]);
219     var hostSecretHash = /** @type {string} */ (a[1]);
220     var xmppCreds = /** @type {remoting.XmppCredentials} */ (a[2]);
221     var keyPair = /** @type {remoting.KeyPair} */ (a[3]);
222     var hostOwner = /** @type {string} */ (a[4]);
223     var hostOwnerEmail = /** @type {string} */ (a[5]);
224     var registerResult =
225         /** @type {remoting.HostListApi.RegisterResult} */ (a[6]);
226     var hostConfig = {
227       xmpp_login: xmppCreds.userEmail,
228       oauth_refresh_token: xmppCreds.refreshToken,
229       host_name: hostName,
230       host_secret_hash: hostSecretHash,
231       private_key: keyPair.privateKey,
232       host_owner: hostOwner
233     };
234     if (hostOwnerEmail != hostOwner) {
235       hostConfig['host_owner_email'] = hostOwnerEmail;
236     }
237     if (registerResult.isLegacy) {
238       hostConfig['host_id'] = registerResult.hostId;
239     }
240     else {
241       hostConfig['gcd_device_id'] = registerResult.hostId;
242     }
243     return hostConfig;
244   });
246   // Start the daemon.
247   /** @type {!Promise<remoting.HostController.AsyncResult>} */
248   var startDaemonResultPromise =
249       hostConfigPromise.then(function(hostConfig) {
250         return that.hostDaemonFacade_.startDaemon(hostConfig, consent);
251       });
253   // Update the UI or report an error.
254   return hostIdPromise.then(function(hostId) {
255     return startDaemonResultPromise.then(function(result) {
256       if (result == remoting.HostController.AsyncResult.OK) {
257         return hostNamePromise.then(function(hostName) {
258           return keyPairPromise.then(function(keyPair) {
259             remoting.hostList.onLocalHostStarted(
260                 hostName, hostId, keyPair.publicKey);
261           });
262         });
263       } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
264         throw new remoting.Error(remoting.Error.Tag.CANCELLED);
265       } else {
266         throw remoting.Error.unexpected();
267       }
268     }).catch(function(error) {
269       remoting.hostList.unregisterHostById(hostId);
270       throw error;
271     });
272   });
276  * Stop the daemon process.
277  * @param {function():void} onDone Callback to be called when done.
278  * @param {function(!remoting.Error):void} onError Callback to be called on
279  *     error.
280  * @return {void} Nothing.
281  */
282 remoting.HostController.prototype.stop = function(onDone, onError) {
283   /** @type {remoting.HostController} */
284   var that = this;
286   /** @param {string?} hostId The host id of the local host. */
287   function unregisterHost(hostId) {
288     if (hostId) {
289       remoting.hostList.unregisterHostById(hostId, onDone);
290       return;
291     }
292     onDone();
293   }
295   /** @param {remoting.HostController.AsyncResult} result */
296   function onStopped(result) {
297     if (result == remoting.HostController.AsyncResult.OK) {
298       that.getLocalHostId(unregisterHost);
299     } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
300       onError(new remoting.Error(remoting.Error.Tag.CANCELLED));
301     } else {
302       onError(remoting.Error.unexpected());
303     }
304   }
306   this.hostDaemonFacade_.stopDaemon().then(
307       onStopped, remoting.Error.handler(onError));
311  * Check the host configuration is valid (non-null, and contains both host_id
312  * and xmpp_login keys).
313  * @param {Object} config The host configuration.
314  * @return {boolean} True if it is valid.
315  */
316 function isHostConfigValid_(config) {
317   return !!config && typeof config['host_id'] == 'string' &&
318       typeof config['xmpp_login'] == 'string';
322  * @param {string} newPin The new PIN to set
323  * @param {function():void} onDone Callback to be called when done.
324  * @param {function(!remoting.Error):void} onError Callback to be called on
325  *     error.
326  * @return {void} Nothing.
327  */
328 remoting.HostController.prototype.updatePin = function(newPin, onDone,
329                                                        onError) {
330   /** @type {remoting.HostController} */
331   var that = this;
333   /** @param {remoting.HostController.AsyncResult} result */
334   function onConfigUpdated(result) {
335     if (result == remoting.HostController.AsyncResult.OK) {
336       that.clearPairedClients(onDone, onError);
337     } else if (result == remoting.HostController.AsyncResult.CANCELLED) {
338       onError(new remoting.Error(remoting.Error.Tag.CANCELLED));
339     } else {
340       onError(remoting.Error.unexpected());
341     }
342   }
344   /** @param {string} pinHash */
345   function updateDaemonConfigWithHash(pinHash) {
346     var newConfig = {
347       host_secret_hash: pinHash
348     };
349     that.hostDaemonFacade_.updateDaemonConfig(newConfig).then(
350         onConfigUpdated, remoting.Error.handler(onError));
351   }
353   /** @param {Object} config */
354   function onConfig(config) {
355     if (!isHostConfigValid_(config)) {
356       onError(remoting.Error.unexpected());
357       return;
358     }
359     /** @type {string} */
360     var hostId = config['host_id'];
361     that.hostDaemonFacade_.getPinHash(hostId, newPin).then(
362         updateDaemonConfigWithHash, remoting.Error.handler(onError));
363   }
365   // TODO(sergeyu): When crbug.com/121518 is fixed: replace this call
366   // with an unprivileged version if that is necessary.
367   this.hostDaemonFacade_.getDaemonConfig().then(
368       onConfig, remoting.Error.handler(onError));
372  * Get the state of the local host.
374  * @param {function(remoting.HostController.State):void} onDone Completion
375  *     callback.
376  */
377 remoting.HostController.prototype.getLocalHostState = function(onDone) {
378   /** @param {!remoting.Error} error */
379   function onError(error) {
380     onDone((error.hasTag(remoting.Error.Tag.MISSING_PLUGIN)) ?
381                remoting.HostController.State.NOT_INSTALLED :
382                remoting.HostController.State.UNKNOWN);
383   }
384   this.hostDaemonFacade_.getDaemonState().then(
385       onDone, remoting.Error.handler(onError));
389  * Get the id of the local host, or null if it is not registered.
391  * @param {function(string?):void} onDone Completion callback.
392  */
393 remoting.HostController.prototype.getLocalHostId = function(onDone) {
394   /** @type {remoting.HostController} */
395   var that = this;
396   /** @param {Object} config */
397   function onConfig(config) {
398     var hostId = null;
399     if (isHostConfigValid_(config)) {
400       // Use the |gcd_device_id| field if it exists, or the |host_id|
401       // field otherwise.
402       hostId = base.getStringAttr(
403           config, 'gcd_device_id', base.getStringAttr(config, 'host_id'));
404     }
405     onDone(hostId);
406   };
408   this.hostDaemonFacade_.getDaemonConfig().then(onConfig, function(error) {
409     onDone(null);
410   });
414  * @return {Promise<string>} Promise that resolves with the host version, if
415  *     installed, or rejects otherwise.
416  */
417 remoting.HostController.prototype.getLocalHostVersion = function() {
418   return this.hostDaemonFacade_.getDaemonVersion();
422  * Fetch the list of paired clients for this host.
424  * @param {function(Array<remoting.PairedClient>):void} onDone
425  * @param {function(!remoting.Error):void} onError
426  * @return {void}
427  */
428 remoting.HostController.prototype.getPairedClients = function(onDone,
429                                                               onError) {
430   this.hostDaemonFacade_.getPairedClients().then(
431       onDone, remoting.Error.handler(onError));
435  * Delete a single paired client.
437  * @param {string} client The client id of the pairing to delete.
438  * @param {function():void} onDone Completion callback.
439  * @param {function(!remoting.Error):void} onError Error callback.
440  * @return {void}
441  */
442 remoting.HostController.prototype.deletePairedClient = function(
443     client, onDone, onError) {
444   this.hostDaemonFacade_.deletePairedClient(client).then(
445       onDone, remoting.Error.handler(onError));
449  * Delete all paired clients.
451  * @param {function():void} onDone Completion callback.
452  * @param {function(!remoting.Error):void} onError Error callback.
453  * @return {void}
454  */
455 remoting.HostController.prototype.clearPairedClients = function(
456     onDone, onError) {
457   this.hostDaemonFacade_.clearPairedClients().then(
458       onDone, remoting.Error.handler(onError));
462  * Gets the host owner's base JID, used by the host for client authorization.
463  * In most cases this is the same as the owner's email address, but for
464  * non-Gmail accounts, it may be different.
466  * @private
467  * @return {!Promise<string>}
468  */
469 remoting.HostController.prototype.getClientBaseJid_ = function() {
470   /** @type {remoting.SignalStrategy} */
471   var signalStrategy = null;
473   var result = new Promise(function(resolve, reject) {
474     /** @param {remoting.SignalStrategy.State} state */
475     var onState = function(state) {
476       switch (state) {
477         case remoting.SignalStrategy.State.CONNECTED:
478         var jid = signalStrategy.getJid().split('/')[0].toLowerCase();
479         base.dispose(signalStrategy);
480         signalStrategy = null;
481         resolve(jid);
482         break;
484         case remoting.SignalStrategy.State.FAILED:
485         var error = signalStrategy.getError();
486         base.dispose(signalStrategy);
487         signalStrategy = null;
488         reject(error);
489         break;
490       }
491     };
493     signalStrategy = remoting.SignalStrategy.create();
494     signalStrategy.setStateChangedCallback(onState);
495   });
497   var tokenPromise = remoting.identity.getToken();
498   var emailPromise = remoting.identity.getEmail();
499   tokenPromise.then(function(/** string */ token) {
500     emailPromise.then(function(/** string */ email) {
501       signalStrategy.connect(remoting.settings.XMPP_SERVER, email, token);
502     });
503   });
505   return result;
508 /** @type {remoting.HostController} */
509 remoting.hostController = null;