diff --git a/package.json b/package.json index 10dc848..b28ef9e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "devDependencies": { "chai": "^4.1.2", "mocha": "^5.2.0", - "proxy-agent": "^3.0.1", "request": "^2.79.0", "request-promise": "^4.2.2" }, @@ -37,6 +36,7 @@ "socksv5": "git+https://github.com/znetstar/socksv5.git#431e541390314adbbe765650d8d810ec1df38d8a", "temp": "^0.8.3", "winston": "^3.0.0-rc5", - "yargs": "^11.0.0" + "yargs": "^11.0.0", + "proxy-agent": "^3.0.1" } } diff --git a/src/ControlServer.js b/src/ControlServer.js index dbda978..8b85181 100644 --- a/src/ControlServer.js +++ b/src/ControlServer.js @@ -144,14 +144,14 @@ class ControlServer { } async createSOCKSServer(port, hostname) { - this.socksServer = new SOCKSServer(this.torPool, this.logger); + this.socksServer = new SOCKSServer(this.torPool, this.logger, (this.nconf.get('proxyByName') ? { deny_unidentified_users: this.nconf.get('denyUnidentifedUsers') } : "")); await this.socksServer.listen(port || default_ports.socks, hostname); this.logger.info(`[socks]: listening on socks5://${hostname}:${port}`); this.socksServer; } async createHTTPServer(port, hostname) { - this.httpServer = new HTTPServer(this.torPool, this.logger); + this.httpServer = new HTTPServer(this.torPool, this.logger, (this.nconf.get('proxyByName') ? { deny_unidentified_users: this.nconf.get('denyUnidentifedUsers') } : "")); await this.httpServer.listen(port || default_ports.http, hostname); this.logger.info(`[http]: listening on http://${hostname}:${port}`); this.httpServer; diff --git a/src/HTTPServer.js b/src/HTTPServer.js index bfbdce2..2ad6a64 100644 --- a/src/HTTPServer.js +++ b/src/HTTPServer.js @@ -4,7 +4,7 @@ const { Server } = http; const Promise = require('bluebird'); const socks = require('socksv5'); -const SocksProxyAgent = require('socks-proxy-agent'); +const ProxyAgent = require('proxy-agent'); const TOR_ROUTER_PROXY_AGENT = 'tor-router'; @@ -21,8 +21,57 @@ class HTTPServer extends Server { }); } - constructor(tor_pool, logger) { + authenticate_user_http(req, res) { + return this.authenticate_user(req, () => { + res.writeHead(407, { 'Proxy-Authenticate': `Basic realm="Name of instance to route to"` }); + res.end(); + return false; + }) + } + + authenticate_user_connect(req, socket) { + return this.authenticate_user(req, () => { + socket.end(); + return false; + }) + } + + authenticate_user(req, deny) { + if (!this.proxy_by_name) + return true; + + let deny_un = this.proxy_by_name.deny_unidentified_users; + + let header = req.headers['authorization'] || req.headers['proxy-authorization']; + if (!header && deny_un) return deny(); + else if (!header) return true; + + let token = header.split(/\s+/).pop(); + if (!token && deny_un) return deny(); + else if (!token) return true; + + let buf = new Buffer.from(token, 'base64').toString(); + let username = buf.split(/:/).shift(); + if ( !username && deny_un ) return deny(); + else if (!username) return true; + + this.logger.verbose(`[http]: connected attempted to instance "${username}"`); + + let instance = this.tor_pool.instance_by_name(username); + + if (!instance) return deny(); + req.instance = instance; + + return true; + } + + constructor(tor_pool, logger, proxy_by_name) { let handle_http_connections = (req, res) => { + if (!this.authenticate_user_http(req, res)) + return; + + let { instance } = req; + let url = URL.parse(req.url); url.port = url.port || 80; @@ -52,7 +101,7 @@ class HTTPServer extends Server { port: url.port, path: url.path, headers: req.headers, - agent: new SocksProxyAgent(`socks://127.0.0.1:${socks_port}`) + agent: new ProxyAgent(`socks://127.0.0.1:${socks_port}`) }, (proxy_res) => { proxy_res.on('data', (chunk) => { res.write(chunk); @@ -83,8 +132,17 @@ class HTTPServer extends Server { proxy_req.end(); }; - - if (tor_pool.instances.length) { + + if (instance) { + if (instance.ready) { + connect(instance); + } + else { + this.logger.debug(`[socks]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`); + instance.once('ready', (() => connect(instance))); + } + } + else if (this.tor_pool.instances.length) { connect(tor_pool.next()); } else { this.logger.debug(`[http-proxy]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`); @@ -93,6 +151,11 @@ class HTTPServer extends Server { }; let handle_connect_connections = (req, inbound_socket, head) => { + if (!this.authenticate_user_connect(req, inbound_socket)) + return; + + let { instance } = req; + let hostname = req.url.split(':').shift(); let port = Number(req.url.split(':').pop()); @@ -135,11 +198,21 @@ class HTTPServer extends Server { inbound_socket.pipe(outbound_socket); }); }; - if (tor_pool.instances.length) { - connect(tor_pool.next()); + + if (instance) { + if (instance.ready) { + connect(instance); + } + else { + this.logger.debug(`[socks]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`); + instance.once('ready', (() => connect(instance))); + } + } + else if (this.tor_pool.instances.length) { + connect(this.tor_pool.next()); } else { this.logger.debug(`[http-connect]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`); - tor_pool.once('instance_created', connect); + this.tor_pool.once('instance_created', connect); } }; @@ -148,6 +221,7 @@ class HTTPServer extends Server { this.logger = logger || require('./winston-silent-logger'); this.tor_pool = tor_pool; + this.proxy_by_name = proxy_by_name; } }; diff --git a/src/SOCKSServer.js b/src/SOCKSServer.js index 5d47723..ebc7aa8 100644 --- a/src/SOCKSServer.js +++ b/src/SOCKSServer.js @@ -15,10 +15,35 @@ class SOCKSServer extends Server{ }); } + authenticate_user(username, password, callback) { + let deny_un = this.proxy_by_name.deny_unidentifed_users; + + // 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); + + // Otherwise allow + callback(true, instance); + } + constructor(tor_pool, logger, proxy_by_name) { let handleConnection = (info, accept, deny) => { let inbound_socket = accept(true); - var outbound_socket; + let instance; + + if (inbound_socket.user) + instance = inbound_socket.user; + + let outbound_socket; let buffer = []; let onInboundData = (data) => buffer.push(data) @@ -70,27 +95,37 @@ class SOCKSServer extends Server{ } }); }; - if (tor_pool.instances.length) { - connect(tor_pool.next()); + + if (instance) { + if (instance.ready) { + connect(instance); + } + else { + this.logger.debug(`[socks]: a connection has been attempted to "${instance.instance_name}", but it is not live... waiting for the instance to come online`); + instance.once('ready', (() => connect(instance))); + } + } + else if (this.tor_pool.instances.length) { + connect(this.tor_pool.next()); } else { this.logger.debug(`[socks]: a connection has been attempted, but no tor instances are live... waiting for an instance to come online`); - tor_pool.once('instance_created', connect); + this.tor_pool.once('instance_created', connect); } }; super(handleConnection); - this.logger = logger || require('./winston-silent-logger');; + this.logger = logger || require('./winston-silent-logger'); + this.tor_pool = tor_pool; + this.proxy_by_name = proxy_by_name; + + let auth = socks.auth.None(); + if (!proxy_by_name) { this.logger.debug(`[socks]: connecting to a specific instance by name has ben turned off`); - let auth = socks.auth.None(); } else { this.logger.debug(`[socks]: connecting to a specific instance by name has ben turned on`); - let auth = socks.auth.UserPassword( - (username, password, cb) => { - - } - ); + auth = socks.auth.UserPassword(this.authenticate_user); } this.useAuth(auth); diff --git a/src/TorPool.js b/src/TorPool.js index 8f7146c..e7de4b3 100644 --- a/src/TorPool.js +++ b/src/TorPool.js @@ -124,11 +124,11 @@ class TorPool extends EventEmitter { } instance_by_name(name) { - return this.instances.filter((i) => i.definition.Name === name)[0]; + return this._instances.filter((i) => i.definition.Name === name)[0]; } instance_at(index) { - return this.instances[index]; + return this._instances[index]; } async remove(instances) { diff --git a/src/TorProcess.js b/src/TorProcess.js index ea0e006..b0ab1b3 100644 --- a/src/TorProcess.js +++ b/src/TorProcess.js @@ -64,6 +64,8 @@ class TorProcess extends EventEmitter { return this._controller || null; } + get ready() { return this._ready; } + /* Passthrough to granax */ async new_identity() { @@ -133,7 +135,7 @@ class TorProcess extends EventEmitter { }); this.once('ready', () => { - this.ready = true; + this._ready = true; this.logger.info(`[tor-${this.instance_name}]: tor is ready`); }); diff --git a/src/default_config.js b/src/default_config.js index 007f25d..3d8a102 100644 --- a/src/default_config.js +++ b/src/default_config.js @@ -9,6 +9,8 @@ module.exports = { "socksHost": null, "dnsHost": null, "httpHost": null, + "proxyByName": true, + "denyUnidentifedUsers": false, "logLevel": "info", "loadBalanceMethod": "round_robin", "torConfig": { diff --git a/src/launch.js b/src/launch.js index b2d0741..a42521e 100644 --- a/src/launch.js +++ b/src/launch.js @@ -177,6 +177,11 @@ let argv_config = alias: "torPath", describe: "Provide the path for the Tor executable that will be used", demand: false + }, + n: { + alias: 'proxyByName', + describe: 'Allow connecting to a specific instance identified by the username field when connecting to a proxy', + demand: false } }); diff --git a/src/nconf_load_env.js b/src/nconf_load_env.js index e51dfb3..0fdf5c0 100644 --- a/src/nconf_load_env.js +++ b/src/nconf_load_env.js @@ -8,6 +8,8 @@ const env_whitelist = [ "CONTROL_HOST", 'PARENT_DATA_DIRECTORIES', 'LOAD_BALANCE_METHOD', "WEBSOCKET_CONTROL_PORT", + "PROXY_BY_NAME", + "DENY_UNIDENTIFIED_USERS", "controlHost", "torPath", "instances", @@ -17,7 +19,9 @@ const env_whitelist = [ "CONTROL_HOST", "logLevel", 'parentDataDirectories', 'loadBalanceMethod', - "websocketControlPort" + "websocketControlPort", + "proxyByName", + "denyUnidentifedUsers" ]; module.exports = (nconf) => {