Commit 43c0916a authored by jrw's avatar jrw Committed by Commit bot

Added HostListApl implementation to connect to legacy directory and GCD.

The new implementation (CombinedHostListApi) attempts to keep the two
directories in sync and present a merged view of the contents of the
directories.

BUG=503790

Review URL: https://codereview.chromium.org/1217643002

Cr-Commit-Position: refs/heads/master@{#339737}
parent d6767048
...@@ -108,6 +108,7 @@ ...@@ -108,6 +108,7 @@
'webapp/base/js/xmpp_login_handler_unittest.js', 'webapp/base/js/xmpp_login_handler_unittest.js',
'webapp/base/js/xmpp_stream_parser_unittest.js', 'webapp/base/js/xmpp_stream_parser_unittest.js',
'webapp/crd/js/apps_v2_migration_unittest.js', 'webapp/crd/js/apps_v2_migration_unittest.js',
'webapp/crd/js/combined_host_list_api_unittest.js',
'webapp/crd/js/gcd_client_unittest.js', 'webapp/crd/js/gcd_client_unittest.js',
'webapp/crd/js/gcd_client_with_mock_xhr_unittest.js', 'webapp/crd/js/gcd_client_with_mock_xhr_unittest.js',
'webapp/crd/js/host_controller_unittest.js', 'webapp/crd/js/host_controller_unittest.js',
...@@ -286,6 +287,7 @@ ...@@ -286,6 +287,7 @@
# JSCompiler. If an implementation of an interface occurs in a # JSCompiler. If an implementation of an interface occurs in a
# file processed before the interface itself, the @override tag # file processed before the interface itself, the @override tag
# doesn't always work correctly. # doesn't always work correctly.
'webapp/crd/js/combined_host_list_api.js',
'webapp/crd/js/gcd_host_list_api.js', 'webapp/crd/js/gcd_host_list_api.js',
'webapp/crd/js/legacy_host_list_api.js', 'webapp/crd/js/legacy_host_list_api.js',
], ],
......
...@@ -27,7 +27,10 @@ remoting.Host = function(hostId) { ...@@ -27,7 +27,10 @@ remoting.Host = function(hostId) {
this.hostId = hostId; this.hostId = hostId;
/** @type {string} */ /** @type {string} */
this.hostName = ''; this.hostName = '';
/** @type {string} */ /**
* Either 'ONLINE' or 'OFFLINE'.
* @type {string}
*/
this.status = ''; this.status = '';
/** @type {string} */ /** @type {string} */
this.jabberId = ''; this.jabberId = '';
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* API implementation that combines two other implementations.
*/
/** @suppress {duplicate} */
var remoting = remoting || {};
(function() {
'use strict';
/**
* Amount of time to wait for GCD results after legacy registry has
* returned.
*/
var GCD_TIMEOUT_MS = 1000;
/**
* @constructor
* @param {!remoting.HostListApi} legacyImpl
* @param {!remoting.HostListApi} gcdImpl
* @implements {remoting.HostListApi}
*/
remoting.CombinedHostListApi = function(legacyImpl, gcdImpl) {
/** @const {!remoting.HostListApi} */
this.legacyImpl_ = legacyImpl;
/** @const {!remoting.HostListApi} */
this.gcdImpl_ = gcdImpl;
/**
* List of host IDs most recently retrieved from |legacyImpl_|.
* @type {!Set<string>}
*/
this.legacyIds_ = new Set();
/**
* List of host IDs most recently retrieved |gcdImpl_|.
* @type {!Set<string>}
*/
this.gcdIds_ = new Set();
};
/** @override */
remoting.CombinedHostListApi.prototype.register = function(
hostName, publicKey, hostClientId) {
var that = this;
// First, register the new host with GCD, which will create a
// service account and generate a host ID.
return this.gcdImpl_.register(hostName, publicKey, hostClientId).then(
function(gcdRegResult) {
// After the GCD registration has been created, copy the
// registration to the legacy directory so that clients not yet
// upgraded to use GCD can see the new host.
//
// This is an ugly hack for multiple reasons:
//
// 1. It completely ignores |this.legacyImpl_|, complicating
// unit tests.
//
// 2. It relies on the fact that, when |hostClientId| is null,
// the legacy directory will "register" a host without
// creating a service account. This is an obsolete feature
// of the legacy directory that is being revived for a new
// purpose.
//
// 3. It assumes the device ID generated by GCD is usable as a
// host ID by the legacy directory. Fortunately both systems
// use UUIDs.
return remoting.LegacyHostListApi.registerWithHostId(
gcdRegResult.hostId, hostName, publicKey, null).then(
function() {
// On success, return the result from GCD, ignoring
// the result returned by the legacy directory.
that.gcdIds_.add(gcdRegResult.hostId);
that.legacyIds_.add(gcdRegResult.hostId);
return gcdRegResult;
},
function(error) {
console.warn(
'Error copying host GCD host registration ' +
'to legacy directory: ' + error);
throw error;
}
);
});
};
/** @override */
remoting.CombinedHostListApi.prototype.get = function() {
// Fetch the host list from both directories and merge hosts that
// have the same ID.
var that = this;
var legacyPromise = this.legacyImpl_.get();
var gcdPromise = this.gcdImpl_.get();
return legacyPromise.then(function(legacyHosts) {
// If GCD is too slow, just act as if it had returned an empty
// result set.
var timeoutPromise = base.Promise.withTimeout(
gcdPromise, GCD_TIMEOUT_MS, []);
// Combine host information from both directories. In the case of
// conflicting information, prefer information from whichever
// directory claims to have newer information.
return timeoutPromise.then(function(gcdHosts) {
// Update |that.gcdIds_| and |that.legacyIds_|.
that.gcdIds_ = new Set();
that.legacyIds_ = new Set();
gcdHosts.forEach(function(host) {
that.gcdIds_.add(host.hostId);
});
legacyHosts.forEach(function(host) {
that.legacyIds_.add(host.hostId);
});
/**
* A mapping from host IDs to the host data that will be
* returned from this method.
* @type {!Map<string,!remoting.Host>}
*/
var hostMap = new Map();
// Add legacy hosts to the output; some of these may be replaced
// by GCD hosts.
legacyHosts.forEach(function(host) {
hostMap.set(host.hostId, host);
});
// Add GCD hosts to the output, possibly replacing some legacy
// host data with newer data from GCD.
gcdHosts.forEach(function(gcdHost) {
var hostId = gcdHost.hostId;
var legacyHost = hostMap.get(hostId);
if (!legacyHost || legacyHost.updatedTime <= gcdHost.updatedTime) {
hostMap.set(hostId, gcdHost);
}
});
// Convert the result to an Array.
// TODO(jrw): Use Array.from once it becomes available.
var hosts = [];
hostMap.forEach(function(host) {
hosts.push(host);
});
return hosts;
});
});
};
/** @override */
remoting.CombinedHostListApi.prototype.put =
function(hostId, hostName, hostPublicKey) {
var legacyPromise = Promise.resolve();
if (this.legacyIds_.has(hostId)) {
legacyPromise = this.legacyImpl_.put(hostId, hostName, hostPublicKey);
}
var gcdPromise = Promise.resolve();
if (this.gcdIds_.has(hostId)) {
gcdPromise = this.gcdImpl_.put(hostId, hostName, hostPublicKey);
}
return legacyPromise.then(function() {
// If GCD is too slow, just ignore it and return result from the
// legacy directory.
return base.Promise.withTimeout(
gcdPromise, GCD_TIMEOUT_MS);
});
};
/** @override */
remoting.CombinedHostListApi.prototype.remove = function(hostId) {
var legacyPromise = Promise.resolve();
if (this.legacyIds_.has(hostId)) {
legacyPromise = this.legacyImpl_.remove(hostId);
}
var gcdPromise = Promise.resolve();
if (this.gcdIds_.has(hostId)) {
gcdPromise = this.gcdImpl_.remove(hostId);
}
return legacyPromise.then(function() {
// If GCD is too slow, just ignore it and return result from the
// legacy directory.
return base.Promise.withTimeout(
gcdPromise, GCD_TIMEOUT_MS);
});
};
/** @override */
remoting.CombinedHostListApi.prototype.getSupportHost = function(supportId) {
return this.legacyImpl_.getSupportHost(supportId);
};
})();
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Unit tests for combined_host_list_api.js.
*/
(function() {
'use strict';
/** @type {!remoting.MockHostListApi} */
var mockGcdApi;
/** @type {!remoting.MockHostListApi} */
var mockLegacyApi;
/** @type {!remoting.CombinedHostListApi} */
var combinedApi;
/** @type {sinon.TestStub} */
var registerWithHostIdStub;
/** @type {!remoting.Host} */
var commonHostGcd;
/** @type {!remoting.Host} */
var commonHostLegacy;
QUnit.module('CombinedHostListApi', {
beforeEach: function(/** QUnit.Assert */ assert) {
remoting.settings = new remoting.Settings();
remoting.settings['USE_GCD'] = true;
remoting.mockIdentity.setAccessToken(
remoting.MockIdentity.AccessToken.VALID);
mockGcdApi = new remoting.MockHostListApi();
mockGcdApi.addMockHost('gcd-host');
commonHostGcd = mockGcdApi.addMockHost('common-host');
commonHostGcd.hostName = 'common-host-gcd';
mockLegacyApi = new remoting.MockHostListApi();
mockLegacyApi.addMockHost('legacy-host');
commonHostLegacy = mockLegacyApi.addMockHost('common-host');
commonHostLegacy.hostName = 'common-host-legacy';
combinedApi = new remoting.CombinedHostListApi(mockLegacyApi, mockGcdApi);
registerWithHostIdStub =
sinon.stub(remoting.LegacyHostListApi, 'registerWithHostId');
},
afterEach: function(/** QUnit.Assert */ assert) {
remoting.settings = null;
registerWithHostIdStub.restore();
}
});
QUnit.test('register', function(/** QUnit.Assert */ assert) {
registerWithHostIdStub.returns(Promise.resolve());
mockGcdApi.authCodeFromRegister = '<fake_auth_code>';
mockGcdApi.emailFromRegister = '<fake_email>';
mockGcdApi.hostIdFromRegister = '<fake_host_id>';
mockLegacyApi.authCodeFromRegister = '<wrong_fake_auth_code>';
mockLegacyApi.emailFromRegister = '<wrong_fake_email>';
mockLegacyApi.hostIdFromRegister = '<wrong_fake_host_id>';
return combinedApi.register('', '', '').then(function(regResult) {
assert.equal(regResult.authCode, '<fake_auth_code>');
assert.equal(regResult.email, '<fake_email>');
assert.equal(regResult.hostId, '<fake_host_id>');
});
});
QUnit.test('get', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function(hosts) {
assert.equal(hosts.length, 3);
var hostIds = new Set();
hosts.forEach(function(host) {
hostIds.add(host.hostId);
if (host.hostId == 'common-host') {
assert.equal(host.hostName, 'common-host-gcd');
};
});
assert.ok(hostIds.has('gcd-host'));
assert.ok(hostIds.has('legacy-host'));
assert.ok(hostIds.has('common-host'));
});
});
QUnit.test('get w/ GCD newer', function(/** QUnit.Assert */ assert) {
commonHostGcd.updatedTime = '1970-01-02';
commonHostLegacy.updatedTime = '1970-01-01';
return combinedApi.get().then(function(hosts) {
hosts.forEach(function(host) {
if (host.hostId == 'common-host') {
assert.equal(host.hostName, 'common-host-gcd');
};
});
});
});
QUnit.test('get w/ legacy newer', function(/** QUnit.Assert */ assert) {
commonHostGcd.updatedTime = '1970-01-01';
commonHostLegacy.updatedTime = '1970-01-02';
return combinedApi.get().then(function(hosts) {
hosts.forEach(function(host) {
if (host.hostId == 'common-host') {
assert.equal(host.hostName, 'common-host-legacy');
};
});
});
});
QUnit.test('put to legacy', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.put('legacy-host', 'new host name', '').then(
function() {
assert.equal(mockLegacyApi.hosts[0].hostName,
'new host name');
});
});
});
QUnit.test('put to GCD', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.put('gcd-host', 'new host name', '').then(
function() {
assert.equal(mockGcdApi.hosts[0].hostName,
'new host name');
});
});
});
QUnit.test('put to both', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.put('common-host', 'new host name', '').then(
function() {
assert.equal(mockGcdApi.hosts[1].hostName,
'new host name');
assert.equal(mockLegacyApi.hosts[1].hostName,
'new host name');
});
});
});
QUnit.test('remove from legacy', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.remove('legacy-host').then(function() {
assert.equal(mockGcdApi.hosts.length, 2);
assert.equal(mockLegacyApi.hosts.length, 1);
assert.notEqual(mockLegacyApi.hosts[0].hostId, 'legacy-host');
});
});
});
QUnit.test('remove from gcd', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.remove('gcd-host').then(function() {
assert.equal(mockLegacyApi.hosts.length, 2);
assert.equal(mockGcdApi.hosts.length, 1);
assert.notEqual(mockGcdApi.hosts[0].hostId, 'gcd-host');
});
});
});
QUnit.test('remove from both', function(/** QUnit.Assert */ assert) {
return combinedApi.get().then(function() {
return combinedApi.remove('common-host').then(function() {
assert.equal(mockGcdApi.hosts.length, 1);
assert.equal(mockLegacyApi.hosts.length, 1);
assert.notEqual(mockGcdApi.hosts[0].hostId, 'common-host');
assert.notEqual(mockLegacyApi.hosts[0].hostId, 'common-host');
});
});
});
})();
\ No newline at end of file
...@@ -352,7 +352,7 @@ remoting.HostController.prototype.updatePin = function(newPin, onDone, ...@@ -352,7 +352,7 @@ remoting.HostController.prototype.updatePin = function(newPin, onDone,
return; return;
} }
/** @type {string} */ /** @type {string} */
var hostId = config['host_id']; var hostId = base.getStringAttr(config, 'host_id');
that.hostDaemonFacade_.getPinHash(hostId, newPin).then( that.hostDaemonFacade_.getPinHash(hostId, newPin).then(
updateDaemonConfigWithHash, remoting.Error.handler(onError)); updateDaemonConfigWithHash, remoting.Error.handler(onError));
} }
......
...@@ -80,9 +80,13 @@ var instance = null; ...@@ -80,9 +80,13 @@ var instance = null;
*/ */
remoting.HostListApi.getInstance = function() { remoting.HostListApi.getInstance = function() {
if (instance == null) { if (instance == null) {
instance = remoting.settings.USE_GCD ? if (remoting.settings.USE_GCD) {
new remoting.GcdHostListApi() : var gcdInstance = new remoting.GcdHostListApi();
new remoting.LegacyHostListApi(); var legacyInstance = new remoting.LegacyHostListApi();
instance = new remoting.CombinedHostListApi(legacyInstance, gcdInstance);
} else {
instance = new remoting.LegacyHostListApi();
}
} }
return instance; return instance;
}; };
......
...@@ -25,6 +25,22 @@ remoting.LegacyHostListApi = function() { ...@@ -25,6 +25,22 @@ remoting.LegacyHostListApi = function() {
remoting.LegacyHostListApi.prototype.register = function( remoting.LegacyHostListApi.prototype.register = function(
hostName, publicKey, hostClientId) { hostName, publicKey, hostClientId) {
var newHostId = base.generateUuid(); var newHostId = base.generateUuid();
return remoting.LegacyHostListApi.registerWithHostId(
newHostId, hostName, publicKey, hostClientId);
};
/**
* Registers a host with the Chromoting directory using a specified
* host ID, which should not be equal to the ID of any existing host.
*
* @param {string} newHostId The host ID of the new host.
* @param {string} hostName The user-visible name of the new host.
* @param {string} publicKey The public half of the host's key pair.
* @param {?string} hostClientId The OAuth2 client ID of the host.
* @return {!Promise<remoting.HostListApi.RegisterResult>}
*/
remoting.LegacyHostListApi.registerWithHostId = function(
newHostId, hostName, publicKey, hostClientId) {
var newHostDetails = { data: { var newHostDetails = { data: {
hostId: newHostId, hostId: newHostId,
hostName: hostName, hostName: hostName,
...@@ -44,7 +60,9 @@ remoting.LegacyHostListApi.prototype.register = function( ...@@ -44,7 +60,9 @@ remoting.LegacyHostListApi.prototype.register = function(
if (response.status == 200) { if (response.status == 200) {
var result = /** @type {!Object} */ (response.getJson()); var result = /** @type {!Object} */ (response.getJson());
var data = base.getObjectAttr(result, 'data'); var data = base.getObjectAttr(result, 'data');
var authCode = base.getStringAttr(data, 'authorizationCode'); var authCode = hostClientId ?
base.getStringAttr(data, 'authorizationCode') :
'';
return { return {
authCode: authCode, authCode: authCode,
email: '', email: '',
......
...@@ -66,7 +66,7 @@ QUnit.test('register', function(assert) { ...@@ -66,7 +66,7 @@ QUnit.test('register', function(assert) {
FAKE_HOST_NAME, FAKE_HOST_NAME,
FAKE_PUBLIC_KEY, FAKE_PUBLIC_KEY,
FAKE_HOST_CLIENT_ID FAKE_HOST_CLIENT_ID
). then(function(regResult) { ).then(function(regResult) {
assert.equal(regResult.authCode, FAKE_AUTH_CODE); assert.equal(regResult.authCode, FAKE_AUTH_CODE);
assert.equal(regResult.email, ''); assert.equal(regResult.email, '');
}); });
......
...@@ -32,32 +32,25 @@ remoting.MockHostListApi = function() { ...@@ -32,32 +32,25 @@ remoting.MockHostListApi = function() {
this.emailFromRegister = null; this.emailFromRegister = null;
/** /**
* This host ID to return from register(), or null if it should fail. * The host ID to return from register(), or null if it should fail.
* @type {?string} * @type {?string}
*/ */
this.hostIdFromRegister = null; this.hostIdFromRegister = null;
/** @type {Array<remoting.Host>} */ /** @type {!Array<!remoting.Host>} */
this.hosts = [ this.hosts = [];
{ };
'hostName': 'Online host',
'hostId': 'online-host-id', /**
'status': 'ONLINE', * Creates and adds a new mock host.
'jabberId': 'online-jid', *
'publicKey': 'online-public-key', * @param {string} hostId The ID of the new host to add.
'tokenUrlPatterns': [], * @return {!remoting.Host} the new mock host
'updatedTime': new Date().toISOString() */
}, remoting.MockHostListApi.prototype.addMockHost = function(hostId) {
{ var newHost = new remoting.Host(hostId);
'hostName': 'Offline host', this.hosts.push(newHost);
'hostId': 'offline-host-id', return newHost;
'status': 'OFFLINE',
'jabberId': 'offline-jid',
'publicKey': 'offline-public-key',
'tokenUrlPatterns': [],
'updatedTime': new Date(1970, 1, 1).toISOString()
}
];
}; };
/** @override */ /** @override */
...@@ -79,11 +72,7 @@ remoting.MockHostListApi.prototype.register = function( ...@@ -79,11 +72,7 @@ remoting.MockHostListApi.prototype.register = function(
/** @override */ /** @override */
remoting.MockHostListApi.prototype.get = function() { remoting.MockHostListApi.prototype.get = function() {
var that = this; return Promise.resolve(this.hosts);
return new Promise(function(resolve, reject) {
remoting.mockIdentity.validateTokenAndCall(
resolve, remoting.Error.handler(reject), [that.hosts]);
});
}; };
/** /**
...@@ -97,22 +86,19 @@ remoting.MockHostListApi.prototype.put = ...@@ -97,22 +86,19 @@ remoting.MockHostListApi.prototype.put =
/** @type {remoting.MockHostListApi} */ /** @type {remoting.MockHostListApi} */
var that = this; var that = this;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var onTokenValid = function() { for (var i = 0; i < that.hosts.length; ++i) {
for (var i = 0; i < that.hosts.length; ++i) { /** type {remoting.Host} */
/** type {remoting.Host} */ var host = that.hosts[i];
var host = that.hosts[i]; if (host.hostId == hostId) {
if (host.hostId == hostId) { host.hostName = hostName;
host.hostName = hostName; host.hostPublicKey = hostPublicKey;
host.hostPublicKey = hostPublicKey; resolve(undefined);
resolve(undefined); return;
return;
}
} }
console.error('PUT request for unknown host: ' + hostId + }
' (' + hostName + ')'); console.error('PUT request for unknown host: ' + hostId +
reject(remoting.Error.unexpected()); ' (' + hostName + ')');
}; reject(remoting.Error.unexpected());
remoting.mockIdentity.validateTokenAndCall(onTokenValid, reject, []);
}); });
}; };
...@@ -124,30 +110,22 @@ remoting.MockHostListApi.prototype.remove = function(hostId) { ...@@ -124,30 +110,22 @@ remoting.MockHostListApi.prototype.remove = function(hostId) {
/** @type {remoting.MockHostListApi} */ /** @type {remoting.MockHostListApi} */
var that = this; var that = this;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var onTokenValid = function() { for (var i = 0; i < that.hosts.length; ++i) {
for (var i = 0; i < that.hosts.length; ++i) { var host = that.hosts[i];
var host = that.hosts[i]; if (host.hostId == hostId) {
if (host.hostId == hostId) { that.hosts.splice(i, 1);
that.hosts.splice(i, 1); resolve(undefined);
resolve(undefined); return;
return;
}
} }
console.error('DELETE request for unknown host: ' + hostId); }
reject(remoting.Error.unexpected()); console.error('DELETE request for unknown host: ' + hostId);
}; reject(remoting.Error.unexpected());
remoting.mockIdentity.validateTokenAndCall(onTokenValid, reject, []);
}); });
}; };
/** @override */ /** @override */
remoting.MockHostListApi.prototype.getSupportHost = function(supportId) { remoting.MockHostListApi.prototype.getSupportHost = function(supportId) {
var that = this; return Promise.resolve(this.hosts[0]);
return new Promise(function(resolve, reject) {
remoting.mockIdentity.validateTokenAndCall(
resolve, remoting.Error.handler(reject), [that.hosts[0]]);
});
}; };
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment