diff --git a/client.js b/client.js index 464e7bee..af543af7 100644 --- a/client.js +++ b/client.js @@ -36,7 +36,7 @@ function Client (peerId, port, torrent, opts) { // required self._peerId = Buffer.isBuffer(peerId) ? peerId - : new Buffer(peerId, 'utf8') + : new Buffer(peerId, 'hex') self._port = port self._infoHash = Buffer.isBuffer(torrent.infoHash) ? torrent.infoHash diff --git a/lib/common.js b/lib/common.js index 727e239c..1e2b7928 100644 --- a/lib/common.js +++ b/lib/common.js @@ -4,6 +4,9 @@ var querystring = require('querystring') +exports.IPV4_RE = /^[\d\.]+$/ +exports.IPV6_RE = /^[\da-fA-F:]+$/ + exports.NUM_ANNOUNCE_PEERS = 50 exports.MAX_ANNOUNCE_PEERS = 82 @@ -15,7 +18,7 @@ exports.EVENT_IDS = { 1: 'completed', 2: 'started', 3: 'stopped' -}; +} function toUInt32 (n) { var buf = new Buffer(4) @@ -24,8 +27,12 @@ function toUInt32 (n) { } exports.toUInt32 = toUInt32 -exports.binaryToUtf8 = function (str) { - return new Buffer(str, 'binary').toString('utf8') +exports.binaryToHex = function (str) { + return new Buffer(str, 'binary').toString('hex') +} + +exports.hexToBinary = function (str) { + return new Buffer(str, 'hex').toString('binary') } /** diff --git a/lib/parse_http.js b/lib/parse_http.js index c6b67d71..fe1c17b2 100644 --- a/lib/parse_http.js +++ b/lib/parse_http.js @@ -2,7 +2,7 @@ module.exports = parseHttpRequest var common = require('./common') -var REMOVE_IPV6_RE = /^::ffff:/ +var REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ function parseHttpRequest (req, options) { var s = req.url.split('?') @@ -11,48 +11,44 @@ function parseHttpRequest (req, options) { if (s[0] === '/announce') { params.action = common.ACTIONS.ANNOUNCE - params.peer_id = typeof params.peer_id === 'string' && common.binaryToUtf8(params.peer_id) - params.port = Number(params.port) + if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) + throw new Error('invalid info_hash') + params.info_hash = common.binaryToHex(params.info_hash) + if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) + throw new Error('invalid peer_id') + params.peer_id = common.binaryToHex(params.peer_id) - if (typeof params.info_hash !== 'string') throw new Error('invalid info_hash') - if (params.info_hash.length !== 20) throw new Error('invalid info_hash length') - if (typeof params.peer_id !== 'string') throw new Error('invalid peer_id') - if (params.peer_id.length !== 20) throw new Error('invalid peer_id length') + params.port = Number(params.port) if (!params.port) throw new Error('invalid port') params.left = Number(params.left) params.compact = Number(params.compact) - - params.ip = options.trustProxy - ? req.headers['x-forwarded-for'] || req.connection.remoteAddress - : req.connection.remoteAddress.replace(REMOVE_IPV6_RE, '') // force ipv4 - params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets? - params.numwant = Math.min( Number(params.numwant) || common.NUM_ANNOUNCE_PEERS, common.MAX_ANNOUNCE_PEERS ) - return params + params.ip = options.trustProxy + ? req.headers['x-forwarded-for'] || req.connection.remoteAddress + : req.connection.remoteAddress.replace(REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + params.addr = (common.IPV6_RE.test(params.ip) ? '[' + params.ip + ']' : params.ip) + ':' + params.port + } else if (s[0] === '/scrape') { // unofficial scrape message params.action = common.ACTIONS.SCRAPE - - if (typeof params.info_hash === 'string') { + if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] - } - - if (params.info_hash) { - if (!Array.isArray(params.info_hash)) throw new Error('invalid info_hash array') - params.info_hash.forEach(function (infoHash) { - if (infoHash.length !== 20) { + if (Array.isArray(params.info_hash)) { + params.info_hash = params.info_hash.map(function (binaryInfoHash) { + if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) throw new Error('invalid info_hash') - } + return common.binaryToHex(binaryInfoHash) }) } - return params } else { - return null + throw new Error('Invalid action in HTTP request: ' + params.action) } + + return params } diff --git a/lib/parse_udp.js b/lib/parse_udp.js index 2122d965..0e7a0089 100644 --- a/lib/parse_udp.js +++ b/lib/parse_udp.js @@ -5,13 +5,11 @@ var ipLib = require('ip') var common = require('./common') function parseUdpRequest (msg, rinfo) { - if (msg.length < 16) { + if (msg.length < 16) throw new Error('received packet is too short') - } - if (rinfo.family !== 'IPv4') { + if (rinfo.family !== 'IPv4') throw new Error('udp tracker does not support IPv6') - } var params = { connectionId: msg.slice(0, 8), // 64-bit @@ -20,41 +18,46 @@ function parseUdpRequest (msg, rinfo) { } // TODO: randomize - if (!bufferEqual(params.connectionId, common.CONNECTION_ID)) { + if (!bufferEqual(params.connectionId, common.CONNECTION_ID)) throw new Error('received packet with invalid connection id') - } if (params.action === common.ACTIONS.CONNECT) { // No further params + } else if (params.action === common.ACTIONS.ANNOUNCE) { - params.info_hash = msg.slice(16, 36).toString('binary') // 20 bytes - params.peer_id = msg.slice(36, 56).toString('utf8') // 20 bytes + params.info_hash = msg.slice(16, 36).toString('hex') // 20 bytes + params.peer_id = msg.slice(36, 56).toString('hex') // 20 bytes params.downloaded = fromUInt64(msg.slice(56, 64)) // TODO: track this? params.left = fromUInt64(msg.slice(64, 72)) params.uploaded = fromUInt64(msg.slice(72, 80)) // TODO: track this? - params.event = msg.readUInt32BE(80) - params.event = common.EVENT_IDS[params.event] + + params.event = common.EVENT_IDS[msg.readUInt32BE(80)] if (!params.event) throw new Error('invalid event') // early return - params.ip = msg.readUInt32BE(84) // optional - params.ip = params.ip ? - ipLib.toString(params.ip) : - rinfo.address + + var ip = msg.readUInt32BE(84) // optional + params.ip = ip + ? ipLib.toString(ip) + : rinfo.address + params.key = msg.readUInt32BE(88) // TODO: what is this for? - params.numwant = msg.readUInt32BE(92) // optional + // never send more than MAX_ANNOUNCE_PEERS or else the UDP packet will get bigger than // 512 bytes which is not safe - params.numwant = Math.min(params.numwant || common.NUM_ANNOUNCE_PEERS, common.MAX_ANNOUNCE_PEERS) + params.numwant = Math.min( + msg.readUInt32BE(92) || common.NUM_ANNOUNCE_PEERS, // optional + common.MAX_ANNOUNCE_PEERS + ) + params.port = msg.readUInt16BE(96) || rinfo.port // optional params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets params.compact = 1 // udp is always compact } else if (params.action === common.ACTIONS.SCRAPE) { // scrape message - params.info_hash = msg.slice(16, 36).toString('binary') // 20 bytes - // TODO: support multiple info_hash scrape - if (msg.length > 36) { - throw new Error('multiple info_hash scrape not supported') - } + if (msg.length > 36) throw new Error('multiple info_hash scrape not supported') + + params.info_hash = [ msg.slice(16, 36).toString('hex') ] // 20 bytes + } else { throw new Error('Invalid action in UDP packet: ' + params.action) } diff --git a/server.js b/server.js index 5b31b93d..ee3cccd9 100644 --- a/server.js +++ b/server.js @@ -94,7 +94,10 @@ Server.prototype.listen = function (port, onlistening) { function onPort (err, port) { if (err) return self.emit('error', err) self.port = port - self._httpServer && self._httpServer.listen(port.http || port) + // ATTENTION: + // binding to :: only receives IPv4 connections if the bindv6only + // sysctl is set 0, which is the default on many operating systems. + self._httpServer && self._httpServer.listen(port.http || port, '::') self._udpServer && self._udpServer.bind(port.udp || port) } @@ -115,13 +118,11 @@ Server.prototype.close = function (cb) { } } -Server.prototype.getSwarm = function (binaryInfoHash) { +Server.prototype.getSwarm = function (infoHash) { var self = this - if (Buffer.isBuffer(binaryInfoHash)) binaryInfoHash = binaryInfoHash.toString('binary') - var swarm = self.torrents[binaryInfoHash] - if (!swarm) { - swarm = self.torrents[binaryInfoHash] = new Swarm(binaryInfoHash, this) - } + if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') + var swarm = self.torrents[infoHash] + if (!swarm) swarm = self.torrents[infoHash] = new Swarm(infoHash, this) return swarm } @@ -130,9 +131,7 @@ Server.prototype._onHttpRequest = function (req, res) { var params try { - params = parseHttpRequest(req, { - trustProxy: self._trustProxy - }) + params = parseHttpRequest(req, { trustProxy: self._trustProxy }) } catch (err) { debug('sent error %s', err.message) res.end(bencode.encode({ @@ -142,7 +141,7 @@ Server.prototype._onHttpRequest = function (req, res) { // even though it's an error for the client, it's just a warning for the server. // don't crash the server because a client sent bad data :) self.emit('warning', err) - + return } @@ -212,11 +211,23 @@ Server.prototype._onAnnounce = function (params, cb) { if (response) { if (!response.action) response.action = common.ACTIONS.ANNOUNCE if (!response.intervalMs) response.intervalMs = self._intervalMs + if (params.compact === 1) { - response.peers = string2compact(response.peers.map(function (peer) { - return peer.ip + ':' + peer.port // TODO: ipv6 brackets + var peers = response.peers + // Find IPv4 peers + response.peers = string2compact(peers.filter(function (peer) { + return common.IPV4_RE.test(peer.ip) + }).map(function (peer) { + return peer.ip + ':' + peer.port + })) + // Find IPv6 peers + response.peers6 = string2compact(peers.filter(function (peer) { + return common.IPV6_RE.test(peer.ip) + }).map(function (peer) { + return '[' + peer.ip + ']:' + peer.port })) } + // IPv6 peers are not separate for non-compact responses } cb(err, response) }) @@ -225,28 +236,12 @@ Server.prototype._onAnnounce = function (params, cb) { Server.prototype._onScrape = function (params, cb) { var self = this - if (typeof params.info_hash === 'string') { - params.info_hash = [ params.info_hash ] - } else if (params.info_hash == null) { + if (params.info_hash == null) { // if info_hash param is omitted, stats for all torrents are returned // TODO: make this configurable! params.info_hash = Object.keys(self.torrents) } - if (!Array.isArray(params.info_hash)) { - var err = new Error('invalid info_hash') - self.emit('warning', err) - return cb(err) - } - - var response = { - action: common.ACTIONS.SCRAPE, - files: {}, - flags: { - min_request_interval: self._intervalMs - } - } - series(params.info_hash.map(function (infoHash) { var swarm = self.getSwarm(infoHash) return function (cb) { @@ -261,8 +256,14 @@ Server.prototype._onScrape = function (params, cb) { }), function (err, results) { if (err) return cb(err) + var response = { + action: common.ACTIONS.SCRAPE, + files: {}, + flags: { min_request_interval: self._intervalMs } + } + results.forEach(function (result) { - response.files[result.infoHash] = { + response.files[common.hexToBinary(result.infoHash)] = { complete: result.complete, incomplete: result.incomplete, downloaded: result.complete // TODO: this only provides a lower-bound @@ -273,7 +274,6 @@ Server.prototype._onScrape = function (params, cb) { }) } - function makeUdpPacket (params) { switch (params.action) { case common.ACTIONS.CONNECT: diff --git a/test/server.js b/test/server.js index f6ddd8dd..da0a6e70 100644 --- a/test/server.js +++ b/test/server.js @@ -2,16 +2,19 @@ var Client = require('../') var Server = require('../').Server var test = require('tape') -var infoHash = new Buffer('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex') -var peerId = '01234567890123456789' -var peerId2 = '12345678901234567890' +var infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' +var peerId = new Buffer('01234567890123456789') +var peerId2 = new Buffer('12345678901234567890') var torrentLength = 50000 -function serverTest (t, serverType) { +function serverTest (t, serverType, serverFamily) { t.plan(26) var opts = serverType === 'http' ? { udp: false } : { http: false } var server = new Server(opts) + var serverAddr = serverFamily === 'inet6' ? '[::1]' : '127.0.0.1' + var clientAddr = serverFamily === 'inet6' ? '[::1]' : '127.0.0.1' + var clientIp = serverFamily === 'inet6' ? '::1' : '127.0.0.1' server.on('error', function (err) { t.fail(err.message) @@ -26,7 +29,7 @@ function serverTest (t, serverType) { }) server.listen(function (port) { - var announceUrl = serverType + '://127.0.0.1:' + port + '/announce' + var announceUrl = serverType + '://' + serverAddr + ':' + port + '/announce' var client = new Client(peerId, 6881, { infoHash: infoHash, @@ -49,10 +52,10 @@ function serverTest (t, serverType) { t.equal(server.getSwarm(infoHash).complete, 0) t.equal(server.getSwarm(infoHash).incomplete, 1) t.equal(Object.keys(server.getSwarm(infoHash).peers).length, 1) - t.deepEqual(server.getSwarm(infoHash).peers['127.0.0.1:6881'], { - ip: '127.0.0.1', + t.deepEqual(server.getSwarm(infoHash).peers[clientAddr + ':6881'], { + ip: clientIp, port: 6881, - peerId: peerId + peerId: peerId.toString('hex') }) client.complete() @@ -83,7 +86,7 @@ function serverTest (t, serverType) { }) client2.once('peer', function (addr) { - t.equal(addr, '127.0.0.1:6881') + t.equal(addr, clientAddr + ':6881') client2.stop() client2.once('update', function (data) { @@ -109,10 +112,14 @@ function serverTest (t, serverType) { }) } -test('http server', function (t) { - serverTest(t, 'http') +test('http ipv4 server', function (t) { + serverTest(t, 'http', 'inet') +}) + +test('http ipv6 server', function (t) { + serverTest(t, 'http', 'inet6') }) test('udp server', function (t) { - serverTest(t, 'udp') + serverTest(t, 'udp', 'inet') })