bittorrent-tracker/client.js
2015-12-02 22:33:15 -08:00

273 lines
7.7 KiB
JavaScript

module.exports = Client
var EventEmitter = require('events').EventEmitter
var debug = require('debug')('bittorrent-tracker')
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
*/
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
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.<string>} 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
}
return opts
}