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: 13 additions & 0 deletions examples/express-embed/package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"license": "MIT",
"dependencies": {
"express": "^4.10.5"
}
}
16 changes: 16 additions & 0 deletions examples/express-embed/server.js
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 7 additions & 3 deletions lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exports.EVENT_IDS = {
1: 'completed',
2: 'started',
3: 'stopped'
};
}

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

/**
Expand Down
42 changes: 19 additions & 23 deletions lib/parse_http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
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
47 changes: 16 additions & 31 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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({
Expand All @@ -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
}

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


function makeUdpPacket (params) {
switch (params.action) {
case common.ACTIONS.CONNECT:
Expand Down
8 changes: 4 additions & 4 deletions test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down