Merge pull request #45 from feross/refactor

Refactor the server-side code
This commit is contained in:
Feross Aboukhadijeh 2014-12-11 12:38:32 -08:00
commit 20cb18272e
7 changed files with 440 additions and 375 deletions

View File

@ -4,9 +4,18 @@
var querystring = require('querystring') var querystring = require('querystring')
exports.NUM_ANNOUNCE_PEERS = 50
exports.MAX_ANNOUNCE_PEERS = 82
exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ]) exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ])
exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 }
exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 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) { function toUInt32 (n) {
var buf = new Buffer(4) var buf = new Buffer(4)

58
lib/parse_http.js Normal file
View File

@ -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
}
}

80
lib/parse_udp.js Normal file
View File

@ -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
}

118
lib/swarm.js Normal file
View File

@ -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
})
}

View File

@ -26,6 +26,7 @@
"ip": "^0.3.0", "ip": "^0.3.0",
"once": "^1.3.0", "once": "^1.3.0",
"portfinder": "^0.2.1", "portfinder": "^0.2.1",
"run-series": "^1.0.2",
"string2compact": "^1.1.1" "string2compact": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {

547
server.js
View File

@ -1,23 +1,23 @@
module.exports = Server module.exports = Server
var bencode = require('bencode') var bencode = require('bencode')
var bufferEqual = require('buffer-equal')
var common = require('./lib/common')
var debug = require('debug')('bittorrent-tracker') var debug = require('debug')('bittorrent-tracker')
var dgram = require('dgram') var dgram = require('dgram')
var EventEmitter = require('events').EventEmitter var EventEmitter = require('events').EventEmitter
var http = require('http') var http = require('http')
var inherits = require('inherits') var inherits = require('inherits')
var ipLib = require('ip')
var portfinder = require('portfinder') var portfinder = require('portfinder')
var series = require('run-series')
var string2compact = require('string2compact') 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 // Use random port above 1024
portfinder.basePort = Math.floor(Math.random() * 60000) + 1025 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) inherits(Server, EventEmitter)
@ -115,410 +115,209 @@ Server.prototype.close = function (cb) {
} }
} }
Server.prototype.getSwarm = function (infoHash) { Server.prototype.getSwarm = function (binaryInfoHash) {
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) {
var self = this var self = this
if (Buffer.isBuffer(binaryInfoHash)) binaryInfoHash = binaryInfoHash.toString('binary')
var swarm = self.torrents[binaryInfoHash] var swarm = self.torrents[binaryInfoHash]
if (!swarm) { if (!swarm) {
swarm = self.torrents[binaryInfoHash] = { swarm = self.torrents[binaryInfoHash] = new Swarm(binaryInfoHash, this)
complete: 0,
incomplete: 0,
peers: {}
}
} }
return swarm return swarm
} }
Server.prototype._onHttpRequest = function (req, res) { Server.prototype._onHttpRequest = function (req, res) {
var self = this var self = this
var s = req.url.split('?') var error
var params = common.querystringParse(s[1]) var params
var response try {
if (s[0] === '/announce') { params = parseHttpRequest(req, {
var infoHash = typeof params.info_hash === 'string' && params.info_hash trustProxy: self._trustProxy
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
}
}) })
} catch (err) {
res.end(bencode.encode(response)) error = err
debug('sent response %s', response)
} else {
error('only /announce and /scrape are valid endpoints')
} }
function error (message) { if (!error && !params) error = new Error('Empty HTTP parameters')
debug('sent error %s', message) if (error) {
debug('sent error %s', error.message)
res.end(bencode.encode({ 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. // 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 :) // 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) { Server.prototype._onUdpRequest = function (msg, rinfo) {
var self = this var self = this
if (msg.length < 16) { var params
return error('received packet is too short') try {
params = parseUdpRequest(msg, rinfo)
} catch (err) {
self.emit('warning', err)
// Do not reply for parsing errors
return
} }
if (rinfo.family !== 'IPv4') { // Handle
return error('udp tracker does not support IPv6') this._onRequest(params, function (err, response) {
} if (err) {
self.emit('warning', err)
var connectionId = msg.slice(0, 8) // 64-bit response = {
var action = msg.readUInt32BE(8) action: common.ACTIONS.ERRROR,
var transactionId = msg.readUInt32BE(12) 'failure reason': err.message
if (!bufferEqual(connectionId, common.CONNECTION_ID)) {
return error('received packet with invalid connection id')
}
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
} }
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 () { var socket = dgram.createSocket('udp4')
if (!peer) { response.transactionId = params.transactionId
debug('unexpected `stopped` event from peer that is not in swarm') response.connectionId = params.connectionId
return // do nothing var buf = makeUdpPacket(response)
}
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'))
socket.send(buf, 0, buf.length, rinfo.port, rinfo.address, function () { socket.send(buf, 0, buf.length, rinfo.port, rinfo.address, function () {
try { try {
socket.close() socket.close()
} catch (err) {} } catch (err) {}
}) })
} })
}
function error (message) { Server.prototype._onRequest = function (params, cb) {
debug('sent error %s', message) var response
send(Buffer.concat([ if (params && params.action === common.ACTIONS.CONNECT) {
common.toUInt32(common.ACTIONS.ERROR), cb(null, { action: common.ACTIONS.CONNECT })
common.toUInt32(transactionId || 0), } else if (params && params.action === common.ACTIONS.ANNOUNCE) {
new Buffer(message, 'utf8') this._onAnnounce(params, cb)
])) } else if (params && params.action === common.ACTIONS.SCRAPE) {
self.emit('warning', new Error(message)) this._onScrape(params, cb)
} else {
cb(new Error('Invalid action'))
} }
} }
Server.prototype._getPeers = function (swarm, numWant) { Server.prototype._onAnnounce = function (params, cb) {
var peers = [] var self = this
for (var peerId in swarm.peers) { var swarm = self.getSwarm(params.info_hash)
if (peers.length >= numWant) break swarm.announce(params, function (err, response) {
var peer = swarm.peers[peerId] if (response) {
if (!peer) continue // ignore null values if (!response.action) response.action = common.ACTIONS.ANNOUNCE
peers.push({ if (!response.intervalMs) response.intervalMs = self._intervalMs
'peer id': peer.peerId, if (params.compact === 1) {
ip: peer.ip, response.peers = string2compact(response.peers.map(function (peer) {
port: peer.port return peer.ip + ':' + peer.port // TODO: ipv6 brackets
}))
}
}
cb(err, response)
})
}
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
}
}
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
}
}) })
cb(null, response)
})
}
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)
} }
return peers
}
Server.prototype._getPeersCompact = 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.ip + ':' + peer.port)
}
return string2compact(peers)
}
// 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
} }

View File

@ -2,7 +2,7 @@ var Client = require('../')
var Server = require('../').Server var Server = require('../').Server
var test = require('tape') var test = require('tape')
var infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' var infoHash = new Buffer('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex')
var peerId = '01234567890123456789' var peerId = '01234567890123456789'
var peerId2 = '12345678901234567890' var peerId2 = '12345678901234567890'
var torrentLength = 50000 var torrentLength = 50000