Skip to content

Commit

Permalink
add bittorrent tracker server implementation!
Browse files Browse the repository at this point in the history
  • Loading branch information
feross committed Mar 27, 2014
1 parent 522f002 commit 31b82e0
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
184 changes: 178 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -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 || {}

Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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'))
}
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -41,4 +44,4 @@
"scripts": {
"test": "tape test/*.js"
}
}
}
8 changes: 6 additions & 2 deletions test/basic.js → test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

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

Expand Down
74 changes: 74 additions & 0 deletions test/server.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
})
})

0 comments on commit 31b82e0

Please sign in to comment.