module.exports = UDPTracker var BN = require('bn.js') var compact2string = require('compact2string') var debug = require('debug')('bittorrent-tracker:udp-tracker') var dgram = require('dgram') var EventEmitter = require('events').EventEmitter var hat = require('hat') var inherits = require('inherits') var url = require('url') var common = require('./common') var TIMEOUT = 15000 inherits(UDPTracker, EventEmitter) /** * UDP torrent tracker client (for an individual tracker) * * @param {Client} client parent bittorrent tracker client * @param {string} announceUrl announce url of tracker * @param {Object} opts options object */ function UDPTracker (client, announceUrl, opts) { var self = this EventEmitter.call(self) debug('new udp tracker %s', announceUrl) self.client = client self._opts = opts self._announceUrl = announceUrl self._intervalMs = self.client._intervalMs // use client interval initially self._interval = null self._cleanupFns = [] } UDPTracker.prototype.announce = function (opts) { var self = this self._request(opts) } UDPTracker.prototype.scrape = function (opts) { var self = this opts._scrape = true self._request(opts) // udp scrape uses same announce url } // TODO: Improve this interface UDPTracker.prototype.setInterval = function (intervalMs) { var self = this clearInterval(self._interval) self._intervalMs = intervalMs if (intervalMs) { // HACK var update = self.announce.bind(self, self.client._defaultAnnounceOpts()) self._interval = setInterval(update, self._intervalMs) } } UDPTracker.prototype.destroy = function () { var self = this if (self.destroyed) return self.destroyed = true self._cleanupFns.slice(0).forEach(function (cleanup) { cleanup() }) self._cleanupFns = [] } UDPTracker.prototype._request = function (opts) { var self = this if (!opts) opts = {} var parsedUrl = url.parse(self._announceUrl) var transactionId = genTransactionId() var socket = dgram.createSocket('udp4') var cleanup = function () { self._cleanupFns.splice(self._cleanupFns.indexOf(cleanup), 1) if (timeout) { clearTimeout(timeout) timeout = null } socket.removeListener('error', onError) socket.removeListener('message', onSocketMessage) try { socket.close() } catch (err) {} socket = null } self._cleanupFns.push(cleanup) // does not matter if `stopped` event arrives, so supress errors & cleanup after timeout var ms = opts.event === 'stopped' ? TIMEOUT / 10 : TIMEOUT var timeout = setTimeout(function () { timeout = null cleanup() if (opts.event !== 'stopped') { onError(new Error('tracker request timed out')) } }, ms) if (timeout.unref) timeout.unref() send(Buffer.concat([ common.CONNECTION_ID, common.toUInt32(common.ACTIONS.CONNECT), transactionId ])) socket.on('error', onError) socket.on('message', onSocketMessage) function onSocketMessage (msg) { if (self.destroyed) return if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { return onError(new Error('tracker sent invalid transaction id')) } var action = msg.readUInt32BE(0) debug('UDP response %s, action %s', self._announceUrl, action) switch (action) { case 0: // handshake if (msg.length < 16) return onError(new Error('invalid udp handshake')) if (opts._scrape) scrape(msg.slice(8, 16)) else announce(msg.slice(8, 16), opts) return case 1: // announce cleanup() if (msg.length < 20) return onError(new Error('invalid announce message')) var interval = msg.readUInt32BE(8) if (interval && !self._opts.interval && self._intervalMs !== 0) { // use the interval the tracker recommends, UNLESS the user manually specifies an // interval they want to use self.setInterval(interval * 1000) } self.client.emit('update', { announce: self._announceUrl, complete: msg.readUInt32BE(16), incomplete: msg.readUInt32BE(12) }) var addrs try { addrs = compact2string.multi(msg.slice(20)) } catch (err) { return self.client.emit('warning', err) } addrs.forEach(function (addr) { self.client.emit('peer', addr) }) break case 2: // scrape cleanup() if (msg.length < 20 || (msg.length - 8) % 12 !== 0) { return onError(new Error('invalid scrape message')) } 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 cleanup() if (msg.length < 8) return onError(new Error('invalid error message')) self.client.emit('warning', new Error(msg.slice(8).toString())) break default: onError(new Error('tracker sent invalid action')) break } } function onError (err) { if (self.destroyed) return cleanup() if (err.message) err.message += ' (' + self._announceUrl + ')' // errors will often happen if a tracker is offline, so don't treat it as fatal self.client.emit('warning', err) } function send (message) { if (!parsedUrl.port) { parsedUrl.port = 80 } socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) } function announce (connectionId, opts) { opts = opts || {} transactionId = genTransactionId() send(Buffer.concat([ connectionId, common.toUInt32(common.ACTIONS.ANNOUNCE), transactionId, self.client._infoHash, self.client._peerId, toUInt64(opts.downloaded || 0), opts.left != null ? toUInt64(opts.left) : new Buffer('FFFFFFFFFFFFFFFF', 'hex'), toUInt64(opts.uploaded || 0), common.toUInt32(common.EVENTS[opts.event] || 0), common.toUInt32(0), // ip address (optional) common.toUInt32(0), // key (optional) common.toUInt32(opts.numWant || common.DEFAULT_ANNOUNCE_PEERS), toUInt16(self.client._port || 0) ])) } 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, infoHash ])) } } function genTransactionId () { return new Buffer(hat(32), 'hex') } function toUInt16 (n) { var buf = new Buffer(2) buf.writeUInt16BE(n, 0) return buf } var MAX_UINT = 4294967295 function toUInt64 (n) { if (n > MAX_UINT || typeof n === 'string') { var bytes = new BN(n).toArray() while (bytes.length < 8) { bytes.unshift(0) } return new Buffer(bytes) } return Buffer.concat([common.toUInt32(0), common.toUInt32(n)]) }