module.exports = Client var debug = require('debug')('bittorrent-tracker') var EventEmitter = require('events').EventEmitter var extend = require('xtend') var inherits = require('inherits') var once = require('once') var parallel = require('run-parallel') 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 {string|Buffer} peerId peer id * @param {Number} port torrent client listening port * @param {Object} torrent parsed torrent * @param {Object} opts options object * @param {Number} opts.rtcConfig RTCPeerConnection configuration object * @param {Number} opts.wrtc custom webrtc implementation * @param {Object} opts.getAnnounceOpts callback to provide data to tracker */ function Client (peerId, port, torrent, opts) { var self = this if (!(self instanceof Client)) return new Client(peerId, port, torrent, opts) EventEmitter.call(self) if (!opts) opts = {} // required self.peerId = typeof peerId === 'string' ? peerId : peerId.toString('hex') self.peerIdBuffer = new Buffer(self.peerId, 'hex') self._peerIdBinary = self.peerIdBuffer.toString('binary') self.infoHash = typeof torrent.infoHash === 'string' ? torrent.infoHash : torrent.infoHash.toString('hex') self.infoHashBuffer = new Buffer(self.infoHash, 'hex') self._infoHashBinary = self.infoHashBuffer.toString('binary') self.torrentLength = torrent.length self.destroyed = false self._port = port self._rtcConfig = opts.rtcConfig self._wrtc = opts.wrtc self._getAnnounceOpts = opts.getAnnounceOpts debug('new client %s', self.infoHash) var webrtcSupport = !!self._wrtc || typeof window !== 'undefined' var announce = (typeof torrent.announce === 'string') ? [ torrent.announce ] : torrent.announce == null ? [] : torrent.announce announce = announce.map(function (announceUrl) { announceUrl = announceUrl.toString() if (announceUrl[announceUrl.length - 1] === '/') { // remove trailing slash from trackers to catch duplicates announceUrl = announceUrl.substring(0, announceUrl.length - 1) } return announceUrl }) announce = uniq(announce) 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) { return new WebSocketTracker(self, announceUrl) } else { process.nextTick(function () { var err = new Error('unsupported tracker protocol for ' + announceUrl) self.emit('warning', err) }) } return null }) .filter(Boolean) } /** * 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. * @param {string} announceUrl * @param {string|Array.} infoHash * @param {function} cb */ Client.scrape = function (announceUrl, infoHash, cb) { cb = once(cb) var peerId = new Buffer('01234567890123456789') // dummy value var port = 6881 // dummy value var torrent = { infoHash: Array.isArray(infoHash) ? infoHash[0] : infoHash, announce: [ announceUrl ] } var client = new Client(peerId, port, torrent) client.once('error', cb) var len = Array.isArray(infoHash) ? 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) } } }) infoHash = Array.isArray(infoHash) ? infoHash.map(function (infoHash) { return new Buffer(infoHash, 'hex') }) : new Buffer(infoHash, 'hex') client.scrape({ infoHash: infoHash }) } /** * 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 = {} if (opts.downloaded == null && self.torrentLength != null) { opts.downloaded = self.torrentLength } 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 = [] } 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 (opts.left == null && self.torrentLength != null) { opts.left = self.torrentLength - opts.downloaded } if (self._getAnnounceOpts) opts = extend(opts, self._getAnnounceOpts()) return opts }