lebab lib/client

This commit is contained in:
Jimmy Wärting 2018-10-03 14:44:11 +02:00
parent 65b2bdc804
commit 386e0a5fbe
4 changed files with 934 additions and 938 deletions

View File

@ -1,18 +1,13 @@
module.exports = HTTPTracker 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 arrayRemove = require('unordered-array-remove') const common = require('../common')
var bencode = require('bencode') const Tracker = require('./tracker')
var compact2string = require('compact2string')
var debug = require('debug')('bittorrent-tracker:http-tracker')
var get = require('simple-get')
var inherits = require('inherits')
var common = require('../common') const HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/
var Tracker = require('./tracker')
var HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/
inherits(HTTPTracker, Tracker)
/** /**
* HTTP torrent tracker client (for an individual tracker) * HTTP torrent tracker client (for an individual tracker)
@ -21,32 +16,32 @@ inherits(HTTPTracker, Tracker)
* @param {string} announceUrl announce url of tracker * @param {string} announceUrl announce url of tracker
* @param {Object} opts options object * @param {Object} opts options object
*/ */
function HTTPTracker (client, announceUrl, opts) { class HTTPTracker extends Tracker {
var self = this constructor (client, announceUrl, opts) {
Tracker.call(self, client, announceUrl) super(client, announceUrl)
const self = this
debug('new http tracker %s', announceUrl) debug('new http tracker %s', announceUrl)
// Determine scrape url (if http tracker supports it) // Determine scrape url (if http tracker supports it)
self.scrapeUrl = null self.scrapeUrl = null
var match = self.announceUrl.match(HTTP_SCRAPE_SUPPORT) const match = self.announceUrl.match(HTTP_SCRAPE_SUPPORT)
if (match) { if (match) {
var pre = self.announceUrl.slice(0, match.index) const pre = self.announceUrl.slice(0, match.index)
var post = self.announceUrl.slice(match.index + 9) const post = self.announceUrl.slice(match.index + 9)
self.scrapeUrl = pre + '/scrape' + post self.scrapeUrl = `${pre}/scrape${post}`
} }
self.cleanupFns = [] self.cleanupFns = []
self.maybeDestroyCleanup = null self.maybeDestroyCleanup = null
} }
HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes announce (opts) {
const self = this
HTTPTracker.prototype.announce = function (opts) {
var self = this
if (self.destroyed) return if (self.destroyed) return
var params = Object.assign({}, opts, { const params = Object.assign({}, opts, {
compact: (opts.compact == null) ? 1 : opts.compact, compact: (opts.compact == null) ? 1 : opts.compact,
info_hash: self.client._infoHashBinary, info_hash: self.client._infoHashBinary,
peer_id: self.client._peerIdBinary, peer_id: self.client._peerIdBinary,
@ -54,37 +49,37 @@ HTTPTracker.prototype.announce = function (opts) {
}) })
if (self._trackerId) params.trackerid = self._trackerId if (self._trackerId) params.trackerid = self._trackerId
self._request(self.announceUrl, params, function (err, data) { self._request(self.announceUrl, params, (err, data) => {
if (err) return self.client.emit('warning', err) if (err) return self.client.emit('warning', err)
self._onAnnounceResponse(data) self._onAnnounceResponse(data)
}) })
} }
HTTPTracker.prototype.scrape = function (opts) { scrape (opts) {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
if (!self.scrapeUrl) { if (!self.scrapeUrl) {
self.client.emit('error', new Error('scrape not supported ' + self.announceUrl)) self.client.emit('error', new Error(`scrape not supported ${self.announceUrl}`))
return return
} }
var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? opts.infoHash.map(function (infoHash) { ? opts.infoHash.map(infoHash => {
return infoHash.toString('binary') return infoHash.toString('binary')
}) })
: (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary
var params = { const params = {
info_hash: infoHashes info_hash: infoHashes
} }
self._request(self.scrapeUrl, params, function (err, data) { self._request(self.scrapeUrl, params, (err, data) => {
if (err) return self.client.emit('warning', err) if (err) return self.client.emit('warning', err)
self._onScrapeResponse(data) self._onScrapeResponse(data)
}) })
} }
HTTPTracker.prototype.destroy = function (cb) { destroy (cb) {
var self = this const self = this
if (self.destroyed) return cb(null) if (self.destroyed) return cb(null)
self.destroyed = true self.destroyed = true
clearInterval(self.interval) clearInterval(self.interval)
@ -98,7 +93,7 @@ HTTPTracker.prototype.destroy = function (cb) {
// But, if all pending requests complete before the timeout fires, do cleanup // But, if all pending requests complete before the timeout fires, do cleanup
// right away. // right away.
self.maybeDestroyCleanup = function () { self.maybeDestroyCleanup = () => {
if (self.cleanupFns.length === 0) destroyCleanup() if (self.cleanupFns.length === 0) destroyCleanup()
} }
@ -108,7 +103,7 @@ HTTPTracker.prototype.destroy = function (cb) {
timeout = null timeout = null
} }
self.maybeDestroyCleanup = null self.maybeDestroyCleanup = null
self.cleanupFns.slice(0).forEach(function (cleanup) { self.cleanupFns.slice(0).forEach(cleanup => {
cleanup() cleanup()
}) })
self.cleanupFns = [] self.cleanupFns = []
@ -116,14 +111,14 @@ HTTPTracker.prototype.destroy = function (cb) {
} }
} }
HTTPTracker.prototype._request = function (requestUrl, params, cb) { _request (requestUrl, params, cb) {
var self = this const self = this
var u = requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + const u = requestUrl + (!requestUrl.includes('?') ? '?' : '&') +
common.querystringStringify(params) common.querystringStringify(params)
self.cleanupFns.push(cleanup) self.cleanupFns.push(cleanup)
var request = get.concat({ let request = get.concat({
url: u, url: u,
timeout: common.REQUEST_TIMEOUT, timeout: common.REQUEST_TIMEOUT,
headers: { headers: {
@ -146,56 +141,54 @@ HTTPTracker.prototype._request = function (requestUrl, params, cb) {
if (err) return cb(err) if (err) return cb(err)
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
return cb(new Error('Non-200 response code ' + return cb(new Error(`Non-200 response code ${res.statusCode} from ${self.announceUrl}`))
res.statusCode + ' from ' + self.announceUrl))
} }
if (!data || data.length === 0) { if (!data || data.length === 0) {
return cb(new Error('Invalid tracker response from' + return cb(new Error(`Invalid tracker response from${self.announceUrl}`))
self.announceUrl))
} }
try { try {
data = bencode.decode(data) data = bencode.decode(data)
} catch (err) { } catch (err) {
return cb(new Error('Error decoding tracker response: ' + err.message)) return cb(new Error(`Error decoding tracker response: ${err.message}`))
} }
var failure = data['failure reason'] const failure = data['failure reason']
if (failure) { if (failure) {
debug('failure from ' + requestUrl + ' (' + failure + ')') debug(`failure from ${requestUrl} (${failure})`)
return cb(new Error(failure)) return cb(new Error(failure))
} }
var warning = data['warning message'] const warning = data['warning message']
if (warning) { if (warning) {
debug('warning from ' + requestUrl + ' (' + warning + ')') debug(`warning from ${requestUrl} (${warning})`)
self.client.emit('warning', new Error(warning)) self.client.emit('warning', new Error(warning))
} }
debug('response from ' + requestUrl) debug(`response from ${requestUrl}`)
cb(null, data) cb(null, data)
} }
} }
HTTPTracker.prototype._onAnnounceResponse = function (data) { _onAnnounceResponse (data) {
var self = this const self = this
var interval = data.interval || data['min interval'] const interval = data.interval || data['min interval']
if (interval) self.setInterval(interval * 1000) if (interval) self.setInterval(interval * 1000)
var trackerId = data['tracker id'] const trackerId = data['tracker id']
if (trackerId) { if (trackerId) {
// If absent, do not discard previous trackerId value // If absent, do not discard previous trackerId value
self._trackerId = trackerId self._trackerId = trackerId
} }
var response = Object.assign({}, data, { const response = Object.assign({}, data, {
announce: self.announceUrl, announce: self.announceUrl,
infoHash: common.binaryToHex(data.info_hash) infoHash: common.binaryToHex(data.info_hash)
}) })
self.client.emit('update', response) self.client.emit('update', response)
var addrs let addrs
if (Buffer.isBuffer(data.peers)) { if (Buffer.isBuffer(data.peers)) {
// tracker returned compact response // tracker returned compact response
try { try {
@ -203,13 +196,13 @@ HTTPTracker.prototype._onAnnounceResponse = function (data) {
} catch (err) { } catch (err) {
return self.client.emit('warning', err) return self.client.emit('warning', err)
} }
addrs.forEach(function (addr) { addrs.forEach(addr => {
self.client.emit('peer', addr) self.client.emit('peer', addr)
}) })
} else if (Array.isArray(data.peers)) { } else if (Array.isArray(data.peers)) {
// tracker returned normal response // tracker returned normal response
data.peers.forEach(function (peer) { data.peers.forEach(peer => {
self.client.emit('peer', peer.ip + ':' + peer.port) self.client.emit('peer', `${peer.ip}:${peer.port}`)
}) })
} }
@ -220,39 +213,44 @@ HTTPTracker.prototype._onAnnounceResponse = function (data) {
} catch (err) { } catch (err) {
return self.client.emit('warning', err) return self.client.emit('warning', err)
} }
addrs.forEach(function (addr) { addrs.forEach(addr => {
self.client.emit('peer', addr) self.client.emit('peer', addr)
}) })
} else if (Array.isArray(data.peers6)) { } else if (Array.isArray(data.peers6)) {
// tracker returned normal response // tracker returned normal response
data.peers6.forEach(function (peer) { data.peers6.forEach(peer => {
var ip = /^\[/.test(peer.ip) || !/:/.test(peer.ip) const ip = /^\[/.test(peer.ip) || !/:/.test(peer.ip)
? peer.ip /* ipv6 w/ brackets or domain name */ ? peer.ip /* ipv6 w/ brackets or domain name */
: '[' + peer.ip + ']' /* ipv6 without brackets */ : `[${peer.ip}]` /* ipv6 without brackets */
self.client.emit('peer', ip + ':' + peer.port) self.client.emit('peer', `${ip}:${peer.port}`)
}) })
} }
} }
HTTPTracker.prototype._onScrapeResponse = function (data) { _onScrapeResponse (data) {
var self = this const self = this
// NOTE: the unofficial spec says to use the 'files' key, 'host' has been // NOTE: the unofficial spec says to use the 'files' key, 'host' has been
// seen in practice // seen in practice
data = data.files || data.host || {} data = data.files || data.host || {}
var keys = Object.keys(data) const keys = Object.keys(data)
if (keys.length === 0) { if (keys.length === 0) {
self.client.emit('warning', new Error('invalid scrape response')) self.client.emit('warning', new Error('invalid scrape response'))
return return
} }
keys.forEach(function (infoHash) { keys.forEach(infoHash => {
// TODO: optionally handle data.flags.min_request_interval // TODO: optionally handle data.flags.min_request_interval
// (separate from announce interval) // (separate from announce interval)
var response = Object.assign(data[infoHash], { const response = Object.assign(data[infoHash], {
announce: self.announceUrl, announce: self.announceUrl,
infoHash: common.binaryToHex(infoHash) infoHash: common.binaryToHex(infoHash)
}) })
self.client.emit('scrape', response) self.client.emit('scrape', response)
}) })
} }
}
HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes
module.exports = HTTPTracker

View File

@ -1,13 +1,10 @@
module.exports = Tracker const EventEmitter = require('events')
var EventEmitter = require('events').EventEmitter class Tracker extends EventEmitter {
var inherits = require('inherits') constructor (client, announceUrl) {
super()
inherits(Tracker, EventEmitter) const self = this
function Tracker (client, announceUrl) {
var self = this
EventEmitter.call(self)
self.client = client self.client = client
self.announceUrl = announceUrl self.announceUrl = announceUrl
@ -15,16 +12,19 @@ function Tracker (client, announceUrl) {
self.destroyed = false self.destroyed = false
} }
Tracker.prototype.setInterval = function (intervalMs) { setInterval (intervalMs) {
var self = this const self = this
if (intervalMs == null) intervalMs = self.DEFAULT_ANNOUNCE_INTERVAL if (intervalMs == null) intervalMs = self.DEFAULT_ANNOUNCE_INTERVAL
clearInterval(self.interval) clearInterval(self.interval)
if (intervalMs) { if (intervalMs) {
self.interval = setInterval(function () { self.interval = setInterval(() => {
self.announce(self.client._defaultAnnounceOpts()) self.announce(self.client._defaultAnnounceOpts())
}, intervalMs) }, intervalMs)
if (self.interval.unref) self.interval.unref() if (self.interval.unref) self.interval.unref()
} }
} }
}
module.exports = Tracker

View File

@ -1,19 +1,14 @@
module.exports = UDPTracker const arrayRemove = require('unordered-array-remove')
const BN = require('bn.js')
const Buffer = require('safe-buffer').Buffer
const compact2string = require('compact2string')
const debug = require('debug')('bittorrent-tracker:udp-tracker')
const dgram = require('dgram')
const randombytes = require('randombytes')
const url = require('url')
var arrayRemove = require('unordered-array-remove') const common = require('../common')
var BN = require('bn.js') const Tracker = require('./tracker')
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')
var common = require('../common')
var Tracker = require('./tracker')
inherits(UDPTracker, Tracker)
/** /**
* UDP torrent tracker client (for an individual tracker) * UDP torrent tracker client (for an individual tracker)
@ -22,32 +17,31 @@ inherits(UDPTracker, Tracker)
* @param {string} announceUrl announce url of tracker * @param {string} announceUrl announce url of tracker
* @param {Object} opts options object * @param {Object} opts options object
*/ */
function UDPTracker (client, announceUrl, opts) { class UDPTracker extends Tracker {
var self = this constructor (client, announceUrl, opts) {
Tracker.call(self, client, announceUrl) super(client, announceUrl)
const self = this
debug('new udp tracker %s', announceUrl) debug('new udp tracker %s', announceUrl)
self.cleanupFns = [] self.cleanupFns = []
self.maybeDestroyCleanup = null self.maybeDestroyCleanup = null
} }
UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes announce (opts) {
const self = this
UDPTracker.prototype.announce = function (opts) {
var self = this
if (self.destroyed) return if (self.destroyed) return
self._request(opts) self._request(opts)
} }
UDPTracker.prototype.scrape = function (opts) { scrape (opts) {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
opts._scrape = true opts._scrape = true
self._request(opts) // udp scrape uses same announce url self._request(opts) // udp scrape uses same announce url
} }
UDPTracker.prototype.destroy = function (cb) { destroy (cb) {
var self = this const self = this
if (self.destroyed) return cb(null) if (self.destroyed) return cb(null)
self.destroyed = true self.destroyed = true
clearInterval(self.interval) clearInterval(self.interval)
@ -61,7 +55,7 @@ UDPTracker.prototype.destroy = function (cb) {
// But, if all pending requests complete before the timeout fires, do cleanup // But, if all pending requests complete before the timeout fires, do cleanup
// right away. // right away.
self.maybeDestroyCleanup = function () { self.maybeDestroyCleanup = () => {
if (self.cleanupFns.length === 0) destroyCleanup() if (self.cleanupFns.length === 0) destroyCleanup()
} }
@ -71,7 +65,7 @@ UDPTracker.prototype.destroy = function (cb) {
timeout = null timeout = null
} }
self.maybeDestroyCleanup = null self.maybeDestroyCleanup = null
self.cleanupFns.slice(0).forEach(function (cleanup) { self.cleanupFns.slice(0).forEach(cleanup => {
cleanup() cleanup()
}) })
self.cleanupFns = [] self.cleanupFns = []
@ -79,17 +73,17 @@ UDPTracker.prototype.destroy = function (cb) {
} }
} }
UDPTracker.prototype._request = function (opts) { _request (opts) {
var self = this const self = this
if (!opts) opts = {} if (!opts) opts = {}
var parsedUrl = url.parse(self.announceUrl) const parsedUrl = url.parse(self.announceUrl)
var transactionId = genTransactionId() let transactionId = genTransactionId()
var socket = dgram.createSocket('udp4') let socket = dgram.createSocket('udp4')
var timeout = setTimeout(function () { let timeout = setTimeout(() => {
// does not matter if `stopped` event arrives, so supress errors // does not matter if `stopped` event arrives, so supress errors
if (opts.event === 'stopped') cleanup() if (opts.event === 'stopped') cleanup()
else onError(new Error('tracker request timed out (' + opts.event + ')')) else onError(new Error(`tracker request timed out (${opts.event})`))
timeout = null timeout = null
}, common.REQUEST_TIMEOUT) }, common.REQUEST_TIMEOUT)
if (timeout.unref) timeout.unref() if (timeout.unref) timeout.unref()
@ -125,7 +119,7 @@ UDPTracker.prototype._request = function (opts) {
cleanup() cleanup()
if (self.destroyed) return if (self.destroyed) return
if (err.message) err.message += ' (' + self.announceUrl + ')' if (err.message) err.message += ` (${self.announceUrl})`
// errors will often happen if a tracker is offline, so don't treat it as fatal // errors will often happen if a tracker is offline, so don't treat it as fatal
self.client.emit('warning', err) self.client.emit('warning', err)
} }
@ -135,7 +129,7 @@ UDPTracker.prototype._request = function (opts) {
return onError(new Error('tracker sent invalid transaction id')) return onError(new Error('tracker sent invalid transaction id'))
} }
var action = msg.readUInt32BE(0) const action = msg.readUInt32BE(0)
debug('UDP response %s, action %s', self.announceUrl, action) debug('UDP response %s, action %s', self.announceUrl, action)
switch (action) { switch (action) {
case 0: // handshake case 0: // handshake
@ -155,7 +149,7 @@ UDPTracker.prototype._request = function (opts) {
if (msg.length < 20) return onError(new Error('invalid announce message')) if (msg.length < 20) return onError(new Error('invalid announce message'))
var interval = msg.readUInt32BE(8) const interval = msg.readUInt32BE(8)
if (interval) self.setInterval(interval * 1000) if (interval) self.setInterval(interval * 1000)
self.client.emit('update', { self.client.emit('update', {
@ -164,13 +158,13 @@ UDPTracker.prototype._request = function (opts) {
incomplete: msg.readUInt32BE(12) incomplete: msg.readUInt32BE(12)
}) })
var addrs let addrs
try { try {
addrs = compact2string.multi(msg.slice(20)) addrs = compact2string.multi(msg.slice(20))
} catch (err) { } catch (err) {
return self.client.emit('warning', err) return self.client.emit('warning', err)
} }
addrs.forEach(function (addr) { addrs.forEach(addr => {
self.client.emit('peer', addr) self.client.emit('peer', addr)
}) })
@ -183,11 +177,11 @@ UDPTracker.prototype._request = function (opts) {
if (msg.length < 20 || (msg.length - 8) % 12 !== 0) { if (msg.length < 20 || (msg.length - 8) % 12 !== 0) {
return onError(new Error('invalid scrape message')) return onError(new Error('invalid scrape message'))
} }
var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? opts.infoHash.map(function (infoHash) { return infoHash.toString('hex') }) ? opts.infoHash.map(infoHash => { return infoHash.toString('hex') })
: [ (opts.infoHash && opts.infoHash.toString('hex')) || self.client.infoHash ] : [ (opts.infoHash && opts.infoHash.toString('hex')) || self.client.infoHash ]
for (var i = 0, len = (msg.length - 8) / 12; i < len; i += 1) { for (let i = 0, len = (msg.length - 8) / 12; i < len; i += 1) {
self.client.emit('scrape', { self.client.emit('scrape', {
announce: self.announceUrl, announce: self.announceUrl,
infoHash: infoHashes[i], infoHash: infoHashes[i],
@ -244,7 +238,7 @@ UDPTracker.prototype._request = function (opts) {
function scrape (connectionId) { function scrape (connectionId) {
transactionId = genTransactionId() transactionId = genTransactionId()
var infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) const infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? Buffer.concat(opts.infoHash) ? Buffer.concat(opts.infoHash)
: (opts.infoHash || self.client._infoHashBuffer) : (opts.infoHash || self.client._infoHashBuffer)
@ -256,22 +250,25 @@ UDPTracker.prototype._request = function (opts) {
])) ]))
} }
} }
}
UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes
function genTransactionId () { function genTransactionId () {
return randombytes(4) return randombytes(4)
} }
function toUInt16 (n) { function toUInt16 (n) {
var buf = Buffer.allocUnsafe(2) const buf = Buffer.allocUnsafe(2)
buf.writeUInt16BE(n, 0) buf.writeUInt16BE(n, 0)
return buf return buf
} }
var MAX_UINT = 4294967295 const MAX_UINT = 4294967295
function toUInt64 (n) { function toUInt64 (n) {
if (n > MAX_UINT || typeof n === 'string') { if (n > MAX_UINT || typeof n === 'string') {
var bytes = new BN(n).toArray() const bytes = new BN(n).toArray()
while (bytes.length < 8) { while (bytes.length < 8) {
bytes.unshift(0) bytes.unshift(0)
} }
@ -281,3 +278,5 @@ function toUInt64 (n) {
} }
function noop () {} function noop () {}
module.exports = UDPTracker

View File

@ -1,31 +1,25 @@
module.exports = WebSocketTracker const debug = require('debug')('bittorrent-tracker:websocket-tracker')
const Peer = require('simple-peer')
const randombytes = require('randombytes')
const Socket = require('simple-websocket')
var debug = require('debug')('bittorrent-tracker:websocket-tracker') const common = require('../common')
var inherits = require('inherits') const Tracker = require('./tracker')
var Peer = require('simple-peer')
var randombytes = require('randombytes')
var Socket = require('simple-websocket')
var common = require('../common')
var Tracker = require('./tracker')
// Use a socket pool, so tracker clients share WebSocket objects for the same server. // 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 // In practice, WebSockets are pretty slow to establish, so this gives a nice performance
// boost, and saves browser resources. // boost, and saves browser resources.
var socketPool = {} const socketPool = {}
// Normally this shouldn't be accessed but is occasionally useful
WebSocketTracker._socketPool = socketPool
var RECONNECT_MINIMUM = 15 * 1000 const RECONNECT_MINIMUM = 15 * 1000
var RECONNECT_MAXIMUM = 30 * 60 * 1000 const RECONNECT_MAXIMUM = 30 * 60 * 1000
var RECONNECT_VARIANCE = 30 * 1000 const RECONNECT_VARIANCE = 30 * 1000
var OFFER_TIMEOUT = 50 * 1000 const OFFER_TIMEOUT = 50 * 1000
inherits(WebSocketTracker, Tracker) class WebSocketTracker extends Tracker {
constructor (client, announceUrl, opts) {
function WebSocketTracker (client, announceUrl, opts) { super(client, announceUrl)
var self = this const self = this
Tracker.call(self, client, announceUrl)
debug('new websocket tracker %s', announceUrl) debug('new websocket tracker %s', announceUrl)
self.peers = {} // peers (offer id -> peer) self.peers = {} // peers (offer id -> peer)
@ -42,19 +36,17 @@ function WebSocketTracker (client, announceUrl, opts) {
self._openSocket() self._openSocket()
} }
WebSocketTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 1000 // 30 seconds announce (opts) {
const self = this
WebSocketTracker.prototype.announce = function (opts) {
var self = this
if (self.destroyed || self.reconnecting) return if (self.destroyed || self.reconnecting) return
if (!self.socket.connected) { if (!self.socket.connected) {
self.socket.once('connect', function () { self.socket.once('connect', () => {
self.announce(opts) self.announce(opts)
}) })
return return
} }
var params = Object.assign({}, opts, { const params = Object.assign({}, opts, {
action: 'announce', action: 'announce',
info_hash: self.client._infoHashBinary, info_hash: self.client._infoHashBinary,
peer_id: self.client._peerIdBinary peer_id: self.client._peerIdBinary
@ -66,9 +58,9 @@ WebSocketTracker.prototype.announce = function (opts) {
self._send(params) self._send(params)
} else { } else {
// Limit the number of offers that are generated, since it can be slow // Limit the number of offers that are generated, since it can be slow
var numwant = Math.min(opts.numwant, 10) const numwant = Math.min(opts.numwant, 10)
self._generateOffers(numwant, function (offers) { self._generateOffers(numwant, offers => {
params.numwant = numwant params.numwant = numwant
params.offers = offers params.offers = offers
self._send(params) self._send(params)
@ -76,22 +68,22 @@ WebSocketTracker.prototype.announce = function (opts) {
} }
} }
WebSocketTracker.prototype.scrape = function (opts) { scrape (opts) {
var self = this const self = this
if (self.destroyed || self.reconnecting) return if (self.destroyed || self.reconnecting) return
if (!self.socket.connected) { if (!self.socket.connected) {
self.socket.once('connect', function () { self.socket.once('connect', () => {
self.scrape(opts) self.scrape(opts)
}) })
return return
} }
var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? opts.infoHash.map(function (infoHash) { ? opts.infoHash.map(infoHash => {
return infoHash.toString('binary') return infoHash.toString('binary')
}) })
: (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary
var params = { const params = {
action: 'scrape', action: 'scrape',
info_hash: infoHashes info_hash: infoHashes
} }
@ -99,8 +91,8 @@ WebSocketTracker.prototype.scrape = function (opts) {
self._send(params) self._send(params)
} }
WebSocketTracker.prototype.destroy = function (cb) { destroy (cb) {
var self = this const self = this
if (!cb) cb = noop if (!cb) cb = noop
if (self.destroyed) return cb(null) if (self.destroyed) return cb(null)
@ -110,8 +102,8 @@ WebSocketTracker.prototype.destroy = function (cb) {
clearTimeout(self.reconnectTimer) clearTimeout(self.reconnectTimer)
// Destroy peers // Destroy peers
for (var peerId in self.peers) { for (const peerId in self.peers) {
var peer = self.peers[peerId] const peer = self.peers[peerId]
clearTimeout(peer.trackerTimeout) clearTimeout(peer.trackerTimeout)
peer.destroy() peer.destroy()
} }
@ -137,7 +129,7 @@ WebSocketTracker.prototype.destroy = function (cb) {
// Other instances are using the socket, so there's nothing left to do here // Other instances are using the socket, so there's nothing left to do here
if (socketPool[self.announceUrl].consumers > 0) return cb() if (socketPool[self.announceUrl].consumers > 0) return cb()
var socket = socketPool[self.announceUrl] let socket = socketPool[self.announceUrl]
delete socketPool[self.announceUrl] delete socketPool[self.announceUrl]
socket.on('error', noop) // ignore all future errors socket.on('error', noop) // ignore all future errors
socket.once('close', cb) socket.once('close', cb)
@ -164,22 +156,22 @@ WebSocketTracker.prototype.destroy = function (cb) {
} }
} }
WebSocketTracker.prototype._openSocket = function () { _openSocket () {
var self = this const self = this
self.destroyed = false self.destroyed = false
if (!self.peers) self.peers = {} if (!self.peers) self.peers = {}
self._onSocketConnectBound = function () { self._onSocketConnectBound = () => {
self._onSocketConnect() self._onSocketConnect()
} }
self._onSocketErrorBound = function (err) { self._onSocketErrorBound = err => {
self._onSocketError(err) self._onSocketError(err)
} }
self._onSocketDataBound = function (data) { self._onSocketDataBound = data => {
self._onSocketData(data) self._onSocketData(data)
} }
self._onSocketCloseBound = function () { self._onSocketCloseBound = () => {
self._onSocketClose() self._onSocketClose()
} }
@ -197,8 +189,8 @@ WebSocketTracker.prototype._openSocket = function () {
self.socket.once('error', self._onSocketErrorBound) self.socket.once('error', self._onSocketErrorBound)
} }
WebSocketTracker.prototype._onSocketConnect = function () { _onSocketConnect () {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
if (self.reconnecting) { if (self.reconnecting) {
@ -208,8 +200,8 @@ WebSocketTracker.prototype._onSocketConnect = function () {
} }
} }
WebSocketTracker.prototype._onSocketData = function (data) { _onSocketData (data) {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
self.expectingResponse = false self.expectingResponse = false
@ -226,12 +218,12 @@ WebSocketTracker.prototype._onSocketData = function (data) {
} else if (data.action === 'scrape') { } else if (data.action === 'scrape') {
self._onScrapeResponse(data) self._onScrapeResponse(data)
} else { } else {
self._onSocketError(new Error('invalid action in WS response: ' + data.action)) self._onSocketError(new Error(`invalid action in WS response: ${data.action}`))
} }
} }
WebSocketTracker.prototype._onAnnounceResponse = function (data) { _onAnnounceResponse (data) {
var self = this const self = this
if (data.info_hash !== self.client._infoHashBinary) { if (data.info_hash !== self.client._infoHashBinary) {
debug( debug(
@ -251,41 +243,41 @@ WebSocketTracker.prototype._onAnnounceResponse = function (data) {
JSON.stringify(data), self.announceUrl, self.client.infoHash JSON.stringify(data), self.announceUrl, self.client.infoHash
) )
var failure = data['failure reason'] const failure = data['failure reason']
if (failure) return self.client.emit('warning', new Error(failure)) if (failure) return self.client.emit('warning', new Error(failure))
var warning = data['warning message'] const warning = data['warning message']
if (warning) self.client.emit('warning', new Error(warning)) if (warning) self.client.emit('warning', new Error(warning))
var interval = data.interval || data['min interval'] const interval = data.interval || data['min interval']
if (interval) self.setInterval(interval * 1000) if (interval) self.setInterval(interval * 1000)
var trackerId = data['tracker id'] const trackerId = data['tracker id']
if (trackerId) { if (trackerId) {
// If absent, do not discard previous trackerId value // If absent, do not discard previous trackerId value
self._trackerId = trackerId self._trackerId = trackerId
} }
if (data.complete != null) { if (data.complete != null) {
var response = Object.assign({}, data, { const response = Object.assign({}, data, {
announce: self.announceUrl, announce: self.announceUrl,
infoHash: common.binaryToHex(data.info_hash) infoHash: common.binaryToHex(data.info_hash)
}) })
self.client.emit('update', response) self.client.emit('update', response)
} }
var peer let peer
if (data.offer && data.peer_id) { if (data.offer && data.peer_id) {
debug('creating peer (from remote offer)') debug('creating peer (from remote offer)')
peer = self._createPeer() peer = self._createPeer()
peer.id = common.binaryToHex(data.peer_id) peer.id = common.binaryToHex(data.peer_id)
peer.once('signal', function (answer) { peer.once('signal', answer => {
var params = { const params = {
action: 'announce', action: 'announce',
info_hash: self.client._infoHashBinary, info_hash: self.client._infoHashBinary,
peer_id: self.client._peerIdBinary, peer_id: self.client._peerIdBinary,
to_peer_id: data.peer_id, to_peer_id: data.peer_id,
answer: answer, answer,
offer_id: data.offer_id offer_id: data.offer_id
} }
if (self._trackerId) params.trackerid = self._trackerId if (self._trackerId) params.trackerid = self._trackerId
@ -296,7 +288,7 @@ WebSocketTracker.prototype._onAnnounceResponse = function (data) {
} }
if (data.answer && data.peer_id) { if (data.answer && data.peer_id) {
var offerId = common.binaryToHex(data.offer_id) const offerId = common.binaryToHex(data.offer_id)
peer = self.peers[offerId] peer = self.peers[offerId]
if (peer) { if (peer) {
peer.id = common.binaryToHex(data.peer_id) peer.id = common.binaryToHex(data.peer_id)
@ -307,25 +299,25 @@ WebSocketTracker.prototype._onAnnounceResponse = function (data) {
peer.trackerTimeout = null peer.trackerTimeout = null
delete self.peers[offerId] delete self.peers[offerId]
} else { } else {
debug('got unexpected answer: ' + JSON.stringify(data.answer)) debug(`got unexpected answer: ${JSON.stringify(data.answer)}`)
} }
} }
} }
WebSocketTracker.prototype._onScrapeResponse = function (data) { _onScrapeResponse (data) {
var self = this const self = this
data = data.files || {} data = data.files || {}
var keys = Object.keys(data) const keys = Object.keys(data)
if (keys.length === 0) { if (keys.length === 0) {
self.client.emit('warning', new Error('invalid scrape response')) self.client.emit('warning', new Error('invalid scrape response'))
return return
} }
keys.forEach(function (infoHash) { keys.forEach(infoHash => {
// TODO: optionally handle data.flags.min_request_interval // TODO: optionally handle data.flags.min_request_interval
// (separate from announce interval) // (separate from announce interval)
var response = Object.assign(data[infoHash], { const response = Object.assign(data[infoHash], {
announce: self.announceUrl, announce: self.announceUrl,
infoHash: common.binaryToHex(infoHash) infoHash: common.binaryToHex(infoHash)
}) })
@ -333,15 +325,15 @@ WebSocketTracker.prototype._onScrapeResponse = function (data) {
}) })
} }
WebSocketTracker.prototype._onSocketClose = function () { _onSocketClose () {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
self.destroy() self.destroy()
self._startReconnectTimer() self._startReconnectTimer()
} }
WebSocketTracker.prototype._onSocketError = function (err) { _onSocketError (err) {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
self.destroy() self.destroy()
// errors will often happen if a tracker is offline, so don't treat it as fatal // errors will often happen if a tracker is offline, so don't treat it as fatal
@ -349,13 +341,13 @@ WebSocketTracker.prototype._onSocketError = function (err) {
self._startReconnectTimer() self._startReconnectTimer()
} }
WebSocketTracker.prototype._startReconnectTimer = function () { _startReconnectTimer () {
var self = this const self = this
var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, self.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM) const ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, self.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM)
self.reconnecting = true self.reconnecting = true
clearTimeout(self.reconnectTimer) clearTimeout(self.reconnectTimer)
self.reconnectTimer = setTimeout(function () { self.reconnectTimer = setTimeout(() => {
self.retries++ self.retries++
self._openSocket() self._openSocket()
}, ms) }, ms)
@ -364,37 +356,37 @@ WebSocketTracker.prototype._startReconnectTimer = function () {
debug('reconnecting socket in %s ms', ms) debug('reconnecting socket in %s ms', ms)
} }
WebSocketTracker.prototype._send = function (params) { _send (params) {
var self = this const self = this
if (self.destroyed) return if (self.destroyed) return
self.expectingResponse = true self.expectingResponse = true
var message = JSON.stringify(params) const message = JSON.stringify(params)
debug('send %s', message) debug('send %s', message)
self.socket.send(message) self.socket.send(message)
} }
WebSocketTracker.prototype._generateOffers = function (numwant, cb) { _generateOffers (numwant, cb) {
var self = this const self = this
var offers = [] const offers = []
debug('generating %s offers', numwant) debug('generating %s offers', numwant)
for (var i = 0; i < numwant; ++i) { for (let i = 0; i < numwant; ++i) {
generateOffer() generateOffer()
} }
checkDone() checkDone()
function generateOffer () { function generateOffer () {
var offerId = randombytes(20).toString('hex') const offerId = randombytes(20).toString('hex')
debug('creating peer (from _generateOffers)') debug('creating peer (from _generateOffers)')
var peer = self.peers[offerId] = self._createPeer({ initiator: true }) const peer = self.peers[offerId] = self._createPeer({ initiator: true })
peer.once('signal', function (offer) { peer.once('signal', offer => {
offers.push({ offers.push({
offer: offer, offer,
offer_id: common.hexToBinary(offerId) offer_id: common.hexToBinary(offerId)
}) })
checkDone() checkDone()
}) })
peer.trackerTimeout = setTimeout(function () { peer.trackerTimeout = setTimeout(() => {
debug('tracker timeout: destroying peer') debug('tracker timeout: destroying peer')
peer.trackerTimeout = null peer.trackerTimeout = null
delete self.peers[offerId] delete self.peers[offerId]
@ -411,8 +403,8 @@ WebSocketTracker.prototype._generateOffers = function (numwant, cb) {
} }
} }
WebSocketTracker.prototype._createPeer = function (opts) { _createPeer (opts) {
var self = this const self = this
opts = Object.assign({ opts = Object.assign({
trickle: false, trickle: false,
@ -420,7 +412,7 @@ WebSocketTracker.prototype._createPeer = function (opts) {
wrtc: self.client._wrtc wrtc: self.client._wrtc
}, opts) }, opts)
var peer = new Peer(opts) const peer = new Peer(opts)
peer.once('error', onError) peer.once('error', onError)
peer.once('connect', onConnect) peer.once('connect', onConnect)
@ -430,7 +422,7 @@ WebSocketTracker.prototype._createPeer = function (opts) {
// Handle peer 'error' events that are fired *before* the peer is emitted in // Handle peer 'error' events that are fired *before* the peer is emitted in
// a 'peer' event. // a 'peer' event.
function onError (err) { function onError (err) {
self.client.emit('warning', new Error('Connection error: ' + err.message)) self.client.emit('warning', new Error(`Connection error: ${err.message}`))
peer.destroy() peer.destroy()
} }
@ -441,5 +433,12 @@ WebSocketTracker.prototype._createPeer = function (opts) {
peer.removeListener('connect', onConnect) 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 () {} function noop () {}
module.exports = WebSocketTracker