diff --git a/client.js b/client.js index 1a8a5f1..ddc345e 100644 --- a/client.js +++ b/client.js @@ -1,21 +1,16 @@ -module.exports = Client +const { Buffer } = require('safe-buffer') +const debug = require('debug')('bittorrent-tracker:client') +const EventEmitter = require('events') +const once = require('once') +const parallel = require('run-parallel') +const Peer = require('simple-peer') +const uniq = require('uniq') +const url = require('url') -var Buffer = require('safe-buffer').Buffer -var debug = require('debug')('bittorrent-tracker:client') -var EventEmitter = require('events').EventEmitter -var inherits = require('inherits') -var once = require('once') -var parallel = require('run-parallel') -var Peer = require('simple-peer') -var uniq = require('uniq') -var url = require('url') - -var common = require('./lib/common') -var HTTPTracker = require('./lib/client/http-tracker') // empty object in browser -var UDPTracker = require('./lib/client/udp-tracker') // empty object in browser -var WebSocketTracker = require('./lib/client/websocket-tracker') - -inherits(Client, EventEmitter) +const common = require('./lib/common') +const HTTPTracker = require('./lib/client/http-tracker') // empty object in browser +const UDPTracker = require('./lib/client/udp-tracker') // empty object in browser +const WebSocketTracker = require('./lib/client/websocket-tracker') /** * BitTorrent tracker client. @@ -32,86 +27,203 @@ inherits(Client, EventEmitter) * @param {number} opts.userAgent User-Agent header for http requests * @param {number} opts.wrtc custom webrtc impl (useful in node.js) */ -function Client (opts) { - var self = this - if (!(self instanceof Client)) return new Client(opts) - EventEmitter.call(self) - if (!opts) opts = {} +class Client extends EventEmitter { + constructor (opts = {}) { + super() - if (!opts.peerId) throw new Error('Option `peerId` is required') - if (!opts.infoHash) throw new Error('Option `infoHash` is required') - if (!opts.announce) throw new Error('Option `announce` is required') - if (!process.browser && !opts.port) throw new Error('Option `port` is required') + if (!opts.peerId) throw new Error('Option `peerId` is required') + if (!opts.infoHash) throw new Error('Option `infoHash` is required') + if (!opts.announce) throw new Error('Option `announce` is required') + if (!process.browser && !opts.port) throw new Error('Option `port` is required') - self.peerId = typeof opts.peerId === 'string' - ? opts.peerId - : opts.peerId.toString('hex') - self._peerIdBuffer = Buffer.from(self.peerId, 'hex') - self._peerIdBinary = self._peerIdBuffer.toString('binary') + this.peerId = typeof opts.peerId === 'string' + ? opts.peerId + : opts.peerId.toString('hex') + this._peerIdBuffer = Buffer.from(this.peerId, 'hex') + this._peerIdBinary = this._peerIdBuffer.toString('binary') - self.infoHash = typeof opts.infoHash === 'string' - ? opts.infoHash.toLowerCase() - : opts.infoHash.toString('hex') - self._infoHashBuffer = Buffer.from(self.infoHash, 'hex') - self._infoHashBinary = self._infoHashBuffer.toString('binary') + this.infoHash = typeof opts.infoHash === 'string' + ? opts.infoHash.toLowerCase() + : opts.infoHash.toString('hex') + this._infoHashBuffer = Buffer.from(this.infoHash, 'hex') + this._infoHashBinary = this._infoHashBuffer.toString('binary') - debug('new client %s', self.infoHash) + debug('new client %s', this.infoHash) - self.destroyed = false + this.destroyed = false - self._port = opts.port - self._getAnnounceOpts = opts.getAnnounceOpts - self._rtcConfig = opts.rtcConfig - self._userAgent = opts.userAgent + this._port = opts.port + this._getAnnounceOpts = opts.getAnnounceOpts + this._rtcConfig = opts.rtcConfig + this._userAgent = opts.userAgent - // Support lazy 'wrtc' module initialization - // See: https://github.com/webtorrent/webtorrent-hybrid/issues/46 - self._wrtc = typeof opts.wrtc === 'function' ? opts.wrtc() : opts.wrtc + // Support lazy 'wrtc' module initialization + // See: https://github.com/webtorrent/webtorrent-hybrid/issues/46 + this._wrtc = typeof opts.wrtc === 'function' ? opts.wrtc() : opts.wrtc - var announce = typeof opts.announce === 'string' - ? [ opts.announce ] - : opts.announce == null ? [] : opts.announce + let announce = typeof opts.announce === 'string' + ? [ opts.announce ] + : opts.announce == null ? [] : opts.announce - // Remove trailing slash from trackers to catch duplicates - announce = announce.map(function (announceUrl) { - announceUrl = announceUrl.toString() - if (announceUrl[announceUrl.length - 1] === '/') { - announceUrl = announceUrl.substring(0, announceUrl.length - 1) + // Remove trailing slash from trackers to catch duplicates + announce = announce.map(announceUrl => { + announceUrl = announceUrl.toString() + if (announceUrl[announceUrl.length - 1] === '/') { + announceUrl = announceUrl.substring(0, announceUrl.length - 1) + } + return announceUrl + }) + announce = uniq(announce) + + const webrtcSupport = this._wrtc !== false && (!!this._wrtc || Peer.WEBRTC_SUPPORT) + + const nextTickWarn = err => { + process.nextTick(() => { + this.emit('warning', err) + }) } - return announceUrl - }) - announce = uniq(announce) - var webrtcSupport = self._wrtc !== false && (!!self._wrtc || Peer.WEBRTC_SUPPORT) - - self._trackers = announce - .map(function (announceUrl) { - var protocol = url.parse(announceUrl).protocol - if ((protocol === 'http:' || protocol === 'https:') && - typeof HTTPTracker === 'function') { - return new HTTPTracker(self, announceUrl) - } else if (protocol === 'udp:' && typeof UDPTracker === 'function') { - return new UDPTracker(self, announceUrl) - } else if ((protocol === 'ws:' || protocol === 'wss:') && webrtcSupport) { - // Skip ws:// trackers on https:// sites because they throw SecurityError - if (protocol === 'ws:' && typeof window !== 'undefined' && - window.location.protocol === 'https:') { - nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl)) + this._trackers = announce + .map(announceUrl => { + const protocol = url.parse(announceUrl).protocol + if ((protocol === 'http:' || protocol === 'https:') && + typeof HTTPTracker === 'function') { + return new HTTPTracker(this, announceUrl) + } else if (protocol === 'udp:' && typeof UDPTracker === 'function') { + return new UDPTracker(this, announceUrl) + } else if ((protocol === 'ws:' || protocol === 'wss:') && webrtcSupport) { + // Skip ws:// trackers on https:// sites because they throw SecurityError + if (protocol === 'ws:' && typeof window !== 'undefined' && + window.location.protocol === 'https:') { + nextTickWarn(new Error(`Unsupported tracker protocol: ${announceUrl}`)) + return null + } + return new WebSocketTracker(this, announceUrl) + } else { + nextTickWarn(new Error(`Unsupported tracker protocol: ${announceUrl}`)) return null } - return new WebSocketTracker(self, announceUrl) - } else { - nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl)) - return null - } - }) - .filter(Boolean) + }) + .filter(Boolean) + } - function nextTickWarn (err) { - process.nextTick(function () { - self.emit('warning', err) + /** + * Send a `start` announce to the trackers. + * @param {Object} opts + * @param {number=} opts.uploaded + * @param {number=} opts.downloaded + * @param {number=} opts.left (if not set, calculated automatically) + */ + start (opts) { + debug('send `start`') + opts = this._defaultAnnounceOpts(opts) + opts.event = 'started' + this._announce(opts) + + // start announcing on intervals + this._trackers.forEach(tracker => { + tracker.setInterval() }) } + + /** + * Send a `stop` announce to the trackers. + * @param {Object} opts + * @param {number=} opts.uploaded + * @param {number=} opts.downloaded + * @param {number=} opts.numwant + * @param {number=} opts.left (if not set, calculated automatically) + */ + stop (opts) { + debug('send `stop`') + opts = this._defaultAnnounceOpts(opts) + opts.event = 'stopped' + this._announce(opts) + } + + /** + * Send a `complete` announce to the trackers. + * @param {Object} opts + * @param {number=} opts.uploaded + * @param {number=} opts.downloaded + * @param {number=} opts.numwant + * @param {number=} opts.left (if not set, calculated automatically) + */ + complete (opts) { + debug('send `complete`') + if (!opts) opts = {} + opts = this._defaultAnnounceOpts(opts) + opts.event = 'completed' + this._announce(opts) + } + + /** + * Send a `update` announce to the trackers. + * @param {Object} opts + * @param {number=} opts.uploaded + * @param {number=} opts.downloaded + * @param {number=} opts.numwant + * @param {number=} opts.left (if not set, calculated automatically) + */ + update (opts) { + debug('send `update`') + opts = this._defaultAnnounceOpts(opts) + if (opts.event) delete opts.event + this._announce(opts) + } + + _announce (opts) { + this._trackers.forEach(tracker => { + // tracker should not modify `opts` object, it's passed to all trackers + tracker.announce(opts) + }) + } + + /** + * Send a scrape request to the trackers. + * @param {Object} opts + */ + scrape (opts) { + debug('send `scrape`') + if (!opts) opts = {} + this._trackers.forEach(tracker => { + // tracker should not modify `opts` object, it's passed to all trackers + tracker.scrape(opts) + }) + } + + setInterval (intervalMs) { + debug('setInterval %d', intervalMs) + this._trackers.forEach(tracker => { + tracker.setInterval(intervalMs) + }) + } + + destroy (cb) { + if (this.destroyed) return + this.destroyed = true + debug('destroy') + + const tasks = this._trackers.map(tracker => cb => { + tracker.destroy(cb) + }) + + parallel(tasks, cb) + + this._trackers = [] + this._getAnnounceOpts = null + } + + _defaultAnnounceOpts (opts = {}) { + if (opts.numwant == null) opts.numwant = common.DEFAULT_ANNOUNCE_PEERS + + if (opts.uploaded == null) opts.uploaded = 0 + if (opts.downloaded == null) opts.downloaded = 0 + + if (this._getAnnounceOpts) opts = Object.assign({}, opts, this._getAnnounceOpts()) + + return opts + } } /** @@ -123,30 +235,30 @@ function Client (opts) { * @param {string} opts.announce * @param {function} cb */ -Client.scrape = function (opts, cb) { +Client.scrape = (opts, cb) => { cb = once(cb) if (!opts.infoHash) throw new Error('Option `infoHash` is required') if (!opts.announce) throw new Error('Option `announce` is required') - var clientOpts = Object.assign({}, opts, { + const clientOpts = Object.assign({}, opts, { infoHash: Array.isArray(opts.infoHash) ? opts.infoHash[0] : opts.infoHash, peerId: Buffer.from('01234567890123456789'), // dummy value port: 6881 // dummy value }) - var client = new Client(clientOpts) + const client = new Client(clientOpts) client.once('error', cb) client.once('warning', cb) - var len = Array.isArray(opts.infoHash) ? opts.infoHash.length : 1 - var results = {} - client.on('scrape', function (data) { + let len = Array.isArray(opts.infoHash) ? opts.infoHash.length : 1 + const results = {} + client.on('scrape', data => { len -= 1 results[data.infoHash] = data if (len === 0) { client.destroy() - var keys = Object.keys(results) + const keys = Object.keys(results) if (keys.length === 1) { cb(null, results[keys[0]]) } else { @@ -156,7 +268,7 @@ Client.scrape = function (opts, cb) { }) opts.infoHash = Array.isArray(opts.infoHash) - ? opts.infoHash.map(function (infoHash) { + ? opts.infoHash.map(infoHash => { return Buffer.from(infoHash, 'hex') }) : Buffer.from(opts.infoHash, 'hex') @@ -164,132 +276,4 @@ Client.scrape = function (opts, cb) { return client } -/** - * Send a `start` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.start = function (opts) { - var self = this - debug('send `start`') - opts = self._defaultAnnounceOpts(opts) - opts.event = 'started' - self._announce(opts) - - // start announcing on intervals - self._trackers.forEach(function (tracker) { - tracker.setInterval() - }) -} - -/** - * Send a `stop` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.numwant - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.stop = function (opts) { - var self = this - debug('send `stop`') - opts = self._defaultAnnounceOpts(opts) - opts.event = 'stopped' - self._announce(opts) -} - -/** - * Send a `complete` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.numwant - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.complete = function (opts) { - var self = this - debug('send `complete`') - if (!opts) opts = {} - opts = self._defaultAnnounceOpts(opts) - opts.event = 'completed' - self._announce(opts) -} - -/** - * Send a `update` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.numwant - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.update = function (opts) { - var self = this - debug('send `update`') - opts = self._defaultAnnounceOpts(opts) - if (opts.event) delete opts.event - self._announce(opts) -} - -Client.prototype._announce = function (opts) { - var self = this - self._trackers.forEach(function (tracker) { - // tracker should not modify `opts` object, it's passed to all trackers - tracker.announce(opts) - }) -} - -/** - * Send a scrape request to the trackers. - * @param {Object} opts - */ -Client.prototype.scrape = function (opts) { - var self = this - debug('send `scrape`') - if (!opts) opts = {} - self._trackers.forEach(function (tracker) { - // tracker should not modify `opts` object, it's passed to all trackers - tracker.scrape(opts) - }) -} - -Client.prototype.setInterval = function (intervalMs) { - var self = this - debug('setInterval %d', intervalMs) - self._trackers.forEach(function (tracker) { - tracker.setInterval(intervalMs) - }) -} - -Client.prototype.destroy = function (cb) { - var self = this - if (self.destroyed) return - self.destroyed = true - debug('destroy') - - var tasks = self._trackers.map(function (tracker) { - return function (cb) { - tracker.destroy(cb) - } - }) - - parallel(tasks, cb) - - self._trackers = [] - self._getAnnounceOpts = null -} - -Client.prototype._defaultAnnounceOpts = function (opts) { - var self = this - if (!opts) opts = {} - - if (opts.numwant == null) opts.numwant = common.DEFAULT_ANNOUNCE_PEERS - - if (opts.uploaded == null) opts.uploaded = 0 - if (opts.downloaded == null) opts.downloaded = 0 - - if (self._getAnnounceOpts) opts = Object.assign({}, opts, self._getAnnounceOpts()) - return opts -} +module.exports = Client diff --git a/index.js b/index.js index 1f79a6e..1e7f2a4 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ -var Client = require('./client') -var Server = require('./server') +const Client = require('./client') +const Server = require('./server') module.exports = Client module.exports.Client = Client