Adds methods to the RPC Interface to interact with groups. Adds tests for those methods

This commit is contained in:
Zachary Boyd 2018-09-11 02:48:56 -04:00
parent 99c59541e0
commit 998185d31e
9 changed files with 227 additions and 43 deletions

View file

@ -6,15 +6,43 @@ The following functions are available via the RPC
Returns an array containing information on the instances currently running under the router. Returns an array containing information on the instances currently running under the router.
## queryInstanceByName(String) ## queryInstanceByName(instance_name: String)
Returns information on an instance identified by name Returns information on an instance identified by name
## queryInstanceAt(Integer) ## queryInstancesByGroup(instance_name: String)
Returns information on an instance identified by index Returns an array containing information on the instances within a group.
## createInstances(Array or Integer) ## queryInstanceAt(instance_index: Integer)
Returns information on an instance identified by index.
## queryInstanceNames()
Returns a list of all instance names.
## queryGroupNames()
Returns a list of all instance groups.
## addInstanceToGroupByName(group: String, instance_name: String)
Adds an instance, identified by name, to a group
## addInstanceToGroupAt(group: String, instance_index: Integer)
Adds an instance, identified by index, to a group
## removeInstanceFromGroupByName(group: String, instance_name: String)
Removes an instance, identified by name, from a group
## removeInstanceFromGroupAt(group: String, instance_index: Integer)
Removes an instance, identified by index, from a group
## createInstances(instances: Array or Integer)
If passed an Integer, creates that many Tor instances. An array can also be passed describing the names, weights and configurations of prospective instances. : If passed an Integer, creates that many Tor instances. An array can also be passed describing the names, weights and configurations of prospective instances. :
@ -38,19 +66,19 @@ var rpcRequest = {
Will wait until the Tor Instance has fully connected to the network before returning Will wait until the Tor Instance has fully connected to the network before returning
## addInstances(Array) ## addInstances(instances: Array)
Serves the same purpose as "createInstances" but only takes an Array Serves the same purpose as "createInstances" but only takes an Array
## removeInstances(Integer) ## removeInstances(instances: Integer)
Removes a number of instances Removes a number of instances
## removeInstanceAt(Integer) ## removeInstanceAt(instance_index: Integer)
Remove a specific instance from the pool by its index Remove a specific instance from the pool by its index
## removeInstanceByName(String) ## removeInstanceByName(instance_name: String)
Remove a specific instance from the pool by its name Remove a specific instance from the pool by its name
@ -58,14 +86,18 @@ Remove a specific instance from the pool by its name
Get new identites for all instances Get new identites for all instances
## newIdentityAt(Integer) ## newIdentityAt(instance_index: Integer)
Get a new identity for a specific instance by its index Get a new identity for a specific instance by its index
## newIdentityByName(String) ## newIdentityByName(instance_name: String)
Get a new identity for a specific instance by its name Get a new identity for a specific instance by its name
## newIdentitiesByGroup(group: String)
Get new identities for all instances in a group
## nextInstance() ## nextInstance()
Cycle to the next instance using the load balancing method Cycle to the next instance using the load balancing method
@ -74,15 +106,19 @@ Cycle to the next instance using the load balancing method
Shutdown all Tor instances Shutdown all Tor instances
## setTorConfig(Object) ## setTorConfig(config: Object)
Applies the configuration to all active instances Apples the provided configuration to all instances using the control protocol. Changes will be applied immediately.
## setTorConfigByGroup(group: String, config: Object)
Apples the provided configuration to all instances in a group using the control protocol. Changes will be applied immediately.
## getDefaultTorConfig() ## getDefaultTorConfig()
Retrieve the default Tor Config for all future instances Retrieve the default Tor Config for all future instances
## setDefaultTorConfig(Object) ## setDefaultTorConfig(config: Object)
Set the default Tor Config for all future instances Set the default Tor Config for all future instances
@ -90,11 +126,11 @@ Set the default Tor Config for all future instances
Get the current load balance method Get the current load balance method
## setLoadBalanceMethod(String) ## setLoadBalanceMethod(load_balance_method: String)
Set the current load balance method Set the current load balance method
## getInstanceConfigAt(Integer: index, String: keyword) ## getInstanceConfigAt(instance_index: Integer, keyword: String)
Retrieves the current value of an option set in the configuration by the index of the instance using the control protocol. Retrieves the current value of an option set in the configuration by the index of the instance using the control protocol.
@ -111,28 +147,32 @@ var rpcRequest = {
}; };
``` ```
## getInstanceConfigByName(String: name, String: keyword) ## getInstanceConfigByName(name: String, keyword: String)
Works the same way as `getInstanceConfigAt` except takes an instance name instead of an index Works the same way as `getInstanceConfigAt` except takes an instance name instead of an index
## setInstanceConfigAt(Integer: index, String: keyword, String: value) ## setInstanceConfigAt(index: Integer, keyword: String, value: String)
Sets the value in the configuration of an instance using the control protocol. Changes will be applied immediately. Sets the value in the configuration of an instance using the control protocol. Changes will be applied immediately.
## setInstanceConfigByName(Integer: index, String: keyword, String: value) ## setInstanceConfigByName(index: Integer, keyword: String, value: String)
Works the same way as `setInstanceConfigAt` except takes an instance name instead of an index Works the same way as `setInstanceConfigAt` except takes an instance name instead of an index
## signalAllInstances(String) ## signalAllInstances(signal: String)
Sends a signal using the control protocol to all instances Sends a signal using the control protocol to all instances
A list of all signals can be [found here](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt) A list of all signals can be [found here](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt)
## signalInstanceAt(Integer: index, String: signal) ## signalInstancesByGroup(group: String, signal: String)
Sends a signal using the control protocol to all instances in a group
## signalInstanceAt(index: Integer, signal: String)
Sends a signal using the control protocol to an instance identified by its index Sends a signal using the control protocol to an instance identified by its index
## signalInstanceByName(String: name, String: signal) ## signalInstanceByName(name: String, signal: String)
Sends a signal using the control protocol to an instance identified by its name Sends a signal using the control protocol to an instance identified by its name

6
package-lock.json generated
View file

@ -1085,9 +1085,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"nanoid": { "nanoid": {
"version": "1.0.2", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.2.3.tgz",
"integrity": "sha512-sCTwJt690lduNHyqknXJp8pRwzm80neOLGaiTHU2KUJZFVSErl778NNCIivEQCX5gNT0xR1Jy3HEMe/TABT6lw==" "integrity": "sha512-BAnxAdaihzMoszwhqRy8FPOX+dijs7esUEUYTIQ1KsOSKmCVNYnitAMmBDFxYzA6VQYvuUKw7o2K1AcMBTGzIg=="
}, },
"native-dns": { "native-dns": {
"version": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb", "version": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb",

View file

@ -29,7 +29,7 @@
"jrpc2": "git+https://github.com/znetstar/jrpc2.git#f1521bd3f2fa73d716e74bf8f746d08d5e03e7d7", "jrpc2": "git+https://github.com/znetstar/jrpc2.git#f1521bd3f2fa73d716e74bf8f746d08d5e03e7d7",
"js-weighted-list": "^0.1.1", "js-weighted-list": "^0.1.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"nanoid": "^1.0.2", "nanoid": "^1.2.3",
"native-dns": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb", "native-dns": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb",
"nconf": "^0.10.0", "nconf": "^0.10.0",
"shelljs": "^0.8.2", "shelljs": "^0.8.2",

View file

@ -53,6 +53,12 @@ class ControlServer {
return instance_info(this.torPool.instance_at(index)); return instance_info(this.torPool.instance_at(index));
}).bind(this)); }).bind(this));
server.expose('queryInstanceNames', (() => this.torPool.instance_names).bind(this));
server.expose('queryGroupNames', (() => Array.from(this.torPool.group_names)).bind(this));
server.expose('queryInstancesByGroup', ((group) => this.torPool.instances_by_group(group).map(instance_info)).bind(this));
server.expose('createInstances', (async (num) => { server.expose('createInstances', (async (num) => {
let instances = await this.torPool.create(num); let instances = await this.torPool.create(num);
@ -77,6 +83,8 @@ class ControlServer {
server.expose('newIdentityByName', this.torPool.new_identity_by_name.bind(this.torPool)); server.expose('newIdentityByName', this.torPool.new_identity_by_name.bind(this.torPool));
server.expose('newIdentitiesByGroup', (async (group) => await this.torPool.new_identites_by_group(group)).bind(this));
server.expose('nextInstance', (async () => instance_info( await this.torPool.next() )).bind(this)); server.expose('nextInstance', (async () => instance_info( await this.torPool.next() )).bind(this));
server.expose('closeInstances', (async () => this.torPool.exit()).bind(this)); server.expose('closeInstances', (async () => this.torPool.exit()).bind(this));
@ -97,6 +105,14 @@ class ControlServer {
})); }));
}).bind(this)); }).bind(this));
server.expose('setTorConfigByGroup', (async (group, config) => {
await Promise.all(Object.keys(config).map((key) => {
let value = config[key];
return this.torPool.set_config_by_group(group, key, value);
}));
}).bind(this));
server.expose('getLoadBalanceMethod', (async () => { server.expose('getLoadBalanceMethod', (async () => {
return this.torPool.load_balance_method; return this.torPool.load_balance_method;
}).bind(this)); }).bind(this));
@ -118,6 +134,16 @@ class ControlServer {
server.expose('signalInstanceAt', this.torPool.signal_at.bind(this.torPool)); server.expose('signalInstanceAt', this.torPool.signal_at.bind(this.torPool));
server.expose('signalInstanceByName', this.torPool.signal_by_name.bind(this.torPool)); server.expose('signalInstanceByName', this.torPool.signal_by_name.bind(this.torPool));
server.expose('signalInstancesByGroup', (async (group, signal) => await this.torPool.signal_by_group(group, signal)).bind(this));
server.expose('addInstanceToGroupByName', ((group, instance_name) => this.torPool.add_instance_to_group_by_name(group, instance_name)).bind(this));
server.expose('addInstanceToGroupAt', ((group, instance_index) => this.torPool.add_instance_to_group_at(group, instance_index)).bind(this));
server.expose('removeInstanceFromGroupByName', ((group, instance_name) => this.torPool.remove_instance_from_group_by_name(group, instance_name)).bind(this));
server.expose('removeInstanceFromGroupAt', ((group, instance_index) => this.torPool.remove_instance_from_group_at(group, instance_index)).bind(this));
} }
async listenTcp(port, hostname) { async listenTcp(port, hostname) {

View file

@ -23,8 +23,6 @@ const fs = require('fs');
const { EventEmitter } = require('eventemitter3'); const { EventEmitter } = require('eventemitter3');
const Promise = require("bluebird"); const Promise = require("bluebird");
const _ = require('lodash'); const _ = require('lodash');
const nanoid = require('nanoid');
const WeightedList = require('js-weighted-list'); const WeightedList = require('js-weighted-list');
const TorProcess = require('./TorProcess'); const TorProcess = require('./TorProcess');
@ -46,7 +44,7 @@ class TorPool extends EventEmitter {
} }
get group_names() { get group_names() {
return new Set(_.uniq(_.flatten(this.instances.map((instance) => instance.instance_group).filter(Boolean))).sort()); return new Set(_.flatten(this.instances.map((instance) => instance.instance_group).filter(Boolean)));
} }
instances_by_group(group_name) { instances_by_group(group_name) {
@ -203,13 +201,10 @@ class TorPool extends EventEmitter {
this._instances._weighted_list = void(0); this._instances._weighted_list = void(0);
instance_definition.Config = _.extend(_.cloneDeep(this.default_tor_config), (instance_definition.Config || {})); instance_definition.Config = _.extend(_.cloneDeep(this.default_tor_config), (instance_definition.Config || {}));
let instance_id = nanoid();
instance_definition.Config.DataDirectory = instance_definition.Config.DataDirectory || path.join(this.data_directory, (instance_definition.Name || instance_id)); instance_definition.Config.DataDirectory = instance_definition.Config.DataDirectory || path.join(this.data_directory, (instance_definition.Name || instance_id));
let instance = new TorProcess(this.tor_path, instance_definition, this.granax_options, this.logger); let instance = new TorProcess(this.tor_path, instance_definition, this.granax_options, this.logger);
instance.id = instance_id;
await instance.create(); await instance.create();
this._instances.push(instance); this._instances.push(instance);

View file

@ -13,6 +13,7 @@ const getPort = require('get-port');
const del = require('del'); const del = require('del');
const temp = require('temp'); const temp = require('temp');
const { TorController } = require('granax'); const { TorController } = require('granax');
const nanoid = require("nanoid");
Promise.promisifyAll(temp); Promise.promisifyAll(temp);
Promise.promisifyAll(fs); Promise.promisifyAll(fs);
@ -31,6 +32,7 @@ class TorProcess extends EventEmitter {
this.tor_path = tor_path; this.tor_path = tor_path;
this.granax_options = granax_options; this.granax_options = granax_options;
this.control_password = crypto.randomBytes(128).toString('base64'); this.control_password = crypto.randomBytes(128).toString('base64');
this._id = nanoid();
this.tor_config.DataDirectory = this.tor_config.DataDirectory || temp.mkdirSync(); this.tor_config.DataDirectory = this.tor_config.DataDirectory || temp.mkdirSync();
} }
@ -41,17 +43,20 @@ class TorProcess extends EventEmitter {
resolve(); resolve();
}); });
}); });
this.process.kill('SIGINT'); this.process.kill('SIGINT');
await p; await p;
} }
get id() { return this._id; }
get instance_group() { get instance_group() {
return (this.definition && this.definition.Group) || null; return (this.definition && this.definition.Group) || null;
} }
get instance_name() { get instance_name() {
return (this.definition && this.definition.Name) || this.process.pid; return (this.definition && this.definition.Name) || this.id;
} }
get definition() { return this._definition; } get definition() { return this._definition; }

View file

@ -93,7 +93,7 @@ async function main(nconf, logger) {
let thereWasAnExitError = false; let thereWasAnExitError = false;
let { handleError } = this; let { handleError } = this;
try { try {
await Promise.all(control.torPool.instances.map((instance) => instance.exit())); await control.torPool.exit();
} catch (exitError) { } catch (exitError) {
logger.error(`[global]: error closing tor instances: ${exitError.message}`); logger.error(`[global]: error closing tor instances: ${exitError.message}`);
thereWasAnExitError = true; thereWasAnExitError = true;

View file

@ -26,7 +26,39 @@ describe('ControlServer - RPC Interface', function () {
describe('#createInstances(number_of_instances)', function () { describe('#createInstances(number_of_instances)', function () {
this.timeout(WAIT_FOR_CREATE*2); this.timeout(WAIT_FOR_CREATE*2);
it('should create an instance', async function () { it('should create an instance', async function () {
await rpcClient.invokeAsync('createInstances', [2]); await rpcClient.invokeAsync('createInstances', [{ Name: 'instance-1', Group: "foo" }]);
});
});
describe('#queryInstanceNames()', function () {
it("should have an instance named \"instance-1\"", async function () {
let raw = await rpcClient.invokeAsync('queryInstanceNames', [ ]);
let instances = JSON.parse(raw).result;
assert.deepEqual(instances, [ 'instance-1' ]);
});
});
describe('#queryGroupNames()', function () {
it("should have a group named \"foo\"", async function () {
let raw = await rpcClient.invokeAsync('queryGroupNames', [ ]);
let groups = JSON.parse(raw).result;
assert.deepEqual(groups, [ 'foo' ]);
});
});
describe('#queryInstancesByGroup()', function () {
it("should return an instance named \"instance-1\"", async function () {
let raw = await rpcClient.invokeAsync('queryInstancesByGroup', [ 'foo' ]);
let instances = JSON.parse(raw).result;
assert.equal(instances.length, 1);
assert.ok(instances[0]);
assert.equal(instances[0].name, 'instance-1');
}); });
}); });
@ -46,14 +78,15 @@ describe('ControlServer - RPC Interface', function () {
describe('#addInstances(definitions)', function () { describe('#addInstances(definitions)', function () {
this.timeout(WAIT_FOR_CREATE); this.timeout(WAIT_FOR_CREATE);
it("should add an instance based on a defintion", async function () { it("should add an instance based on a defintion", async function () {
var def = { let def = {
Name: 'instance-1' Name: 'instance-2',
Group: 'bar'
}; };
await rpcClient.invokeAsync('addInstances', [ [ def ] ]); await rpcClient.invokeAsync('addInstances', [ def ]);
}); });
it("tor pool should now contain and instance that has the same name as the name specified in the defintion", function () { it("tor pool should now contain and instance that has the same name as the name specified in the defintion", function () {
assert.ok(rpcControlServer.torPool.instance_by_name('instance-1')); assert.ok(rpcControlServer.torPool.instance_by_name('instance-2'));
}); });
}); });
@ -79,6 +112,63 @@ describe('ControlServer - RPC Interface', function () {
}); });
}); });
describe('#addInstanceToGroupByName()', function () {
it(`should add "instance-1" to baz`, async function () {
await rpcClient.invokeAsync('addInstanceToGroupByName', [ 'baz', "instance-1" ]);
});
it('"instance-1" should be added to "baz"', function () {
assert.include(rpcControlServer.torPool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
after('remove from group', function () {
rpcControlServer.torPool.groups['baz'].remove_by_name('instance-1');
});
});
describe('#addInstanceToGroupAt()', function () {
it(`should add "instance-1" to baz`, async function () {
await rpcClient.invokeAsync('addInstanceToGroupAt', [ 'baz', 0 ]);
});
it('"instance-1" should be added to "baz"', function () {
assert.include(rpcControlServer.torPool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
after('remove from group', function () {
rpcControlServer.torPool.groups['baz'].remove_by_name('instance-1');
});
});
describe('#removeInstanceFromGroupByName()', function () {
before('add to group', function () {
rpcControlServer.torPool.groups['baz'].add_by_name('instance-1');
});
it(`should remove "instance-1" from baz`, async function () {
await rpcClient.invokeAsync('removeInstanceFromGroupByName', [ 'baz', "instance-1" ]);
});
it('"instance-1" should be remove from to "baz"', function () {
assert.notInclude(rpcControlServer.torPool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
});
describe('#removeInstanceFromGroupAt()', function () {
before('add to group', function () {
rpcControlServer.torPool.groups['baz'].add_by_name('instance-1');
});
it(`should remove "instance-1" from baz`, async function () {
await rpcClient.invokeAsync('removeInstanceFromGroupAt', [ 'baz', 0 ]);
});
it('"instance-1" should be remove from to "baz"', function () {
assert.notInclude(rpcControlServer.torPool.instances_by_group('baz').map((i) => i.instance_name), "instance-1");
});
});
describe('#newIdentites()', function () { describe('#newIdentites()', function () {
this.timeout(3000); this.timeout(3000);
it('should request new identities for all instances', async function () { it('should request new identities for all instances', async function () {
@ -100,6 +190,13 @@ describe('ControlServer - RPC Interface', function () {
}); });
}); });
describe('#newIdentitiesByGroup()', function () {
it(`should get new identites for all instances in group`, async function () {
await rpcClient.invokeAsync('newIdentitiesByGroup', [ 'foo' ]);
});
});
describe("#setTorConfig(config_object)", function () { describe("#setTorConfig(config_object)", function () {
this.timeout(3000); this.timeout(3000);
it('should set several config variables on all instances', async function () { it('should set several config variables on all instances', async function () {
@ -145,6 +242,22 @@ describe('ControlServer - RPC Interface', function () {
}); });
}); });
describe('#setTorConfigByGroup()', function () {
it(`should set the config value on all instances`, async function () {
await rpcClient.invokeAsync('setTorConfigByGroup', [ 'foo', { 'ProtocolWarnings': 1 } ]);
});
it('all instances should have the config value set', async function () {
let values = _.flatten(await Promise.all(rpcControlServer.torPool.instances_by_group('foo').map((i) => i.get_config('ProtocolWarnings'))));
assert.isTrue(values.every((v) => v === "1"));
});
after('unset config values', async function () {
rpcControlServer.torPool.set_config_by_group('foo', 'ProtocolWarnings', 0);
});
});
describe('#getDefaultTorConfig()', function () { describe('#getDefaultTorConfig()', function () {
before('set tor config', function () { before('set tor config', function () {
nconf.set('torConfig', { TestSocks: 1 }); nconf.set('torConfig', { TestSocks: 1 });
@ -295,6 +408,12 @@ describe('ControlServer - RPC Interface', function () {
}); });
}); });
describe('#signalInstancesByGroup()', function () {
it(`should get new identites for all instances in group`, async function () {
await rpcClient.invokeAsync('signalInstancesByGroup', [ 'foo', 'DEBUG' ]);
});
});
describe("#nextInstance()", function () { describe("#nextInstance()", function () {
this.timeout(3000); this.timeout(3000);
let instance_name; let instance_name;
@ -341,7 +460,6 @@ describe('ControlServer - RPC Interface', function () {
it('no instances should be present in the pool', function () { it('no instances should be present in the pool', function () {
assert.equal(rpcControlServer.torPool.instances.length, 0); assert.equal(rpcControlServer.torPool.instances.length, 0);
assert.notEqual(rpcControlServer.torPool.instances.length, instance_num);
}); });
}); });

View file

@ -248,7 +248,7 @@ describe('TorPool', function () {
await torPool.remove_by_name('instance-3'); await torPool.remove_by_name('instance-3');
assert.notIncludeDeepOrderedMembers(torPool.instance_names, [ "instance-3" ]); assert.notInclude(torPool.instance_names, "instance-3");
}); });
after('shutdown tor pool', async function () { await torPool.exit(); }); after('shutdown tor pool', async function () { await torPool.exit(); });
@ -672,7 +672,7 @@ describe('TorPool', function () {
}); });
it('the instance should be no longer be in the group', function () { it('the instance should be no longer be in the group', function () {
assert.notIncludeOrderedMembers(instance.instance_group, ["foo"]); assert.notInclude(instance.instance_group, "foo");
}); });
after('shutdown tor pool', async function () { await tor_pool.exit(); }); after('shutdown tor pool', async function () { await tor_pool.exit(); });
@ -693,7 +693,7 @@ describe('TorPool', function () {
}); });
it('the instance should no longer be in the group', function () { it('the instance should no longer be in the group', function () {
assert.notIncludeOrderedMembers(instance.instance_group, ["foo"]); assert.notInclude(instance.instance_group, "foo");
}); });
after('shutdown tor pool', async function () { await tor_pool.exit(); }); after('shutdown tor pool', async function () { await tor_pool.exit(); });
@ -714,7 +714,7 @@ describe('TorPool', function () {
}); });
it('the instance should no longer be in the group', function () { it('the instance should no longer be in the group', function () {
assert.notIncludeOrderedMembers(instance.instance_group, ["foo"]); assert.notInclude(instance.instance_group, "foo");
}); });
after('shutdown tor pool', async function () { await tor_pool.exit(); }); after('shutdown tor pool', async function () { await tor_pool.exit(); });