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