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.} 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 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.} 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 }