add bittorrent tracker server implementation!

This commit is contained in:
Feross Aboukhadijeh 2014-03-27 00:17:49 -07:00
parent 522f00230f
commit 31b82e0a2b
5 changed files with 264 additions and 11 deletions

View File

@ -9,7 +9,7 @@ clients. The requests include metrics from clients that help the tracker keep ov
statistics about the torrent. The response includes a peer list that helps the client
participate in the torrent.
Also see [BitTorrent DHT](https://github.com/feross/bittorrent-dht). This module is used
Also see [bittorrent-dht](https://github.com/feross/bittorrent-dht). This module is used
by [WebTorrent](http://webtorrent.io).
## install

184
index.js
View File

@ -1,20 +1,21 @@
exports.Client = Client
// TODO: exports.Server = Server
// TODO: support connecting to UDP trackers (http://www.bittorrent.org/beps/bep_0015.html)
exports.Server = Server
var bncode = require('bncode')
var compact2string = require('compact2string')
var EventEmitter = require('events').EventEmitter
var extend = require('extend.js')
var hat = require('hat')
var http = require('http')
var inherits = require('inherits')
var querystring = require('querystring')
var string2compact = require('string2compact')
inherits(Client, EventEmitter)
function Client (peerId, port, torrent, opts) {
var self = this
if (!(self instanceof Client)) return new Client(peerId, port, torrent)
if (!(self instanceof Client)) return new Client(peerId, port, torrent, opts)
EventEmitter.call(self)
self._opts = opts || {}
@ -143,7 +144,7 @@ Client.prototype._handleResponse = function (data, announce) {
if (interval && !self._opts.interval && self._intervalMs !== 0) {
// use the interval the tracker recommends, UNLESS the user manually specifies an
// interval they want to use
self.setInterval(interval)
self.setInterval(interval * 1000)
}
var trackerId = data['tracker id']
@ -173,9 +174,180 @@ Client.prototype._handleResponse = function (data, announce) {
}
}
inherits(Server, EventEmitter)
function Server (opts) {
var self = this
if (!(self instanceof Server)) return new Server(opts)
EventEmitter.call(self)
opts = opts || {}
self._interval = opts.interval
? opts.interval / 1000
: 10 * 60 // 10 min (in secs)
self._trustProxy = !!opts.trustProxy
self._swarms = {}
self._server = http.createServer()
self._server.on('request', self._onRequest.bind(self))
}
Server.prototype.listen = function (port) {
var self = this
self._server.listen(port, function () {
self.emit('listening')
})
}
Server.prototype.close = function (cb) {
var self = this
self._server.close(cb)
}
Server.prototype._onRequest = function (req, res) {
var self = this
var s = req.url.split('?')
if (s[0] === '/announce') {
var params = querystring.parse(s[1])
var ip = self._trustProxy
? req.headers['x-forwarded-for'] || req.connection.remoteAddress
: req.connection.remoteAddress
var port = Number(params.port)
var addr = ip + ':' + port
var infoHash = bytewiseDecodeURIComponent(params.info_hash)
var peerId = bytewiseDecodeURIComponent(params.peer_id)
var swarm = self._swarms[infoHash]
if (!swarm) {
swarm = self._swarms[infoHash] = {
complete: 0,
incomplete: 0,
peers: {}
}
}
var peer = swarm.peers[addr]
switch (params.event) {
case 'started':
if (peer) {
return
}
var left = Number(params.left)
if (left === 0) {
swarm.complete += 1
} else {
swarm.incomplete += 1
}
peer = swarm.peers[addr] = {
ip: ip,
port: port,
peerId: peerId
}
self.emit('start', addr, params)
break
case 'stopped':
if (!peer) {
return
}
if (peer.complete) {
swarm.complete -= 1
} else {
swarm.incomplete -= 1
}
delete swarm.peers[addr]
self.emit('stop', addr, params)
break
case 'completed':
if (!peer || peer.complete) {
return
}
peer.complete = true
swarm.complete += 1
swarm.incomplete -= 1
self.emit('complete', addr, params)
break
case '': // update
case undefined:
if (!peer) {
return
}
self.emit('update', addr, params)
break
default:
res.end(bncode.encode({
'failure reason': 'unexpected event: ' + params.event
}))
self.emit('error', new Error('unexpected event: ' + params.event))
return // early return
}
// send peers
var peers = Number(params.compact) === 1
? self._getPeersCompact(swarm)
: self._getPeers(swarm)
res.end(bncode.encode({
complete: swarm.complete,
incomplete: swarm.incomplete,
peers: peers,
interval: self._interval
}))
}
}
Server.prototype._getPeers = function (swarm) {
var self = this
var peers = []
for (var peerId in swarm.peers) {
var peer = swarm.peers[peerId]
peers.push({
'peer id': peer.peerId,
ip: peer.ip,
port: peer.port
})
}
return peers
}
Server.prototype._getPeersCompact = function (swarm) {
var self = this
var addrs = Object.keys(swarm.peers).map(function (peerId) {
var peer = swarm.peers[peerId]
return peer.ip + ':' + peer.port
})
return string2compact(addrs)
}
//
// HELPERS
//
function bytewiseEncodeURIComponent (buf) {
if (!Buffer.isBuffer(buf)) {
buf = new Buffer(buf, 'hex')
}
return escape(buf.toString('binary'))
}
return encodeURIComponent(buf.toString('binary'))
}
function bytewiseDecodeURIComponent (str) {
if (Buffer.isBuffer(str)) {
str = str.toString('utf8')
}
return (new Buffer(decodeURIComponent(str), 'binary').toString('hex'))
}

View File

@ -14,11 +14,14 @@
"bncode": "^0.5.2",
"compact2string": "^1.2.0",
"extend.js": "0.0.1",
"hat": "0.0.3",
"inherits": "^2.0.1",
"querystring": "^0.2.0"
"querystring": "^0.2.0",
"string2compact": "^1.1.0"
},
"devDependencies": {
"parse-torrent": "^0.6.0",
"portfinder": "^0.2.1",
"tape": "2.x"
},
"homepage": "http://webtorrent.io",
@ -41,4 +44,4 @@
"scripts": {
"test": "tape test/*.js"
}
}
}

View File

@ -5,7 +5,7 @@ var test = require('tape')
var torrent = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent')
var parsedTorrent = parseTorrent(torrent)
console.log(parsedTorrent.infoHash.toString('hex'))
var peerId = new Buffer('01234567890123456789')
var port = 6881
@ -33,7 +33,7 @@ test('client.start()', function (t) {
})
test('client.stop()', function (t) {
t.plan(3)
t.plan(4)
var client = new Client(peerId, port, parsedTorrent)
@ -52,6 +52,10 @@ test('client.stop()', function (t) {
t.equal(typeof data.complete, 'number')
t.equal(typeof data.incomplete, 'number')
})
client.once('peer', function () {
t.pass('should get more peers on stop()')
})
}, 1000)
})

74
test/server.js Normal file
View File

@ -0,0 +1,74 @@
var Client = require('../').Client
var portfinder = require('portfinder')
var Server = require('../').Server
var test = require('tape')
var peerId = new Buffer('12345678901234567890')
var infoHash = new Buffer('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex')
var torrentLength = 50000
test('server', function (t) {
t.plan(12)
var server = new Server() // { interval: 50000, compactOnly: false }
server.on('error', function (err) {
t.fail(err.message)
})
server.on('start', function () {
t.pass('got start message')
})
server.on('complete', function () {})
server.on('update', function () {})
server.on('stop', function () {})
server.on('listening', function () {
t.pass('server listening')
})
// server.torrents //
// server.torrents[infoHash] //
// server.torrents[infoHash].complete //
// server.torrents[infoHash].incomplete //
// server.torrents[infoHash].peers //
portfinder.getPort(function (err, port) {
t.error(err, 'found free port')
server.listen(port)
var announceUrl = 'http://127.0.0.1:' + port + '/announce'
var client = new Client(peerId, 6881, {
infoHash: infoHash,
length: torrentLength,
announce: [ announceUrl ]
})
client.start()
client.once('update', function (data) {
t.equal(data.announce, announceUrl)
t.equal(data.complete, 0)
t.equal(data.incomplete, 1)
client.complete()
client.once('update', function (data) {
t.equal(data.announce, announceUrl)
t.equal(data.complete, 1)
t.equal(data.incomplete, 0)
client.stop()
client.once('update', function (data) {
t.equal(data.announce, announceUrl)
t.equal(data.complete, 0)
t.equal(data.incomplete, 0)
server.close()
})
})
})
})
})