2015-03-24 08:52:21 +00:00
|
|
|
module.exports = WebSocketTracker
|
|
|
|
|
2015-03-29 07:39:18 +00:00
|
|
|
var debug = require('debug')('bittorrent-tracker:websocket-tracker')
|
2015-03-24 08:52:21 +00:00
|
|
|
var hat = require('hat')
|
|
|
|
var inherits = require('inherits')
|
|
|
|
var Peer = require('simple-peer')
|
|
|
|
var Socket = require('simple-websocket')
|
|
|
|
|
2015-07-29 07:26:44 +00:00
|
|
|
var common = require('../common')
|
2015-07-29 08:47:09 +00:00
|
|
|
var Tracker = require('./tracker')
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2015-05-27 11:09:15 +00:00
|
|
|
// Use a socket pool, so 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.
|
|
|
|
var socketPool = {}
|
|
|
|
|
2015-05-17 07:07:21 +00:00
|
|
|
var RECONNECT_VARIANCE = 30 * 1000
|
|
|
|
var RECONNECT_MINIMUM = 5 * 1000
|
2015-12-05 11:06:46 +00:00
|
|
|
var OFFER_TIMEOUT = 50 * 1000
|
2015-05-17 07:07:21 +00:00
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
inherits(WebSocketTracker, Tracker)
|
2015-03-24 08:52:21 +00:00
|
|
|
|
|
|
|
function WebSocketTracker (client, announceUrl, opts) {
|
|
|
|
var self = this
|
2015-07-29 08:47:09 +00:00
|
|
|
Tracker.call(self, client, announceUrl)
|
2015-03-24 08:52:21 +00:00
|
|
|
debug('new websocket tracker %s', announceUrl)
|
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
self.peers = {} // peers (offer id -> peer)
|
|
|
|
self.socket = null
|
2015-07-29 10:56:22 +00:00
|
|
|
self.reconnecting = false
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2015-05-17 07:07:21 +00:00
|
|
|
self._openSocket()
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
2015-07-29 09:12:14 +00:00
|
|
|
WebSocketTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 1000 // 30 seconds
|
|
|
|
|
2015-03-24 08:52:21 +00:00
|
|
|
WebSocketTracker.prototype.announce = function (opts) {
|
|
|
|
var self = this
|
2015-07-29 10:56:22 +00:00
|
|
|
if (self.destroyed || self.reconnecting) return
|
2015-07-29 08:47:09 +00:00
|
|
|
if (!self.socket.connected) {
|
|
|
|
return self.socket.once('connect', self.announce.bind(self, opts))
|
2015-04-10 23:58:21 +00:00
|
|
|
}
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2015-12-05 07:38:50 +00:00
|
|
|
// Limit the number of offers that are generated, since it can be slow
|
2015-12-05 09:01:23 +00:00
|
|
|
var numwant = Math.min(opts.numwant, 10)
|
2015-07-27 22:19:18 +00:00
|
|
|
|
2015-11-04 17:01:35 +00:00
|
|
|
// Refresh opts if the callback is provided
|
|
|
|
var cbopts
|
|
|
|
if (opts.getAnnounceOpts) {
|
|
|
|
cbopts = opts.getAnnounceOpts()
|
|
|
|
if (cbopts.uploaded) opts.uploaded = cbopts.uploaded
|
|
|
|
if (cbopts.downloaded) opts.downloaded = cbopts.downloaded
|
|
|
|
if (cbopts.left) opts.left = cbopts.left
|
|
|
|
}
|
|
|
|
|
2015-07-27 22:19:18 +00:00
|
|
|
self._generateOffers(numwant, function (offers) {
|
|
|
|
var params = {
|
|
|
|
numwant: numwant,
|
|
|
|
uploaded: opts.uploaded || 0,
|
|
|
|
downloaded: opts.downloaded,
|
2015-11-04 17:01:35 +00:00
|
|
|
left: opts.left,
|
2015-07-27 22:19:18 +00:00
|
|
|
event: opts.event,
|
|
|
|
info_hash: self.client._infoHashBinary,
|
|
|
|
peer_id: self.client._peerIdBinary,
|
2015-11-04 17:01:35 +00:00
|
|
|
offers: offers,
|
|
|
|
extras: cbopts && cbopts.extraAnnounceOpts
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
2015-07-27 22:19:18 +00:00
|
|
|
if (self._trackerId) params.trackerid = self._trackerId
|
|
|
|
|
|
|
|
self._send(params)
|
2015-03-24 08:52:21 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
WebSocketTracker.prototype.scrape = function (opts) {
|
|
|
|
var self = this
|
2015-07-29 10:56:22 +00:00
|
|
|
if (self.destroyed || self.reconnecting) return
|
2015-07-29 08:47:09 +00:00
|
|
|
self._onSocketError(new Error('scrape not supported ' + self.announceUrl))
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
2015-07-17 01:33:50 +00:00
|
|
|
WebSocketTracker.prototype.destroy = function (onclose) {
|
2015-05-17 06:25:34 +00:00
|
|
|
var self = this
|
|
|
|
if (self.destroyed) return
|
|
|
|
self.destroyed = true
|
2015-07-29 08:47:09 +00:00
|
|
|
clearInterval(self.interval)
|
2015-05-17 07:07:21 +00:00
|
|
|
|
2015-07-29 10:21:31 +00:00
|
|
|
socketPool[self.announceUrl] = null
|
|
|
|
|
2015-07-29 10:56:22 +00:00
|
|
|
self.socket.removeListener('connect', self._onSocketConnectBound)
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket.removeListener('data', self._onSocketDataBound)
|
|
|
|
self.socket.removeListener('close', self._onSocketCloseBound)
|
|
|
|
self.socket.removeListener('error', self._onSocketErrorBound)
|
2015-07-17 01:33:50 +00:00
|
|
|
|
2015-07-29 10:56:22 +00:00
|
|
|
self._onSocketConnectBound = null
|
2015-05-17 07:07:21 +00:00
|
|
|
self._onSocketErrorBound = null
|
|
|
|
self._onSocketDataBound = null
|
|
|
|
self._onSocketCloseBound = null
|
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket.on('error', noop) // ignore all future errors
|
2015-07-17 01:33:50 +00:00
|
|
|
try {
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket.destroy(onclose)
|
2015-07-17 01:33:50 +00:00
|
|
|
} catch (err) {
|
|
|
|
if (onclose) onclose()
|
|
|
|
}
|
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket = null
|
2015-05-17 06:25:34 +00:00
|
|
|
}
|
|
|
|
|
2015-05-17 07:07:21 +00:00
|
|
|
WebSocketTracker.prototype._openSocket = function () {
|
2015-04-10 23:58:21 +00:00
|
|
|
var self = this
|
2015-07-29 10:56:22 +00:00
|
|
|
self.destroyed = false
|
|
|
|
|
|
|
|
self._onSocketConnectBound = self._onSocketConnect.bind(self)
|
2015-05-17 07:07:21 +00:00
|
|
|
self._onSocketErrorBound = self._onSocketError.bind(self)
|
|
|
|
self._onSocketDataBound = self._onSocketData.bind(self)
|
|
|
|
self._onSocketCloseBound = self._onSocketClose.bind(self)
|
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket = socketPool[self.announceUrl]
|
|
|
|
if (!self.socket) {
|
|
|
|
self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl)
|
2015-07-29 10:56:22 +00:00
|
|
|
self.socket.on('connect', self._onSocketConnectBound)
|
2015-05-27 11:09:15 +00:00
|
|
|
}
|
|
|
|
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket.on('data', self._onSocketDataBound)
|
|
|
|
self.socket.on('close', self._onSocketCloseBound)
|
|
|
|
self.socket.on('error', self._onSocketErrorBound)
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
2015-07-29 10:56:22 +00:00
|
|
|
WebSocketTracker.prototype._onSocketConnect = function () {
|
|
|
|
var self = this
|
|
|
|
if (self.destroyed) return
|
|
|
|
|
|
|
|
if (self.reconnecting) {
|
|
|
|
self.reconnecting = false
|
|
|
|
self.announce(self.client._defaultAnnounceOpts())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-10 23:58:21 +00:00
|
|
|
WebSocketTracker.prototype._onSocketData = function (data) {
|
2015-03-24 08:52:21 +00:00
|
|
|
var self = this
|
2015-05-17 06:25:34 +00:00
|
|
|
if (self.destroyed) return
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2016-02-16 03:36:47 +00:00
|
|
|
try {
|
|
|
|
data = JSON.parse(data)
|
|
|
|
} catch (err) {
|
|
|
|
self.client.emit('warning', new Error('Invalid tracker response'))
|
|
|
|
return
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
2015-05-20 13:45:59 +00:00
|
|
|
if (data.info_hash !== self.client._infoHashBinary) {
|
2015-05-27 11:09:15 +00:00
|
|
|
debug(
|
|
|
|
'ignoring websocket data from %s for %s (looking for %s: reused socket)',
|
2015-12-02 23:46:41 +00:00
|
|
|
self.announceUrl, common.binaryToHex(data.info_hash), self.client.infoHash
|
2015-05-27 11:09:15 +00:00
|
|
|
)
|
|
|
|
return
|
2015-03-29 07:40:21 +00:00
|
|
|
}
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2015-05-27 11:09:15 +00:00
|
|
|
if (data.peer_id && data.peer_id === self.client._peerIdBinary) {
|
|
|
|
// ignore offers/answers from this client
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
debug(
|
|
|
|
'received %s from %s for %s',
|
2015-12-02 23:46:41 +00:00
|
|
|
JSON.stringify(data), self.announceUrl, self.client.infoHash
|
2015-05-27 11:09:15 +00:00
|
|
|
)
|
2015-03-24 08:52:21 +00:00
|
|
|
|
|
|
|
var failure = data['failure reason']
|
|
|
|
if (failure) return self.client.emit('warning', new Error(failure))
|
|
|
|
|
|
|
|
var warning = data['warning message']
|
|
|
|
if (warning) self.client.emit('warning', new Error(warning))
|
|
|
|
|
|
|
|
var interval = data.interval || data['min interval']
|
2015-07-29 08:47:09 +00:00
|
|
|
if (interval) self.setInterval(interval * 1000)
|
2015-03-24 08:52:21 +00:00
|
|
|
|
|
|
|
var trackerId = data['tracker id']
|
|
|
|
if (trackerId) {
|
|
|
|
// If absent, do not discard previous trackerId value
|
|
|
|
self._trackerId = trackerId
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data.complete) {
|
|
|
|
self.client.emit('update', {
|
2015-07-29 08:47:09 +00:00
|
|
|
announce: self.announceUrl,
|
2015-03-24 08:52:21 +00:00
|
|
|
complete: data.complete,
|
|
|
|
incomplete: data.incomplete
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
var peer
|
2015-05-27 11:09:15 +00:00
|
|
|
if (data.offer && data.peer_id) {
|
2015-12-05 07:38:50 +00:00
|
|
|
debug('creating peer (from remote offer)')
|
2015-05-04 00:21:08 +00:00
|
|
|
peer = new Peer({
|
|
|
|
trickle: false,
|
|
|
|
config: self.client._rtcConfig,
|
|
|
|
wrtc: self.client._wrtc
|
|
|
|
})
|
2015-03-24 08:52:21 +00:00
|
|
|
peer.id = common.binaryToHex(data.peer_id)
|
|
|
|
peer.once('signal', function (answer) {
|
2015-03-29 07:41:25 +00:00
|
|
|
var params = {
|
2015-05-20 13:45:59 +00:00
|
|
|
info_hash: self.client._infoHashBinary,
|
|
|
|
peer_id: self.client._peerIdBinary,
|
2015-03-24 08:52:21 +00:00
|
|
|
to_peer_id: data.peer_id,
|
|
|
|
answer: answer,
|
|
|
|
offer_id: data.offer_id
|
|
|
|
}
|
2015-03-29 07:41:25 +00:00
|
|
|
if (self._trackerId) params.trackerid = self._trackerId
|
|
|
|
self._send(params)
|
2015-03-24 08:52:21 +00:00
|
|
|
})
|
|
|
|
peer.signal(data.offer)
|
|
|
|
self.client.emit('peer', peer)
|
|
|
|
}
|
|
|
|
|
2015-05-27 11:09:15 +00:00
|
|
|
if (data.answer && data.peer_id) {
|
2015-12-05 09:01:04 +00:00
|
|
|
var offerId = common.binaryToHex(data.offer_id)
|
|
|
|
peer = self.peers[offerId]
|
2015-03-24 08:52:21 +00:00
|
|
|
if (peer) {
|
|
|
|
peer.id = common.binaryToHex(data.peer_id)
|
|
|
|
peer.signal(data.answer)
|
|
|
|
self.client.emit('peer', peer)
|
2015-12-05 09:01:04 +00:00
|
|
|
|
|
|
|
clearTimeout(peer.trackerTimeout)
|
2015-12-05 11:06:54 +00:00
|
|
|
peer.trackerTimeout = null
|
2015-12-05 09:01:04 +00:00
|
|
|
self.peers[offerId] = null
|
2015-03-24 08:52:21 +00:00
|
|
|
} else {
|
|
|
|
debug('got unexpected answer: ' + JSON.stringify(data.answer))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-17 07:07:21 +00:00
|
|
|
WebSocketTracker.prototype._onSocketClose = function () {
|
|
|
|
var self = this
|
2015-05-27 11:09:15 +00:00
|
|
|
if (self.destroyed) return
|
2015-05-17 07:07:21 +00:00
|
|
|
self.destroy()
|
2015-05-27 11:09:15 +00:00
|
|
|
self._startReconnectTimer()
|
2015-05-17 07:07:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
WebSocketTracker.prototype._onSocketError = function (err) {
|
|
|
|
var self = this
|
|
|
|
if (self.destroyed) return
|
2015-05-27 11:09:15 +00:00
|
|
|
self.destroy()
|
|
|
|
// errors will often happen if a tracker is offline, so don't treat it as fatal
|
|
|
|
self.client.emit('warning', err)
|
|
|
|
self._startReconnectTimer()
|
|
|
|
}
|
|
|
|
|
|
|
|
WebSocketTracker.prototype._startReconnectTimer = function () {
|
|
|
|
var self = this
|
|
|
|
var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + RECONNECT_MINIMUM
|
2015-07-17 01:33:54 +00:00
|
|
|
|
2015-07-29 10:56:22 +00:00
|
|
|
self.reconnecting = true
|
2015-07-17 01:33:54 +00:00
|
|
|
var reconnectTimer = setTimeout(function () {
|
2015-05-27 11:09:15 +00:00
|
|
|
self._openSocket()
|
|
|
|
}, ms)
|
2015-07-17 01:33:54 +00:00
|
|
|
if (reconnectTimer.unref) reconnectTimer.unref()
|
|
|
|
|
2015-05-27 11:09:15 +00:00
|
|
|
debug('reconnecting socket in %s ms', ms)
|
2015-05-17 07:07:21 +00:00
|
|
|
}
|
|
|
|
|
2015-03-29 07:41:25 +00:00
|
|
|
WebSocketTracker.prototype._send = function (params) {
|
2015-03-24 08:52:21 +00:00
|
|
|
var self = this
|
2015-05-17 06:25:34 +00:00
|
|
|
if (self.destroyed) return
|
|
|
|
|
2015-05-06 06:05:10 +00:00
|
|
|
var message = JSON.stringify(params)
|
|
|
|
debug('send %s', message)
|
2015-07-29 08:47:09 +00:00
|
|
|
self.socket.send(message)
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
2015-07-27 22:19:18 +00:00
|
|
|
WebSocketTracker.prototype._generateOffers = function (numwant, cb) {
|
2015-03-24 08:52:21 +00:00
|
|
|
var self = this
|
|
|
|
var offers = []
|
2015-07-27 22:19:18 +00:00
|
|
|
debug('generating %s offers', numwant)
|
2015-03-24 08:52:21 +00:00
|
|
|
|
2015-07-27 22:19:18 +00:00
|
|
|
for (var i = 0; i < numwant; ++i) {
|
2015-03-24 08:52:21 +00:00
|
|
|
generateOffer()
|
|
|
|
}
|
|
|
|
|
|
|
|
function generateOffer () {
|
|
|
|
var offerId = hat(160)
|
2015-12-05 07:38:50 +00:00
|
|
|
debug('creating peer (from _generateOffers)')
|
2015-07-29 08:47:09 +00:00
|
|
|
var peer = self.peers[offerId] = new Peer({
|
2015-03-24 08:52:21 +00:00
|
|
|
initiator: true,
|
|
|
|
trickle: false,
|
2015-05-04 00:21:08 +00:00
|
|
|
config: self.client._rtcConfig,
|
|
|
|
wrtc: self.client._wrtc
|
2015-03-24 08:52:21 +00:00
|
|
|
})
|
|
|
|
peer.once('signal', function (offer) {
|
|
|
|
offers.push({
|
|
|
|
offer: offer,
|
2015-03-29 08:08:26 +00:00
|
|
|
offer_id: common.hexToBinary(offerId)
|
2015-03-24 08:52:21 +00:00
|
|
|
})
|
|
|
|
checkDone()
|
|
|
|
})
|
2015-12-05 09:01:04 +00:00
|
|
|
peer.trackerTimeout = setTimeout(function () {
|
2015-12-05 11:06:54 +00:00
|
|
|
debug('tracker timeout: destroying peer')
|
|
|
|
peer.trackerTimeout = null
|
2015-12-05 09:01:04 +00:00
|
|
|
self.peers[offerId] = null
|
2015-12-05 11:06:54 +00:00
|
|
|
peer.destroy()
|
2015-12-05 09:01:04 +00:00
|
|
|
}, OFFER_TIMEOUT)
|
2015-03-24 08:52:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function checkDone () {
|
2015-07-27 22:19:18 +00:00
|
|
|
if (offers.length === numwant) {
|
|
|
|
debug('generated %s offers', numwant)
|
2015-03-24 08:52:21 +00:00
|
|
|
cb(offers)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-05-17 20:53:36 +00:00
|
|
|
|
|
|
|
function noop () {}
|