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
9 changes: 9 additions & 0 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
58 changes: 58 additions & 0 deletions lib/parse_http.js
Original file line number Diff line number Diff line change
@@ -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 changes: 80 additions & 0 deletions lib/parse_udp.js
Original file line number Diff line number Diff line change
@@ -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 changes: 118 additions & 0 deletions lib/swarm.js
Original file line number Diff line number Diff line change
@@ -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
})
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading