diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8e51f..97bc84d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ ## [4.0.0] - 2018-09-09 ### Added -- Instances can now added to one or multiple groups by setting the `Group` field in the instance definition to a single string or array. +- Instances can now added to one or more groups by setting the `Group` field in the instance definition to a single string or array. - You can now proxy through a specific instance using the username field when connecting to a proxy. Setting `--proxyByName` or `-n` to false will disable this feature. For example to connect to an instance named `instance-1` via http use `http://instance-1:@localhost:9080`. +- You can also connect to a specific group of instances by setting `--proxyByName` or `-n` to "group". If enabled requests made to `://foo:@localhost:9080` would be routed to instances in the `foo` group. - The control server will accept WebSocket connections if the `--websocketControlHost` or `-w` argument is set. If the argument is used without a hostname it will default to 9078 on all interfaces. - All servers (DNS, HTTP, SOCKS and Control) all have a `listen` method which takes a port and optionally, a host returns a Promise that will resolve when the server is listening. diff --git a/src/HTTPServer.js b/src/HTTPServer.js index 0002a7e..c573a2a 100644 --- a/src/HTTPServer.js +++ b/src/HTTPServer.js @@ -39,6 +39,8 @@ class HTTPServer extends Server { if (!this.proxy_by_name) return true; + this.logger.verbose(`[http]: connected attempted to instance "${username}"`); + let deny_un = this.proxy_by_name.deny_unidentified_users; let header = req.headers['authorization'] || req.headers['proxy-authorization']; @@ -54,9 +56,17 @@ class HTTPServer extends Server { if ( !username && deny_un ) return deny(); else if (!username) return true; - this.logger.verbose(`[http]: connected attempted to instance "${username}"`); + let instance; - let instance = this.tor_pool.instance_by_name(username); + if (this.proxy_by_name.mode === 'individual') + instance = this.tor_pool.instance_by_name(username); + else if (this.proxy_by_name.mode === 'group') { + if (!this.tor_pool.group_names.has(username)) return deny(); + + instance = this.tor_pool.next_by_group(username); + } + else + throw Error(`Unknown "proxy_by_name" mode ${this.proxy_by_name.mode}`); if (!instance) return deny(); req.instance = instance; diff --git a/src/SOCKSServer.js b/src/SOCKSServer.js index c201a9e..427e9db 100644 --- a/src/SOCKSServer.js +++ b/src/SOCKSServer.js @@ -15,21 +15,33 @@ class SOCKSServer extends Server{ }); } + get_instance_pbn(username) { + if (this.proxy_by_name.mode === 'individual') + return this.tor_pool.instance_by_name(username); + else if (this.proxy_by_name.mode === 'group') { + return this.tor_pool.next_by_group(username); + } else + throw Error(`Unknown "proxy_by_name" mode ${this.proxy_by_name.mode}`); + } + authenticate_user(username, password, callback) { let deny_un = this.proxy_by_name.deny_unidentified_users; + this.logger.verbose(`[socks]: connected attempted to instance "${username}"`); + // No username and deny unindentifed then deny if (!username && deny_un) callback(false); // Otherwise if there is no username allow else if (!username) callback(true); - - this.logger.verbose(`[socks]: connected attempted to instance "${username}"`); - let instance = this.tor_pool.instance_by_name(username); - - // If a username is specified but no instances match that username deny - if (!instance) - return callback(false); + if (this.proxy_by_name.mode === 'individual'){ + if (!this.tor_pool.instance_names.includes(username)) return callback(false); + } + else if (this.proxy_by_name.mode === 'group') { + if (!this.tor_pool.group_names.has(username)) return callback(false); + } + else + throw Error(`Unknown "proxy_by_name" mode "${this.proxy_by_name.mode}"`); // Otherwise allow callback(true, true); @@ -41,7 +53,7 @@ class SOCKSServer extends Server{ let instance; if (inbound_socket.user) - instance = this.tor_pool.instance_by_name(inbound_socket.user); + instance = this.get_instance_pbn(inbound_socket.user); let outbound_socket; let buffer = []; diff --git a/src/TorPool.js b/src/TorPool.js index 27751ea..5143205 100644 --- a/src/TorPool.js +++ b/src/TorPool.js @@ -149,8 +149,8 @@ class TorPool extends EventEmitter { return instances.length; if (prop === 'rotate') { - return () => { - instances.rotate(1); + return (num) => { + instances.rotate(typeof(num) === 'undefined' ? 1 : num); save_index(); }; } @@ -167,7 +167,6 @@ class TorPool extends EventEmitter { instances_in_group = this.instances.filter((instance) => instance.instance_group.indexOf(prop) !== -1); } - instances_in_group = _.sortBy(instances_in_group, ['_index', 'instance_name']); instances_in_group.group_name = prop; @@ -294,7 +293,7 @@ class TorPool extends EventEmitter { } next_by_group(group) { - this.groups[group].rotate(); + this.groups[group].rotate(1); return this.groups[group][0]; } diff --git a/test/HTTPServer.js b/test/HTTPServer.js index eec96d8..93e51db 100644 --- a/test/HTTPServer.js +++ b/test/HTTPServer.js @@ -2,6 +2,7 @@ const nconf = require('nconf'); const request = require('request-promise'); const getPort = require('get-port'); const { assert } = require('chai'); +const _ = require('lodash'); const { TorPool, HTTPServer } = require('../'); const { WAIT_FOR_CREATE, PAGE_LOAD_TIME } = require('./constants'); @@ -89,12 +90,13 @@ describe('HTTPServer', function () { let httpPort; let instance_def = { - Name: 'instance-3' + Name: 'instance-3', + Group: 'foo' }; before('start up server', async function (){ httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null); - httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true }); + httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true, mode: 'individual' }); this.timeout(WAIT_FOR_CREATE * 3); @@ -105,17 +107,21 @@ describe('HTTPServer', function () { await httpServer.listen(httpPort); }); - it(`should service a request for example.com through ${instance_def.Name}`, function (done) { + it(`should service a request for example.com through the instance named ${instance_def.Name}`, function (done) { this.timeout(PAGE_LOAD_TIME); let req; - httpServer.on('instance-connection', (instance, source) => { + let connectionHandler = (instance, source) => { assert.equal(instance.instance_name, instance_def.Name); assert.isTrue(source.by_name); + req.cancel(); + httpServer.removeAllListeners('instance-connection');; done(); - }); + }; + + httpServer.on('instance-connection', connectionHandler); req = request({ url: 'http://example.com', @@ -124,6 +130,57 @@ describe('HTTPServer', function () { .catch(done) }); + + it(`four requests made to example.com through the group named "foo" should come from the instances in "foo"`, function (done) { + (async () => { + this.timeout(PAGE_LOAD_TIME + (WAIT_FOR_CREATE)); + + await httpServerTorPool.add([ + { + Name: 'instance-4', + Group: 'foo' + } + ]); + })() + .then(async () => { + httpServer.proxy_by_name.mode = "group"; + + let request = require('request-promise').defaults({ proxy: `http://foo:@127.0.0.1:${httpPort}` }); + + + let names_requested = []; + + let connectionHandler = (instance, source) => { + names_requested.push(instance.instance_name); + + if (names_requested.length === httpServerTorPool.instances.length) { + names_requested = _.uniq(names_requested).sort(); + + let names_in_group = httpServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort() + + assert.deepEqual(names_requested, names_in_group); + httpServer.removeAllListeners('instance-connection'); + done(); + } + }; + + httpServer.on('instance-connection', connectionHandler); + + let i = 0; + while (i < httpServerTorPool.instances.length) { + await request({ + url: 'http://example.com' + }); + i++; + } + }) + .then(async () => { + await httpServerTorPool.remove_by_name('instance-4'); + httpServer.proxy_by_name.mode = "individual"; + }) + .catch(done); + }); + it(`shouldn't be able to send a request without a username`, async function() { let f = () => {}; try { @@ -169,12 +226,13 @@ describe('HTTPServer', function () { let httpPort; let instance_def = { - Name: 'instance-3' + Name: 'instance-3', + Group: 'foo' }; before('start up server', async function (){ httpServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null); - httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true }); + httpServer = new HTTPServer(httpServerTorPool, null, { deny_unidentified_users: true, mode: "individual" }); this.timeout(WAIT_FOR_CREATE * 3); @@ -188,12 +246,16 @@ describe('HTTPServer', function () { it(`should service a request for example.com through ${instance_def.Name}`, function (done) { this.timeout(PAGE_LOAD_TIME); let req; - httpServer.on('instance-connection', (instance, source) => { + + let connectionHandler = (instance, source) => { assert.equal(instance.instance_name, instance_def.Name); assert.isTrue(source.by_name); req.cancel(); + httpServer.removeAllListeners('instance-connection'); done(); - }); + }; + + httpServer.on('instance-connection', connectionHandler); req = request({ url: 'https://example.com', @@ -202,6 +264,55 @@ describe('HTTPServer', function () { .catch(done); }); + it(`four requests made to example.com through the group named "foo" should come from instances in the "foo" group`, function (done) { + (async () => { + this.timeout(PAGE_LOAD_TIME + (WAIT_FOR_CREATE)); + + await httpServerTorPool.add([ + { + Name: 'instance-4', + Group: 'foo' + } + ]); + + httpServer.proxy_by_name.mode = "group"; + })() + .then(async () => { + let request = require('request-promise').defaults({ proxy: `http://foo:@127.0.0.1:${httpPort}` }); + + let names_requested = []; + + let connectionHandler = (instance, source) => { + names_requested.push(instance.instance_name); + + if (names_requested.length === httpServerTorPool.instances.length) { + names_requested = _.uniq(names_requested).sort(); + + let names_in_group = httpServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort() + + assert.deepEqual(names_requested, names_in_group); + httpServer.removeAllListeners('instance-connection'); + done(); + } + }; + + httpServer.on('instance-connection', connectionHandler); + + let i = 0; + while (i < httpServerTorPool.instances.length) { + await request({ + url: 'https://example.com' + }); + i++; + } + }) + .then(async () => { + await httpServerTorPool.remove_by_name('instance-4'); + httpServer.proxy_by_name.mode = "individual"; + }) + .catch(done); + }); + it(`shouldn't be able to send a request without a username`, async function() { let f = () => {}; try { diff --git a/test/SOCKSServer.js b/test/SOCKSServer.js index d04a413..2408876 100644 --- a/test/SOCKSServer.js +++ b/test/SOCKSServer.js @@ -3,6 +3,7 @@ const request = require('request-promise'); const getPort = require('get-port'); const { HttpAgent, auth } = require('socksv5'); const { assert } = require('chai'); +const _ = require('lodash'); const { TorPool, SOCKSServer } = require('../'); const { WAIT_FOR_CREATE, PAGE_LOAD_TIME } = require('./constants'); @@ -28,7 +29,6 @@ describe('SOCKSServer', function () { await socksServer.listen(socksPort); }); - it('should service a request for example.com', async function () { this.timeout(PAGE_LOAD_TIME); @@ -53,17 +53,18 @@ describe('SOCKSServer', function () { }); }); - describe('#authenticate_user(username, password) - proxy by name', function () { + describe('#authenticate_user(username, password)', function () { let socksPort; let socksServerTorPool; let socksServer; let instance_def = { - Name: 'instance-3' + Name: 'instance-3', + Group: "foo" }; before('start up server', async function (){ socksServerTorPool = new TorPool(nconf.get('torPath'), {}, nconf.get('parentDataDirectory'), 'round_robin', null); - socksServer = new SOCKSServer(socksServerTorPool, null, { deny_unidentified_users: true }); + socksServer = new SOCKSServer(socksServerTorPool, null, { deny_unidentified_users: true, mode: "individual" }); this.timeout(WAIT_FOR_CREATE * 3); @@ -79,13 +80,15 @@ describe('SOCKSServer', function () { this.timeout(PAGE_LOAD_TIME); let req; - - socksServer.on('instance-connection', (instance, source) => { + let connectionHandler = (instance, source) => { assert.equal(instance.instance_name, instance_def.Name); assert.isTrue(source.by_name); req.cancel(); + socksServer.removeAllListeners('instance-connection'); done(); - }); + }; + + socksServer.on('instance-connection', connectionHandler); req = request({ url: 'http://example.com', @@ -99,6 +102,63 @@ describe('SOCKSServer', function () { .catch(done); }); + it(`four requests made to example.com through the group named "foo" should come from the instances in "foo"`, function (done) { + (async () => { + this.timeout(PAGE_LOAD_TIME + (WAIT_FOR_CREATE)); + + await socksServerTorPool.add([ + { + Name: 'instance-4', + Group: 'foo' + } + ]); + })() + .then(async () => { + socksServer.proxy_by_name.mode = "group"; + + let request = require('request-promise').defaults({ + agent: new HttpAgent({ + proxyHost: '127.0.0.1', + proxyPort: socksPort, + localDNS: false, + auths: [ auth.UserPassword('foo', "doesn't mater") ] + }) + }); + + + let names_requested = []; + + let connectionHandler = (instance, source) => { + names_requested.push(instance.instance_name); + + if (names_requested.length === socksServerTorPool.instances.length) { + names_requested = _.uniq(names_requested).sort(); + + let names_in_group = socksServerTorPool.instances_by_group('foo').map((i) => i.instance_name).sort() + + assert.deepEqual(names_requested, names_in_group); + socksServer.removeAllListeners('instance-connection'); + done(); + } + }; + + socksServer.on('instance-connection', connectionHandler); + + let i = 0; + while (i < socksServerTorPool.instances.length) { + await request({ + url: 'http://example.com' + }); + i++; + } + }) + .then(async () => { + await socksServerTorPool.remove_by_name('instance-4'); + socksServer.proxy_by_name.mode = "individual"; + }) + .catch(done); + }); + // it(`shouldn't be able to send a request without a username`, async function() { // let f = () => {}; // try { diff --git a/test/constants.js b/test/constants.js index f48edd2..c814caa 100644 --- a/test/constants.js +++ b/test/constants.js @@ -1,4 +1,4 @@ module.exports = { WAIT_FOR_CREATE: 240000, - PAGE_LOAD_TIME: 60000 + PAGE_LOAD_TIME: 60000 * 5 }; \ No newline at end of file