Skip to content

Commit 5064a05

Browse files
added client and server support for scrape messages in addition to announce messages (tcp and udp)
1 parent 931f0a5 commit 5064a05

File tree

4 files changed

+212
-69
lines changed

4 files changed

+212
-69
lines changed

index.js

Lines changed: 166 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var string2compact = require('string2compact')
1818
var url = require('url')
1919

2020
var CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ])
21-
var ACTIONS = { CONNECT: 0, ANNOUNCE: 1 }
21+
var ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2 }
2222
var EVENTS = { completed: 1, started: 2, stopped: 3 }
2323
var MAX_UINT = 4294967295
2424

@@ -83,6 +83,30 @@ Tracker.prototype.update = function (opts) {
8383
self._request(opts)
8484
}
8585

86+
Tracker.prototype.scrape = function (opts) {
87+
var self = this
88+
89+
if (!self._scrapeUrl) {
90+
var announce = 'announce'
91+
var i = self._announceUrl.lastIndexOf('\/') + 1
92+
93+
if (i >= 1 && self._announceUrl.slice(i, i + announce.length) === announce) {
94+
self._scrapeUrl = self._announceUrl.slice(0, i) + 'scrape' + self._announceUrl.slice(i + announce.length)
95+
}
96+
}
97+
98+
if (!self._scrapeUrl) {
99+
self.client.emit('error', new Error('scrape not supported for announceUrl ' + self._announceUrl))
100+
return
101+
}
102+
103+
opts = extend({
104+
info_hash: bytewiseEncodeURIComponent(self.client._infoHash)
105+
}, opts)
106+
107+
self._requestImpl(self._scrapeUrl, opts)
108+
}
109+
86110
Tracker.prototype.setInterval = function (intervalMs) {
87111
var self = this
88112
if (self._interval) {
@@ -96,7 +120,7 @@ Tracker.prototype.setInterval = function (intervalMs) {
96120
}
97121

98122
/**
99-
* Send a request to the tracker
123+
* Send an announce request to the tracker
100124
*/
101125
Tracker.prototype._request = function (opts) {
102126
var self = this
@@ -115,12 +139,12 @@ Tracker.prototype._request = function (opts) {
115139
opts.trackerid = self._trackerId
116140
}
117141

118-
self._requestImpl(opts)
142+
self._requestImpl(self._announceUrl, opts)
119143
}
120144

121-
Tracker.prototype._requestHttp = function (opts) {
145+
Tracker.prototype._requestHttp = function (requestUrl, opts) {
122146
var self = this
123-
var fullUrl = self._announceUrl + '?' + querystring.stringify(opts)
147+
var fullUrl = requestUrl + '?' + querystring.stringify(opts)
124148

125149
var req = http.get(fullUrl, function (res) {
126150
var data = ''
@@ -133,7 +157,7 @@ Tracker.prototype._requestHttp = function (opts) {
133157
data += chunk
134158
})
135159
res.on('end', function () {
136-
self._handleResponse(data)
160+
self._handleResponse(requestUrl, data)
137161
})
138162
})
139163

@@ -142,9 +166,9 @@ Tracker.prototype._requestHttp = function (opts) {
142166
})
143167
}
144168

145-
Tracker.prototype._requestUdp = function (opts) {
169+
Tracker.prototype._requestUdp = function (requestUrl, opts) {
146170
var self = this
147-
var parsedUrl = url.parse(self._announceUrl)
171+
var parsedUrl = url.parse(requestUrl)
148172
var socket = dgram.createSocket('udp4')
149173
var transactionId = new Buffer(hat(32), 'hex')
150174

@@ -169,21 +193,21 @@ Tracker.prototype._requestUdp = function (opts) {
169193
socket.on('message', function (message, rinfo) {
170194

171195
if (message.length < 8 || message.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
172-
return error(new Error('tracker sent back invalid transaction id'))
196+
return error('tracker sent back invalid transaction id')
173197
}
174198

175199
var action = message.readUInt32BE(0)
176200
switch (action) {
177-
case 0:
201+
case 0: // handshake
178202
if (message.length < 16) {
179-
return error(new Error('invalid udp handshake'))
203+
return error('invalid udp handshake')
180204
}
181205
announce(message.slice(8, 16), opts)
182206
return
183207

184-
case 1:
208+
case 1: // announce
185209
if (message.length < 20) {
186-
return error(new Error('invalid announce message'))
210+
return error('invalid announce message')
187211
}
188212

189213
var interval = message.readUInt32BE(8)
@@ -205,6 +229,23 @@ Tracker.prototype._requestUdp = function (opts) {
205229

206230
clearTimeout(timeout)
207231
socket.close()
232+
return
233+
234+
case 2: // scrape
235+
if (message.length < 20) {
236+
return error('invalid scrape message')
237+
}
238+
239+
self.client.emit('scrape', {
240+
announce: self._announceUrl,
241+
complete: message.readUInt32BE(8),
242+
downloaded: message.readUInt32BE(12),
243+
incomplete: message.readUInt32BE(16)
244+
})
245+
246+
clearTimeout(timeout)
247+
socket.close()
248+
return
208249
}
209250
})
210251

@@ -215,9 +256,13 @@ Tracker.prototype._requestUdp = function (opts) {
215256
socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname)
216257
}
217258

259+
function genTransactionId () {
260+
transactionId = new Buffer(hat(32), 'hex')
261+
}
262+
218263
function announce (connectionId, opts) {
219264
opts = opts || {}
220-
transactionId = new Buffer(hat(32), 'hex')
265+
genTransactionId()
221266

222267
send(Buffer.concat([
223268
connectionId,
@@ -235,6 +280,17 @@ Tracker.prototype._requestUdp = function (opts) {
235280
toUInt16(self.client._port || 0)
236281
]))
237282
}
283+
284+
function scrape (connectionId, opts) {
285+
genTransactionId()
286+
287+
send(Buffer.concat([
288+
connectionId,
289+
toUInt32(ACTIONS.SCRAPE),
290+
transactionId,
291+
self.client._infoHash
292+
]))
293+
}
238294

239295
send(Buffer.concat([
240296
CONNECTION_ID,
@@ -243,7 +299,7 @@ Tracker.prototype._requestUdp = function (opts) {
243299
]))
244300
}
245301

246-
Tracker.prototype._handleResponse = function (data) {
302+
Tracker.prototype._handleResponse = function (requestUrl, data) {
247303
var self = this
248304

249305
try {
@@ -260,37 +316,55 @@ Tracker.prototype._handleResponse = function (data) {
260316
if (warning) {
261317
self.client.emit('warning', warning);
262318
}
319+
320+
if (requestUrl === self._announceUrl) {
321+
var interval = data.interval || data['min interval']
322+
if (interval && !self._opts.interval && self._intervalMs !== 0) {
323+
// use the interval the tracker recommends, UNLESS the user manually specifies an
324+
// interval they want to use
325+
self.setInterval(interval * 1000)
326+
}
263327

264-
var interval = data.interval || data['min interval']
265-
if (interval && !self._opts.interval && self._intervalMs !== 0) {
266-
// use the interval the tracker recommends, UNLESS the user manually specifies an
267-
// interval they want to use
268-
self.setInterval(interval * 1000)
269-
}
270-
271-
var trackerId = data['tracker id']
272-
if (trackerId) {
273-
// If absent, do not discard previous trackerId value
274-
self._trackerId = trackerId
275-
}
276-
277-
self.client.emit('update', {
278-
announce: self._announceUrl,
279-
complete: data.complete,
280-
incomplete: data.incomplete
281-
})
328+
var trackerId = data['tracker id']
329+
if (trackerId) {
330+
// If absent, do not discard previous trackerId value
331+
self._trackerId = trackerId
332+
}
282333

283-
if (Buffer.isBuffer(data.peers)) {
284-
// tracker returned compact response
285-
compact2string.multi(data.peers).forEach(function (addr) {
286-
self.client.emit('peer', addr)
287-
})
288-
} else if (Array.isArray(data.peers)) {
289-
// tracker returned normal response
290-
data.peers.forEach(function (peer) {
291-
var ip = peer.ip
292-
self.client.emit('peer', ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3] + ':' + peer.port)
334+
self.client.emit('update', {
335+
announce: self._announceUrl,
336+
complete: data.complete,
337+
incomplete: data.incomplete
293338
})
339+
340+
if (Buffer.isBuffer(data.peers)) {
341+
// tracker returned compact response
342+
compact2string.multi(data.peers).forEach(function (addr) {
343+
self.client.emit('peer', addr)
344+
})
345+
} else if (Array.isArray(data.peers)) {
346+
// tracker returned normal response
347+
data.peers.forEach(function (peer) {
348+
var ip = peer.ip
349+
self.client.emit('peer', ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3] + ':' + peer.port)
350+
})
351+
}
352+
} else if (requestUrl === self._scrapeUrl) {
353+
// note: the unofficial spec says to use the 'files' key but i've seen 'host' in practice
354+
data = data.files || data.host || {}
355+
data = data[bytewiseEncodeURIComponent(self.client._infoHash)]
356+
357+
if (!data) {
358+
self.client.emit('error', new Error('invalid scrape response'))
359+
} else {
360+
// TODO: optionally handle data.flags.min_request_interval (separate from announce interval)
361+
self.client.emit('scrape', {
362+
announce: self._announceUrl,
363+
complete: data.complete,
364+
incomplete: data.incomplete,
365+
downloaded: data.downloaded
366+
})
367+
}
294368
}
295369
}
296370

@@ -360,6 +434,13 @@ Client.prototype.update = function (opts) {
360434
})
361435
}
362436

437+
Client.prototype.scrape = function (opts) {
438+
var self = this
439+
self._trackers.forEach(function (tracker) {
440+
tracker.scrape(opts)
441+
})
442+
}
443+
363444
Client.prototype.setInterval = function (intervalMs) {
364445
var self = this
365446
self._intervalMs = intervalMs
@@ -452,18 +533,17 @@ Server.prototype._onHttpRequest = function (req, res) {
452533
var port = Number(params.port)
453534
var addr = ip + ':' + port
454535

536+
// TODO: support multiple info_hash parameters as a concatenation of individual requests
455537
var infoHash = bytewiseDecodeURIComponent(params.info_hash).toString('hex')
456538
var peerId = bytewiseDecodeURIComponent(params.peer_id).toString('utf8')
457-
458-
var swarm = self.torrents[infoHash]
459-
if (!swarm) {
460-
swarm = self.torrents[infoHash] = {
461-
complete: 0,
462-
incomplete: 0,
463-
peers: {}
464-
}
539+
540+
if (!infoHash) {
541+
return error('bittorrent-tracker server only supports announcing one torrent at a time')
465542
}
543+
544+
var swarm = self._getSwarm(infoHash)
466545
var peer = swarm.peers[addr]
546+
467547
switch (params.event) {
468548
case 'started':
469549
if (peer) {
@@ -547,8 +627,42 @@ Server.prototype._onHttpRequest = function (req, res) {
547627
}
548628

549629
res.end(bncode.encode(response))
550-
} else { // TODO: handle unofficial scrape messages
630+
} else if (s[0] === '/scrape') { // unofficial scrape message
631+
var params = querystring.parse(s[1])
632+
var infoHash = bytewiseDecodeURIComponent(params.info_hash).toString('hex')
633+
634+
if (!infoHash) {
635+
return error('bittorrent-tracker server only supports scraping one torrent at a time')
636+
}
637+
638+
var swarm = self._getSwarm(infoHash)
639+
var response = { files : { } }
640+
641+
response.files[params.info_hash] = {
642+
complete: swarm.complete,
643+
incomplete: swarm.incomplete,
644+
downloaded: swarm.complete, // TODO: this only provides a lower-bound
645+
flags: {
646+
min_request_interval: self._interval
647+
}
648+
}
649+
650+
res.end(bncode.encode(response))
651+
}
652+
}
653+
654+
Server.prototype._getSwarm = function (infoHash) {
655+
var self = this
656+
var swarm = self.torrents[infoHash]
657+
if (!swarm) {
658+
swarm = self.torrents[infoHash] = {
659+
complete: 0,
660+
incomplete: 0,
661+
peers: {}
662+
}
551663
}
664+
665+
return swarm
552666
}
553667

554668
Server.prototype._onUdpRequest = function (req, res) {

0 commit comments

Comments
 (0)