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/examples/express-embed/package.json b/examples/express-embed/package.json new file mode 100644 index 00000000..07eceecf --- /dev/null +++ b/examples/express-embed/package.json @@ -0,0 +1,13 @@ +{ + "name": "bittorrent-tracker-example-express-embed", + "version": "0.0.0", + "description": "Example for embedding bittorrent-tracker server in express.js", + "scripts": { + "server": "./server.js" + }, + "author": "Astro ", + "license": "MIT", + "dependencies": { + "express": "^4.10.5" + } +} diff --git a/examples/express-embed/server.js b/examples/express-embed/server.js new file mode 100755 index 00000000..294f0137 --- /dev/null +++ b/examples/express-embed/server.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +var Server = require('../..').Server +var express = require('express') +var app = express() + +var server = new Server({ + http: false, // we do our own + udp: false // not interested +}) + +var onHttpRequest = server.onHttpRequest.bind(server) +app.get('/announce', onHttpRequest) +app.get('/scrape', onHttpRequest) + +app.listen(8080) diff --git a/lib/common.js b/lib/common.js index 727e239c..aadc7053 100644 --- a/lib/common.js +++ b/lib/common.js @@ -15,7 +15,7 @@ exports.EVENT_IDS = { 1: 'completed', 2: 'started', 3: 'stopped' -}; +} function toUInt32 (n) { var buf = new Buffer(4) @@ -24,8 +24,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..9a4a95b1 100644 --- a/lib/parse_http.js +++ b/lib/parse_http.js @@ -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.numwant = Math.min( + Number(params.numwant) || common.NUM_ANNOUNCE_PEERS, + common.MAX_ANNOUNCE_PEERS + ) 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 } 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..e3b19eba 100644 --- a/server.js +++ b/server.js @@ -54,7 +54,7 @@ function Server (opts) { // default to starting an http server unless the user explictly says no if (opts.http !== false) { self._httpServer = http.createServer() - self._httpServer.on('request', self._onHttpRequest.bind(self)) + self._httpServer.on('request', self.onHttpRequest.bind(self)) self._httpServer.on('error', self._onError.bind(self)) self._httpServer.on('listening', onListening) } @@ -115,24 +115,20 @@ 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 } -Server.prototype._onHttpRequest = function (req, res) { +Server.prototype.onHttpRequest = function (req, res) { var self = this 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 +138,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 } @@ -225,28 +221,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 +241,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 +259,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..0dce512f 100644 --- a/test/server.js +++ b/test/server.js @@ -2,9 +2,9 @@ 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) { @@ -52,7 +52,7 @@ function serverTest (t, serverType) { t.deepEqual(server.getSwarm(infoHash).peers['127.0.0.1:6881'], { ip: '127.0.0.1', port: 6881, - peerId: peerId + peerId: peerId.toString('hex') }) client.complete()