Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,7 +18,7 @@ exports.EVENT_IDS = {
1: 'completed',
2: 'started',
3: 'stopped'
};
}

function toUInt32 (n) {
var buf = new Buffer(4)
Expand All @@ -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')
}

/**
Expand Down
46 changes: 21 additions & 25 deletions lib/parse_http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('?')
Expand All @@ -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
}
45 changes: 24 additions & 21 deletions lib/parse_udp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
64 changes: 32 additions & 32 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

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

Expand All @@ -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({
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
})
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -273,7 +274,6 @@ Server.prototype._onScrape = function (params, cb) {
})
}


function makeUdpPacket (params) {
switch (params.action) {
case common.ACTIONS.CONNECT:
Expand Down
Loading