diff --git a/docs/rpc-methods.md b/docs/rpc-methods.md index 9653d7e..4e42feb 100644 --- a/docs/rpc-methods.md +++ b/docs/rpc-methods.md @@ -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. -## queryInstanceByName(String) +## queryInstanceByName(instance_name: String) 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. : @@ -38,19 +66,19 @@ var rpcRequest = { 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 -## removeInstances(Integer) +## removeInstances(instances: Integer) Removes a number of instances -## removeInstanceAt(Integer) +## removeInstanceAt(instance_index: Integer) 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 @@ -58,14 +86,18 @@ Remove a specific instance from the pool by its name Get new identites for all instances -## newIdentityAt(Integer) +## newIdentityAt(instance_index: Integer) 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 +## newIdentitiesByGroup(group: String) + +Get new identities for all instances in a group + ## nextInstance() 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 -## 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() Retrieve the default Tor Config for all future instances -## setDefaultTorConfig(Object) +## setDefaultTorConfig(config: Object) 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 -## setLoadBalanceMethod(String) +## setLoadBalanceMethod(load_balance_method: String) 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. @@ -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 -## 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. -## 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 -## signalAllInstances(String) +## signalAllInstances(signal: String) 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) -## 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 -## signalInstanceByName(String: name, String: signal) +## signalInstanceByName(name: String, signal: String) Sends a signal using the control protocol to an instance identified by its name \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5ed2a50..ea5bba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1085,9 +1085,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nanoid": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.0.2.tgz", - "integrity": "sha512-sCTwJt690lduNHyqknXJp8pRwzm80neOLGaiTHU2KUJZFVSErl778NNCIivEQCX5gNT0xR1Jy3HEMe/TABT6lw==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.2.3.tgz", + "integrity": "sha512-BAnxAdaihzMoszwhqRy8FPOX+dijs7esUEUYTIQ1KsOSKmCVNYnitAMmBDFxYzA6VQYvuUKw7o2K1AcMBTGzIg==" }, "native-dns": { "version": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb", diff --git a/package.json b/package.json index 1a3dd7b..d7f188a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "jrpc2": "git+https://github.com/znetstar/jrpc2.git#f1521bd3f2fa73d716e74bf8f746d08d5e03e7d7", "js-weighted-list": "^0.1.1", "lodash": "^4.17.4", - "nanoid": "^1.0.2", + "nanoid": "^1.2.3", "native-dns": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb", "nconf": "^0.10.0", "shelljs": "^0.8.2", diff --git a/src/ControlServer.js b/src/ControlServer.js index d15e34c..fa683a2 100644 --- a/src/ControlServer.js +++ b/src/ControlServer.js @@ -53,6 +53,12 @@ class ControlServer { return instance_info(this.torPool.instance_at(index)); }).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) => { 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('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('closeInstances', (async () => this.torPool.exit()).bind(this)); @@ -97,6 +105,14 @@ class ControlServer { })); }).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 () => { return this.torPool.load_balance_method; }).bind(this)); @@ -118,6 +134,16 @@ class ControlServer { server.expose('signalInstanceAt', this.torPool.signal_at.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) { diff --git a/src/TorPool.js b/src/TorPool.js index 88286ba..86f2073 100644 --- a/src/TorPool.js +++ b/src/TorPool.js @@ -23,8 +23,6 @@ const fs = require('fs'); const { EventEmitter } = require('eventemitter3'); const Promise = require("bluebird"); const _ = require('lodash'); - -const nanoid = require('nanoid'); const WeightedList = require('js-weighted-list'); const TorProcess = require('./TorProcess'); @@ -46,7 +44,7 @@ class TorPool extends EventEmitter { } 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) { @@ -203,13 +201,10 @@ class TorPool extends EventEmitter { this._instances._weighted_list = void(0); 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)); let instance = new TorProcess(this.tor_path, instance_definition, this.granax_options, this.logger); - instance.id = instance_id; - await instance.create(); this._instances.push(instance); diff --git a/src/TorProcess.js b/src/TorProcess.js index c0ff1a1..ab27265 100644 --- a/src/TorProcess.js +++ b/src/TorProcess.js @@ -13,6 +13,7 @@ const getPort = require('get-port'); const del = require('del'); const temp = require('temp'); const { TorController } = require('granax'); +const nanoid = require("nanoid"); Promise.promisifyAll(temp); Promise.promisifyAll(fs); @@ -31,6 +32,7 @@ class TorProcess extends EventEmitter { this.tor_path = tor_path; this.granax_options = granax_options; this.control_password = crypto.randomBytes(128).toString('base64'); + this._id = nanoid(); this.tor_config.DataDirectory = this.tor_config.DataDirectory || temp.mkdirSync(); } @@ -41,17 +43,20 @@ class TorProcess extends EventEmitter { resolve(); }); }); + this.process.kill('SIGINT'); await p; } + get id() { return this._id; } + get instance_group() { return (this.definition && this.definition.Group) || null; } 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; } diff --git a/src/launch.js b/src/launch.js index 51a8331..49990ab 100644 --- a/src/launch.js +++ b/src/launch.js @@ -93,7 +93,7 @@ async function main(nconf, logger) { let thereWasAnExitError = false; let { handleError } = this; try { - await Promise.all(control.torPool.instances.map((instance) => instance.exit())); + await control.torPool.exit(); } catch (exitError) { logger.error(`[global]: error closing tor instances: ${exitError.message}`); thereWasAnExitError = true; diff --git a/test/RPCInterface.js b/test/RPCInterface.js index 8397b62..05cd8d7 100644 --- a/test/RPCInterface.js +++ b/test/RPCInterface.js @@ -26,7 +26,39 @@ describe('ControlServer - RPC Interface', function () { describe('#createInstances(number_of_instances)', function () { this.timeout(WAIT_FOR_CREATE*2); 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 () { this.timeout(WAIT_FOR_CREATE); it("should add an instance based on a defintion", async function () { - var def = { - Name: 'instance-1' + let def = { + 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 () { - 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 () { this.timeout(3000); 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 () { this.timeout(3000); 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 () { before('set tor config', function () { 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 () { this.timeout(3000); let instance_name; @@ -341,7 +460,6 @@ describe('ControlServer - RPC Interface', function () { it('no instances should be present in the pool', function () { assert.equal(rpcControlServer.torPool.instances.length, 0); - assert.notEqual(rpcControlServer.torPool.instances.length, instance_num); }); }); diff --git a/test/TorPool.js b/test/TorPool.js index 6474cf0..a6ecb20 100644 --- a/test/TorPool.js +++ b/test/TorPool.js @@ -248,7 +248,7 @@ describe('TorPool', function () { 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(); }); @@ -672,7 +672,7 @@ describe('TorPool', 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(); }); @@ -693,7 +693,7 @@ describe('TorPool', 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(); }); @@ -714,7 +714,7 @@ describe('TorPool', 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(); });