mirror of
https://github.com/webtorrent/bittorrent-tracker.git
synced 2024-12-14 19:36:24 +00:00
3f3db7deb1
Caught this issue because of the new eviction tests. Essentially, this change moves the socketPool into the client instance instead of a reused variable at the module level. When a client sends stop (or is evicted) the server will close the websocket connection if that client is not in any other swarms (based on peerId). However, if we are using a single socket for multiple clients (as was the case before this commit), then other clients will have their sockets unintentionally closed by the server.
302 lines
9.2 KiB
JavaScript
302 lines
9.2 KiB
JavaScript
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)
|
|
|
|
/**
|
|
* BitTorrent tracker client.
|
|
*
|
|
* Find torrent peers, to help a torrent client participate in a torrent swarm.
|
|
*
|
|
* @param {Object} opts options object
|
|
* @param {string|Buffer} opts.infoHash torrent info hash
|
|
* @param {string|Buffer} opts.peerId peer id
|
|
* @param {string|Array.<string>} opts.announce announce
|
|
* @param {number} opts.port torrent client listening port
|
|
* @param {function} opts.getAnnounceOpts callback to provide data to tracker
|
|
* @param {number} opts.rtcConfig RTCPeerConnection configuration object
|
|
* @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 = {}
|
|
|
|
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
|
|
: 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/feross/webtorrent-hybrid/issues/46
|
|
self._wrtc = typeof opts.wrtc === 'function' ? opts.wrtc() : opts.wrtc
|
|
|
|
// Use a socket pool, so WebSocket 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.
|
|
self._socketPool = {}
|
|
|
|
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)
|
|
}
|
|
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))
|
|
return null
|
|
}
|
|
return new WebSocketTracker(self, announceUrl)
|
|
} else {
|
|
nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl))
|
|
return null
|
|
}
|
|
})
|
|
.filter(Boolean)
|
|
|
|
function nextTickWarn (err) {
|
|
process.nextTick(function () {
|
|
self.emit('warning', err)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple convenience function to scrape a tracker for an info hash without needing to
|
|
* create a Client, pass it a parsed torrent, etc. Support scraping a tracker for multiple
|
|
* torrents at the same time.
|
|
* @params {Object} opts
|
|
* @param {string|Array.<string>} opts.infoHash
|
|
* @param {string} opts.announce
|
|
* @param {function} cb
|
|
*/
|
|
Client.scrape = function (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, {
|
|
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)
|
|
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) {
|
|
len -= 1
|
|
results[data.infoHash] = data
|
|
if (len === 0) {
|
|
client.destroy()
|
|
var keys = Object.keys(results)
|
|
if (keys.length === 1) {
|
|
cb(null, results[keys[0]])
|
|
} else {
|
|
cb(null, results)
|
|
}
|
|
}
|
|
})
|
|
|
|
opts.infoHash = Array.isArray(opts.infoHash)
|
|
? opts.infoHash.map(function (infoHash) {
|
|
return Buffer.from(infoHash, 'hex')
|
|
})
|
|
: Buffer.from(opts.infoHash, 'hex')
|
|
client.scrape({ infoHash: opts.infoHash })
|
|
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
|
|
}
|