@@ -18,7 +18,7 @@ var string2compact = require('string2compact')
1818var url = require ( 'url' )
1919
2020var CONNECTION_ID = Buffer . concat ( [ toUInt32 ( 0x417 ) , toUInt32 ( 0x27101980 ) ] )
21- var ACTIONS = { CONNECT : 0 , ANNOUNCE : 1 }
21+ var ACTIONS = { CONNECT : 0 , ANNOUNCE : 1 , SCRAPE : 2 }
2222var EVENTS = { completed : 1 , started : 2 , stopped : 3 }
2323var 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+
86110Tracker . 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 */
101125Tracker . 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+
363444Client . 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
554668Server . prototype . _onUdpRequest = function ( req , res ) {
0 commit comments