diff --git a/AUTHORS.md b/AUTHORS.md index 3d185d1f..0f21e9a1 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -43,5 +43,8 @@ - crapthings (crapthings@gmail.com) - daiyu (qqdaiyu55@gmail.com) - LI (kslrwang@gmail.com) +- Jimmy Wärting (jimmy@warting.se) +- Justin Kalland (justin@kalland.ch) +- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com) #### Generated by tools/update-authors.sh. diff --git a/client.js b/client.js index ee18dd06..9560fe48 100644 --- a/client.js +++ b/client.js @@ -1,22 +1,14 @@ -module.exports = Client - -var Buffer = require('safe-buffer').Buffer -var debug = require('debug')('bittorrent-tracker:client') -var EventEmitter = require('events').EventEmitter -var extend = require('xtend') -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 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 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. @@ -33,86 +25,217 @@ 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') + + 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') + + 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', this.infoHash) + + this.destroyed = false + + 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 + this._wrtc = typeof opts.wrtc === 'function' ? opts.wrtc() : opts.wrtc + + let announce = typeof opts.announce === 'string' + ? [opts.announce] + : opts.announce == null ? [] : opts.announce + + // 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) - 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') - - 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') - - debug('new client %s', self.infoHash) - - self.destroyed = false - - self._port = opts.port - self._getAnnounceOpts = opts.getAnnounceOpts - self._rtcConfig = opts.rtcConfig - self._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 - - var 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) + 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 => { + let parsedUrl + try { + parsedUrl = new URL(announceUrl) + } catch (err) { + nextTickWarn(new Error(`Invalid tracker URL: ${announceUrl}`)) return null } - return new WebSocketTracker(self, announceUrl) - } else { - nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl)) - return null - } + + const port = parsedUrl.port + if (port < 0 || port > 65535) { + nextTickWarn(new Error(`Invalid tracker port: ${announceUrl}`)) + return null + } + + const protocol = parsedUrl.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 + } + }) + .filter(Boolean) + } + + /** + * 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) }) - .filter(Boolean) + } - function nextTickWarn (err) { - process.nextTick(function () { - self.emit('warning', err) + 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 + } } /** @@ -124,30 +247,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 = extend(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 { @@ -157,7 +280,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') @@ -165,132 +288,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 = extend(opts, self._getAnnounceOpts()) - return opts -} +module.exports = Client diff --git a/index.js b/index.js index 1f79a6e6..1e7f2a45 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 diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js index 2cffc2a6..5f14a37d 100644 --- a/lib/client/http-tracker.js +++ b/lib/client/http-tracker.js @@ -1,19 +1,13 @@ -module.exports = HTTPTracker - -var arrayRemove = require('unordered-array-remove') -var bencode = require('bencode') -var compact2string = require('compact2string') -var debug = require('debug')('bittorrent-tracker:http-tracker') -var extend = require('xtend') -var get = require('simple-get') -var inherits = require('inherits') - -var common = require('../common') -var Tracker = require('./tracker') +const arrayRemove = require('unordered-array-remove') +const bencode = require('bencode') +const compact2string = require('compact2string') +const debug = require('debug')('bittorrent-tracker:http-tracker') +const get = require('simple-get') -var HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/ +const common = require('../common') +const Tracker = require('./tracker') -inherits(HTTPTracker, Tracker) +const HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/ /** * HTTP torrent tracker client (for an individual tracker) @@ -22,238 +16,235 @@ inherits(HTTPTracker, Tracker) * @param {string} announceUrl announce url of tracker * @param {Object} opts options object */ -function HTTPTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new http tracker %s', announceUrl) - - // Determine scrape url (if http tracker supports it) - self.scrapeUrl = null - - var match = self.announceUrl.match(HTTP_SCRAPE_SUPPORT) - if (match) { - var pre = self.announceUrl.slice(0, match.index) - var post = self.announceUrl.slice(match.index + 9) - self.scrapeUrl = pre + '/scrape' + post - } +class HTTPTracker extends Tracker { + constructor (client, announceUrl, opts) { + super(client, announceUrl) - self.cleanupFns = [] - self.maybeDestroyCleanup = null -} + debug('new http tracker %s', announceUrl) -HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes + // Determine scrape url (if http tracker supports it) + this.scrapeUrl = null -HTTPTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed) return - - var params = extend(opts, { - compact: (opts.compact == null) ? 1 : opts.compact, - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary, - port: self.client._port - }) - if (self._trackerId) params.trackerid = self._trackerId - - self._request(self.announceUrl, params, function (err, data) { - if (err) return self.client.emit('warning', err) - self._onAnnounceResponse(data) - }) -} - -HTTPTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed) return + const match = this.announceUrl.match(HTTP_SCRAPE_SUPPORT) + if (match) { + const pre = this.announceUrl.slice(0, match.index) + const post = this.announceUrl.slice(match.index + 9) + this.scrapeUrl = `${pre}/scrape${post}` + } - if (!self.scrapeUrl) { - self.client.emit('error', new Error('scrape not supported ' + self.announceUrl)) - return + this.cleanupFns = [] + this.maybeDestroyCleanup = null } - var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(function (infoHash) { - return infoHash.toString('binary') - }) - : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary - var params = { - info_hash: infoHashes - } - self._request(self.scrapeUrl, params, function (err, data) { - if (err) return self.client.emit('warning', err) - self._onScrapeResponse(data) - }) -} + announce (opts) { + if (this.destroyed) return -HTTPTracker.prototype.destroy = function (cb) { - var self = this - if (self.destroyed) return cb(null) - self.destroyed = true - clearInterval(self.interval) + const params = Object.assign({}, opts, { + compact: (opts.compact == null) ? 1 : opts.compact, + info_hash: this.client._infoHashBinary, + peer_id: this.client._peerIdBinary, + port: this.client._port + }) + if (this._trackerId) params.trackerid = this._trackerId - // If there are no pending requests, destroy immediately. - if (self.cleanupFns.length === 0) return destroyCleanup() + this._request(this.announceUrl, params, (err, data) => { + if (err) return this.client.emit('warning', err) + this._onAnnounceResponse(data) + }) + } - // Otherwise, wait a short time for pending requests to complete, then force - // destroy them. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) + scrape (opts) { + if (this.destroyed) return - // But, if all pending requests complete before the timeout fires, do cleanup - // right away. - self.maybeDestroyCleanup = function () { - if (self.cleanupFns.length === 0) destroyCleanup() - } + if (!this.scrapeUrl) { + this.client.emit('error', new Error(`scrape not supported ${this.announceUrl}`)) + return + } - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null + const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) + ? opts.infoHash.map(infoHash => { + return infoHash.toString('binary') + }) + : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + const params = { + info_hash: infoHashes } - self.maybeDestroyCleanup = null - self.cleanupFns.slice(0).forEach(function (cleanup) { - cleanup() + this._request(this.scrapeUrl, params, (err, data) => { + if (err) return this.client.emit('warning', err) + this._onScrapeResponse(data) }) - self.cleanupFns = [] - cb(null) } -} -HTTPTracker.prototype._request = function (requestUrl, params, cb) { - var self = this - var u = requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + - common.querystringStringify(params) + destroy (cb) { + const self = this + if (this.destroyed) return cb(null) + this.destroyed = true + clearInterval(this.interval) - self.cleanupFns.push(cleanup) + // If there are no pending requests, destroy immediately. + if (this.cleanupFns.length === 0) return destroyCleanup() - var request = get.concat({ - url: u, - timeout: common.REQUEST_TIMEOUT, - headers: { - 'user-agent': self.client._userAgent || '' - } - }, onResponse) + // Otherwise, wait a short time for pending requests to complete, then force + // destroy them. + var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - function cleanup () { - if (request) { - arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup)) - request.abort() - request = null + // But, if all pending requests complete before the timeout fires, do cleanup + // right away. + this.maybeDestroyCleanup = () => { + if (this.cleanupFns.length === 0) destroyCleanup() } - if (self.maybeDestroyCleanup) self.maybeDestroyCleanup() - } - - function onResponse (err, res, data) { - cleanup() - if (self.destroyed) return - if (err) return cb(err) - if (res.statusCode !== 200) { - return cb(new Error('Non-200 response code ' + - res.statusCode + ' from ' + self.announceUrl)) - } - if (!data || data.length === 0) { - return cb(new Error('Invalid tracker response from' + - self.announceUrl)) + function destroyCleanup () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + self.maybeDestroyCleanup = null + self.cleanupFns.slice(0).forEach(cleanup => { + cleanup() + }) + self.cleanupFns = [] + cb(null) } + } - try { - data = bencode.decode(data) - } catch (err) { - return cb(new Error('Error decoding tracker response: ' + err.message)) - } - var failure = data['failure reason'] - if (failure) { - debug('failure from ' + requestUrl + ' (' + failure + ')') - return cb(new Error(failure)) + _request (requestUrl, params, cb) { + const self = this + const u = requestUrl + (!requestUrl.includes('?') ? '?' : '&') + + common.querystringStringify(params) + + this.cleanupFns.push(cleanup) + + let request = get.concat({ + url: u, + timeout: common.REQUEST_TIMEOUT, + headers: { + 'user-agent': this.client._userAgent || '' + } + }, onResponse) + + function cleanup () { + if (request) { + arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup)) + request.abort() + request = null + } + if (self.maybeDestroyCleanup) self.maybeDestroyCleanup() } - var warning = data['warning message'] - if (warning) { - debug('warning from ' + requestUrl + ' (' + warning + ')') - self.client.emit('warning', new Error(warning)) + function onResponse (err, res, data) { + cleanup() + if (self.destroyed) return + + if (err) return cb(err) + if (res.statusCode !== 200) { + return cb(new Error(`Non-200 response code ${res.statusCode} from ${self.announceUrl}`)) + } + if (!data || data.length === 0) { + return cb(new Error(`Invalid tracker response from${self.announceUrl}`)) + } + + try { + data = bencode.decode(data) + } catch (err) { + return cb(new Error(`Error decoding tracker response: ${err.message}`)) + } + const failure = data['failure reason'] + if (failure) { + debug(`failure from ${requestUrl} (${failure})`) + return cb(new Error(failure)) + } + + const warning = data['warning message'] + if (warning) { + debug(`warning from ${requestUrl} (${warning})`) + self.client.emit('warning', new Error(warning)) + } + + debug(`response from ${requestUrl}`) + + cb(null, data) } - - debug('response from ' + requestUrl) - - cb(null, data) } -} -HTTPTracker.prototype._onAnnounceResponse = function (data) { - var self = this + _onAnnounceResponse (data) { + const interval = data.interval || data['min interval'] + if (interval) this.setInterval(interval * 1000) - var interval = data.interval || data['min interval'] - if (interval) self.setInterval(interval * 1000) + const trackerId = data['tracker id'] + if (trackerId) { + // If absent, do not discard previous trackerId value + this._trackerId = trackerId + } - var trackerId = data['tracker id'] - if (trackerId) { - // If absent, do not discard previous trackerId value - self._trackerId = trackerId - } + const response = Object.assign({}, data, { + announce: this.announceUrl, + infoHash: common.binaryToHex(data.info_hash) + }) + this.client.emit('update', response) + + let addrs + if (Buffer.isBuffer(data.peers)) { + // tracker returned compact response + try { + addrs = compact2string.multi(data.peers) + } catch (err) { + return this.client.emit('warning', err) + } + addrs.forEach(addr => { + this.client.emit('peer', addr) + }) + } else if (Array.isArray(data.peers)) { + // tracker returned normal response + data.peers.forEach(peer => { + this.client.emit('peer', `${peer.ip}:${peer.port}`) + }) + } - var response = Object.assign({}, data, { - announce: self.announceUrl, - infoHash: common.binaryToHex(data.info_hash) - }) - self.client.emit('update', response) - - var addrs - if (Buffer.isBuffer(data.peers)) { - // tracker returned compact response - try { - addrs = compact2string.multi(data.peers) - } catch (err) { - return self.client.emit('warning', err) + if (Buffer.isBuffer(data.peers6)) { + // tracker returned compact response + try { + addrs = compact2string.multi6(data.peers6) + } catch (err) { + return this.client.emit('warning', err) + } + addrs.forEach(addr => { + this.client.emit('peer', addr) + }) + } else if (Array.isArray(data.peers6)) { + // tracker returned normal response + data.peers6.forEach(peer => { + const ip = /^\[/.test(peer.ip) || !/:/.test(peer.ip) + ? peer.ip /* ipv6 w/ brackets or domain name */ + : `[${peer.ip}]` /* ipv6 without brackets */ + this.client.emit('peer', `${ip}:${peer.port}`) + }) } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) - } else if (Array.isArray(data.peers)) { - // tracker returned normal response - data.peers.forEach(function (peer) { - self.client.emit('peer', peer.ip + ':' + peer.port) - }) } - if (Buffer.isBuffer(data.peers6)) { - // tracker returned compact response - try { - addrs = compact2string.multi6(data.peers6) - } catch (err) { - return self.client.emit('warning', err) + _onScrapeResponse (data) { + // NOTE: the unofficial spec says to use the 'files' key, 'host' has been + // seen in practice + data = data.files || data.host || {} + + const keys = Object.keys(data) + if (keys.length === 0) { + this.client.emit('warning', new Error('invalid scrape response')) + return } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) - } else if (Array.isArray(data.peers6)) { - // tracker returned normal response - data.peers6.forEach(function (peer) { - var ip = /^\[/.test(peer.ip) || !/:/.test(peer.ip) - ? peer.ip /* ipv6 w/ brackets or domain name */ - : '[' + peer.ip + ']' /* ipv6 without brackets */ - self.client.emit('peer', ip + ':' + peer.port) + + keys.forEach(infoHash => { + // TODO: optionally handle data.flags.min_request_interval + // (separate from announce interval) + const response = Object.assign(data[infoHash], { + announce: this.announceUrl, + infoHash: common.binaryToHex(infoHash) + }) + this.client.emit('scrape', response) }) } } -HTTPTracker.prototype._onScrapeResponse = function (data) { - var self = this - // NOTE: the unofficial spec says to use the 'files' key, 'host' has been - // seen in practice - data = data.files || data.host || {} - - var keys = Object.keys(data) - if (keys.length === 0) { - self.client.emit('warning', new Error('invalid scrape response')) - return - } +HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes - keys.forEach(function (infoHash) { - // TODO: optionally handle data.flags.min_request_interval - // (separate from announce interval) - var response = Object.assign(data[infoHash], { - announce: self.announceUrl, - infoHash: common.binaryToHex(infoHash) - }) - self.client.emit('scrape', response) - }) -} +module.exports = HTTPTracker diff --git a/lib/client/tracker.js b/lib/client/tracker.js index 73de2cd2..cbbd23de 100644 --- a/lib/client/tracker.js +++ b/lib/client/tracker.js @@ -1,30 +1,28 @@ -module.exports = Tracker - -var EventEmitter = require('events').EventEmitter -var inherits = require('inherits') +const EventEmitter = require('events') -inherits(Tracker, EventEmitter) +class Tracker extends EventEmitter { + constructor (client, announceUrl) { + super() -function Tracker (client, announceUrl) { - var self = this - EventEmitter.call(self) - self.client = client - self.announceUrl = announceUrl + this.client = client + this.announceUrl = announceUrl - self.interval = null - self.destroyed = false -} + this.interval = null + this.destroyed = false + } -Tracker.prototype.setInterval = function (intervalMs) { - var self = this - if (intervalMs == null) intervalMs = self.DEFAULT_ANNOUNCE_INTERVAL + setInterval (intervalMs) { + if (intervalMs == null) intervalMs = this.DEFAULT_ANNOUNCE_INTERVAL - clearInterval(self.interval) + clearInterval(this.interval) - if (intervalMs) { - self.interval = setInterval(function () { - self.announce(self.client._defaultAnnounceOpts()) - }, intervalMs) - if (self.interval.unref) self.interval.unref() + if (intervalMs) { + this.interval = setInterval(() => { + this.announce(this.client._defaultAnnounceOpts()) + }, intervalMs) + if (this.interval.unref) this.interval.unref() + } } } + +module.exports = Tracker diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js index 3198edc5..24a30800 100644 --- a/lib/client/udp-tracker.js +++ b/lib/client/udp-tracker.js @@ -1,19 +1,12 @@ -module.exports = UDPTracker - -var arrayRemove = require('unordered-array-remove') -var BN = require('bn.js') -var Buffer = require('safe-buffer').Buffer -var compact2string = require('compact2string') -var debug = require('debug')('bittorrent-tracker:udp-tracker') -var dgram = require('dgram') -var inherits = require('inherits') -var randombytes = require('randombytes') -var url = require('url') +const arrayRemove = require('unordered-array-remove') +const BN = require('bn.js') +const compact2string = require('compact2string') +const debug = require('debug')('bittorrent-tracker:udp-tracker') +const dgram = require('dgram') +const randombytes = require('randombytes') -var common = require('../common') -var Tracker = require('./tracker') - -inherits(UDPTracker, Tracker) +const common = require('../common') +const Tracker = require('./tracker') /** * UDP torrent tracker client (for an individual tracker) @@ -22,256 +15,255 @@ inherits(UDPTracker, Tracker) * @param {string} announceUrl announce url of tracker * @param {Object} opts options object */ -function UDPTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new udp tracker %s', announceUrl) +class UDPTracker extends Tracker { + constructor (client, announceUrl, opts) { + super(client, announceUrl) + debug('new udp tracker %s', announceUrl) - self.cleanupFns = [] - self.maybeDestroyCleanup = null -} - -UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes + this.cleanupFns = [] + this.maybeDestroyCleanup = null + } -UDPTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed) return - self._request(opts) -} + announce (opts) { + if (this.destroyed) return + this._request(opts) + } -UDPTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed) return - opts._scrape = true - self._request(opts) // udp scrape uses same announce url -} + scrape (opts) { + if (this.destroyed) return + opts._scrape = true + this._request(opts) // udp scrape uses same announce url + } -UDPTracker.prototype.destroy = function (cb) { - var self = this - if (self.destroyed) return cb(null) - self.destroyed = true - clearInterval(self.interval) + destroy (cb) { + const self = this + if (this.destroyed) return cb(null) + this.destroyed = true + clearInterval(this.interval) - // If there are no pending requests, destroy immediately. - if (self.cleanupFns.length === 0) return destroyCleanup() + // If there are no pending requests, destroy immediately. + if (this.cleanupFns.length === 0) return destroyCleanup() - // Otherwise, wait a short time for pending requests to complete, then force - // destroy them. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) + // Otherwise, wait a short time for pending requests to complete, then force + // destroy them. + var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - // But, if all pending requests complete before the timeout fires, do cleanup - // right away. - self.maybeDestroyCleanup = function () { - if (self.cleanupFns.length === 0) destroyCleanup() - } + // But, if all pending requests complete before the timeout fires, do cleanup + // right away. + this.maybeDestroyCleanup = () => { + if (this.cleanupFns.length === 0) destroyCleanup() + } - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null + function destroyCleanup () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + self.maybeDestroyCleanup = null + self.cleanupFns.slice(0).forEach(cleanup => { + cleanup() + }) + self.cleanupFns = [] + cb(null) } - self.maybeDestroyCleanup = null - self.cleanupFns.slice(0).forEach(function (cleanup) { - cleanup() - }) - self.cleanupFns = [] - cb(null) } -} -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 timeout = setTimeout(function () { - // does not matter if `stopped` event arrives, so supress errors - if (opts.event === 'stopped') cleanup() - else onError(new Error('tracker request timed out (' + opts.event + ')')) - timeout = null - }, common.REQUEST_TIMEOUT) - if (timeout.unref) timeout.unref() - - self.cleanupFns.push(cleanup) - - send(Buffer.concat([ - common.CONNECTION_ID, - common.toUInt32(common.ACTIONS.CONNECT), - transactionId - ])) - - socket.once('error', onError) - socket.on('message', onSocketMessage) - - function cleanup () { - if (timeout) { - clearTimeout(timeout) + _request (opts) { + const self = this + if (!opts) opts = {} + const parsedUrl = new URL(this.announceUrl) + let transactionId = genTransactionId() + let socket = dgram.createSocket('udp4') + + let timeout = setTimeout(() => { + // does not matter if `stopped` event arrives, so supress errors + if (opts.event === 'stopped') cleanup() + else onError(new Error(`tracker request timed out (${opts.event})`)) timeout = null - } - if (socket) { - arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup)) - socket.removeListener('error', onError) - socket.removeListener('message', onSocketMessage) - socket.on('error', noop) // ignore all future errors - try { socket.close() } catch (err) {} - socket = null - } - if (self.maybeDestroyCleanup) self.maybeDestroyCleanup() - } + }, common.REQUEST_TIMEOUT) + if (timeout.unref) timeout.unref() - function onError (err) { - cleanup() - if (self.destroyed) return + this.cleanupFns.push(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) - } + send(Buffer.concat([ + common.CONNECTION_ID, + common.toUInt32(common.ACTIONS.CONNECT), + transactionId + ])) - function onSocketMessage (msg) { - if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { - return onError(new Error('tracker sent invalid transaction id')) + socket.once('error', onError) + socket.on('message', onSocketMessage) + + function cleanup () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + if (socket) { + arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup)) + socket.removeListener('error', onError) + socket.removeListener('message', onSocketMessage) + socket.on('error', noop) // ignore all future errors + try { socket.close() } catch (err) {} + socket = null + } + if (self.maybeDestroyCleanup) self.maybeDestroyCleanup() } - var action = msg.readUInt32BE(0) - debug('UDP response %s, action %s', self.announceUrl, action) - switch (action) { - case 0: // handshake - // Note: no check for `self.destroyed` so that pending messages to the - // tracker can still be sent/received even after destroy() is called - - 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) + function onError (err) { + cleanup() + if (self.destroyed) return - break + 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) + } - case 1: // announce - cleanup() - if (self.destroyed) return + function onSocketMessage (msg) { + if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { + return onError(new Error('tracker sent invalid transaction id')) + } - if (msg.length < 20) return onError(new Error('invalid announce message')) + const action = msg.readUInt32BE(0) + debug('UDP response %s, action %s', self.announceUrl, action) + switch (action) { + case 0: { // handshake + // Note: no check for `self.destroyed` so that pending messages to the + // tracker can still be sent/received even after destroy() is called - var interval = msg.readUInt32BE(8) - if (interval) self.setInterval(interval * 1000) + if (msg.length < 16) return onError(new Error('invalid udp handshake')) - self.client.emit('update', { - announce: self.announceUrl, - complete: msg.readUInt32BE(16), - incomplete: msg.readUInt32BE(12) - }) + if (opts._scrape) scrape(msg.slice(8, 16)) + else announce(msg.slice(8, 16), opts) - var addrs - try { - addrs = compact2string.multi(msg.slice(20)) - } catch (err) { - return self.client.emit('warning', err) + break } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) + case 1: { // announce + cleanup() + if (self.destroyed) return - break + if (msg.length < 20) return onError(new Error('invalid announce message')) - case 2: // scrape - cleanup() - if (self.destroyed) return + const interval = msg.readUInt32BE(8) + if (interval) self.setInterval(interval * 1000) - 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 && opts.infoHash.toString('hex')) || self.client.infoHash ] - - for (var i = 0, len = (msg.length - 8) / 12; i < len; i += 1) { - self.client.emit('scrape', { + self.client.emit('update', { announce: self.announceUrl, - infoHash: infoHashes[i], - complete: msg.readUInt32BE(8 + (i * 12)), - downloaded: msg.readUInt32BE(12 + (i * 12)), - incomplete: msg.readUInt32BE(16 + (i * 12)) + complete: msg.readUInt32BE(16), + incomplete: msg.readUInt32BE(12) }) - } - - break - case 3: // error - cleanup() - if (self.destroyed) return + let addrs + try { + addrs = compact2string.multi(msg.slice(20)) + } catch (err) { + return self.client.emit('warning', err) + } + addrs.forEach(addr => { + self.client.emit('peer', addr) + }) - if (msg.length < 8) return onError(new Error('invalid error message')) - self.client.emit('warning', new Error(msg.slice(8).toString())) + break + } + case 2: { // scrape + cleanup() + if (self.destroyed) return + + if (msg.length < 20 || (msg.length - 8) % 12 !== 0) { + return onError(new Error('invalid scrape message')) + } + const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) + ? opts.infoHash.map(infoHash => { return infoHash.toString('hex') }) + : [(opts.infoHash && opts.infoHash.toString('hex')) || self.client.infoHash] + + for (let 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 (self.destroyed) return - break + if (msg.length < 8) return onError(new Error('invalid error message')) + self.client.emit('warning', new Error(msg.slice(8).toString())) - default: - onError(new Error('tracker sent invalid action')) - break + break + } + default: + onError(new Error('tracker sent invalid action')) + break + } } - } - function send (message) { - if (!parsedUrl.port) { - parsedUrl.port = 80 + function send (message) { + if (!parsedUrl.port) { + parsedUrl.port = 80 + } + socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) } - socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) - } - - function announce (connectionId, opts) { - transactionId = genTransactionId() - send(Buffer.concat([ - connectionId, - common.toUInt32(common.ACTIONS.ANNOUNCE), - transactionId, - self.client._infoHashBuffer, - self.client._peerIdBuffer, - toUInt64(opts.downloaded), - opts.left != null ? toUInt64(opts.left) : Buffer.from('FFFFFFFFFFFFFFFF', 'hex'), - toUInt64(opts.uploaded), - common.toUInt32(common.EVENTS[opts.event] || 0), - common.toUInt32(0), // ip address (optional) - common.toUInt32(0), // key (optional) - common.toUInt32(opts.numwant), - toUInt16(self.client._port) - ])) - } + function announce (connectionId, opts) { + transactionId = genTransactionId() + + send(Buffer.concat([ + connectionId, + common.toUInt32(common.ACTIONS.ANNOUNCE), + transactionId, + self.client._infoHashBuffer, + self.client._peerIdBuffer, + toUInt64(opts.downloaded), + opts.left != null ? toUInt64(opts.left) : Buffer.from('FFFFFFFFFFFFFFFF', 'hex'), + toUInt64(opts.uploaded), + common.toUInt32(common.EVENTS[opts.event] || 0), + common.toUInt32(0), // ip address (optional) + common.toUInt32(0), // key (optional) + common.toUInt32(opts.numwant), + toUInt16(self.client._port) + ])) + } - function scrape (connectionId) { - transactionId = genTransactionId() + function scrape (connectionId) { + transactionId = genTransactionId() - var infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? Buffer.concat(opts.infoHash) - : (opts.infoHash || self.client._infoHashBuffer) + const infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) + ? Buffer.concat(opts.infoHash) + : (opts.infoHash || self.client._infoHashBuffer) - send(Buffer.concat([ - connectionId, - common.toUInt32(common.ACTIONS.SCRAPE), - transactionId, - infoHash - ])) + send(Buffer.concat([ + connectionId, + common.toUInt32(common.ACTIONS.SCRAPE), + transactionId, + infoHash + ])) + } } } +UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes + function genTransactionId () { return randombytes(4) } function toUInt16 (n) { - var buf = Buffer.allocUnsafe(2) + const buf = Buffer.allocUnsafe(2) buf.writeUInt16BE(n, 0) return buf } -var MAX_UINT = 4294967295 +const MAX_UINT = 4294967295 function toUInt64 (n) { if (n > MAX_UINT || typeof n === 'string') { - var bytes = new BN(n).toArray() + const bytes = new BN(n).toArray() while (bytes.length < 8) { bytes.unshift(0) } @@ -281,3 +273,5 @@ function toUInt64 (n) { } function noop () {} + +module.exports = UDPTracker diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index 04e99320..6f9e9e7b 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -1,446 +1,429 @@ -module.exports = WebSocketTracker - -var debug = require('debug')('bittorrent-tracker:websocket-tracker') -var extend = require('xtend') -var inherits = require('inherits') -var Peer = require('simple-peer') -var randombytes = require('randombytes') -var Socket = require('simple-websocket') +const debug = require('debug')('bittorrent-tracker:websocket-tracker') +const Peer = require('simple-peer') +const randombytes = require('randombytes') +const Socket = require('simple-websocket') -var common = require('../common') -var Tracker = require('./tracker') +const common = require('../common') +const Tracker = require('./tracker') // Use a socket pool, so tracker clients share WebSocket objects for the same server. // In practice, WebSockets are pretty slow to establish, so this gives a nice performance // boost, and saves browser resources. -var socketPool = {} -// Normally this shouldn't be accessed but is occasionally useful -WebSocketTracker._socketPool = socketPool +const socketPool = {} -var RECONNECT_MINIMUM = 15 * 1000 -var RECONNECT_MAXIMUM = 30 * 60 * 1000 -var RECONNECT_VARIANCE = 30 * 1000 -var OFFER_TIMEOUT = 50 * 1000 +const RECONNECT_MINIMUM = 10 * 1000 +const RECONNECT_MAXIMUM = 30 * 60 * 1000 +const RECONNECT_VARIANCE = 2 * 60 * 1000 +const OFFER_TIMEOUT = 50 * 1000 -inherits(WebSocketTracker, Tracker) +class WebSocketTracker extends Tracker { + constructor (client, announceUrl, opts) { + super(client, announceUrl) + debug('new websocket tracker %s', announceUrl) -function WebSocketTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new websocket tracker %s', announceUrl) + this.peers = {} // peers (offer id -> peer) + this.socket = null - self.peers = {} // peers (offer id -> peer) - self.socket = null + this.reconnecting = false + this.retries = 0 + this.reconnectTimer = null - self.reconnecting = false - self.retries = 0 - self.reconnectTimer = null + // Simple boolean flag to track whether the socket has received data from + // the websocket server since the last time socket.send() was called. + this.expectingResponse = false - // Simple boolean flag to track whether the socket has received data from - // the websocket server since the last time socket.send() was called. - self.expectingResponse = false - - self._openSocket() -} + this._openSocket() + } -WebSocketTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 1000 // 30 seconds + announce (opts) { + if (this.destroyed || this.reconnecting) return + if (!this.socket.connected) { + this.socket.once('connect', () => { + this.announce(opts) + }) + return + } -WebSocketTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed || self.reconnecting) return - if (!self.socket.connected) { - self.socket.once('connect', function () { - self.announce(opts) + const params = Object.assign({}, opts, { + action: 'announce', + info_hash: this.client._infoHashBinary, + peer_id: this.client._peerIdBinary }) - return - } + if (this._trackerId) params.trackerid = this._trackerId - var params = extend(opts, { - action: 'announce', - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary - }) - if (self._trackerId) params.trackerid = self._trackerId - - if (opts.event === 'stopped' || opts.event === 'completed') { - // Don't include offers with 'stopped' or 'completed' event - self._send(params) - } else { - // Limit the number of offers that are generated, since it can be slow - var numwant = Math.min(opts.numwant, 10) - - self._generateOffers(numwant, function (offers) { - params.numwant = numwant - params.offers = offers - self._send(params) - }) - } -} + if (opts.event === 'stopped' || opts.event === 'completed') { + // Don't include offers with 'stopped' or 'completed' event + this._send(params) + } else { + // Limit the number of offers that are generated, since it can be slow + const numwant = Math.min(opts.numwant, 10) -WebSocketTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed || self.reconnecting) return - if (!self.socket.connected) { - self.socket.once('connect', function () { - self.scrape(opts) - }) - return + this._generateOffers(numwant, offers => { + params.numwant = numwant + params.offers = offers + this._send(params) + }) + } } - var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(function (infoHash) { - return infoHash.toString('binary') - }) - : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary - var params = { - action: 'scrape', - info_hash: infoHashes - } + scrape (opts) { + if (this.destroyed || this.reconnecting) return + if (!this.socket.connected) { + this.socket.once('connect', () => { + this.scrape(opts) + }) + return + } - self._send(params) -} + const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) + ? opts.infoHash.map(infoHash => { + return infoHash.toString('binary') + }) + : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + const params = { + action: 'scrape', + info_hash: infoHashes + } -WebSocketTracker.prototype.destroy = function (cb) { - var self = this - if (!cb) cb = noop - if (self.destroyed) return cb(null) + this._send(params) + } - self.destroyed = true + destroy (cb = noop) { + if (this.destroyed) return cb(null) - clearInterval(self.interval) - clearTimeout(self.reconnectTimer) + this.destroyed = true - // Destroy peers - for (var peerId in self.peers) { - var peer = self.peers[peerId] - clearTimeout(peer.trackerTimeout) - peer.destroy() - } - self.peers = null - - if (self.socket) { - self.socket.removeListener('connect', self._onSocketConnectBound) - self.socket.removeListener('data', self._onSocketDataBound) - self.socket.removeListener('close', self._onSocketCloseBound) - self.socket.removeListener('error', self._onSocketErrorBound) - self.socket = null - } + clearInterval(this.interval) + clearTimeout(this.reconnectTimer) - self._onSocketConnectBound = null - self._onSocketErrorBound = null - self._onSocketDataBound = null - self._onSocketCloseBound = null + // Destroy peers + for (const peerId in this.peers) { + const peer = this.peers[peerId] + clearTimeout(peer.trackerTimeout) + peer.destroy() + } + this.peers = null + + if (this.socket) { + this.socket.removeListener('connect', this._onSocketConnectBound) + this.socket.removeListener('data', this._onSocketDataBound) + this.socket.removeListener('close', this._onSocketCloseBound) + this.socket.removeListener('error', this._onSocketErrorBound) + this.socket = null + } - if (socketPool[self.announceUrl]) { - socketPool[self.announceUrl].consumers -= 1 - } + this._onSocketConnectBound = null + this._onSocketErrorBound = null + this._onSocketDataBound = null + this._onSocketCloseBound = null + + if (socketPool[this.announceUrl]) { + socketPool[this.announceUrl].consumers -= 1 + } - // Other instances are using the socket, so there's nothing left to do here - if (socketPool[self.announceUrl].consumers > 0) return cb() + // Other instances are using the socket, so there's nothing left to do here + if (socketPool[this.announceUrl].consumers > 0) return cb() - var socket = socketPool[self.announceUrl] - delete socketPool[self.announceUrl] - socket.on('error', noop) // ignore all future errors - socket.once('close', cb) + let socket = socketPool[this.announceUrl] + delete socketPool[this.announceUrl] + socket.on('error', noop) // ignore all future errors + socket.once('close', cb) - // If there is no data response expected, destroy immediately. - if (!self.expectingResponse) return destroyCleanup() + // If there is no data response expected, destroy immediately. + if (!this.expectingResponse) return destroyCleanup() - // Otherwise, wait a short time for potential responses to come in from the - // server, then force close the socket. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) + // Otherwise, wait a short time for potential responses to come in from the + // server, then force close the socket. + var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - // But, if a response comes from the server before the timeout fires, do cleanup - // right away. - socket.once('data', destroyCleanup) + // But, if a response comes from the server before the timeout fires, do cleanup + // right away. + socket.once('data', destroyCleanup) - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null + function destroyCleanup () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + socket.removeListener('data', destroyCleanup) + socket.destroy() + socket = null } - socket.removeListener('data', destroyCleanup) - socket.destroy() - socket = null } -} -WebSocketTracker.prototype._openSocket = function () { - var self = this - self.destroyed = false + _openSocket () { + this.destroyed = false - if (!self.peers) self.peers = {} + if (!this.peers) this.peers = {} - self._onSocketConnectBound = function () { - self._onSocketConnect() - } - self._onSocketErrorBound = function (err) { - self._onSocketError(err) - } - self._onSocketDataBound = function (data) { - self._onSocketData(data) - } - self._onSocketCloseBound = function () { - self._onSocketClose() - } + this._onSocketConnectBound = () => { + this._onSocketConnect() + } + this._onSocketErrorBound = err => { + this._onSocketError(err) + } + this._onSocketDataBound = data => { + this._onSocketData(data) + } + this._onSocketCloseBound = () => { + this._onSocketClose() + } - self.socket = socketPool[self.announceUrl] - if (self.socket) { - socketPool[self.announceUrl].consumers += 1 - } else { - self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl) - self.socket.consumers = 1 - self.socket.once('connect', self._onSocketConnectBound) - } + this.socket = socketPool[this.announceUrl] + if (this.socket) { + socketPool[this.announceUrl].consumers += 1 + } else { + this.socket = socketPool[this.announceUrl] = new Socket(this.announceUrl) + this.socket.consumers = 1 + this.socket.once('connect', this._onSocketConnectBound) + } - self.socket.on('data', self._onSocketDataBound) - self.socket.once('close', self._onSocketCloseBound) - self.socket.once('error', self._onSocketErrorBound) -} + this.socket.on('data', this._onSocketDataBound) + this.socket.once('close', this._onSocketCloseBound) + this.socket.once('error', this._onSocketErrorBound) + } -WebSocketTracker.prototype._onSocketConnect = function () { - var self = this - if (self.destroyed) return + _onSocketConnect () { + if (this.destroyed) return - if (self.reconnecting) { - self.reconnecting = false - self.retries = 0 - self.announce(self.client._defaultAnnounceOpts()) + if (this.reconnecting) { + this.reconnecting = false + this.retries = 0 + this.announce(this.client._defaultAnnounceOpts()) + } } -} -WebSocketTracker.prototype._onSocketData = function (data) { - var self = this - if (self.destroyed) return + _onSocketData (data) { + if (this.destroyed) return - self.expectingResponse = false + this.expectingResponse = false - try { - data = JSON.parse(data) - } catch (err) { - self.client.emit('warning', new Error('Invalid tracker response')) - return - } + try { + data = JSON.parse(data) + } catch (err) { + this.client.emit('warning', new Error('Invalid tracker response')) + return + } - if (data.action === 'announce') { - self._onAnnounceResponse(data) - } else if (data.action === 'scrape') { - self._onScrapeResponse(data) - } else { - self._onSocketError(new Error('invalid action in WS response: ' + data.action)) + if (data.action === 'announce') { + this._onAnnounceResponse(data) + } else if (data.action === 'scrape') { + this._onScrapeResponse(data) + } else { + this._onSocketError(new Error(`invalid action in WS response: ${data.action}`)) + } } -} -WebSocketTracker.prototype._onAnnounceResponse = function (data) { - var self = this + _onAnnounceResponse (data) { + if (data.info_hash !== this.client._infoHashBinary) { + debug( + 'ignoring websocket data from %s for %s (looking for %s: reused socket)', + this.announceUrl, common.binaryToHex(data.info_hash), this.client.infoHash + ) + return + } + + if (data.peer_id && data.peer_id === this.client._peerIdBinary) { + // ignore offers/answers from this client + return + } - if (data.info_hash !== self.client._infoHashBinary) { debug( - 'ignoring websocket data from %s for %s (looking for %s: reused socket)', - self.announceUrl, common.binaryToHex(data.info_hash), self.client.infoHash + 'received %s from %s for %s', + JSON.stringify(data), this.announceUrl, this.client.infoHash ) - return - } - - if (data.peer_id && data.peer_id === self.client._peerIdBinary) { - // ignore offers/answers from this client - return - } - debug( - 'received %s from %s for %s', - JSON.stringify(data), self.announceUrl, self.client.infoHash - ) + const failure = data['failure reason'] + if (failure) return this.client.emit('warning', new Error(failure)) - var failure = data['failure reason'] - if (failure) return self.client.emit('warning', new Error(failure)) + const warning = data['warning message'] + if (warning) this.client.emit('warning', new Error(warning)) - var warning = data['warning message'] - if (warning) self.client.emit('warning', new Error(warning)) + const interval = data.interval || data['min interval'] + if (interval) this.setInterval(interval * 1000) - var interval = data.interval || data['min interval'] - if (interval) self.setInterval(interval * 1000) - - var trackerId = data['tracker id'] - if (trackerId) { - // If absent, do not discard previous trackerId value - self._trackerId = trackerId - } - - if (data.complete != null) { - var response = Object.assign({}, data, { - announce: self.announceUrl, - infoHash: common.binaryToHex(data.info_hash) - }) - self.client.emit('update', response) - } + const trackerId = data['tracker id'] + if (trackerId) { + // If absent, do not discard previous trackerId value + this._trackerId = trackerId + } - var peer - if (data.offer && data.peer_id) { - debug('creating peer (from remote offer)') - peer = self._createPeer() - peer.id = common.binaryToHex(data.peer_id) - peer.once('signal', function (answer) { - var params = { - action: 'announce', - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary, - to_peer_id: data.peer_id, - answer: answer, - offer_id: data.offer_id - } - if (self._trackerId) params.trackerid = self._trackerId - self._send(params) - }) - peer.signal(data.offer) - self.client.emit('peer', peer) - } + if (data.complete != null) { + const response = Object.assign({}, data, { + announce: this.announceUrl, + infoHash: common.binaryToHex(data.info_hash) + }) + this.client.emit('update', response) + } - if (data.answer && data.peer_id) { - var offerId = common.binaryToHex(data.offer_id) - peer = self.peers[offerId] - if (peer) { + let peer + if (data.offer && data.peer_id) { + debug('creating peer (from remote offer)') + peer = this._createPeer() peer.id = common.binaryToHex(data.peer_id) - peer.signal(data.answer) - self.client.emit('peer', peer) + peer.once('signal', answer => { + const params = { + action: 'announce', + info_hash: this.client._infoHashBinary, + peer_id: this.client._peerIdBinary, + to_peer_id: data.peer_id, + answer, + offer_id: data.offer_id + } + if (this._trackerId) params.trackerid = this._trackerId + this._send(params) + }) + peer.signal(data.offer) + this.client.emit('peer', peer) + } - clearTimeout(peer.trackerTimeout) - peer.trackerTimeout = null - delete self.peers[offerId] - } else { - debug('got unexpected answer: ' + JSON.stringify(data.answer)) + if (data.answer && data.peer_id) { + const offerId = common.binaryToHex(data.offer_id) + peer = this.peers[offerId] + if (peer) { + peer.id = common.binaryToHex(data.peer_id) + peer.signal(data.answer) + this.client.emit('peer', peer) + + clearTimeout(peer.trackerTimeout) + peer.trackerTimeout = null + delete this.peers[offerId] + } else { + debug(`got unexpected answer: ${JSON.stringify(data.answer)}`) + } } } -} -WebSocketTracker.prototype._onScrapeResponse = function (data) { - var self = this - data = data.files || {} + _onScrapeResponse (data) { + data = data.files || {} - var keys = Object.keys(data) - if (keys.length === 0) { - self.client.emit('warning', new Error('invalid scrape response')) - return - } + const keys = Object.keys(data) + if (keys.length === 0) { + this.client.emit('warning', new Error('invalid scrape response')) + return + } - keys.forEach(function (infoHash) { - // TODO: optionally handle data.flags.min_request_interval - // (separate from announce interval) - var response = Object.assign(data[infoHash], { - announce: self.announceUrl, - infoHash: common.binaryToHex(infoHash) + keys.forEach(infoHash => { + // TODO: optionally handle data.flags.min_request_interval + // (separate from announce interval) + const response = Object.assign(data[infoHash], { + announce: this.announceUrl, + infoHash: common.binaryToHex(infoHash) + }) + this.client.emit('scrape', response) }) - self.client.emit('scrape', response) - }) -} + } -WebSocketTracker.prototype._onSocketClose = function () { - var self = this - if (self.destroyed) return - self.destroy() - self._startReconnectTimer() -} + _onSocketClose () { + if (this.destroyed) return + this.destroy() + this._startReconnectTimer() + } -WebSocketTracker.prototype._onSocketError = function (err) { - var self = this - if (self.destroyed) return - self.destroy() - // errors will often happen if a tracker is offline, so don't treat it as fatal - self.client.emit('warning', err) - self._startReconnectTimer() -} + _onSocketError (err) { + if (this.destroyed) return + this.destroy() + // errors will often happen if a tracker is offline, so don't treat it as fatal + this.client.emit('warning', err) + this._startReconnectTimer() + } -WebSocketTracker.prototype._startReconnectTimer = function () { - var self = this - var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, self.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM) + _startReconnectTimer () { + const ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, this.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM) - self.reconnecting = true - clearTimeout(self.reconnectTimer) - self.reconnectTimer = setTimeout(function () { - self.retries++ - self._openSocket() - }, ms) - if (self.reconnectTimer.unref) self.reconnectTimer.unref() + this.reconnecting = true + clearTimeout(this.reconnectTimer) + this.reconnectTimer = setTimeout(() => { + this.retries++ + this._openSocket() + }, ms) + if (this.reconnectTimer.unref) this.reconnectTimer.unref() - debug('reconnecting socket in %s ms', ms) -} + debug('reconnecting socket in %s ms', ms) + } -WebSocketTracker.prototype._send = function (params) { - var self = this - if (self.destroyed) return - self.expectingResponse = true - var message = JSON.stringify(params) - debug('send %s', message) - self.socket.send(message) -} + _send (params) { + if (this.destroyed) return + this.expectingResponse = true + const message = JSON.stringify(params) + debug('send %s', message) + this.socket.send(message) + } -WebSocketTracker.prototype._generateOffers = function (numwant, cb) { - var self = this - var offers = [] - debug('generating %s offers', numwant) + _generateOffers (numwant, cb) { + const self = this + const offers = [] + debug('generating %s offers', numwant) - for (var i = 0; i < numwant; ++i) { - generateOffer() - } - checkDone() - - function generateOffer () { - var offerId = randombytes(20).toString('hex') - debug('creating peer (from _generateOffers)') - var peer = self.peers[offerId] = self._createPeer({ initiator: true }) - peer.once('signal', function (offer) { - offers.push({ - offer: offer, - offer_id: common.hexToBinary(offerId) + for (let i = 0; i < numwant; ++i) { + generateOffer() + } + checkDone() + + function generateOffer () { + const offerId = randombytes(20).toString('hex') + debug('creating peer (from _generateOffers)') + const peer = self.peers[offerId] = self._createPeer({ initiator: true }) + peer.once('signal', offer => { + offers.push({ + offer, + offer_id: common.hexToBinary(offerId) + }) + checkDone() }) - checkDone() - }) - peer.trackerTimeout = setTimeout(function () { - debug('tracker timeout: destroying peer') - peer.trackerTimeout = null - delete self.peers[offerId] - peer.destroy() - }, OFFER_TIMEOUT) - if (peer.trackerTimeout.unref) peer.trackerTimeout.unref() - } + peer.trackerTimeout = setTimeout(() => { + debug('tracker timeout: destroying peer') + peer.trackerTimeout = null + delete self.peers[offerId] + peer.destroy() + }, OFFER_TIMEOUT) + if (peer.trackerTimeout.unref) peer.trackerTimeout.unref() + } - function checkDone () { - if (offers.length === numwant) { - debug('generated %s offers', numwant) - cb(offers) + function checkDone () { + if (offers.length === numwant) { + debug('generated %s offers', numwant) + cb(offers) + } } } -} -WebSocketTracker.prototype._createPeer = function (opts) { - var self = this + _createPeer (opts) { + const self = this - opts = Object.assign({ - trickle: false, - config: self.client._rtcConfig, - wrtc: self.client._wrtc - }, opts) + opts = Object.assign({ + trickle: false, + config: self.client._rtcConfig, + wrtc: self.client._wrtc + }, opts) - var peer = new Peer(opts) + const peer = new Peer(opts) - peer.once('error', onError) - peer.once('connect', onConnect) + peer.once('error', onError) + peer.once('connect', onConnect) - return peer + return peer - // Handle peer 'error' events that are fired *before* the peer is emitted in - // a 'peer' event. - function onError (err) { - self.client.emit('warning', new Error('Connection error: ' + err.message)) - peer.destroy() - } + // Handle peer 'error' events that are fired *before* the peer is emitted in + // a 'peer' event. + function onError (err) { + self.client.emit('warning', new Error(`Connection error: ${err.message}`)) + peer.destroy() + } - // Once the peer is emitted in a 'peer' event, then it's the consumer's - // responsibility to listen for errors, so the listeners are removed here. - function onConnect () { - peer.removeListener('error', onError) - peer.removeListener('connect', onConnect) + // Once the peer is emitted in a 'peer' event, then it's the consumer's + // responsibility to listen for errors, so the listeners are removed here. + function onConnect () { + peer.removeListener('error', onError) + peer.removeListener('connect', onConnect) + } } } +WebSocketTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 1000 // 30 seconds +// Normally this shouldn't be accessed but is occasionally useful +WebSocketTracker._socketPool = socketPool + function noop () {} + +module.exports = WebSocketTracker diff --git a/lib/common-node.js b/lib/common-node.js index b06d0004..7a9e87d6 100644 --- a/lib/common-node.js +++ b/lib/common-node.js @@ -3,14 +3,13 @@ * These are separate from common.js so they can be skipped when bundling for the browser. */ -var Buffer = require('safe-buffer').Buffer var querystring = require('querystring') exports.IPV4_RE = /^[\d.]+$/ exports.IPV6_RE = /^[\da-fA-F:]+$/ exports.REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ -exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ]) +exports.CONNECTION_ID = Buffer.concat([toUInt32(0x417), toUInt32(0x27101980)]) exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 } exports.EVENT_IDS = { diff --git a/lib/common.js b/lib/common.js index 493b14a9..f735a856 100644 --- a/lib/common.js +++ b/lib/common.js @@ -2,9 +2,6 @@ * Functions/constants needed by both the client and server. */ -var Buffer = require('safe-buffer').Buffer -var extend = require('xtend/mutable') - exports.DEFAULT_ANNOUNCE_PEERS = 50 exports.MAX_ANNOUNCE_PEERS = 82 @@ -23,4 +20,4 @@ exports.hexToBinary = function (str) { } var config = require('./common-node') -extend(exports, config) +Object.assign(exports, config) diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js index bf6cb2c4..85a1c872 100644 --- a/lib/server/parse-http.js +++ b/lib/server/parse-http.js @@ -42,7 +42,7 @@ function parseHttpRequest (req, opts) { } else if (opts.action === 'scrape' || s[0] === '/scrape') { params.action = common.ACTIONS.SCRAPE - if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] + if (typeof params.info_hash === 'string') params.info_hash = [params.info_hash] if (Array.isArray(params.info_hash)) { params.info_hash = params.info_hash.map(function (binaryInfoHash) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { diff --git a/lib/server/parse-websocket.js b/lib/server/parse-websocket.js index ec4e606d..4416008a 100644 --- a/lib/server/parse-websocket.js +++ b/lib/server/parse-websocket.js @@ -39,7 +39,7 @@ function parseWebSocketRequest (socket, opts, params) { } else if (params.action === 'scrape') { params.action = common.ACTIONS.SCRAPE - if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] + if (typeof params.info_hash === 'string') params.info_hash = [params.info_hash] if (Array.isArray(params.info_hash)) { params.info_hash = params.info_hash.map(function (binaryInfoHash) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { diff --git a/package.json b/package.json index ce4d46bc..bd55af19 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bittorrent-tracker", "description": "Simple, robust, BitTorrent tracker (client & server) implementation", - "version": "9.10.1", + "version": "9.13.0", "author": { "name": "WebTorrent, LLC", "email": "feross@webtorrent.io", @@ -19,10 +19,9 @@ "dependencies": { "bencode": "^2.0.0", "bittorrent-peerid": "^1.0.2", - "bn.js": "^4.4.0", + "bn.js": "^5.0.0", "compact2string": "^1.2.0", - "debug": "^3.1.0", - "inherits": "^2.0.1", + "debug": "^4.0.1", "ip": "^1.0.1", "lru": "^3.0.0", "minimist": "^1.1.1", @@ -31,15 +30,13 @@ "randombytes": "^2.0.3", "run-parallel": "^1.1.2", "run-series": "^1.0.2", - "safe-buffer": "^5.0.0", "simple-get": "^3.0.0", "simple-peer": "^9.0.0", - "simple-websocket": "^7.0.1", + "simple-websocket": "^8.0.0", "string2compact": "^1.1.1", "uniq": "^1.0.1", "unordered-array-remove": "^1.0.2", - "ws": "^6.0.0", - "xtend": "^4.0.0" + "ws": "^7.0.0" }, "devDependencies": { "electron-webrtc": "^0.3.0", @@ -48,9 +45,8 @@ "tape": "^4.0.0", "webtorrent-fixtures": "^1.3.0" }, - "optionalDependencies": { - "bufferutil": "^4.0.0", - "utf-8-validate": "^5.0.1" + "engines": { + "node": ">=10" }, "keywords": [ "bittorrent", @@ -64,12 +60,16 @@ ], "license": "MIT", "main": "index.js", + "optionalDependencies": { + "bufferutil": "^4.0.0", + "utf-8-validate": "^5.0.1" + }, "repository": { "type": "git", "url": "git://github.com/webtorrent/bittorrent-tracker.git" }, "scripts": { - "update-authors": "./tools/update-authors.sh", - "test": "standard && tape test/*.js" + "test": "standard && tape test/*.js", + "update-authors": "./tools/update-authors.sh" } } diff --git a/server.js b/server.js index 4f905699..80f5cf52 100644 --- a/server.js +++ b/server.js @@ -1,24 +1,20 @@ -module.exports = Server - -var Buffer = require('safe-buffer').Buffer -var bencode = require('bencode') -var debug = require('debug')('bittorrent-tracker:server') -var dgram = require('dgram') -var EventEmitter = require('events').EventEmitter -var http = require('http') -var inherits = require('inherits') -var peerid = require('bittorrent-peerid') -var series = require('run-series') -var string2compact = require('string2compact') -var WebSocketServer = require('ws').Server - -var common = require('./lib/common') -var Swarm = require('./lib/server/swarm') -var parseHttpRequest = require('./lib/server/parse-http') -var parseUdpRequest = require('./lib/server/parse-udp') -var parseWebSocketRequest = require('./lib/server/parse-websocket') - -inherits(Server, EventEmitter) +const bencode = require('bencode') +const debug = require('debug')('bittorrent-tracker:server') +const dgram = require('dgram') +const EventEmitter = require('events') +const http = require('http') +const peerid = require('bittorrent-peerid') +const series = require('run-series') +const string2compact = require('string2compact') +const WebSocketServer = require('ws').Server + +const common = require('./lib/common') +const Swarm = require('./lib/server/swarm') +const parseHttpRequest = require('./lib/server/parse-http') +const parseUdpRequest = require('./lib/server/parse-udp') +const parseWebSocketRequest = require('./lib/server/parse-websocket') + +const hasOwnProperty = Object.prototype.hasOwnProperty /** * BitTorrent tracker server. @@ -36,754 +32,734 @@ inherits(Server, EventEmitter) * @param {boolean} opts.stats enable web-based statistics? (default: true) * @param {function} opts.filter black/whitelist fn for disallowing/allowing torrents */ -function Server (opts) { - var self = this - if (!(self instanceof Server)) return new Server(opts) - EventEmitter.call(self) - if (!opts) opts = {} - - debug('new server %s', JSON.stringify(opts)) - - self.intervalMs = opts.interval - ? opts.interval - : 10 * 60 * 1000 // 10 min - - self._trustProxy = !!opts.trustProxy - if (typeof opts.filter === 'function') self._filter = opts.filter - - self.peersCacheLength = opts.peersCacheLength - self.peersCacheTtl = opts.peersCacheTtl - - self._listenCalled = false - self.listening = false - self.destroyed = false - self.torrents = {} - - self.http = null - self.udp4 = null - self.udp6 = null - self.ws = null - - // start an http tracker unless the user explictly says no - if (opts.http !== false) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) - - // Add default http request handler on next tick to give user the chance to add - // their own handler first. Handle requests untouched by user's handler. - process.nextTick(function () { - self.http.on('request', function (req, res) { - if (res.headersSent) return - self.onHttpRequest(req, res) - }) - }) - } +class Server extends EventEmitter { + constructor (opts = {}) { + super() + debug('new server %s', JSON.stringify(opts)) - // start a udp tracker unless the user explicitly says no - if (opts.udp !== false) { - var isNode10 = /^v0.10./.test(process.version) - - self.udp4 = self.udp = dgram.createSocket( - isNode10 ? 'udp4' : { type: 'udp4', reuseAddr: true } - ) - self.udp4.on('message', function (msg, rinfo) { self.onUdpRequest(msg, rinfo) }) - self.udp4.on('error', function (err) { self._onError(err) }) - self.udp4.on('listening', onListening) - - self.udp6 = dgram.createSocket( - isNode10 ? 'udp6' : { type: 'udp6', reuseAddr: true } - ) - self.udp6.on('message', function (msg, rinfo) { self.onUdpRequest(msg, rinfo) }) - self.udp6.on('error', function (err) { self._onError(err) }) - self.udp6.on('listening', onListening) - } + this.intervalMs = opts.interval + ? opts.interval + : 10 * 60 * 1000 // 10 min - // start a websocket tracker (for WebTorrent) unless the user explicitly says no - if (opts.ws !== false) { - if (!self.http) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) + this._trustProxy = !!opts.trustProxy + if (typeof opts.filter === 'function') this._filter = opts.filter + + this.peersCacheLength = opts.peersCacheLength + this.peersCacheTtl = opts.peersCacheTtl + + this._listenCalled = false + this.listening = false + this.destroyed = false + this.torrents = {} + + this.http = null + this.udp4 = null + this.udp6 = null + this.ws = null + + // start an http tracker unless the user explictly says no + if (opts.http !== false) { + this.http = http.createServer() + this.http.on('error', err => { this._onError(err) }) + this.http.on('listening', onListening) // Add default http request handler on next tick to give user the chance to add // their own handler first. Handle requests untouched by user's handler. - process.nextTick(function () { - self.http.on('request', function (req, res) { + process.nextTick(() => { + this.http.on('request', (req, res) => { if (res.headersSent) return - // For websocket trackers, we only need to handle the UPGRADE http method. - // Return 404 for all other request types. - res.statusCode = 404 - res.end('404 Not Found') + this.onHttpRequest(req, res) }) }) } - self.ws = new WebSocketServer({ - server: self.http, - perMessageDeflate: false, - clientTracking: false - }) - self.ws.address = function () { - return self.http.address() + + // start a udp tracker unless the user explicitly says no + if (opts.udp !== false) { + const isNode10 = /^v0.10./.test(process.version) + + this.udp4 = this.udp = dgram.createSocket( + isNode10 ? 'udp4' : { type: 'udp4', reuseAddr: true } + ) + this.udp4.on('message', (msg, rinfo) => { this.onUdpRequest(msg, rinfo) }) + this.udp4.on('error', err => { this._onError(err) }) + this.udp4.on('listening', onListening) + + this.udp6 = dgram.createSocket( + isNode10 ? 'udp6' : { type: 'udp6', reuseAddr: true } + ) + this.udp6.on('message', (msg, rinfo) => { this.onUdpRequest(msg, rinfo) }) + this.udp6.on('error', err => { this._onError(err) }) + this.udp6.on('listening', onListening) } - self.ws.on('error', function (err) { self._onError(err) }) - self.ws.on('connection', function (socket, req) { - // Note: socket.upgradeReq was removed in ws@3.0.0, so re-add it. - // https://github.com/websockets/ws/pull/1099 - socket.upgradeReq = req - self.onWebSocketConnection(socket) - }) - } - if (opts.stats !== false) { - if (!self.http) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) + // start a websocket tracker (for WebTorrent) unless the user explicitly says no + if (opts.ws !== false) { + if (!this.http) { + this.http = http.createServer() + this.http.on('error', err => { this._onError(err) }) + this.http.on('listening', onListening) + + // Add default http request handler on next tick to give user the chance to add + // their own handler first. Handle requests untouched by user's handler. + process.nextTick(() => { + this.http.on('request', (req, res) => { + if (res.headersSent) return + // For websocket trackers, we only need to handle the UPGRADE http method. + // Return 404 for all other request types. + res.statusCode = 404 + res.end('404 Not Found') + }) + }) + } + this.ws = new WebSocketServer({ + server: this.http, + perMessageDeflate: false, + clientTracking: false + }) + this.ws.address = () => { + return this.http.address() + } + this.ws.on('error', err => { this._onError(err) }) + this.ws.on('connection', (socket, req) => { + // Note: socket.upgradeReq was removed in ws@3.0.0, so re-add it. + // https://github.com/websockets/ws/pull/1099 + socket.upgradeReq = req + this.onWebSocketConnection(socket) + }) } - // Http handler for '/stats' route - self.http.on('request', function (req, res) { - if (res.headersSent) return + if (opts.stats !== false) { + if (!this.http) { + this.http = http.createServer() + this.http.on('error', err => { this._onError(err) }) + this.http.on('listening', onListening) + } + + // Http handler for '/stats' route + this.http.on('request', (req, res) => { + if (res.headersSent) return - var infoHashes = Object.keys(self.torrents) - var activeTorrents = 0 - var allPeers = {} + const infoHashes = Object.keys(this.torrents) + let activeTorrents = 0 + const allPeers = {} - function countPeers (filterFunction) { - var count = 0 - var key + function countPeers (filterFunction) { + let count = 0 + let key - for (key in allPeers) { - if (allPeers.hasOwnProperty(key) && filterFunction(allPeers[key])) { - count++ + for (key in allPeers) { + if (hasOwnProperty.call(allPeers, key) && filterFunction(allPeers[key])) { + count++ + } } - } - return count - } + return count + } - function groupByClient () { - var clients = {} - for (var key in allPeers) { - if (allPeers.hasOwnProperty(key)) { - var peer = allPeers[key] + function groupByClient () { + const clients = {} + for (const key in allPeers) { + if (hasOwnProperty.call(allPeers, key)) { + const peer = allPeers[key] - if (!clients[peer.client.client]) { - clients[peer.client.client] = {} - } - var client = clients[peer.client.client] - // If the client is not known show 8 chars from peerId as version - var version = peer.client.version || Buffer.from(peer.peerId, 'hex').toString().substring(0, 8) - if (!client[version]) { - client[version] = 0 + if (!clients[peer.client.client]) { + clients[peer.client.client] = {} + } + const client = clients[peer.client.client] + // If the client is not known show 8 chars from peerId as version + const version = peer.client.version || Buffer.from(peer.peerId, 'hex').toString().substring(0, 8) + if (!client[version]) { + client[version] = 0 + } + client[version]++ } - client[version]++ } + return clients } - return clients - } - function printClients (clients) { - var html = '' - return html - } - if (req.method === 'GET' && (req.url === '/stats' || req.url === '/stats.json')) { - infoHashes.forEach(function (infoHash) { - var peers = self.torrents[infoHash].peers - var keys = peers.keys - if (keys.length > 0) activeTorrents++ - - keys.forEach(function (peerId) { - // Don't mark the peer as most recently used for stats - var peer = peers.peek(peerId) - if (peer == null) return // peers.peek() can evict the peer - - if (!allPeers.hasOwnProperty(peerId)) { - allPeers[peerId] = { - ipv4: false, - ipv6: false, - seeder: false, - leecher: false + if (req.method === 'GET' && (req.url === '/stats' || req.url === '/stats.json')) { + infoHashes.forEach(infoHash => { + const peers = this.torrents[infoHash].peers + const keys = peers.keys + if (keys.length > 0) activeTorrents++ + + keys.forEach(peerId => { + // Don't mark the peer as most recently used for stats + const peer = peers.peek(peerId) + if (peer == null) return // peers.peek() can evict the peer + + if (!hasOwnProperty.call(allPeers, peerId)) { + allPeers[peerId] = { + ipv4: false, + ipv6: false, + seeder: false, + leecher: false + } } - } - if (peer.ip.indexOf(':') >= 0) { - allPeers[peerId].ipv6 = true - } else { - allPeers[peerId].ipv4 = true - } + if (peer.ip.includes(':')) { + allPeers[peerId].ipv6 = true + } else { + allPeers[peerId].ipv4 = true + } - if (peer.complete) { - allPeers[peerId].seeder = true - } else { - allPeers[peerId].leecher = true - } + if (peer.complete) { + allPeers[peerId].seeder = true + } else { + allPeers[peerId].leecher = true + } - allPeers[peerId].peerId = peer.peerId - allPeers[peerId].client = peerid(peer.peerId) + allPeers[peerId].peerId = peer.peerId + allPeers[peerId].client = peerid(peer.peerId) + }) }) - }) - var isSeederOnly = function (peer) { return peer.seeder && peer.leecher === false } - var isLeecherOnly = function (peer) { return peer.leecher && peer.seeder === false } - var isSeederAndLeecher = function (peer) { return peer.seeder && peer.leecher } - var isIPv4 = function (peer) { return peer.ipv4 } - var isIPv6 = function (peer) { return peer.ipv6 } - - var stats = { - torrents: infoHashes.length, - activeTorrents: activeTorrents, - peersAll: Object.keys(allPeers).length, - peersSeederOnly: countPeers(isSeederOnly), - peersLeecherOnly: countPeers(isLeecherOnly), - peersSeederAndLeecher: countPeers(isSeederAndLeecher), - peersIPv4: countPeers(isIPv4), - peersIPv6: countPeers(isIPv6), - clients: groupByClient() - } + const isSeederOnly = peer => { return peer.seeder && peer.leecher === false } + const isLeecherOnly = peer => { return peer.leecher && peer.seeder === false } + const isSeederAndLeecher = peer => { return peer.seeder && peer.leecher } + const isIPv4 = peer => { return peer.ipv4 } + const isIPv6 = peer => { return peer.ipv6 } + + const stats = { + torrents: infoHashes.length, + activeTorrents, + peersAll: Object.keys(allPeers).length, + peersSeederOnly: countPeers(isSeederOnly), + peersLeecherOnly: countPeers(isLeecherOnly), + peersSeederAndLeecher: countPeers(isSeederAndLeecher), + peersIPv4: countPeers(isIPv4), + peersIPv6: countPeers(isIPv6), + clients: groupByClient() + } - if (req.url === '/stats.json' || req.headers['accept'] === 'application/json') { - res.write(JSON.stringify(stats)) - res.end() - } else if (req.url === '/stats') { - res.end('

' + stats.torrents + ' torrents (' + stats.activeTorrents + ' active)

\n' + - '

Connected Peers: ' + stats.peersAll + '

\n' + - '

Peers Seeding Only: ' + stats.peersSeederOnly + '

\n' + - '

Peers Leeching Only: ' + stats.peersLeecherOnly + '

\n' + - '

Peers Seeding & Leeching: ' + stats.peersSeederAndLeecher + '

\n' + - '

IPv4 Peers: ' + stats.peersIPv4 + '

\n' + - '

IPv6 Peers: ' + stats.peersIPv6 + '

\n' + - '

Clients:

\n' + - printClients(stats.clients) - ) + if (req.url === '/stats.json' || req.headers.accept === 'application/json') { + res.write(JSON.stringify(stats)) + res.end() + } else if (req.url === '/stats') { + res.end(` +

${stats.torrents} torrents (${stats.activeTorrents} active)

+

Connected Peers: ${stats.peersAll}

+

Peers Seeding Only: ${stats.peersSeederOnly}

+

Peers Leeching Only: ${stats.peersLeecherOnly}

+

Peers Seeding & Leeching: ${stats.peersSeederAndLeecher}

+

IPv4 Peers: ${stats.peersIPv4}

+

IPv6 Peers: ${stats.peersIPv6}

+

Clients:

+ ${printClients(stats.clients)} + `.replace(/^\s+/gm, '')) // trim left + } } + }) + } + + let num = !!this.http + !!this.udp4 + !!this.udp6 + const self = this + function onListening () { + num -= 1 + if (num === 0) { + self.listening = true + debug('listening') + self.emit('listening') } - }) + } } - var num = !!self.http + !!self.udp4 + !!self.udp6 - function onListening () { - num -= 1 - if (num === 0) { - self.listening = true - debug('listening') - self.emit('listening') - } + _onError (err) { + this.emit('error', err) } -} -Server.Swarm = Swarm + listen (...args) /* port, hostname, onlistening */{ + if (this._listenCalled || this.listening) throw new Error('server already listening') + this._listenCalled = true -Server.prototype._onError = function (err) { - var self = this - self.emit('error', err) -} + const lastArg = args[args.length - 1] + if (typeof lastArg === 'function') this.once('listening', lastArg) -Server.prototype.listen = function (/* port, hostname, onlistening */) { - var self = this + const port = toNumber(args[0]) || args[0] || 0 + const hostname = typeof args[1] !== 'function' ? args[1] : undefined - if (self._listenCalled || self.listening) throw new Error('server already listening') - self._listenCalled = true + debug('listen (port: %o hostname: %o)', port, hostname) - var lastArg = arguments[arguments.length - 1] - if (typeof lastArg === 'function') self.once('listening', lastArg) + function isObject (obj) { + return typeof obj === 'object' && obj !== null + } - var port = toNumber(arguments[0]) || arguments[0] || 0 - var hostname = typeof arguments[1] !== 'function' ? arguments[1] : undefined + const httpPort = isObject(port) ? (port.http || 0) : port + const udpPort = isObject(port) ? (port.udp || 0) : port - debug('listen (port: %o hostname: %o)', port, hostname) + // binding to :: only receives IPv4 connections if the bindv6only sysctl is set 0, + // which is the default on many operating systems + const httpHostname = isObject(hostname) ? hostname.http : hostname + const udp4Hostname = isObject(hostname) ? hostname.udp : hostname + const udp6Hostname = isObject(hostname) ? hostname.udp6 : hostname - function isObject (obj) { - return typeof obj === 'object' && obj !== null + if (this.http) this.http.listen(httpPort, httpHostname) + if (this.udp4) this.udp4.bind(udpPort, udp4Hostname) + if (this.udp6) this.udp6.bind(udpPort, udp6Hostname) } - var httpPort = isObject(port) ? (port.http || 0) : port - var udpPort = isObject(port) ? (port.udp || 0) : port + close (cb = noop) { + debug('close') - // binding to :: only receives IPv4 connections if the bindv6only sysctl is set 0, - // which is the default on many operating systems - var httpHostname = isObject(hostname) ? hostname.http : hostname - var udp4Hostname = isObject(hostname) ? hostname.udp : hostname - var udp6Hostname = isObject(hostname) ? hostname.udp6 : hostname + this.listening = false + this.destroyed = true - if (self.http) self.http.listen(httpPort, httpHostname) - if (self.udp4) self.udp4.bind(udpPort, udp4Hostname) - if (self.udp6) self.udp6.bind(udpPort, udp6Hostname) -} + if (this.udp4) { + try { + this.udp4.close() + } catch (err) {} + } -Server.prototype.close = function (cb) { - var self = this - if (!cb) cb = noop - debug('close') + if (this.udp6) { + try { + this.udp6.close() + } catch (err) {} + } - self.listening = false - self.destroyed = true + if (this.ws) { + try { + this.ws.close() + } catch (err) {} + } - if (self.udp4) { - try { - self.udp4.close() - } catch (err) {} + if (this.http) this.http.close(cb) + else cb(null) } - if (self.udp6) { - try { - self.udp6.close() - } catch (err) {} - } + createSwarm (infoHash, cb) { + if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') - if (self.ws) { - try { - self.ws.close() - } catch (err) {} + process.nextTick(() => { + const swarm = this.torrents[infoHash] = new Server.Swarm(infoHash, this) + cb(null, swarm) + }) } - if (self.http) self.http.close(cb) - else cb(null) -} - -Server.prototype.createSwarm = function (infoHash, cb) { - var self = this - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') + getSwarm (infoHash, cb) { + if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') - process.nextTick(function () { - var swarm = self.torrents[infoHash] = new Server.Swarm(infoHash, self) - cb(null, swarm) - }) -} - -Server.prototype.getSwarm = function (infoHash, cb) { - var self = this - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') - - process.nextTick(function () { - cb(null, self.torrents[infoHash]) - }) -} - -Server.prototype.onHttpRequest = function (req, res, opts) { - var self = this - if (!opts) opts = {} - opts.trustProxy = opts.trustProxy || self._trustProxy - - var params - try { - params = parseHttpRequest(req, opts) - params.httpReq = req - params.httpRes = res - } catch (err) { - res.end(bencode.encode({ - 'failure reason': err.message - })) - - // even though it's an error for the client, it's just a warning for the server. - // don't crash the server because a client sent bad data :) - self.emit('warning', err) - return + process.nextTick(() => { + cb(null, this.torrents[infoHash]) + }) } - self._onRequest(params, function (err, response) { - if (err) { - self.emit('warning', err) - response = { - 'failure reason': err.message - } - } - if (self.destroyed) return res.end() + onHttpRequest (req, res, opts = {}) { + opts.trustProxy = opts.trustProxy || this._trustProxy - delete response.action // only needed for UDP encoding - res.end(bencode.encode(response)) + let params + try { + params = parseHttpRequest(req, opts) + params.httpReq = req + params.httpRes = res + } catch (err) { + res.end(bencode.encode({ + 'failure reason': err.message + })) - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.addr, params) + // even though it's an error for the client, it's just a warning for the server. + // don't crash the server because a client sent bad data :) + this.emit('warning', err) + return } - }) -} - -Server.prototype.onUdpRequest = function (msg, rinfo) { - var self = this - - var params - try { - params = parseUdpRequest(msg, rinfo) - } catch (err) { - self.emit('warning', err) - // Do not reply for parsing errors - return - } - self._onRequest(params, function (err, response) { - if (err) { - self.emit('warning', err) - response = { - action: common.ACTIONS.ERROR, - 'failure reason': err.message + this._onRequest(params, (err, response) => { + if (err) { + this.emit('warning', err) + response = { + 'failure reason': err.message + } } - } - if (self.destroyed) return + if (this.destroyed) return res.end() - response.transactionId = params.transactionId - response.connectionId = params.connectionId + delete response.action // only needed for UDP encoding + res.end(bencode.encode(response)) - var buf = makeUdpPacket(response) + if (params.action === common.ACTIONS.ANNOUNCE) { + this.emit(common.EVENT_NAMES[params.event], params.addr, params) + } + }) + } + onUdpRequest (msg, rinfo) { + let params try { - var udp = (rinfo.family === 'IPv4') ? self.udp4 : self.udp6 - udp.send(buf, 0, buf.length, rinfo.port, rinfo.address) + params = parseUdpRequest(msg, rinfo) } catch (err) { - self.emit('warning', err) + this.emit('warning', err) + // Do not reply for parsing errors + return } - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.addr, params) - } - }) -} + this._onRequest(params, (err, response) => { + if (err) { + this.emit('warning', err) + response = { + action: common.ACTIONS.ERROR, + 'failure reason': err.message + } + } + if (this.destroyed) return -Server.prototype.onWebSocketConnection = function (socket, opts) { - var self = this - if (!opts) opts = {} - opts.trustProxy = opts.trustProxy || self._trustProxy + response.transactionId = params.transactionId + response.connectionId = params.connectionId - socket.peerId = null // as hex - socket.infoHashes = [] // swarms that this socket is participating in - socket.onSend = function (err) { - self._onWebSocketSend(socket, err) - } + const buf = makeUdpPacket(response) - socket.onMessageBound = function (params) { - self._onWebSocketRequest(socket, opts, params) - } - socket.on('message', socket.onMessageBound) + try { + const udp = (rinfo.family === 'IPv4') ? this.udp4 : this.udp6 + udp.send(buf, 0, buf.length, rinfo.port, rinfo.address) + } catch (err) { + this.emit('warning', err) + } - socket.onErrorBound = function (err) { - self._onWebSocketError(socket, err) + if (params.action === common.ACTIONS.ANNOUNCE) { + this.emit(common.EVENT_NAMES[params.event], params.addr, params) + } + }) } - socket.on('error', socket.onErrorBound) - socket.onCloseBound = function () { - self._onWebSocketClose(socket) - } - socket.on('close', socket.onCloseBound) -} + onWebSocketConnection (socket, opts = {}) { + opts.trustProxy = opts.trustProxy || this._trustProxy -Server.prototype._onWebSocketRequest = function (socket, opts, params) { - var self = this + socket.peerId = null // as hex + socket.infoHashes = [] // swarms that this socket is participating in + socket.onSend = err => { + this._onWebSocketSend(socket, err) + } - try { - params = parseWebSocketRequest(socket, opts, params) - } catch (err) { - socket.send(JSON.stringify({ - 'failure reason': err.message - }), socket.onSend) + socket.onMessageBound = params => { + this._onWebSocketRequest(socket, opts, params) + } + socket.on('message', socket.onMessageBound) - // even though it's an error for the client, it's just a warning for the server. - // don't crash the server because a client sent bad data :) - self.emit('warning', err) - return - } + socket.onErrorBound = err => { + this._onWebSocketError(socket, err) + } + socket.on('error', socket.onErrorBound) - if (!socket.peerId) socket.peerId = params.peer_id // as hex + socket.onCloseBound = () => { + this._onWebSocketClose(socket) + } + socket.on('close', socket.onCloseBound) + } - self._onRequest(params, function (err, response) { - if (self.destroyed || socket.destroyed) return - if (err) { + _onWebSocketRequest (socket, opts, params) { + try { + params = parseWebSocketRequest(socket, opts, params) + } catch (err) { socket.send(JSON.stringify({ - action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', - 'failure reason': err.message, - info_hash: common.hexToBinary(params.info_hash) + 'failure reason': err.message }), socket.onSend) - self.emit('warning', err) + // even though it's an error for the client, it's just a warning for the server. + // don't crash the server because a client sent bad data :) + this.emit('warning', err) return } - response.action = params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape' + if (!socket.peerId) socket.peerId = params.peer_id // as hex - var peers - if (response.action === 'announce') { - peers = response.peers - delete response.peers + this._onRequest(params, (err, response) => { + if (this.destroyed || socket.destroyed) return + if (err) { + socket.send(JSON.stringify({ + action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', + 'failure reason': err.message, + info_hash: common.hexToBinary(params.info_hash) + }), socket.onSend) - if (socket.infoHashes.indexOf(params.info_hash) === -1) { - socket.infoHashes.push(params.info_hash) + this.emit('warning', err) + return } - response.info_hash = common.hexToBinary(params.info_hash) + response.action = params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape' - // WebSocket tracker should have a shorter interval – default: 2 minutes - response.interval = Math.ceil(self.intervalMs / 1000 / 5) - } - - // Skip sending update back for 'answer' announce messages – not needed - if (!params.answer) { - socket.send(JSON.stringify(response), socket.onSend) - debug('sent response %s to %s', JSON.stringify(response), params.peer_id) - } + let peers + if (response.action === 'announce') { + peers = response.peers + delete response.peers - if (Array.isArray(params.offers)) { - debug('got %s offers from %s', params.offers.length, params.peer_id) - debug('got %s peers from swarm %s', peers.length, params.info_hash) - peers.forEach(function (peer, i) { - peer.socket.send(JSON.stringify({ - action: 'announce', - offer: params.offers[i].offer, - offer_id: params.offers[i].offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) - }), peer.socket.onSend) - debug('sent offer to %s from %s', peer.peerId, params.peer_id) - }) - } + if (!socket.infoHashes.includes(params.info_hash)) { + socket.infoHashes.push(params.info_hash) + } - if (params.answer) { - debug('got answer %s from %s', JSON.stringify(params.answer), params.peer_id) + response.info_hash = common.hexToBinary(params.info_hash) - self.getSwarm(params.info_hash, function (err, swarm) { - if (self.destroyed) return - if (err) return self.emit('warning', err) - if (!swarm) { - return self.emit('warning', new Error('no swarm with that `info_hash`')) - } - // Mark the destination peer as recently used in cache - var toPeer = swarm.peers.get(params.to_peer_id) - if (!toPeer) { - return self.emit('warning', new Error('no peer with that `to_peer_id`')) - } + // WebSocket tracker should have a shorter interval – default: 2 minutes + response.interval = Math.ceil(this.intervalMs / 1000 / 5) + } - toPeer.socket.send(JSON.stringify({ - action: 'announce', - answer: params.answer, - offer_id: params.offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) - }), toPeer.socket.onSend) - debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) + // Skip sending update back for 'answer' announce messages – not needed + if (!params.answer) { + socket.send(JSON.stringify(response), socket.onSend) + debug('sent response %s to %s', JSON.stringify(response), params.peer_id) + } - done() - }) - } else { - done() - } + if (Array.isArray(params.offers)) { + debug('got %s offers from %s', params.offers.length, params.peer_id) + debug('got %s peers from swarm %s', peers.length, params.info_hash) + peers.forEach((peer, i) => { + peer.socket.send(JSON.stringify({ + action: 'announce', + offer: params.offers[i].offer, + offer_id: params.offers[i].offer_id, + peer_id: common.hexToBinary(params.peer_id), + info_hash: common.hexToBinary(params.info_hash) + }), peer.socket.onSend) + debug('sent offer to %s from %s', peer.peerId, params.peer_id) + }) + } - function done () { - // emit event once the announce is fully "processed" - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.peer_id, params) + const done = () => { + // emit event once the announce is fully "processed" + if (params.action === common.ACTIONS.ANNOUNCE) { + this.emit(common.EVENT_NAMES[params.event], params.peer_id, params) + } } - } - }) -} -Server.prototype._onWebSocketSend = function (socket, err) { - var self = this - if (err) self._onWebSocketError(socket, err) -} + if (params.answer) { + debug('got answer %s from %s', JSON.stringify(params.answer), params.peer_id) -Server.prototype._onWebSocketClose = function (socket) { - var self = this - debug('websocket close %s', socket.peerId) - socket.destroyed = true - - if (socket.peerId) { - socket.infoHashes.slice(0).forEach(function (infoHash) { - var swarm = self.torrents[infoHash] - if (swarm) { - swarm.announce({ - type: 'ws', - event: 'stopped', - numwant: 0, - peer_id: socket.peerId - }, noop) + this.getSwarm(params.info_hash, (err, swarm) => { + if (this.destroyed) return + if (err) return this.emit('warning', err) + if (!swarm) { + return this.emit('warning', new Error('no swarm with that `info_hash`')) + } + // Mark the destination peer as recently used in cache + const toPeer = swarm.peers.get(params.to_peer_id) + if (!toPeer) { + return this.emit('warning', new Error('no peer with that `to_peer_id`')) + } + + toPeer.socket.send(JSON.stringify({ + action: 'announce', + answer: params.answer, + offer_id: params.offer_id, + peer_id: common.hexToBinary(params.peer_id), + info_hash: common.hexToBinary(params.info_hash) + }), toPeer.socket.onSend) + debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) + + done() + }) + } else { + done() } }) } - // ignore all future errors - socket.onSend = noop - socket.on('error', noop) + _onWebSocketSend (socket, err) { + if (err) this._onWebSocketError(socket, err) + } - socket.peerId = null - socket.infoHashes = null + _onWebSocketClose (socket) { + debug('websocket close %s', socket.peerId) + socket.destroyed = true - if (typeof socket.onMessageBound === 'function') { - socket.removeListener('message', socket.onMessageBound) - } - socket.onMessageBound = null + if (socket.peerId) { + socket.infoHashes.slice(0).forEach(infoHash => { + const swarm = this.torrents[infoHash] + if (swarm) { + swarm.announce({ + type: 'ws', + event: 'stopped', + numwant: 0, + peer_id: socket.peerId + }, noop) + } + }) + } - if (typeof socket.onErrorBound === 'function') { - socket.removeListener('error', socket.onErrorBound) - } - socket.onErrorBound = null + // ignore all future errors + socket.onSend = noop + socket.on('error', noop) - if (typeof socket.onCloseBound === 'function') { - socket.removeListener('close', socket.onCloseBound) + socket.peerId = null + socket.infoHashes = null + + if (typeof socket.onMessageBound === 'function') { + socket.removeListener('message', socket.onMessageBound) + } + socket.onMessageBound = null + + if (typeof socket.onErrorBound === 'function') { + socket.removeListener('error', socket.onErrorBound) + } + socket.onErrorBound = null + + if (typeof socket.onCloseBound === 'function') { + socket.removeListener('close', socket.onCloseBound) + } + socket.onCloseBound = null } - socket.onCloseBound = null -} -Server.prototype._onWebSocketError = function (socket, err) { - var self = this - debug('websocket error %s', err.message || err) - self.emit('warning', err) - self._onWebSocketClose(socket) -} + _onWebSocketError (socket, err) { + debug('websocket error %s', err.message || err) + this.emit('warning', err) + this._onWebSocketClose(socket) + } -Server.prototype._onRequest = function (params, cb) { - var self = this - if (params && params.action === common.ACTIONS.CONNECT) { - cb(null, { action: common.ACTIONS.CONNECT }) - } else if (params && params.action === common.ACTIONS.ANNOUNCE) { - self._onAnnounce(params, cb) - } else if (params && params.action === common.ACTIONS.SCRAPE) { - self._onScrape(params, cb) - } else { - cb(new Error('Invalid action')) + _onRequest (params, cb) { + if (params && params.action === common.ACTIONS.CONNECT) { + cb(null, { action: common.ACTIONS.CONNECT }) + } else if (params && params.action === common.ACTIONS.ANNOUNCE) { + this._onAnnounce(params, cb) + } else if (params && params.action === common.ACTIONS.SCRAPE) { + this._onScrape(params, cb) + } else { + cb(new Error('Invalid action')) + } } -} -Server.prototype._onAnnounce = function (params, cb) { - var self = this + _onAnnounce (params, cb) { + const self = this - if (self._filter) { - self._filter(params.info_hash, params, function (err) { - // Presence of `err` means that this announce request is disallowed - if (err) return cb(err) + if (this._filter) { + this._filter(params.info_hash, params, err => { + // Presence of `err` means that this announce request is disallowed + if (err) return cb(err) - getOrCreateSwarm(function (err, swarm) { + getOrCreateSwarm((err, swarm) => { + if (err) return cb(err) + announce(swarm) + }) + }) + } else { + getOrCreateSwarm((err, swarm) => { if (err) return cb(err) announce(swarm) }) - }) - } else { - getOrCreateSwarm(function (err, swarm) { - if (err) return cb(err) - announce(swarm) - }) - } + } - // Get existing swarm, or create one if one does not exist - function getOrCreateSwarm (cb) { - self.getSwarm(params.info_hash, function (err, swarm) { - if (err) return cb(err) - if (swarm) return cb(null, swarm) - self.createSwarm(params.info_hash, function (err, swarm) { + // Get existing swarm, or create one if one does not exist + function getOrCreateSwarm (cb) { + self.getSwarm(params.info_hash, (err, swarm) => { if (err) return cb(err) - cb(null, swarm) + if (swarm) return cb(null, swarm) + self.createSwarm(params.info_hash, (err, swarm) => { + if (err) return cb(err) + cb(null, swarm) + }) }) - }) - } + } - function announce (swarm) { - if (!params.event || params.event === 'empty') params.event = 'update' - swarm.announce(params, function (err, response) { - if (err) return cb(err) + function announce (swarm) { + if (!params.event || params.event === 'empty') params.event = 'update' + swarm.announce(params, (err, response) => { + if (err) return cb(err) - if (!response.action) response.action = common.ACTIONS.ANNOUNCE - if (!response.interval) response.interval = Math.ceil(self.intervalMs / 1000) - - if (params.compact === 1) { - var peers = response.peers - - // Find IPv4 peers - response.peers = string2compact(peers.filter(function (peer) { - return common.IPV4_RE.test(peer.ip) - }).map(function (peer) { - return peer.ip + ':' + peer.port - })) - // Find IPv6 peers - response.peers6 = string2compact(peers.filter(function (peer) { - return common.IPV6_RE.test(peer.ip) - }).map(function (peer) { - return '[' + peer.ip + ']:' + peer.port - })) - } else if (params.compact === 0) { - // IPv6 peers are not separate for non-compact responses - response.peers = response.peers.map(function (peer) { - return { - 'peer id': common.hexToBinary(peer.peerId), - ip: peer.ip, - port: peer.port - } - }) - } // else, return full peer objects (used for websocket responses) + if (!response.action) response.action = common.ACTIONS.ANNOUNCE + if (!response.interval) response.interval = Math.ceil(self.intervalMs / 1000) + + if (params.compact === 1) { + const peers = response.peers + + // Find IPv4 peers + response.peers = string2compact(peers.filter(peer => { + return common.IPV4_RE.test(peer.ip) + }).map(peer => { + return `${peer.ip}:${peer.port}` + })) + // Find IPv6 peers + response.peers6 = string2compact(peers.filter(peer => { + return common.IPV6_RE.test(peer.ip) + }).map(peer => { + return `[${peer.ip}]:${peer.port}` + })) + } else if (params.compact === 0) { + // IPv6 peers are not separate for non-compact responses + response.peers = response.peers.map(peer => { + return { + 'peer id': common.hexToBinary(peer.peerId), + ip: peer.ip, + port: peer.port + } + }) + } // else, return full peer objects (used for websocket responses) - cb(null, response) - }) + cb(null, response) + }) + } } -} - -Server.prototype._onScrape = function (params, cb) { - var self = this - if (params.info_hash == null) { - // if info_hash param is omitted, stats for all torrents are returned - // TODO: make this configurable! - params.info_hash = Object.keys(self.torrents) - } + _onScrape (params, cb) { + if (params.info_hash == null) { + // if info_hash param is omitted, stats for all torrents are returned + // TODO: make this configurable! + params.info_hash = Object.keys(this.torrents) + } - series(params.info_hash.map(function (infoHash) { - return function (cb) { - self.getSwarm(infoHash, function (err, swarm) { - if (err) return cb(err) - if (swarm) { - swarm.scrape(params, function (err, scrapeInfo) { - if (err) return cb(err) - cb(null, { - infoHash: infoHash, - complete: (scrapeInfo && scrapeInfo.complete) || 0, - incomplete: (scrapeInfo && scrapeInfo.incomplete) || 0 + series(params.info_hash.map(infoHash => { + return cb => { + this.getSwarm(infoHash, (err, swarm) => { + if (err) return cb(err) + if (swarm) { + swarm.scrape(params, (err, scrapeInfo) => { + if (err) return cb(err) + cb(null, { + infoHash, + complete: (scrapeInfo && scrapeInfo.complete) || 0, + incomplete: (scrapeInfo && scrapeInfo.incomplete) || 0 + }) }) - }) - } else { - cb(null, { infoHash: infoHash, complete: 0, incomplete: 0 }) + } else { + cb(null, { infoHash, complete: 0, incomplete: 0 }) + } + }) + } + }), (err, results) => { + if (err) return cb(err) + + const response = { + action: common.ACTIONS.SCRAPE, + files: {}, + flags: { min_request_interval: Math.ceil(this.intervalMs / 1000) } + } + + results.forEach(result => { + response.files[common.hexToBinary(result.infoHash)] = { + complete: result.complete || 0, + incomplete: result.incomplete || 0, + downloaded: result.complete || 0 // TODO: this only provides a lower-bound } }) - } - }), function (err, results) { - if (err) return cb(err) - - var response = { - action: common.ACTIONS.SCRAPE, - files: {}, - flags: { min_request_interval: Math.ceil(self.intervalMs / 1000) } - } - results.forEach(function (result) { - response.files[common.hexToBinary(result.infoHash)] = { - complete: result.complete || 0, - incomplete: result.incomplete || 0, - downloaded: result.complete || 0 // TODO: this only provides a lower-bound - } + cb(null, response) }) - - cb(null, response) - }) + } } +Server.Swarm = Swarm + function makeUdpPacket (params) { - var packet + let packet switch (params.action) { - case common.ACTIONS.CONNECT: + case common.ACTIONS.CONNECT: { packet = Buffer.concat([ common.toUInt32(common.ACTIONS.CONNECT), common.toUInt32(params.transactionId), params.connectionId ]) break - case common.ACTIONS.ANNOUNCE: + } + case common.ACTIONS.ANNOUNCE: { packet = Buffer.concat([ common.toUInt32(common.ACTIONS.ANNOUNCE), common.toUInt32(params.transactionId), @@ -793,13 +769,14 @@ function makeUdpPacket (params) { params.peers ]) break - case common.ACTIONS.SCRAPE: - var scrapeResponse = [ + } + case common.ACTIONS.SCRAPE: { + const scrapeResponse = [ common.toUInt32(common.ACTIONS.SCRAPE), common.toUInt32(params.transactionId) ] - for (var infoHash in params.files) { - var file = params.files[infoHash] + for (const infoHash in params.files) { + const file = params.files[infoHash] scrapeResponse.push( common.toUInt32(file.complete), common.toUInt32(file.downloaded), // TODO: this only provides a lower-bound @@ -808,15 +785,17 @@ function makeUdpPacket (params) { } packet = Buffer.concat(scrapeResponse) break - case common.ACTIONS.ERROR: + } + case common.ACTIONS.ERROR: { packet = Buffer.concat([ common.toUInt32(common.ACTIONS.ERROR), common.toUInt32(params.transactionId || 0), Buffer.from(String(params['failure reason'])) ]) break + } default: - throw new Error('Action not implemented: ' + params.action) + throw new Error(`Action not implemented: ${params.action}`) } return packet } @@ -827,3 +806,5 @@ function toNumber (x) { } function noop () {} + +module.exports = Server diff --git a/test/client-large-torrent.js b/test/client-large-torrent.js index 7f9f2497..00a62c21 100644 --- a/test/client-large-torrent.js +++ b/test/client-large-torrent.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/client-magnet.js b/test/client-magnet.js index ba1949c9..bb768eab 100644 --- a/test/client-magnet.js +++ b/test/client-magnet.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/client-ws-socket-pool.js b/test/client-ws-socket-pool.js index 94f6b2dd..a87619f9 100644 --- a/test/client-ws-socket-pool.js +++ b/test/client-ws-socket-pool.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/client.js b/test/client.js index 9858bf55..e622c1ab 100644 --- a/test/client.js +++ b/test/client.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') @@ -357,7 +356,7 @@ function testClientAnnounceWithNumWant (t, serverType) { common.createServer(t, serverType, function (server, announceUrl) { var client1 = new Client({ infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId1, port: port, wrtc: {} @@ -468,3 +467,77 @@ test('http: userAgent', function (t) { client.start() }) }) + +function testSupportedTracker (t, serverType) { + t.plan(1) + + common.createServer(t, serverType, function (server, announceUrl) { + var client = new Client({ + infoHash: fixtures.leaves.parsedTorrent.infoHash, + announce: announceUrl, + peerId: peerId1, + port: port, + wrtc: {} + }) + + if (serverType === 'ws') common.mockWebsocketTracker(client) + client.on('error', function (err) { t.error(err) }) + client.on('warning', function (err) { t.error(err) }) + + client.start() + + client.once('update', function (data) { + t.pass('tracker is valid') + + server.close() + client.destroy() + }) + }) +} + +test('http: valid tracker port', function (t) { + testSupportedTracker(t, 'http') +}) + +test('udp: valid tracker port', function (t) { + testSupportedTracker(t, 'udp') +}) + +test('ws: valid tracker port', function (t) { + testSupportedTracker(t, 'ws') +}) + +function testUnsupportedTracker (t, announceUrl) { + t.plan(1) + + var client = new Client({ + infoHash: fixtures.leaves.parsedTorrent.infoHash, + announce: announceUrl, + peerId: peerId1, + port: port, + wrtc: {} + }) + + client.on('error', function (err) { t.error(err) }) + client.on('warning', function (err) { + t.ok(err.message.includes('tracker'), 'got warning') + + client.destroy() + }) +} + +test('unsupported tracker protocol', function (t) { + testUnsupportedTracker(t, 'badprotocol://127.0.0.1:8080/announce') +}) + +test('http: invalid tracker port', function (t) { + testUnsupportedTracker(t, 'http://127.0.0.1:69691337/announce') +}) + +test('udp: invalid tracker port', function (t) { + testUnsupportedTracker(t, 'udp://127.0.0.1:69691337') +}) + +test('ws: invalid tracker port', function (t) { + testUnsupportedTracker(t, 'ws://127.0.0.1:69691337') +}) diff --git a/test/destroy.js b/test/destroy.js index f6eafb64..5437b8ac 100644 --- a/test/destroy.js +++ b/test/destroy.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/evict.js b/test/evict.js index 1ff60bbb..605b03bf 100644 --- a/test/evict.js +++ b/test/evict.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var test = require('tape') @@ -31,7 +30,7 @@ function serverTest (t, serverType, serverFamily) { var client1 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId, port: 6881, wrtc: wrtc @@ -43,7 +42,7 @@ function serverTest (t, serverType, serverFamily) { client1.once('update', function (data) { var client2 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId2, port: 6882, wrtc: wrtc @@ -68,7 +67,7 @@ function serverTest (t, serverType, serverFamily) { var client3 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId3, port: 6880, wrtc: wrtc diff --git a/test/filter.js b/test/filter.js index 8eb4dd80..395e2336 100644 --- a/test/filter.js +++ b/test/filter.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/querystring.js b/test/querystring.js index e20ddfad..217e3624 100644 --- a/test/querystring.js +++ b/test/querystring.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var common = require('../lib/common') var test = require('tape') diff --git a/test/request-handler.js b/test/request-handler.js index e04e6651..d825a31e 100644 --- a/test/request-handler.js +++ b/test/request-handler.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var fixtures = require('webtorrent-fixtures') diff --git a/test/scrape.js b/test/scrape.js index 6391449b..b372ec2d 100644 --- a/test/scrape.js +++ b/test/scrape.js @@ -1,5 +1,4 @@ var bencode = require('bencode') -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var commonLib = require('../lib/common') @@ -119,7 +118,7 @@ function clientScrapeMulti (t, serverType) { commonTest.createServer(t, serverType, function (server, announceUrl) { Client.scrape({ - infoHash: [ infoHash1, infoHash2 ], + infoHash: [infoHash1, infoHash2], announce: announceUrl }, function (err, results) { t.error(err) @@ -161,7 +160,7 @@ test('server: multiple info_hash scrape (manual http request)', function (t) { var scrapeUrl = announceUrl.replace('/announce', '/scrape') var url = scrapeUrl + '?' + commonLib.querystringStringify({ - info_hash: [ binaryInfoHash1, binaryInfoHash2 ] + info_hash: [binaryInfoHash1, binaryInfoHash2] }) get.concat(url, function (err, res, data) { diff --git a/test/server.js b/test/server.js index 73e9fe66..1c611bc2 100644 --- a/test/server.js +++ b/test/server.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') var test = require('tape') @@ -33,7 +32,7 @@ function serverTest (t, serverType, serverFamily) { var client1 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId, port: 6881, wrtc: wrtc @@ -93,7 +92,7 @@ function serverTest (t, serverType, serverFamily) { var client2 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId2, port: 6882, wrtc: wrtc @@ -113,7 +112,7 @@ function serverTest (t, serverType, serverFamily) { var client3 = new Client({ infoHash: infoHash, - announce: [ announceUrl ], + announce: [announceUrl], peerId: peerId3, port: 6880, wrtc: wrtc diff --git a/test/stats.js b/test/stats.js index 15d0c0f5..5b14f81b 100644 --- a/test/stats.js +++ b/test/stats.js @@ -1,4 +1,3 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var commonTest = require('./common') var fixtures = require('webtorrent-fixtures') @@ -145,7 +144,7 @@ test('server: get leecher stats.json', function (t) { t.equal(stats.peersSeederOnly, 0) t.equal(stats.peersLeecherOnly, 1) t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients['WebTorrent']['0.91'], 1) + t.equal(stats.clients.WebTorrent['0.91'], 1) client.destroy(function () { t.pass('client destroyed') }) server.close(function () { t.pass('server closed') }) @@ -186,7 +185,7 @@ test('server: get leecher stats.json (unknown peerId)', function (t) { t.equal(stats.peersSeederOnly, 0) t.equal(stats.peersLeecherOnly, 1) t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients['unknown']['01234567'], 1) + t.equal(stats.clients.unknown['01234567'], 1) client.destroy(function () { t.pass('client destroyed') }) server.close(function () { t.pass('server closed') })