diff --git a/README.md b/README.md index f9ae868..d6e1a91 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This module is used by [WebTorrent](http://webtorrent.io). - Robust and well-tested - Comprehensive test suite (runs entirely offline, so it's reliable) - Used by popular clients: [WebTorrent](http://webtorrent.io), [peerflix](https://github.com/mafintosh/peerflix), and [playback](https://mafintosh.github.io/playback/) -- Tracker statistics available via web interface at `/stats` +- Tracker statistics available via web interface at `/stats` or JSON data at `/stats.json` Also see [bittorrent-dht](https://github.com/feross/bittorrent-dht). diff --git a/server.js b/server.js index 34778da..62ca7be 100644 --- a/server.js +++ b/server.js @@ -150,7 +150,7 @@ function Server (opts) { return count } - if (req.method === 'GET' && req.url === '/stats') { + if (req.method === 'GET' && (req.url === '/stats' || req.url === '/stats.json')) { infoHashes.forEach(function (infoHash) { var peers = self.torrents[infoHash].peers var keys = Object.keys(peers) @@ -185,13 +185,29 @@ function Server (opts) { var isIPv4 = function (peer) { return peer.ipv4 } var isIPv6 = function (peer) { return peer.ipv6 } - res.end('

' + infoHashes.length + ' torrents (' + activeTorrents + ' active)

\n' + - '

Connected Peers: ' + Object.keys(allPeers).length + '

\n' + - '

Peers Seeding Only: ' + countPeers(isSeederOnly) + '

\n' + - '

Peers Leeching Only: ' + countPeers(isLeecherOnly) + '

\n' + - '

Peers Seeding & Leeching: ' + countPeers(isSeederAndLeecher) + '

\n' + - '

IPv4 Peers: ' + countPeers(isIPv4) + '

\n' + - '

IPv6 Peers: ' + countPeers(isIPv6) + '

\n') + var stats = { + torrents: infoHashes.length, + activeTorrents: activeTorrents, + peersAll: Object.keys(allPeers).length, + peersSeederOnly: countPeers(isSeederOnly), + peersLeecherOnly: countPeers(isLeecherOnly), + peersSeederAndLeecher: countPeers(isSeederAndLeecher), + peersIPv4: countPeers(isIPv4), + peersIPv6: countPeers(isIPv6) + } + + if (req.url === '/stats.json' || req.headers['content-type'] === 'application/json') { + res.write(JSON.stringify(stats)) + res.end() + } else if (req.url === '/stats') { + res.end('

' + stats.torrents + ' torrents (' + stats.activeTorrents + ' active)

\n' + + '

Connected Peers: ' + stats.peersAll + '

\n' + + '

Peers Seeding Only: ' + stats.peersSeederOnly + '

\n' + + '

Peers Leeching Only: ' + stats.peersLeecherOnly + '

\n' + + '

Peers Seeding & Leeching: ' + stats.peersSeederAndLeecher + '

\n' + + '

IPv4 Peers: ' + stats.peersIPv4 + '

\n' + + '

IPv6 Peers: ' + stats.peersIPv6 + '

\n') + } } }) } diff --git a/test/stats.js b/test/stats.js new file mode 100644 index 0000000..8971cd6 --- /dev/null +++ b/test/stats.js @@ -0,0 +1,152 @@ +var Buffer = require('safe-buffer').Buffer +var Client = require('../') +var commonTest = require('./common') +var fixtures = require('webtorrent-fixtures') +var get = require('simple-get') +var test = require('tape') + +var peerId = Buffer.from('01234567890123456789') + +function parseHtml (html) { + var extractValue = new RegExp('[^v^h](\\d+)') + var array = html.replace('torrents', '\n').split('\n').filter(function (line) { + return line && line.trim().length > 0 + }).map(function (line) { + var a = extractValue.exec(line) + return parseInt(a[1]) + }) + var i = 0 + return { + torrents: array[i++], + activeTorrents: array[i++], + peersAll: array[i++], + peersSeederOnly: array[i++], + peersLeecherOnly: array[i++], + peersSeederAndLeecher: array[i++], + peersIPv4: array[i++], + peersIPv6: array[i] + } +} + +test('server: get empty stats', function (t) { + t.plan(11) + + commonTest.createServer(t, 'http', function (server, announceUrl) { + var url = announceUrl.replace('/announce', '/stats') + + get.concat(url, function (err, res, data) { + t.error(err) + + var stats = parseHtml(data.toString()) + t.equal(res.statusCode, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(function () { t.pass('server closed') }) + }) + }) +}) + +test('server: get empty stats with json header', function (t) { + t.plan(11) + + commonTest.createServer(t, 'http', function (server, announceUrl) { + var opts = { + url: announceUrl.replace('/announce', '/stats'), + headers: { + 'content-type': 'json' + }, + json: true + } + + get.concat(opts, function (err, res, stats) { + t.error(err) + + t.equal(res.statusCode, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(function () { t.pass('server closed') }) + }) + }) +}) + +test('server: get empty stats on stats.json', function (t) { + t.plan(11) + + commonTest.createServer(t, 'http', function (server, announceUrl) { + var opts = { + url: announceUrl.replace('/announce', '/stats.json'), + json: true + } + + get.concat(opts, function (err, res, stats) { + t.error(err) + + t.equal(res.statusCode, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(function () { t.pass('server closed') }) + }) + }) +}) + +test('server: get leecher stats.json', function (t) { + t.plan(10) + + commonTest.createServer(t, 'http', function (server, announceUrl) { + // announce a torrent to the tracker + var client = new Client({ + infoHash: fixtures.leaves.parsedTorrent.infoHash, + announce: announceUrl, + peerId: peerId, + port: 6881 + }) + client.on('error', function (err) { t.error(err) }) + client.on('warning', function (err) { t.error(err) }) + + client.start() + + server.once('start', function () { + var opts = { + url: announceUrl.replace('/announce', '/stats.json'), + json: true + } + + get.concat(opts, function (err, res, stats) { + t.error(err) + console.log(stats) + + t.equal(res.statusCode, 200) + t.equal(stats.torrents, 1) + t.equal(stats.activeTorrents, 1) + t.equal(stats.peersAll, 1) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 1) + t.equal(stats.peersSeederAndLeecher, 0) + + client.destroy(function () { t.pass('client destroyed') }) + server.close(function () { t.pass('server closed') }) + }) + }) + }) +})