diff --git a/README.md b/README.md index a0c3aac9..515d4f8f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,44 @@ var optionalOpts = { left: 0, customParam: 'blah' // custom parameters supported } + }, + + proxyOpts: { + // Socks proxy options (used to proxy requests in node) + socksProxy: { + // Configuration from socks module (https://github.com/JoshGlazebrook/socks) + proxy: { + // IP Address of Proxy (Required) + ipaddress: "1.2.3.4", + // TCP Port of Proxy (Required) + port: 1080, + // Proxy Type [4, 5] (Required) + // Note: 4 works for both 4 and 4a. + // Type 4 does not support UDP association relay + type: 5, + + // SOCKS 4 Specific: + + // UserId used when making a SOCKS 4/4a request. (Optional) + userid: "someuserid", + + // SOCKS 5 Specific: + + // Authentication used for SOCKS 5 (when it's required) (Optional) + authentication: { + username: "Josh", + password: "somepassword" + } + }, + + // Amount of time to wait for a connection to be established. (Optional) + // - defaults to 10000ms (10 seconds) + timeout: 10000 + }, + // NodeJS HTTP agents (used to proxy HTTP and Websocket requests in node) + // Populated with Socks.Agent if socksProxy is provided + httpAgent: {}, + httpsAgent: {} } } diff --git a/client.js b/client.js index 5f35ce29..e1ac7194 100644 --- a/client.js +++ b/client.js @@ -31,6 +31,7 @@ inherits(Client, EventEmitter) * @param {function} opts.getAnnounceOpts callback to provide data to tracker * @param {number} opts.rtcConfig RTCPeerConnection configuration object * @param {number} opts.wrtc custom webrtc impl (useful in node.js) + * @param {object} opts.proxyOpts proxy options (useful in node.js) */ function Client (opts) { var self = this @@ -63,6 +64,7 @@ function Client (opts) { self._rtcConfig = opts.rtcConfig self._wrtc = opts.wrtc self._getAnnounceOpts = opts.getAnnounceOpts + self._proxyOpts = opts.proxyOpts debug('new client %s', self.infoHash) diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js index 8b767e2e..11df7fae 100644 --- a/lib/client/http-tracker.js +++ b/lib/client/http-tracker.js @@ -2,10 +2,13 @@ module.exports = HTTPTracker var bencode = require('bencode') var compact2string = require('compact2string') +var clone = require('clone') var debug = require('debug')('bittorrent-tracker:http-tracker') var extend = require('xtend') var get = require('simple-get') var inherits = require('inherits') +var Socks = require('socks') +var url = require('url') var common = require('../common') var Tracker = require('./tracker') @@ -87,8 +90,19 @@ HTTPTracker.prototype.destroy = function (cb) { HTTPTracker.prototype._request = function (requestUrl, params, cb) { var self = this - var u = requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + - common.querystringStringify(params) + var parsedUrl = url.parse(requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + common.querystringStringify(params)) + var agent + if (self.client._proxyOpts) { + agent = parsedUrl.protocol === 'https:' + ? self.client._proxyOpts.httpsAgent : self.client._proxyOpts.httpAgent + if (!agent && self.client._proxyOpts.socksProxy) { + agent = new Socks.Agent(clone(self.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'https:')) + } + } + var u = { + url: parsedUrl, + agent: agent + } get.concat(u, function (err, res, data) { if (self.destroyed) return diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js index b9ccbb22..e95a55c0 100644 --- a/lib/client/udp-tracker.js +++ b/lib/client/udp-tracker.js @@ -2,11 +2,13 @@ module.exports = UDPTracker var BN = require('bn.js') var Buffer = require('safe-buffer').Buffer +var clone = require('clone') var compact2string = require('compact2string') var debug = require('debug')('bittorrent-tracker:udp-tracker') var dgram = require('dgram') var inherits = require('inherits') var randombytes = require('randombytes') +var Socks = require('socks') var url = require('url') var common = require('../common') @@ -63,8 +65,42 @@ UDPTracker.prototype._request = function (opts) { var self = this if (!opts) opts = {} var parsedUrl = url.parse(self.announceUrl) + if (!parsedUrl.port) { + parsedUrl.port = 80 + } + var timeout + // Socket used to connect to the socks server to create a relay, null if socks is disabled + var proxySocket + // Socket used to connect to the tracker or to the socks relay if socks is enabled + var socket + // Contains the host/port of the socks relay + var relay var transactionId = genTransactionId() - var socket = dgram.createSocket('udp4') + + var proxyOpts = self.client._proxyOpts && clone(self.client._proxyOpts.socksProxy) + if (proxyOpts) { + if (!proxyOpts.proxy) { + proxyOpts.proxy = {} + } + // UDP requests uses the associate command + proxyOpts.proxy.command = 'associate' + if (!proxyOpts.target) { + // This should contain client IP and port but can be set to 0 if we don't have this information + proxyOpts.target = { + host: '0.0.0.0', + port: 0 + } + } + + if (proxyOpts.proxy.type === 5) { + Socks.createConnection(proxyOpts, onGotConnection) + } else { + debug('Ignoring Socks proxy for UDP request because type 5 is required') + onGotConnection(null) + } + } else { + onGotConnection(null) + } var cleanup = function () { if (!socket) return @@ -78,29 +114,42 @@ UDPTracker.prototype._request = function (opts) { socket.on('error', noop) // ignore all future errors try { socket.close() } catch (err) {} socket = null + if (proxySocket) { + try { proxySocket.close() } catch (err) {} + proxySocket = null + } } self.cleanupFns.push(cleanup) - // does not matter if `stopped` event arrives, so supress errors & cleanup after timeout - var ms = opts.event === 'stopped' ? TIMEOUT / 10 : TIMEOUT - var timeout = setTimeout(function () { - timeout = null - if (opts.event === 'stopped') cleanup() - else onError(new Error('tracker request timed out (' + opts.event + ')')) - }, ms) - if (timeout.unref) timeout.unref() + function onGotConnection (err, s, info) { + if (err) return onError(err) + + proxySocket = s + socket = dgram.createSocket('udp4') + relay = info + + // does not matter if `stopped` event arrives, so supress errors & cleanup after timeout + var ms = opts.event === 'stopped' ? TIMEOUT / 10 : TIMEOUT + timeout = setTimeout(function () { + timeout = null + if (opts.event === 'stopped') cleanup() + else onError(new Error('tracker request timed out (' + opts.event + ')')) + }, ms) + if (timeout.unref) timeout.unref() - send(Buffer.concat([ - common.CONNECTION_ID, - common.toUInt32(common.ACTIONS.CONNECT), - transactionId - ])) + send(Buffer.concat([ + common.CONNECTION_ID, + common.toUInt32(common.ACTIONS.CONNECT), + transactionId + ]), relay) - socket.on('error', onError) - socket.on('message', onSocketMessage) + socket.on('error', onError) + socket.on('message', onSocketMessage) + } function onSocketMessage (msg) { if (self.destroyed) return + if (proxySocket) msg = msg.slice(10) if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { return onError(new Error('tracker sent invalid transaction id')) } @@ -180,11 +229,13 @@ UDPTracker.prototype._request = function (opts) { self.client.emit('warning', err) } - function send (message) { - if (!parsedUrl.port) { - parsedUrl.port = 80 + function send (message, proxyInfo) { + if (proxyInfo) { + var pack = Socks.createUDPFrame({ host: parsedUrl.hostname, port: parsedUrl.port }, message) + socket.send(pack, 0, pack.length, proxyInfo.port, proxyInfo.host) + } else { + socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) } - socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) } function announce (connectionId, opts) { @@ -204,7 +255,7 @@ UDPTracker.prototype._request = function (opts) { common.toUInt32(0), // key (optional) common.toUInt32(opts.numwant), toUInt16(self.client._port) - ])) + ]), relay) } function scrape (connectionId) { @@ -219,7 +270,7 @@ UDPTracker.prototype._request = function (opts) { common.toUInt32(common.ACTIONS.SCRAPE), transactionId, infoHash - ])) + ]), relay) } } diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index b0aa6b51..16c8c67c 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -1,11 +1,14 @@ module.exports = WebSocketTracker +var clone = require('clone') var debug = require('debug')('bittorrent-tracker:websocket-tracker') var extend = require('xtend') var inherits = require('inherits') var Peer = require('simple-peer') var randombytes = require('randombytes') var Socket = require('simple-websocket') +var Socks = require('socks') +var url = require('url') var common = require('../common') var Tracker = require('./tracker') @@ -167,7 +170,16 @@ WebSocketTracker.prototype._openSocket = function () { if (self.socket) { socketPool[self.announceUrl].consumers += 1 } else { - self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl) + var parsedUrl = url.parse(self.announceUrl) + var agent + if (self.client._proxyOpts) { + agent = parsedUrl.protocol === 'wss:' + ? self.client._proxyOpts.httpsAgent : self.client._proxyOpts.httpAgent + if (!agent && self.client._proxyOpts.socksProxy) { + agent = new Socks.Agent(clone(self.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'wss:')) + } + } + self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl, { agent: agent }) self.socket.consumers = 1 self.socket.on('connect', self._onSocketConnectBound) } diff --git a/package.json b/package.json index 49692ec7..61733be2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "./lib/common-node.js": false, "./lib/client/http-tracker.js": false, "./lib/client/udp-tracker.js": false, - "./server.js": false + "./server.js": false, + "socks": false }, "bugs": { "url": "https://github.com/feross/bittorrent-tracker/issues" @@ -23,6 +24,7 @@ "bencode": "^0.10.0", "bittorrent-peerid": "^1.0.2", "bn.js": "^4.4.0", + "clone": "^1.0.2", "compact2string": "^1.2.0", "debug": "^2.0.0", "inherits": "^2.0.1", @@ -38,6 +40,7 @@ "simple-get": "^2.0.0", "simple-peer": "^6.0.0", "simple-websocket": "^4.0.0", + "socks": "^1.1.9", "string2compact": "^1.1.1", "uniq": "^1.0.1", "ws": "^1.0.0", diff --git a/test/client.js b/test/client.js index 9f1c397c..94fb5b37 100644 --- a/test/client.js +++ b/test/client.js @@ -2,6 +2,8 @@ var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') +var http = require('http') +var net = require('net') var test = require('tape') var peerId1 = Buffer.from('01234567890123456789') @@ -380,3 +382,56 @@ test('http: client announce with numwant', function (t) { test('udp: client announce with numwant', function (t) { testClientAnnounceWithNumWant(t, 'udp') }) + +function testClientStartHttpAgent (t, serverType) { + t.plan(5) + + common.createServer(t, serverType, function (server, announceUrl) { + var agent = new http.Agent() + var agentUsed = false + agent.createConnection = function (opts, fn) { + agentUsed = true + return net.createConnection(opts, fn) + } + var client = new Client({ + infoHash: fixtures.leaves.parsedTorrent.infoHash, + announce: announceUrl, + peerId: peerId1, + port: port, + wrtc: {}, + proxyOpts: { + httpAgent: agent + } + }) + + if (serverType === 'ws') common.mockWebsocketTracker(client) + client.on('error', function (err) { t.error(err) }) + client.on('warning', function (err) { t.error(err) }) + + client.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') + + t.ok(agentUsed) + + client.stop() + + client.once('update', function () { + t.pass('got response to stop') + server.close() + client.destroy() + }) + }) + + client.start() + }) +} + +test('http: client.start(httpAgent)', function (t) { + testClientStartHttpAgent(t, 'http') +}) + +test('ws: client.start(httpAgent)', function (t) { + testClientStartHttpAgent(t, 'ws') +})