Merge branch 'master' into modernize_lib_client

This commit is contained in:
Diego Rodríguez Baquero 2018-11-22 11:00:48 -05:00 committed by GitHub
commit d410c6c088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 223 deletions

420
client.js
View File

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

View File

@ -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

View File

@ -11,9 +11,9 @@ const Tracker = require('./tracker')
// boost, and saves browser resources.
const socketPool = {}
const RECONNECT_MINIMUM = 15 * 1000
const RECONNECT_MINIMUM = 10 * 1000
const RECONNECT_MAXIMUM = 30 * 60 * 1000
const RECONNECT_VARIANCE = 30 * 1000
const RECONNECT_VARIANCE = 2 * 60 * 1000
const OFFER_TIMEOUT = 50 * 1000
class WebSocketTracker extends Tracker {

View File

@ -24,7 +24,7 @@
"bittorrent-peerid": "^1.0.2",
"bn.js": "^4.4.0",
"compact2string": "^1.2.0",
"debug": "^3.1.0",
"debug": "^4.0.1",
"inherits": "^2.0.1",
"ip": "^1.0.1",
"lru": "^3.0.0",