Skip to content

Commit 1cc5a51

Browse files
committed
udp server: support multiple info_hash scrape
Fixes webtorrent#33
1 parent 085db1e commit 1cc5a51

File tree

7 files changed

+135
-57
lines changed

7 files changed

+135
-57
lines changed

client.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,11 @@ function Client (peerId, port, torrent, opts) {
7171
}
7272

7373
/**
74-
* Simple convenience function to scrape a tracker for an infoHash without
75-
* needing to create a Client, pass it a parsed torrent, etc.
76-
* @param {string} announceUrl
77-
* @param {string} infoHash
74+
* Simple convenience function to scrape a tracker for an info hash without needing to
75+
* create a Client, pass it a parsed torrent, etc. Support scraping a tracker for multiple
76+
* torrents at the same time.
77+
* @param {string} announceUrl
78+
* @param {string|Array.<string>} infoHash
7879
* @param {function} cb
7980
*/
8081
Client.scrape = function (announceUrl, infoHash, cb) {
@@ -83,15 +84,33 @@ Client.scrape = function (announceUrl, infoHash, cb) {
8384
var peerId = new Buffer('01234567890123456789') // dummy value
8485
var port = 6881 // dummy value
8586
var torrent = {
86-
infoHash: infoHash,
87+
infoHash: Array.isArray(infoHash) ? infoHash[0] : infoHash,
8788
announce: [ announceUrl ]
8889
}
8990
var client = new Client(peerId, port, torrent)
9091
client.once('error', cb)
91-
client.once('scrape', function (data) {
92-
cb(null, data)
92+
93+
var len = Array.isArray(infoHash) ? infoHash.length : 1
94+
var results = {}
95+
client.on('scrape', function (data) {
96+
len -= 1
97+
results[data.infoHash] = data
98+
if (len === 0) {
99+
client.destroy()
100+
var keys = Object.keys(results)
101+
if (keys.length === 1) {
102+
cb(null, results[keys[0]])
103+
} else {
104+
cb(null, results)
105+
}
106+
}
93107
})
94-
client.scrape()
108+
109+
infoHash = Array.isArray(infoHash)
110+
? infoHash.map(function (infoHash) { return new Buffer(infoHash, 'hex') })
111+
: new Buffer(infoHash, 'hex')
112+
client.scrape({ infoHash: infoHash })
113+
95114
}
96115

97116
/**

lib/http-tracker.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ HTTPTracker.prototype.scrape = function (opts) {
6363
return
6464
}
6565

66-
opts.info_hash = self.client._infoHash.toString('binary')
66+
opts.info_hash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
67+
? opts.infoHash.map(function (infoHash) { return infoHash.toString('binary') })
68+
: (opts.infoHash || self.client._infoHash).toString('binary')
69+
70+
if (opts.infoHash) delete opts.infoHash
71+
6772
self._request(self._scrapeUrl, opts, self._onScrapeResponse.bind(self))
6873
}
6974

@@ -186,18 +191,23 @@ HTTPTracker.prototype._onScrapeResponse = function (data) {
186191
// NOTE: the unofficial spec says to use the 'files' key, 'host' has been
187192
// seen in practice
188193
data = data.files || data.host || {}
189-
data = data[self.client._infoHash.toString('binary')]
190194

191-
if (!data) {
195+
var keys = Object.keys(data)
196+
if (keys.length === 0) {
192197
self.client.emit('warning', new Error('invalid scrape response'))
193-
} else {
198+
return
199+
}
200+
201+
keys.forEach(function (infoHash) {
202+
var response = data[infoHash]
194203
// TODO: optionally handle data.flags.min_request_interval
195204
// (separate from announce interval)
196205
self.client.emit('scrape', {
197206
announce: self._announceUrl,
198-
complete: data.complete,
199-
incomplete: data.incomplete,
200-
downloaded: data.downloaded
207+
infoHash: common.binaryToHex(infoHash),
208+
complete: response.complete,
209+
incomplete: response.incomplete,
210+
downloaded: response.downloaded
201211
})
202-
}
212+
})
203213
}

lib/parse_http.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ function parseHttpRequest (req, opts) {
3838
params.addr = (common.IPV6_RE.test(params.ip) ? '[' + params.ip + ']' : params.ip) + ':' + params.port
3939
} else if (opts.action === 'scrape' || s[0] === '/scrape') {
4040
params.action = common.ACTIONS.SCRAPE
41-
if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ]
4241

42+
if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ]
4343
if (Array.isArray(params.info_hash)) {
4444
params.info_hash = params.info_hash.map(function (binaryInfoHash) {
4545
if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) {

lib/parse_udp.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ function parseUdpRequest (msg, rinfo) {
5050
params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets
5151
params.compact = 1 // udp is always compact
5252
} else if (params.action === common.ACTIONS.SCRAPE) { // scrape message
53-
// TODO: support multiple info_hash scrape
54-
if (msg.length > 36) throw new Error('multiple info_hash scrape not supported')
55-
56-
params.info_hash = [ msg.slice(16, 36).toString('hex') ] // 20 bytes
53+
if ((msg.length - 16) % 20 !== 0) throw new Error('invalid scrape message')
54+
params.info_hash = []
55+
for (var i = 0, len = (msg.length - 16) / 20; i < len; i += 1) {
56+
var infoHash = msg.slice(16 + (i * 20), 36 + (i * 20)).toString('hex') // 20 bytes
57+
params.info_hash.push(infoHash)
58+
}
5759
} else {
5860
throw new Error('Invalid action in UDP packet: ' + params.action)
5961
}

lib/udp-tracker.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,19 @@ function UDPTracker (client, announceUrl, opts) {
3535

3636
UDPTracker.prototype.announce = function (opts) {
3737
var self = this
38-
self._request(self._announceUrl, opts)
38+
self._request(opts)
3939
}
4040

4141
UDPTracker.prototype.scrape = function (opts) {
4242
var self = this
4343
opts._scrape = true
44-
self._request(self._announceUrl, opts) // udp scrape uses same announce url
44+
self._request(opts) // udp scrape uses same announce url
4545
}
4646

47-
UDPTracker.prototype._request = function (requestUrl, opts) {
47+
UDPTracker.prototype._request = function (opts) {
4848
var self = this
4949
if (!opts) opts = {}
50-
var parsedUrl = url.parse(requestUrl)
50+
var parsedUrl = url.parse(self._announceUrl)
5151
var socket = dgram.createSocket('udp4')
5252
var transactionId = genTransactionId()
5353

@@ -78,7 +78,7 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
7878
}
7979

8080
var action = msg.readUInt32BE(0)
81-
debug(requestUrl + ' UDP response, action ' + action)
81+
debug(self._announceUrl + ' UDP response, action ' + action)
8282
switch (action) {
8383
case 0: // handshake
8484
if (msg.length < 16) {
@@ -125,15 +125,22 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
125125

126126
case 2: // scrape
127127
cleanup()
128-
if (msg.length < 20) {
128+
if (msg.length < 20 || (msg.length - 8) % 12 !== 0) {
129129
return error('invalid scrape message')
130130
}
131-
self.client.emit('scrape', {
132-
announce: self._announceUrl,
133-
complete: msg.readUInt32BE(8),
134-
downloaded: msg.readUInt32BE(12),
135-
incomplete: msg.readUInt32BE(16)
136-
})
131+
var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
132+
? opts.infoHash.map(function (infoHash) { return infoHash.toString('hex') })
133+
: (opts.infoHash || self.client._infoHash).toString('hex')
134+
135+
for (var i = 0, len = (msg.length - 8) / 12; i < len; i += 1) {
136+
self.client.emit('scrape', {
137+
announce: self._announceUrl,
138+
infoHash: infoHashes[i],
139+
complete: msg.readUInt32BE(8 + (i * 12)),
140+
downloaded: msg.readUInt32BE(12 + (i * 12)),
141+
incomplete: msg.readUInt32BE(16 + (i * 12))
142+
})
143+
}
137144
break
138145

139146
case 3: // error
@@ -159,7 +166,7 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
159166

160167
function error (message) {
161168
// errors will often happen if a tracker is offline, so don't treat it as fatal
162-
self.client.emit('warning', new Error(message + ' (' + requestUrl + ')'))
169+
self.client.emit('warning', new Error(message + ' (' + self._announceUrl + ')'))
163170
cleanup()
164171
}
165172

@@ -195,11 +202,15 @@ UDPTracker.prototype._request = function (requestUrl, opts) {
195202
function scrape (connectionId) {
196203
transactionId = genTransactionId()
197204

205+
var infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
206+
? Buffer.concat(opts.infoHash)
207+
: (opts.infoHash || self.client._infoHash)
208+
198209
send(Buffer.concat([
199210
connectionId,
200211
common.toUInt32(common.ACTIONS.SCRAPE),
201212
transactionId,
202-
self.client._infoHash
213+
infoHash
203214
]))
204215
}
205216
}

server.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -437,19 +437,19 @@ function makeUdpPacket (params) {
437437
])
438438
break
439439
case common.ACTIONS.SCRAPE:
440-
var firstInfoHash = Object.keys(params.files)[0]
441-
var scrapeInfo = firstInfoHash ? {
442-
complete: params.files[firstInfoHash].complete,
443-
incomplete: params.files[firstInfoHash].incomplete,
444-
completed: params.files[firstInfoHash].complete // TODO: this only provides a lower-bound
445-
} : {}
446-
packet = Buffer.concat([
440+
var scrapeResponse = [
447441
common.toUInt32(common.ACTIONS.SCRAPE),
448-
common.toUInt32(params.transactionId),
449-
common.toUInt32(scrapeInfo.complete),
450-
common.toUInt32(scrapeInfo.completed),
451-
common.toUInt32(scrapeInfo.incomplete)
452-
])
442+
common.toUInt32(params.transactionId)
443+
]
444+
for (var infoHash in params.files) {
445+
var file = params.files[infoHash]
446+
scrapeResponse.push(
447+
common.toUInt32(file.complete),
448+
common.toUInt32(file.downloaded), // TODO: this only provides a lower-bound
449+
common.toUInt32(file.incomplete)
450+
)
451+
}
452+
packet = Buffer.concat(scrapeResponse)
453453
break
454454
case common.ACTIONS.ERROR:
455455
packet = Buffer.concat([

test/scrape.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,33 @@ var parseTorrent = require('parse-torrent')
88
var Server = require('../').Server
99
var test = require('tape')
1010

11-
function hexToBinary (str) {
12-
return new Buffer(str, 'hex').toString('binary')
13-
}
14-
1511
var infoHash1 = 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa'
16-
var binaryInfoHash1 = hexToBinary(infoHash1)
12+
var binaryInfoHash1 = commonLib.hexToBinary(infoHash1)
1713
var infoHash2 = 'bbb67059ed6bd08362da625b3ae77f6f4a075bbb'
18-
var binaryInfoHash2 = hexToBinary(infoHash2)
14+
var binaryInfoHash2 = commonLib.hexToBinary(infoHash2)
1915

2016
var bitlove = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent')
2117
var parsedBitlove = parseTorrent(bitlove)
22-
var binaryBitlove = hexToBinary(parsedBitlove.infoHash)
18+
var binaryBitlove = commonLib.hexToBinary(parsedBitlove.infoHash)
2319

2420
var peerId = new Buffer('01234567890123456789')
2521

2622
function testSingle (t, serverType) {
2723
commonTest.createServer(t, serverType, function (server, announceUrl) {
28-
Client.scrape(announceUrl, infoHash1, function (err, data) {
24+
parsedBitlove.announce = [ announceUrl ]
25+
var client = new Client(peerId, 6881, parsedBitlove)
26+
27+
client.on('error', function (err) {
28+
t.error(err)
29+
})
30+
31+
client.on('warning', function (err) {
2932
t.error(err)
33+
})
34+
35+
client.scrape()
36+
37+
client.on('scrape', function (data) {
3038
t.equal(data.announce, announceUrl)
3139
t.equal(typeof data.complete, 'number')
3240
t.equal(typeof data.incomplete, 'number')
@@ -69,9 +77,37 @@ test('udp: scrape using Client.scrape static method', function (t) {
6977
clientScrapeStatic(t, 'udp')
7078
})
7179

72-
// TODO: test client for multiple scrape for UDP trackers
80+
function clientScrapeMulti (t, serverType) {
81+
commonTest.createServer(t, serverType, function (server, announceUrl) {
82+
Client.scrape(announceUrl, [ infoHash1, infoHash2 ], function (err, results) {
83+
t.error(err)
84+
85+
t.equal(results[infoHash1].announce, announceUrl)
86+
t.equal(typeof results[infoHash1].complete, 'number')
87+
t.equal(typeof results[infoHash1].incomplete, 'number')
88+
t.equal(typeof results[infoHash1].downloaded, 'number')
89+
90+
t.equal(results[infoHash2].announce, announceUrl)
91+
t.equal(typeof results[infoHash2].complete, 'number')
92+
t.equal(typeof results[infoHash2].incomplete, 'number')
93+
t.equal(typeof results[infoHash2].downloaded, 'number')
94+
95+
server.close(function () {
96+
t.end()
97+
})
98+
})
99+
})
100+
}
101+
102+
test('http: MULTI scrape using Client.scrape static method', function (t) {
103+
clientScrapeMulti(t, 'http')
104+
})
105+
106+
test('udp: MULTI scrape using Client.scrape static method', function (t) {
107+
clientScrapeMulti(t, 'udp')
108+
})
73109

74-
test('server: multiple info_hash scrape', function (t) {
110+
test('server: multiple info_hash scrape (manual http request)', function (t) {
75111
var server = new Server({ udp: false })
76112
server.on('error', function (err) {
77113
t.error(err)

0 commit comments

Comments
 (0)