Skip to content

Commit 31b82e0

Browse files
committed
add bittorrent tracker server implementation!
1 parent 522f002 commit 31b82e0

File tree

5 files changed

+264
-11
lines changed

5 files changed

+264
-11
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ clients. The requests include metrics from clients that help the tracker keep ov
99
statistics about the torrent. The response includes a peer list that helps the client
1010
participate in the torrent.
1111

12-
Also see [BitTorrent DHT](https://github.com/feross/bittorrent-dht). This module is used
12+
Also see [bittorrent-dht](https://github.com/feross/bittorrent-dht). This module is used
1313
by [WebTorrent](http://webtorrent.io).
1414

1515
## install

index.js

Lines changed: 178 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
exports.Client = Client
2-
// TODO: exports.Server = Server
3-
// TODO: support connecting to UDP trackers (http://www.bittorrent.org/beps/bep_0015.html)
2+
exports.Server = Server
43

54
var bncode = require('bncode')
65
var compact2string = require('compact2string')
76
var EventEmitter = require('events').EventEmitter
87
var extend = require('extend.js')
8+
var hat = require('hat')
99
var http = require('http')
1010
var inherits = require('inherits')
1111
var querystring = require('querystring')
12+
var string2compact = require('string2compact')
1213

1314
inherits(Client, EventEmitter)
1415

1516
function Client (peerId, port, torrent, opts) {
1617
var self = this
17-
if (!(self instanceof Client)) return new Client(peerId, port, torrent)
18+
if (!(self instanceof Client)) return new Client(peerId, port, torrent, opts)
1819
EventEmitter.call(self)
1920
self._opts = opts || {}
2021

@@ -143,7 +144,7 @@ Client.prototype._handleResponse = function (data, announce) {
143144
if (interval && !self._opts.interval && self._intervalMs !== 0) {
144145
// use the interval the tracker recommends, UNLESS the user manually specifies an
145146
// interval they want to use
146-
self.setInterval(interval)
147+
self.setInterval(interval * 1000)
147148
}
148149

149150
var trackerId = data['tracker id']
@@ -173,9 +174,180 @@ Client.prototype._handleResponse = function (data, announce) {
173174
}
174175
}
175176

177+
inherits(Server, EventEmitter)
178+
179+
function Server (opts) {
180+
var self = this
181+
if (!(self instanceof Server)) return new Server(opts)
182+
EventEmitter.call(self)
183+
opts = opts || {}
184+
185+
self._interval = opts.interval
186+
? opts.interval / 1000
187+
: 10 * 60 // 10 min (in secs)
188+
189+
self._trustProxy = !!opts.trustProxy
190+
191+
self._swarms = {}
192+
193+
self._server = http.createServer()
194+
self._server.on('request', self._onRequest.bind(self))
195+
}
196+
197+
Server.prototype.listen = function (port) {
198+
var self = this
199+
self._server.listen(port, function () {
200+
self.emit('listening')
201+
})
202+
}
203+
204+
Server.prototype.close = function (cb) {
205+
var self = this
206+
self._server.close(cb)
207+
}
208+
209+
Server.prototype._onRequest = function (req, res) {
210+
var self = this
211+
var s = req.url.split('?')
212+
if (s[0] === '/announce') {
213+
var params = querystring.parse(s[1])
214+
215+
var ip = self._trustProxy
216+
? req.headers['x-forwarded-for'] || req.connection.remoteAddress
217+
: req.connection.remoteAddress
218+
var port = Number(params.port)
219+
var addr = ip + ':' + port
220+
221+
var infoHash = bytewiseDecodeURIComponent(params.info_hash)
222+
var peerId = bytewiseDecodeURIComponent(params.peer_id)
223+
224+
var swarm = self._swarms[infoHash]
225+
if (!swarm) {
226+
swarm = self._swarms[infoHash] = {
227+
complete: 0,
228+
incomplete: 0,
229+
peers: {}
230+
}
231+
}
232+
var peer = swarm.peers[addr]
233+
switch (params.event) {
234+
case 'started':
235+
if (peer) {
236+
return
237+
}
238+
239+
var left = Number(params.left)
240+
241+
if (left === 0) {
242+
swarm.complete += 1
243+
} else {
244+
swarm.incomplete += 1
245+
}
246+
247+
peer = swarm.peers[addr] = {
248+
ip: ip,
249+
port: port,
250+
peerId: peerId
251+
}
252+
self.emit('start', addr, params)
253+
break
254+
255+
case 'stopped':
256+
if (!peer) {
257+
return
258+
}
259+
260+
if (peer.complete) {
261+
swarm.complete -= 1
262+
} else {
263+
swarm.incomplete -= 1
264+
}
265+
266+
delete swarm.peers[addr]
267+
268+
self.emit('stop', addr, params)
269+
break
270+
271+
case 'completed':
272+
if (!peer || peer.complete) {
273+
return
274+
}
275+
peer.complete = true
276+
277+
swarm.complete += 1
278+
swarm.incomplete -= 1
279+
280+
self.emit('complete', addr, params)
281+
break
282+
283+
case '': // update
284+
case undefined:
285+
if (!peer) {
286+
return
287+
}
288+
289+
self.emit('update', addr, params)
290+
break
291+
292+
default:
293+
res.end(bncode.encode({
294+
'failure reason': 'unexpected event: ' + params.event
295+
}))
296+
self.emit('error', new Error('unexpected event: ' + params.event))
297+
return // early return
298+
}
299+
300+
// send peers
301+
var peers = Number(params.compact) === 1
302+
? self._getPeersCompact(swarm)
303+
: self._getPeers(swarm)
304+
305+
res.end(bncode.encode({
306+
complete: swarm.complete,
307+
incomplete: swarm.incomplete,
308+
peers: peers,
309+
interval: self._interval
310+
}))
311+
}
312+
}
313+
314+
Server.prototype._getPeers = function (swarm) {
315+
var self = this
316+
var peers = []
317+
for (var peerId in swarm.peers) {
318+
var peer = swarm.peers[peerId]
319+
peers.push({
320+
'peer id': peer.peerId,
321+
ip: peer.ip,
322+
port: peer.port
323+
})
324+
}
325+
return peers
326+
}
327+
328+
Server.prototype._getPeersCompact = function (swarm) {
329+
var self = this
330+
var addrs = Object.keys(swarm.peers).map(function (peerId) {
331+
var peer = swarm.peers[peerId]
332+
return peer.ip + ':' + peer.port
333+
})
334+
return string2compact(addrs)
335+
}
336+
337+
//
338+
// HELPERS
339+
//
340+
176341
function bytewiseEncodeURIComponent (buf) {
177342
if (!Buffer.isBuffer(buf)) {
178343
buf = new Buffer(buf, 'hex')
179344
}
180-
return escape(buf.toString('binary'))
181-
}
345+
return encodeURIComponent(buf.toString('binary'))
346+
}
347+
348+
function bytewiseDecodeURIComponent (str) {
349+
if (Buffer.isBuffer(str)) {
350+
str = str.toString('utf8')
351+
}
352+
return (new Buffer(decodeURIComponent(str), 'binary').toString('hex'))
353+
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
"bncode": "^0.5.2",
1515
"compact2string": "^1.2.0",
1616
"extend.js": "0.0.1",
17+
"hat": "0.0.3",
1718
"inherits": "^2.0.1",
18-
"querystring": "^0.2.0"
19+
"querystring": "^0.2.0",
20+
"string2compact": "^1.1.0"
1921
},
2022
"devDependencies": {
2123
"parse-torrent": "^0.6.0",
24+
"portfinder": "^0.2.1",
2225
"tape": "2.x"
2326
},
2427
"homepage": "http://webtorrent.io",
@@ -41,4 +44,4 @@
4144
"scripts": {
4245
"test": "tape test/*.js"
4346
}
44-
}
47+
}

test/basic.js renamed to test/client.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var test = require('tape')
55

66
var torrent = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent')
77
var parsedTorrent = parseTorrent(torrent)
8-
8+
console.log(parsedTorrent.infoHash.toString('hex'))
99
var peerId = new Buffer('01234567890123456789')
1010
var port = 6881
1111

@@ -33,7 +33,7 @@ test('client.start()', function (t) {
3333
})
3434

3535
test('client.stop()', function (t) {
36-
t.plan(3)
36+
t.plan(4)
3737

3838
var client = new Client(peerId, port, parsedTorrent)
3939

@@ -52,6 +52,10 @@ test('client.stop()', function (t) {
5252
t.equal(typeof data.complete, 'number')
5353
t.equal(typeof data.incomplete, 'number')
5454
})
55+
56+
client.once('peer', function () {
57+
t.pass('should get more peers on stop()')
58+
})
5559
}, 1000)
5660
})
5761

test/server.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
var Client = require('../').Client
2+
var portfinder = require('portfinder')
3+
var Server = require('../').Server
4+
var test = require('tape')
5+
6+
var peerId = new Buffer('12345678901234567890')
7+
var infoHash = new Buffer('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex')
8+
var torrentLength = 50000
9+
10+
test('server', function (t) {
11+
t.plan(12)
12+
13+
var server = new Server() // { interval: 50000, compactOnly: false }
14+
15+
server.on('error', function (err) {
16+
t.fail(err.message)
17+
})
18+
19+
server.on('start', function () {
20+
t.pass('got start message')
21+
})
22+
server.on('complete', function () {})
23+
server.on('update', function () {})
24+
server.on('stop', function () {})
25+
26+
server.on('listening', function () {
27+
t.pass('server listening')
28+
})
29+
30+
// server.torrents //
31+
// server.torrents[infoHash] //
32+
// server.torrents[infoHash].complete //
33+
// server.torrents[infoHash].incomplete //
34+
// server.torrents[infoHash].peers //
35+
36+
portfinder.getPort(function (err, port) {
37+
t.error(err, 'found free port')
38+
server.listen(port)
39+
40+
var announceUrl = 'http://127.0.0.1:' + port + '/announce'
41+
42+
var client = new Client(peerId, 6881, {
43+
infoHash: infoHash,
44+
length: torrentLength,
45+
announce: [ announceUrl ]
46+
})
47+
48+
client.start()
49+
50+
client.once('update', function (data) {
51+
t.equal(data.announce, announceUrl)
52+
t.equal(data.complete, 0)
53+
t.equal(data.incomplete, 1)
54+
55+
client.complete()
56+
57+
client.once('update', function (data) {
58+
t.equal(data.announce, announceUrl)
59+
t.equal(data.complete, 1)
60+
t.equal(data.incomplete, 0)
61+
62+
client.stop()
63+
64+
client.once('update', function (data) {
65+
t.equal(data.announce, announceUrl)
66+
t.equal(data.complete, 0)
67+
t.equal(data.incomplete, 0)
68+
69+
server.close()
70+
})
71+
})
72+
})
73+
})
74+
})

0 commit comments

Comments
 (0)