diff --git a/lib/common.js b/lib/common.js index ccf1bd41..727e239c 100644 --- a/lib/common.js +++ b/lib/common.js @@ -4,9 +4,18 @@ var querystring = require('querystring') +exports.NUM_ANNOUNCE_PEERS = 50 +exports.MAX_ANNOUNCE_PEERS = 82 + exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ]) exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 } +exports.EVENT_IDS = { + 0: 'update', + 1: 'completed', + 2: 'started', + 3: 'stopped' +}; function toUInt32 (n) { var buf = new Buffer(4) diff --git a/lib/parse_http.js b/lib/parse_http.js new file mode 100644 index 00000000..85e35197 --- /dev/null +++ b/lib/parse_http.js @@ -0,0 +1,58 @@ +var common = require('./common') + +var REMOVE_IPV6_RE = /^::ffff:/ + +module.exports = parseHttpRequest + +function parseHttpRequest (req, options) { + var s = req.url.split('?') + var params = common.querystringParse(s[1]) + + 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') 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') + 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 + } else if (s[0] === '/scrape') { // unofficial scrape message + params.action = common.ACTIONS.SCRAPE + + 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) { + throw new Error('invalid info_hash') + } + }) + } + + return params + } else { + return null + } +} diff --git a/lib/parse_udp.js b/lib/parse_udp.js new file mode 100644 index 00000000..7e5ccdfc --- /dev/null +++ b/lib/parse_udp.js @@ -0,0 +1,80 @@ +var bufferEqual = require('buffer-equal') +var ipLib = require('ip') +var common = require('./common') + + +module.exports = parseUdpRequest + +function parseUdpRequest (msg, rinfo) { + if (msg.length < 16) { + throw new Error('received packet is too short') + } + + if (rinfo.family !== 'IPv4') { + throw new Error('udp tracker does not support IPv6') + } + + var params = { + connectionId: msg.slice(0, 8), // 64-bit + action: msg.readUInt32BE(8), + transactionId: msg.readUInt32BE(12) + } + + // TODO: randomize: + 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.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] + 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 + 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.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') + } + } else { + throw new Error('Invalid action in UDP packet: ' + params.action) + } + + return params +} + +// HELPER FUNCTIONS + +var TWO_PWR_32 = (1 << 16) * 2 + +/** + * Return the closest floating-point representation to the buffer value. Precision will be + * lost for big numbers. + */ +function fromUInt64 (buf) { + var high = buf.readUInt32BE(0) | 0 // force + var low = buf.readUInt32BE(4) | 0 + var lowUnsigned = (low >= 0) ? low : TWO_PWR_32 + low + + return high * TWO_PWR_32 + lowUnsigned +} diff --git a/lib/swarm.js b/lib/swarm.js new file mode 100644 index 00000000..40e4577e --- /dev/null +++ b/lib/swarm.js @@ -0,0 +1,118 @@ +var debug = require('debug')('bittorrent-tracker') + +module.exports = Swarm + +// Regard this as the default implementation of an interface that you +// need to support when overriding Server.getSwarm() +function Swarm (infoHash, server) { + this.peers = {} + this.complete = 0 + this.incomplete = 0 + this.emit = server.emit.bind(server) +} + +Swarm.prototype.announce = function (params, cb) { + var self = this + var peer = self.peers[params.addr] + + // Dispatch announce event + if (!params.event || params.event === 'empty') params.event = 'update' + var fn = '_onAnnounce_' + params.event + if (self[fn]) { + self[fn](params, peer, function (err) { + // event processed, prepare response: + + if (params.left === 0 && peer) peer.complete = true + + // send peers + var peers = self._getPeers(params.numwant) + + cb(null, { + complete: self.complete, + incomplete: self.incomplete, + peers: peers + }) + }) + } else { + cb(new Error('invalid event')) + } +} + +Swarm.prototype._onAnnounce_started = function (params, peer, cb) { + if (peer) { + debug('unexpected `started` event from peer that is already in swarm') + return this._onAnnounce_update() // treat as an update + } + if (params.left === 0) this.complete += 1 + else this.incomplete += 1 + peer = this.peers[params.addr] = { + ip: params.ip, + port: params.port, + peerId: params.peer_id + } + this.emit('start', params.addr) + + cb() +} + +Swarm.prototype._onAnnounce_stopped = function (params, peer, cb) { + if (!peer) { + debug('unexpected `stopped` event from peer that is not in swarm') + return // do nothing + } + if (peer.complete) this.complete -= 1 + else this.incomplete -= 1 + this.peers[params.addr] = null + this.emit('stop', params.addr) + + cb() +} + +Swarm.prototype._onAnnounce_completed = function (params, peer, cb) { + if (!peer) { + debug('unexpected `completed` event from peer that is not in swarm') + return start() // treat as a start + } + if (peer.complete) { + debug('unexpected `completed` event from peer that is already marked as completed') + return // do nothing + } + this.complete += 1 + this.incomplete -= 1 + peer.complete = true + this.emit('complete', params.addr) + + cb() +} + +Swarm.prototype._onAnnounce_update = function (params, peer, cb) { + if (!peer) { + debug('unexpected `update` event from peer that is not in swarm') + return start() // treat as a start + } + this.emit('update', params.addr) + + cb() +} + +Swarm.prototype._getPeers = function (numwant) { + var peers = [] + for (var peerId in this.peers) { + if (peers.length >= numwant) break + var peer = this.peers[peerId] + if (!peer) continue // ignore null values + peers.push({ + 'peer id': peer.peerId, + ip: peer.ip, + port: peer.port + }) + } + return peers +} + +Swarm.prototype.scrape = function (infoHash, params, cb) { + cb(null, { + complete: this.complete, + incomplete: this.incomplete + }) +} diff --git a/package.json b/package.json index a83fc5c5..e002d8a3 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "ip": "^0.3.0", "once": "^1.3.0", "portfinder": "^0.2.1", + "run-series": "^1.0.2", "string2compact": "^1.1.1" }, "devDependencies": { diff --git a/server.js b/server.js index 3370b561..ed1a2296 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,23 @@ module.exports = Server var bencode = require('bencode') -var bufferEqual = require('buffer-equal') -var common = require('./lib/common') var debug = require('debug')('bittorrent-tracker') var dgram = require('dgram') var EventEmitter = require('events').EventEmitter var http = require('http') var inherits = require('inherits') -var ipLib = require('ip') var portfinder = require('portfinder') +var series = require('run-series') var string2compact = require('string2compact') +var common = require('./lib/common') +var Swarm = require('./lib/swarm') +var parseHttpRequest = require('./lib/parse_http') +var parseUdpRequest = require('./lib/parse_udp') + // Use random port above 1024 portfinder.basePort = Math.floor(Math.random() * 60000) + 1025 -var NUM_ANNOUNCE_PEERS = 50 -var MAX_ANNOUNCE_PEERS = 82 -var REMOVE_IPV6_RE = /^::ffff:/ inherits(Server, EventEmitter) @@ -115,410 +115,209 @@ Server.prototype.close = function (cb) { } } -Server.prototype.getSwarm = function (infoHash) { - var self = this - var binaryInfoHash = Buffer.isBuffer(infoHash) - ? infoHash.toString('binary') - : new Buffer(infoHash, 'hex').toString('binary') - return self._getSwarm(binaryInfoHash) -} - -Server.prototype._getSwarm = function (binaryInfoHash) { +Server.prototype.getSwarm = function (binaryInfoHash) { var self = this + if (Buffer.isBuffer(binaryInfoHash)) binaryInfoHash = binaryInfoHash.toString('binary') var swarm = self.torrents[binaryInfoHash] if (!swarm) { - swarm = self.torrents[binaryInfoHash] = { - complete: 0, - incomplete: 0, - peers: {} - } + swarm = self.torrents[binaryInfoHash] = new Swarm(binaryInfoHash, this) } return swarm } Server.prototype._onHttpRequest = function (req, res) { var self = this - var s = req.url.split('?') - var params = common.querystringParse(s[1]) - var response - if (s[0] === '/announce') { - var infoHash = typeof params.info_hash === 'string' && params.info_hash - var peerId = typeof params.peer_id === 'string' && common.binaryToUtf8(params.peer_id) - var port = Number(params.port) - - if (!infoHash) return error('invalid info_hash') - if (infoHash.length !== 20) return error('invalid info_hash') - if (!peerId) return error('invalid peer_id') - if (peerId.length !== 20) return error('invalid peer_id') - if (!port) return error('invalid port') - - var left = Number(params.left) - var compact = Number(params.compact) - - var ip = self._trustProxy - ? req.headers['x-forwarded-for'] || req.connection.remoteAddress - : req.connection.remoteAddress.replace(REMOVE_IPV6_RE, '') // force ipv4 - var addr = ip + ':' + port - var swarm = self._getSwarm(infoHash) - var peer = swarm.peers[addr] - - var numWant = Math.min( - Number(params.numwant) || NUM_ANNOUNCE_PEERS, - MAX_ANNOUNCE_PEERS - ) - - var start = function () { - if (peer) { - debug('unexpected `started` event from peer that is already in swarm') - return update() // treat as an update - } - if (left === 0) swarm.complete += 1 - else swarm.incomplete += 1 - peer = swarm.peers[addr] = { - ip: ip, - port: port, - peerId: peerId - } - self.emit('start', addr) - } - - var stop = function () { - if (!peer) { - debug('unexpected `stopped` event from peer that is not in swarm') - return // do nothing - } - if (peer.complete) swarm.complete -= 1 - else swarm.incomplete -= 1 - swarm.peers[addr] = null - self.emit('stop', addr) - } - - var complete = function () { - if (!peer) { - debug('unexpected `completed` event from peer that is not in swarm') - return start() // treat as a start - } - if (peer.complete) { - debug('unexpected `completed` event from peer that is already marked as completed') - return // do nothing - } - swarm.complete += 1 - swarm.incomplete -= 1 - peer.complete = true - self.emit('complete', addr) - } - - var update = function () { - if (!peer) { - debug('unexpected `update` event from peer that is not in swarm') - return start() // treat as a start - } - self.emit('update', addr) - } - - switch (params.event) { - case 'started': - start() - break - case 'stopped': - stop() - break - case 'completed': - complete() - break - case '': case undefined: // update - update() - break - default: - return error('invalid event') // early return - } - - if (left === 0 && peer) peer.complete = true - - // send peers - var peers = compact === 1 - ? self._getPeersCompact(swarm, numWant) - : self._getPeers(swarm, numWant) - - response = { - complete: swarm.complete, - incomplete: swarm.incomplete, - peers: peers, - interval: self._intervalMs - } - - res.end(bencode.encode(response)) - debug('sent response %s', response) - - } else if (s[0] === '/scrape') { // unofficial scrape message - if (typeof params.info_hash === 'string') { - params.info_hash = [ params.info_hash ] - } else if (params.info_hash == null) { - // if info_hash param is omitted, stats for all torrents are returned - params.info_hash = Object.keys(self.torrents) - } - - if (!Array.isArray(params.info_hash)) return error('invalid info_hash') - - response = { - files: {}, - flags: { - min_request_interval: self._intervalMs - } - } - - params.info_hash.some(function (infoHash) { - if (infoHash.length !== 20) { - error('invalid info_hash') - return true // early return - } - - var swarm = self._getSwarm(infoHash) - - response.files[infoHash] = { - complete: swarm.complete, - incomplete: swarm.incomplete, - downloaded: swarm.complete // TODO: this only provides a lower-bound - } + var error + var params + try { + params = parseHttpRequest(req, { + trustProxy: self._trustProxy }) - - res.end(bencode.encode(response)) - debug('sent response %s', response) - - } else { - error('only /announce and /scrape are valid endpoints') + } catch (err) { + error = err } - function error (message) { - debug('sent error %s', message) + if (!error && !params) error = new Error('Empty HTTP parameters') + if (error) { + debug('sent error %s', error.message) res.end(bencode.encode({ - 'failure reason': message + 'failure reason': error.message })) // 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', new Error(message)) + self.emit('warning', error) + + return } + + this._onRequest(params, function (err, response) { + if (err) { + self.emit('warning', err) + response = { + 'failure reason': err.message + } + } + + delete response.action // only needed for UDP encoding + res.end(bencode.encode(response)) + }) } Server.prototype._onUdpRequest = function (msg, rinfo) { var self = this - if (msg.length < 16) { - return error('received packet is too short') - } - - if (rinfo.family !== 'IPv4') { - return error('udp tracker does not support IPv6') - } - - var connectionId = msg.slice(0, 8) // 64-bit - var action = msg.readUInt32BE(8) - var transactionId = msg.readUInt32BE(12) - - if (!bufferEqual(connectionId, common.CONNECTION_ID)) { - return error('received packet with invalid connection id') + var params + try { + params = parseUdpRequest(msg, rinfo) + } catch (err) { + self.emit('warning', err) + // Do not reply for parsing errors + return } - var socket = dgram.createSocket('udp4') - - var infoHash, swarm - if (action === common.ACTIONS.CONNECT) { - send(Buffer.concat([ - common.toUInt32(common.ACTIONS.CONNECT), - common.toUInt32(transactionId), - connectionId - ])) - } else if (action === common.ACTIONS.ANNOUNCE) { - infoHash = msg.slice(16, 36).toString('binary') // 20 bytes - var peerId = msg.slice(36, 56).toString('utf8') // 20 bytes - var downloaded = fromUInt64(msg.slice(56, 64)) // TODO: track this? - var left = fromUInt64(msg.slice(64, 72)) - var uploaded = fromUInt64(msg.slice(72, 80)) // TODO: track this? - var event = msg.readUInt32BE(80) - var ip = msg.readUInt32BE(84) // optional - var key = msg.readUInt32BE(88) // TODO: what is this for? - var numWant = msg.readUInt32BE(92) // optional - var port = msg.readUInt16BE(96) // optional - - if (ip) { - ip = ipLib.toString(ip) - } else { - ip = rinfo.address - } - - if (!port) { - port = rinfo.port - } - - var addr = ip + ':' + port - - swarm = self._getSwarm(infoHash) - var peer = swarm.peers[addr] - - // never send more than MAX_ANNOUNCE_PEERS or else the UDP packet will get bigger than - // 512 bytes which is not safe - numWant = Math.min(numWant || NUM_ANNOUNCE_PEERS, MAX_ANNOUNCE_PEERS) - - var start = function () { - if (peer) { - debug('unexpected `started` event from peer that is already in swarm') - return update() // treat as an update + // Handle + this._onRequest(params, function (err, response) { + if (err) { + self.emit('warning', err) + response = { + action: common.ACTIONS.ERRROR, + 'failure reason': err.message } - if (left === 0) swarm.complete += 1 - else swarm.incomplete += 1 - peer = swarm.peers[addr] = { - ip: ip, - port: port, - peerId: peerId - } - self.emit('start', addr) - } - - var stop = function () { - if (!peer) { - debug('unexpected `stopped` event from peer that is not in swarm') - return // do nothing - } - if (peer.complete) swarm.complete -= 1 - else swarm.incomplete -= 1 - swarm.peers[addr] = null - self.emit('stop', addr) } - - var complete = function () { - if (!peer) { - debug('unexpected `completed` event from peer that is not in swarm') - return start() // treat as a start - } - if (peer.complete) { - debug('unexpected `completed` event from peer that is already marked as completed') - return // do nothing - } - swarm.complete += 1 - swarm.incomplete -= 1 - peer.complete = true - self.emit('complete', addr) - } - - var update = function () { - if (!peer) { - debug('unexpected `update` event from peer that is not in swarm') - return start() // treat as a start - } - self.emit('update', addr) - } - - switch (event) { - case common.EVENTS.started: - start() - break - case common.EVENTS.stopped: - stop() - break - case common.EVENTS.completed: - complete() - break - case common.EVENTS.update: // update - update() - break - default: - return error('invalid event') // early return - } - - if (left === 0 && peer) peer.complete = true - - // send peers - var peers = self._getPeersCompact(swarm, numWant) - - send(Buffer.concat([ - common.toUInt32(common.ACTIONS.ANNOUNCE), - common.toUInt32(transactionId), - common.toUInt32(self._intervalMs), - common.toUInt32(swarm.incomplete), - common.toUInt32(swarm.complete), - peers - ])) - - } else if (action === common.ACTIONS.SCRAPE) { // scrape message - infoHash = msg.slice(16, 36).toString('binary') // 20 bytes - - // TODO: support multiple info_hash scrape - if (msg.length > 36) { - error('multiple info_hash scrape not supported') - } - - swarm = self._getSwarm(infoHash) - - send(Buffer.concat([ - common.toUInt32(common.ACTIONS.SCRAPE), - common.toUInt32(transactionId), - common.toUInt32(swarm.complete), - common.toUInt32(swarm.complete), // TODO: this only provides a lower-bound - common.toUInt32(swarm.incomplete) - ])) - } - - function send (buf) { - debug('sent response %s', buf.toString('hex')) + + var socket = dgram.createSocket('udp4') + response.transactionId = params.transactionId + response.connectionId = params.connectionId + var buf = makeUdpPacket(response) socket.send(buf, 0, buf.length, rinfo.port, rinfo.address, function () { try { socket.close() } catch (err) {} }) - } - - function error (message) { - debug('sent error %s', message) - send(Buffer.concat([ - common.toUInt32(common.ACTIONS.ERROR), - common.toUInt32(transactionId || 0), - new Buffer(message, 'utf8') - ])) - self.emit('warning', new Error(message)) - } + }) } -Server.prototype._getPeers = function (swarm, numWant) { - var peers = [] - for (var peerId in swarm.peers) { - if (peers.length >= numWant) break - var peer = swarm.peers[peerId] - if (!peer) continue // ignore null values - peers.push({ - 'peer id': peer.peerId, - ip: peer.ip, - port: peer.port - }) +Server.prototype._onRequest = function (params, cb) { + var response + if (params && params.action === common.ACTIONS.CONNECT) { + cb(null, { action: common.ACTIONS.CONNECT }) + } else if (params && params.action === common.ACTIONS.ANNOUNCE) { + this._onAnnounce(params, cb) + } else if (params && params.action === common.ACTIONS.SCRAPE) { + this._onScrape(params, cb) + } else { + cb(new Error('Invalid action')) } - return peers } -Server.prototype._getPeersCompact = function (swarm, numWant) { - var peers = [] +Server.prototype._onAnnounce = function (params, cb) { + var self = this + var swarm = self.getSwarm(params.info_hash) + swarm.announce(params, function (err, response) { + 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 + })) + } + } + cb(err, response) + }) +} - for (var peerId in swarm.peers) { - if (peers.length >= numWant) break - var peer = swarm.peers[peerId] - if (!peer) continue // ignore null values - peers.push(peer.ip + ':' + peer.port) +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 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 + } } - return string2compact(peers) -} - -// HELPER FUNCTIONS + series(params.info_hash.map(function (infoHash) { + var swarm = self.getSwarm(infoHash) + return function (cb) { + swarm.scrape(infoHash, params, function (err, scrapeInfo) { + cb(err, scrapeInfo && { + infoHash: infoHash, + complete: scrapeInfo.complete || 0, + incomplete: scrapeInfo.incomplete || 0 + }) + }) + } + }), function (err, results) { + if (err) return cb(err) + + results.forEach(function (result) { + response.files[result.infoHash] = { + complete: result.complete, + incomplete: result.incomplete, + downloaded: result.complete // TODO: this only provides a lower-bound + } + }) -var TWO_PWR_32 = (1 << 16) * 2 + cb(null, response) + }) +} -/** - * Return the closest floating-point representation to the buffer value. Precision will be - * lost for big numbers. - */ -function fromUInt64 (buf) { - var high = buf.readUInt32BE(0) | 0 // force - var low = buf.readUInt32BE(4) | 0 - var lowUnsigned = (low >= 0) ? low : TWO_PWR_32 + low - return high * TWO_PWR_32 + lowUnsigned +function makeUdpPacket (params) { + switch (params.action) { + case common.ACTIONS.CONNECT: + return Buffer.concat([ + common.toUInt32(common.ACTIONS.CONNECT), + common.toUInt32(params.transactionId), + params.connectionId + ]) + case common.ACTIONS.ANNOUNCE: + return Buffer.concat([ + common.toUInt32(common.ACTIONS.ANNOUNCE), + common.toUInt32(params.transactionId), + common.toUInt32(params.intervalMs), + common.toUInt32(params.incomplete), + common.toUInt32(params.complete), + params.peers + ]) + case common.ACTIONS.SCRAPE: + var firstInfoHash = Object.keys(params.files)[0] + var scrapeInfo = firstInfoHash ? { + complete: params.files[firstInfoHash].complete, + incomplete: params.files[firstInfoHash].incomplete, + completed: params.files[firstInfoHash].complete // TODO: this only provides a lower-bound + } : {} + return Buffer.concat([ + common.toUInt32(common.ACTIONS.SCRAPE), + common.toUInt32(params.transactionId), + common.toUInt32(scrapeInfo.complete), + common.toUInt32(scrapeInfo.completed), + common.toUInt32(scrapeInfo.incomplete) + ]) + case common.ACTIONS.ERROR: + return Buffer.concat([ + common.toUInt32(common.ACTIONS.ERROR), + common.toUInt32(params.transactionId || 0), + new Buffer(params.message, 'utf8') + ]) + default: + throw new Error('Action not implemented: ' + params.action) + } } diff --git a/test/server.js b/test/server.js index c27b4f5a..f6ddd8dd 100644 --- a/test/server.js +++ b/test/server.js @@ -2,7 +2,7 @@ var Client = require('../') var Server = require('../').Server var test = require('tape') -var infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' +var infoHash = new Buffer('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex') var peerId = '01234567890123456789' var peerId2 = '12345678901234567890' var torrentLength = 50000