mirror of
https://github.com/webtorrent/bittorrent-tracker.git
synced 2025-01-31 02:21:36 +00:00
parent
085db1e972
commit
1cc5a511bd
35
client.js
35
client.js
@ -71,10 +71,11 @@ function Client (peerId, port, torrent, opts) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple convenience function to scrape a tracker for an infoHash without
|
||||
* needing to create a Client, pass it a parsed torrent, etc.
|
||||
* @param {string} announceUrl
|
||||
* @param {string} infoHash
|
||||
* Simple convenience function to scrape a tracker for an info hash without needing to
|
||||
* create a Client, pass it a parsed torrent, etc. Support scraping a tracker for multiple
|
||||
* torrents at the same time.
|
||||
* @param {string} announceUrl
|
||||
* @param {string|Array.<string>} infoHash
|
||||
* @param {function} cb
|
||||
*/
|
||||
Client.scrape = function (announceUrl, infoHash, cb) {
|
||||
@ -83,15 +84,33 @@ Client.scrape = function (announceUrl, infoHash, cb) {
|
||||
var peerId = new Buffer('01234567890123456789') // dummy value
|
||||
var port = 6881 // dummy value
|
||||
var torrent = {
|
||||
infoHash: infoHash,
|
||||
infoHash: Array.isArray(infoHash) ? infoHash[0] : infoHash,
|
||||
announce: [ announceUrl ]
|
||||
}
|
||||
var client = new Client(peerId, port, torrent)
|
||||
client.once('error', cb)
|
||||
client.once('scrape', function (data) {
|
||||
cb(null, data)
|
||||
|
||||
var len = Array.isArray(infoHash) ? infoHash.length : 1
|
||||
var results = {}
|
||||
client.on('scrape', function (data) {
|
||||
len -= 1
|
||||
results[data.infoHash] = data
|
||||
if (len === 0) {
|
||||
client.destroy()
|
||||
var keys = Object.keys(results)
|
||||
if (keys.length === 1) {
|
||||
cb(null, results[keys[0]])
|
||||
} else {
|
||||
cb(null, results)
|
||||
}
|
||||
}
|
||||
})
|
||||
client.scrape()
|
||||
|
||||
infoHash = Array.isArray(infoHash)
|
||||
? infoHash.map(function (infoHash) { return new Buffer(infoHash, 'hex') })
|
||||
: new Buffer(infoHash, 'hex')
|
||||
client.scrape({ infoHash: infoHash })
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,7 +63,12 @@ HTTPTracker.prototype.scrape = function (opts) {
|
||||
return
|
||||
}
|
||||
|
||||
opts.info_hash = self.client._infoHash.toString('binary')
|
||||
opts.info_hash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
|
||||
? opts.infoHash.map(function (infoHash) { return infoHash.toString('binary') })
|
||||
: (opts.infoHash || self.client._infoHash).toString('binary')
|
||||
|
||||
if (opts.infoHash) delete opts.infoHash
|
||||
|
||||
self._request(self._scrapeUrl, opts, self._onScrapeResponse.bind(self))
|
||||
}
|
||||
|
||||
@ -186,18 +191,23 @@ HTTPTracker.prototype._onScrapeResponse = function (data) {
|
||||
// NOTE: the unofficial spec says to use the 'files' key, 'host' has been
|
||||
// seen in practice
|
||||
data = data.files || data.host || {}
|
||||
data = data[self.client._infoHash.toString('binary')]
|
||||
|
||||
if (!data) {
|
||||
var keys = Object.keys(data)
|
||||
if (keys.length === 0) {
|
||||
self.client.emit('warning', new Error('invalid scrape response'))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
keys.forEach(function (infoHash) {
|
||||
var response = data[infoHash]
|
||||
// TODO: optionally handle data.flags.min_request_interval
|
||||
// (separate from announce interval)
|
||||
self.client.emit('scrape', {
|
||||
announce: self._announceUrl,
|
||||
complete: data.complete,
|
||||
incomplete: data.incomplete,
|
||||
downloaded: data.downloaded
|
||||
infoHash: common.binaryToHex(infoHash),
|
||||
complete: response.complete,
|
||||
incomplete: response.incomplete,
|
||||
downloaded: response.downloaded
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ function parseHttpRequest (req, opts) {
|
||||
params.addr = (common.IPV6_RE.test(params.ip) ? '[' + params.ip + ']' : params.ip) + ':' + params.port
|
||||
} else if (opts.action === 'scrape' || s[0] === '/scrape') {
|
||||
params.action = common.ACTIONS.SCRAPE
|
||||
if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ]
|
||||
|
||||
if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ]
|
||||
if (Array.isArray(params.info_hash)) {
|
||||
params.info_hash = params.info_hash.map(function (binaryInfoHash) {
|
||||
if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) {
|
||||
|
@ -50,10 +50,12 @@ function parseUdpRequest (msg, rinfo) {
|
||||
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
|
||||
// TODO: support multiple info_hash scrape
|
||||
if (msg.length > 36) throw new Error('multiple info_hash scrape not supported')
|
||||
|
||||
params.info_hash = [ msg.slice(16, 36).toString('hex') ] // 20 bytes
|
||||
if ((msg.length - 16) % 20 !== 0) throw new Error('invalid scrape message')
|
||||
params.info_hash = []
|
||||
for (var i = 0, len = (msg.length - 16) / 20; i < len; i += 1) {
|
||||
var infoHash = msg.slice(16 + (i * 20), 36 + (i * 20)).toString('hex') // 20 bytes
|
||||
params.info_hash.push(infoHash)
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid action in UDP packet: ' + params.action)
|
||||
}
|
||||
|
@ -35,19 +35,19 @@ function UDPTracker (client, announceUrl, opts) {
|
||||
|
||||
UDPTracker.prototype.announce = function (opts) {
|
||||
var self = this
|
||||
self._request(self._announceUrl, opts)
|
||||
self._request(opts)
|
||||
}
|
||||
|
||||
UDPTracker.prototype.scrape = function (opts) {
|
||||
var self = this
|
||||
opts._scrape = true
|
||||
self._request(self._announceUrl, opts) // udp scrape uses same announce url
|
||||
self._request(opts) // udp scrape uses same announce url
|
||||
}
|
||||
|
||||
UDPTracker.prototype._request = function (requestUrl, opts) {
|
||||
UDPTracker.prototype._request = function (opts) {
|
||||
var self = this
|
||||
if (!opts) opts = {}
|
||||
var parsedUrl = url.parse(requestUrl)
|
||||
var parsedUrl = url.parse(self._announceUrl)
|
||||
var socket = dgram.createSocket('udp4')
|
||||
var transactionId = genTransactionId()
|
||||
|
||||
@ -78,7 +78,7 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
|
||||
}
|
||||
|
||||
var action = msg.readUInt32BE(0)
|
||||
debug(requestUrl + ' UDP response, action ' + action)
|
||||
debug(self._announceUrl + ' UDP response, action ' + action)
|
||||
switch (action) {
|
||||
case 0: // handshake
|
||||
if (msg.length < 16) {
|
||||
@ -125,15 +125,22 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
|
||||
|
||||
case 2: // scrape
|
||||
cleanup()
|
||||
if (msg.length < 20) {
|
||||
if (msg.length < 20 || (msg.length - 8) % 12 !== 0) {
|
||||
return error('invalid scrape message')
|
||||
}
|
||||
self.client.emit('scrape', {
|
||||
announce: self._announceUrl,
|
||||
complete: msg.readUInt32BE(8),
|
||||
downloaded: msg.readUInt32BE(12),
|
||||
incomplete: msg.readUInt32BE(16)
|
||||
})
|
||||
var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
|
||||
? opts.infoHash.map(function (infoHash) { return infoHash.toString('hex') })
|
||||
: (opts.infoHash || self.client._infoHash).toString('hex')
|
||||
|
||||
for (var i = 0, len = (msg.length - 8) / 12; i < len; i += 1) {
|
||||
self.client.emit('scrape', {
|
||||
announce: self._announceUrl,
|
||||
infoHash: infoHashes[i],
|
||||
complete: msg.readUInt32BE(8 + (i * 12)),
|
||||
downloaded: msg.readUInt32BE(12 + (i * 12)),
|
||||
incomplete: msg.readUInt32BE(16 + (i * 12))
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 3: // error
|
||||
@ -159,7 +166,7 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
|
||||
|
||||
function error (message) {
|
||||
// errors will often happen if a tracker is offline, so don't treat it as fatal
|
||||
self.client.emit('warning', new Error(message + ' (' + requestUrl + ')'))
|
||||
self.client.emit('warning', new Error(message + ' (' + self._announceUrl + ')'))
|
||||
cleanup()
|
||||
}
|
||||
|
||||
@ -195,11 +202,15 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
|
||||
function scrape (connectionId) {
|
||||
transactionId = genTransactionId()
|
||||
|
||||
var infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
|
||||
? Buffer.concat(opts.infoHash)
|
||||
: (opts.infoHash || self.client._infoHash)
|
||||
|
||||
send(Buffer.concat([
|
||||
connectionId,
|
||||
common.toUInt32(common.ACTIONS.SCRAPE),
|
||||
transactionId,
|
||||
self.client._infoHash
|
||||
infoHash
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
24
server.js
24
server.js
@ -437,19 +437,19 @@ function makeUdpPacket (params) {
|
||||
])
|
||||
break
|
||||
case common.ACTIONS.SCRAPE:
|
||||
var firstInfoHash = Object.keys(params.files)[0]
|
||||
var scrapeInfo = firstInfoHash ? {
|
||||
complete: params.files[firstInfoHash].complete,
|
||||
incomplete: params.files[firstInfoHash].incomplete,
|
||||
completed: params.files[firstInfoHash].complete // TODO: this only provides a lower-bound
|
||||
} : {}
|
||||
packet = Buffer.concat([
|
||||
var scrapeResponse = [
|
||||
common.toUInt32(common.ACTIONS.SCRAPE),
|
||||
common.toUInt32(params.transactionId),
|
||||
common.toUInt32(scrapeInfo.complete),
|
||||
common.toUInt32(scrapeInfo.completed),
|
||||
common.toUInt32(scrapeInfo.incomplete)
|
||||
])
|
||||
common.toUInt32(params.transactionId)
|
||||
]
|
||||
for (var infoHash in params.files) {
|
||||
var file = params.files[infoHash]
|
||||
scrapeResponse.push(
|
||||
common.toUInt32(file.complete),
|
||||
common.toUInt32(file.downloaded), // TODO: this only provides a lower-bound
|
||||
common.toUInt32(file.incomplete)
|
||||
)
|
||||
}
|
||||
packet = Buffer.concat(scrapeResponse)
|
||||
break
|
||||
case common.ACTIONS.ERROR:
|
||||
packet = Buffer.concat([
|
||||
|
@ -8,25 +8,33 @@ var parseTorrent = require('parse-torrent')
|
||||
var Server = require('../').Server
|
||||
var test = require('tape')
|
||||
|
||||
function hexToBinary (str) {
|
||||
return new Buffer(str, 'hex').toString('binary')
|
||||
}
|
||||
|
||||
var infoHash1 = 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa'
|
||||
var binaryInfoHash1 = hexToBinary(infoHash1)
|
||||
var binaryInfoHash1 = commonLib.hexToBinary(infoHash1)
|
||||
var infoHash2 = 'bbb67059ed6bd08362da625b3ae77f6f4a075bbb'
|
||||
var binaryInfoHash2 = hexToBinary(infoHash2)
|
||||
var binaryInfoHash2 = commonLib.hexToBinary(infoHash2)
|
||||
|
||||
var bitlove = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent')
|
||||
var parsedBitlove = parseTorrent(bitlove)
|
||||
var binaryBitlove = hexToBinary(parsedBitlove.infoHash)
|
||||
var binaryBitlove = commonLib.hexToBinary(parsedBitlove.infoHash)
|
||||
|
||||
var peerId = new Buffer('01234567890123456789')
|
||||
|
||||
function testSingle (t, serverType) {
|
||||
commonTest.createServer(t, serverType, function (server, announceUrl) {
|
||||
Client.scrape(announceUrl, infoHash1, function (err, data) {
|
||||
parsedBitlove.announce = [ announceUrl ]
|
||||
var client = new Client(peerId, 6881, parsedBitlove)
|
||||
|
||||
client.on('error', function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
|
||||
client.on('warning', function (err) {
|
||||
t.error(err)
|
||||
})
|
||||
|
||||
client.scrape()
|
||||
|
||||
client.on('scrape', function (data) {
|
||||
t.equal(data.announce, announceUrl)
|
||||
t.equal(typeof data.complete, 'number')
|
||||
t.equal(typeof data.incomplete, 'number')
|
||||
@ -69,9 +77,37 @@ test('udp: scrape using Client.scrape static method', function (t) {
|
||||
clientScrapeStatic(t, 'udp')
|
||||
})
|
||||
|
||||
// TODO: test client for multiple scrape for UDP trackers
|
||||
function clientScrapeMulti (t, serverType) {
|
||||
commonTest.createServer(t, serverType, function (server, announceUrl) {
|
||||
Client.scrape(announceUrl, [ infoHash1, infoHash2 ], function (err, results) {
|
||||
t.error(err)
|
||||
|
||||
test('server: multiple info_hash scrape', function (t) {
|
||||
t.equal(results[infoHash1].announce, announceUrl)
|
||||
t.equal(typeof results[infoHash1].complete, 'number')
|
||||
t.equal(typeof results[infoHash1].incomplete, 'number')
|
||||
t.equal(typeof results[infoHash1].downloaded, 'number')
|
||||
|
||||
t.equal(results[infoHash2].announce, announceUrl)
|
||||
t.equal(typeof results[infoHash2].complete, 'number')
|
||||
t.equal(typeof results[infoHash2].incomplete, 'number')
|
||||
t.equal(typeof results[infoHash2].downloaded, 'number')
|
||||
|
||||
server.close(function () {
|
||||
t.end()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test('http: MULTI scrape using Client.scrape static method', function (t) {
|
||||
clientScrapeMulti(t, 'http')
|
||||
})
|
||||
|
||||
test('udp: MULTI scrape using Client.scrape static method', function (t) {
|
||||
clientScrapeMulti(t, 'udp')
|
||||
})
|
||||
|
||||
test('server: multiple info_hash scrape (manual http request)', function (t) {
|
||||
var server = new Server({ udp: false })
|
||||
server.on('error', function (err) {
|
||||
t.error(err)
|
||||
|
Loading…
Reference in New Issue
Block a user