diff --git a/README.md b/README.md index a0c3aac9..6283aab7 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ var optionalOpts = { uploaded: 0, downloaded: 0, left: 0, + ip: 'custom.ip.address.or.hostname', customParam: 'blah' // custom parameters supported } } @@ -252,6 +253,7 @@ $ bittorrent-tracker --help Options: -p, --port [number] change the port [default: 8000] + --trust-ip trust 'ip' parameter in GET requests --trust-proxy trust 'x-forwarded-for' header from reverse proxy --interval client announce interval (ms) [default: 600000] --http enable http server diff --git a/bin/cmd.js b/bin/cmd.js index 49c8fdd0..2e7f5ddd 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -16,6 +16,7 @@ var argv = minimist(process.argv.slice(2), { 'http', 'quiet', 'silent', + 'trust-ip', 'trust-proxy', 'udp', 'version', @@ -53,6 +54,7 @@ if (argv.help) { --http-hostname [string] change the http server hostname [default: '::'] --udp-hostname [string] change the udp hostname [default: '0.0.0.0'] --udp6-hostname [string] change the udp6 hostname [default: '::'] + --trust-ip trust 'ip' parameter in GET requests --trust-proxy trust 'x-forwarded-for' header from reverse proxy --interval client announce interval (ms) [default: 600000] --http enable http server @@ -82,6 +84,7 @@ var server = new Server({ http: argv.http, interval: argv.interval, stats: argv.stats, + trustIp: argv['trust-ip'], trustProxy: argv['trust-proxy'], udp: argv.udp, ws: argv.ws diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js index 9e83a59e..82743661 100644 --- a/lib/server/parse-http.js +++ b/lib/server/parse-http.js @@ -31,9 +31,18 @@ function parseHttpRequest (req, opts) { common.MAX_ANNOUNCE_PEERS ) - params.ip = opts.trustProxy - ? req.headers['x-forwarded-for'] || req.connection.remoteAddress - : req.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + // If we're trusting IPs supplied in the GET parameters, simply use that if available + if (opts.trustIp && params.ip) { + // No operation needed + // Else, if we're trusting proxied headers, use that value if available, + } else if (opts.trustProxy && req.headers['x-forwarded-for']) { + params.ip = req.headers['x-forwarded-for'] + // Otherwise, simply use the connection's remote host address. + } else { + params.ip = req.connection.remoteAddress + // Remove IPv4-Mapped address prefix from IPv6 address if present + .replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') + } params.addr = (common.IPV6_RE.test(params.ip) ? '[' + params.ip + ']' : params.ip) + ':' + params.port params.headers = req.headers diff --git a/server.js b/server.js index 87d08a1e..fe88d362 100644 --- a/server.js +++ b/server.js @@ -29,6 +29,7 @@ inherits(Server, EventEmitter) * * @param {Object} opts options object * @param {Number} opts.interval tell clients to announce on this interval (ms) + * @param {Number} opts.trustIp trust 'ip' parameter in GET requests * @param {Number} opts.trustProxy trust 'x-forwarded-for' header from reverse proxy * @param {boolean} opts.http start an http server? (default: true) * @param {boolean} opts.udp start a udp server? (default: true) @@ -49,6 +50,7 @@ function Server (opts) { : 10 * 60 * 1000 // 10 min self._trustProxy = !!opts.trustProxy + self._trustIp = !!opts.trustIp if (typeof opts.filter === 'function') self._filter = opts.filter self.peersCacheLength = opts.peersCacheLength @@ -366,6 +368,7 @@ Server.prototype.onHttpRequest = function (req, res, opts) { var self = this if (!opts) opts = {} opts.trustProxy = opts.trustProxy || self._trustProxy + opts.trustIp = opts.trustIp || self._trustIp var params try { @@ -445,6 +448,7 @@ Server.prototype.onWebSocketConnection = function (socket, opts) { var self = this if (!opts) opts = {} opts.trustProxy = opts.trustProxy || self._trustProxy + opts.trustIp = opts.trustIp || self._trustIp socket.peerId = null // as hex socket.infoHashes = [] // swarms that this socket is participating in diff --git a/test/client.js b/test/client.js index 9f1c397c..9de3bc44 100644 --- a/test/client.js +++ b/test/client.js @@ -255,7 +255,8 @@ function testClientGetAnnounceOpts (t, serverType) { port: port, getAnnounceOpts: function () { return { - testParam: 'this is a test' + testParam: 'this is a test', + ip: 'test.bittorrenttest.xyz' } }, wrtc: {} diff --git a/test/server.js b/test/server.js index b3a7b163..6c561458 100644 --- a/test/server.js +++ b/test/server.js @@ -178,3 +178,160 @@ test('http ipv6 server', function (t) { test('udp server', function (t) { serverTest(t, 'udp', 'inet') }) + +function serverTestTrustIP (t, serverType, serverFamily) { + t.plan(32) + + var hostname = serverFamily === 'inet6' + ? '[::1]' + : '127.0.0.1' + var customIp = 'custom.ip.address.or.hostname' + + var opts = { + serverType: serverType, + peersCacheLength: 2, + trustIp: true + } + + common.createServer(t, opts, function (server) { + var port = server[serverType].address().port + var announceUrl = serverType + '://' + hostname + ':' + port + '/announce' + + var client1 = new Client({ + infoHash: infoHash, + announce: [ announceUrl ], + peerId: peerId, + port: 6881, + getAnnounceOpts: function () { + // provide a callback that will be called whenever announce() is called + // internally (on timer), or by the user + return { + uploaded: 0, + downloaded: 0, + left: 0, + ip: customIp, + customParam: 'blah' // custom parameters supported + } + } + // wrtc: wrtc + }) + + client1.start() + + server.once('start', function () { + t.pass('got start message from client1') + }) + + client1.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(data.complete, 0) + t.equal(data.incomplete, 1) + + server.getSwarm(infoHash, function (err, swarm) { + t.error(err) + + t.equal(Object.keys(server.torrents).length, 1) + t.equal(swarm.complete, 0) + t.equal(swarm.incomplete, 1) + t.equal(swarm.peers.length, 1) + + var id = serverType === 'ws' + ? peerId.toString('hex') + : customIp + ':6881' + + var peer = swarm.peers.peek(id) + + t.equal(peer.type, serverType) + t.equal(peer.ip, customIp) + t.equal(peer.peerId, peerId.toString('hex')) + t.equal(peer.complete, false) + if (serverType === 'ws') { + t.equal(typeof peer.port, 'number') + t.ok(peer.socket) + } else { + t.equal(peer.port, 6881) + t.notOk(peer.socket) + } + + client1.complete() + + client1.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(data.complete, 1) + t.equal(data.incomplete, 0) + + client1.scrape() + + client1.once('scrape', function (data) { + t.equal(data.announce, announceUrl) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') + t.equal(typeof data.downloaded, 'number') + + var client2 = new Client({ + infoHash: infoHash, + announce: [ announceUrl ], + peerId: peerId2, + port: 6882 + // wrtc: wrtc + }) + + client2.start() + + server.once('start', function () { + t.pass('got start message from client2') + }) + + client2.once('peer', function (addr) { + t.ok(addr === hostname + ':6881' || addr === hostname + ':6882' || addr.id === peerId.toString('hex')) + + swarm.peers.once('evict', function (evicted) { + t.equals(evicted.value.peerId, peerId.toString('hex')) + }) + var client3 = new Client({ + infoHash: infoHash, + announce: [ announceUrl ], + peerId: peerId3, + port: 6880 + // wrtc: wrtc + }) + client3.start() + + server.once('start', function () { + t.pass('got start message from client3') + }) + + client3.once('update', function () { + client2.stop() + client2.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(data.complete, 1) + t.equal(data.incomplete, 1) + client2.destroy() + + client3.stop() + client3.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(data.complete, 1) + t.equal(data.incomplete, 0) + + client3.destroy(function () { + client1.destroy(function () { + server.close() + }) + // if (serverType === 'ws') wrtc.close() + }) + }) + }) + }) + }) + }) + }) + }) + }) + }) +} + +test('http ipv4 server', function (t) { + serverTestTrustIP(t, 'http', 'inet') +})