Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8e2dbdc
Reconnect timeout increase lineally
DiegoRBaquero Mar 11, 2016
529203e
Max reconnect time
DiegoRBaquero Mar 11, 2016
39507bf
Scrape implementation for websocket. Issue #116
yciabaud Mar 13, 2016
5694f39
Adds unit testing for websocket server
yciabaud Mar 13, 2016
4fae4fa
Close the websocket only when no one is using it
yciabaud Mar 14, 2016
1b605a3
Clear reconnect timeout on destroy
yciabaud Mar 14, 2016
88ac7fb
Destroy peers and tracker timeouts on destroy
yciabaud Mar 14, 2016
09e2b55
invoke callbacks of destroyed trackers
autarc Mar 14, 2016
2fee125
Merge pull request #126 from Autarc/fix-callback-on-destroy
feross Mar 15, 2016
6df64ff
Fixes for PR #126
feross Mar 15, 2016
7daf7ec
7.4.1
feross Mar 15, 2016
3d84d44
Don't add insecure trackers on a https site
DiegoRBaquero Mar 15, 2016
0133c96
Remove unneeded parenthesis
DiegoRBaquero Mar 15, 2016
3d753ab
Merge pull request #125 from yciabaud/ws-scrape
feross Mar 16, 2016
ba4a52d
Fixes for PR #125
feross Mar 16, 2016
7a7ed57
check for params.offers to determine if offers were received
feross Mar 16, 2016
d3389f6
7.5.0
feross Mar 16, 2016
8c01cb7
do not throw and crash client
feross Mar 16, 2016
4b62a41
7.5.1
feross Mar 16, 2016
b2ff218
Merge pull request #128 from DiegoRBaquero/patch-2
feross Mar 16, 2016
050d95a
Variance and higher max
DiegoRBaquero Mar 16, 2016
5d4cf75
Merge pull request #123 from DiegoRBaquero/patch-1
feross Mar 16, 2016
ede2119
Fixes for PR #128
feross Mar 16, 2016
1f4a68a
7.5.2
feross Mar 16, 2016
ae52ed5
Switch to electron-webrtc
yciabaud Mar 16, 2016
306c86f
Adds unit testing for websocket server
yciabaud Mar 13, 2016
482b68f
Close the websocket only when no one is using it
yciabaud Mar 14, 2016
788de2d
Clear reconnect timeout on destroy
yciabaud Mar 14, 2016
8178d8b
Destroy peers and tracker timeouts on destroy
yciabaud Mar 14, 2016
f847af8
Switch to electron-webrtc
yciabaud Mar 16, 2016
5828c6f
Merge branch 'ws-test' of https://github.com/yciabaud/bittorrent-trac…
yciabaud Mar 16, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,25 @@ function Client (peerId, port, torrent, opts) {
} 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 {
process.nextTick(function () {
var err = new Error('unsupported tracker protocol for ' + announceUrl)
self.emit('warning', err)
})
nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl))
return null
}
return null
})
.filter(Boolean)

function nextTickWarn (err) {
process.nextTick(function () {
self.emit('warning', err)
})
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/client/http-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ HTTPTracker.prototype.scrape = function (opts) {

HTTPTracker.prototype.destroy = function (cb) {
var self = this
if (self.destroyed) return
if (self.destroyed) return cb(null)
self.destroyed = true
clearInterval(self.interval)

Expand Down
2 changes: 1 addition & 1 deletion lib/client/udp-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ UDPTracker.prototype.scrape = function (opts) {

UDPTracker.prototype.destroy = function (cb) {
var self = this
if (self.destroyed) return
if (self.destroyed) return cb(null)
self.destroyed = true
clearInterval(self.interval)

Expand Down
98 changes: 86 additions & 12 deletions lib/client/websocket-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ var Tracker = require('./tracker')
// boost, and saves browser resources.
var socketPool = {}

var RECONNECT_MINIMUM = 15 * 1000
var RECONNECT_MAXIMUM = 30 * 60 * 1000
var RECONNECT_VARIANCE = 30 * 1000
var RECONNECT_MINIMUM = 5 * 1000
var OFFER_TIMEOUT = 50 * 1000

inherits(WebSocketTracker, Tracker)
Expand All @@ -29,6 +30,7 @@ function WebSocketTracker (client, announceUrl, opts) {
self.peers = {} // peers (offer id -> peer)
self.socket = null
self.reconnecting = false
self.retries = 0

self._openSocket()
}
Expand All @@ -47,6 +49,7 @@ WebSocketTracker.prototype.announce = function (opts) {

self._generateOffers(numwant, function (offers) {
var params = extend(opts, {
action: 'announce',
numwant: numwant,
info_hash: self.client._infoHashBinary,
peer_id: self.client._peerIdBinary,
Expand All @@ -61,16 +64,41 @@ WebSocketTracker.prototype.announce = function (opts) {
WebSocketTracker.prototype.scrape = function (opts) {
var self = this
if (self.destroyed || self.reconnecting) return
self._onSocketError(new Error('scrape not supported ' + self.announceUrl))
if (!self.socket.connected) {
return self.socket.once('connect', self.scrape.bind(self, opts))
}

var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? opts.infoHash.map(function (infoHash) {
return infoHash.toString('binary')
})
: (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary
var params = {
action: 'scrape',
info_hash: infoHashes
}

self._send(params)
}

WebSocketTracker.prototype.destroy = function (onclose) {
WebSocketTracker.prototype.destroy = function (cb) {
var self = this
if (self.destroyed) return
if (!cb) cb = noop
if (self.destroyed) return cb(null)
self.destroyed = true
clearInterval(self.interval)
clearTimeout(self.reconnectTimer)

delete socketPool[self.announceUrl]
// Destroy peers
for (var peerId in self.peers) {
var peer = self.peers[peerId]
clearTimeout(peer.trackerTimeout)
peer.destroy()
}
delete self.peers

// Close socked
if (socketPool[self.announceUrl]) socketPool[self.announceUrl].consumers--

self.socket.removeListener('connect', self._onSocketConnectBound)
self.socket.removeListener('data', self._onSocketDataBound)
Expand All @@ -82,11 +110,16 @@ WebSocketTracker.prototype.destroy = function (onclose) {
self._onSocketDataBound = null
self._onSocketCloseBound = null

self.socket.on('error', noop) // ignore all future errors
try {
self.socket.destroy(onclose)
} catch (err) {
if (onclose) onclose()
if (socketPool[self.announceUrl].consumers === 0) {
delete socketPool[self.announceUrl]

self.socket.on('error', noop) // ignore all future errors

try {
self.socket.destroy(cb)
} catch (err) {
if (cb) cb()
}
}

self.socket = null
Expand All @@ -104,7 +137,10 @@ WebSocketTracker.prototype._openSocket = function () {
self.socket = socketPool[self.announceUrl]
if (!self.socket) {
self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl)
self.socket.consumers = 1
self.socket.on('connect', self._onSocketConnectBound)
} else {
socketPool[self.announceUrl].consumers++
}

self.socket.on('data', self._onSocketDataBound)
Expand All @@ -118,6 +154,7 @@ WebSocketTracker.prototype._onSocketConnect = function () {

if (self.reconnecting) {
self.reconnecting = false
self.retries = 0
self.announce(self.client._defaultAnnounceOpts())
}
}
Expand All @@ -133,6 +170,18 @@ WebSocketTracker.prototype._onSocketData = function (data) {
return
}

if (data.action === 'announce' || data.offer || data.answer) {
self._onAnnounceResponse(data)
} else if (data.action === 'scrape') {
self._onScrapeResponse(data)
} else {
self._onSocketError(new Error('invalid action in WS response: ' + data.action))
}
}

WebSocketTracker.prototype._onAnnounceResponse = function (data) {
var self = this

if (data.info_hash !== self.client._infoHashBinary) {
debug(
'ignoring websocket data from %s for %s (looking for %s: reused socket)',
Expand Down Expand Up @@ -215,6 +264,30 @@ WebSocketTracker.prototype._onSocketData = function (data) {
}
}

WebSocketTracker.prototype._onScrapeResponse = function (data) {
var self = this
data = data.files || {}

var keys = Object.keys(data)
if (keys.length === 0) {
self.client.emit('warning', new Error('invalid scrape response'))
return
}

keys.forEach(function (infoHash) {
var response = data[infoHash]
// TODO: optionally handle data.flags.min_request_interval
// (separate from announce interval)
self.client.emit('scrape', {
announce: self.announceUrl,
infoHash: common.binaryToHex(infoHash),
complete: response.complete,
incomplete: response.incomplete,
downloaded: response.downloaded
})
})
}

WebSocketTracker.prototype._onSocketClose = function () {
var self = this
if (self.destroyed) return
Expand All @@ -233,13 +306,14 @@ WebSocketTracker.prototype._onSocketError = function (err) {

WebSocketTracker.prototype._startReconnectTimer = function () {
var self = this
var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + RECONNECT_MINIMUM
var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, self.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM)

self.reconnecting = true
var reconnectTimer = setTimeout(function () {
self.retries++
self._openSocket()
}, ms)
if (reconnectTimer.unref) reconnectTimer.unref()
if (self.reconnectTimer.unref) self.reconnectTimer.unref()

debug('reconnecting socket in %s ms', ms)
}
Expand Down
56 changes: 36 additions & 20 deletions lib/server/parse-websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,49 @@ function parseWebSocketRequest (socket, opts, params) {
if (!opts) opts = {}
params = JSON.parse(params) // may throw

params.action = common.ACTIONS.ANNOUNCE
params.type = 'ws'
params.socket = socket
if (params.action === 'announce' || params.offers || params.answer) {
params.action = common.ACTIONS.ANNOUNCE

if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) {
throw new Error('invalid info_hash')
}
params.info_hash = common.binaryToHex(params.info_hash)
if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) {
throw new Error('invalid info_hash')
}
params.info_hash = common.binaryToHex(params.info_hash)

if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) {
throw new Error('invalid peer_id')
}
params.peer_id = common.binaryToHex(params.peer_id)
if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) {
throw new Error('invalid peer_id')
}
params.peer_id = common.binaryToHex(params.peer_id)

if (params.answer) {
if (typeof params.to_peer_id !== 'string' || params.to_peer_id.length !== 20) {
throw new Error('invalid `to_peer_id` (required with `answer`)')
if (params.answer) {
if (typeof params.to_peer_id !== 'string' || params.to_peer_id.length !== 20) {
throw new Error('invalid `to_peer_id` (required with `answer`)')
}
params.to_peer_id = common.binaryToHex(params.to_peer_id)
}
params.to_peer_id = common.binaryToHex(params.to_peer_id)
}

params.left = Number(params.left) || Infinity
params.numwant = Math.min(
Number(params.offers && params.offers.length) || 0, // no default - explicit only
common.MAX_ANNOUNCE_PEERS
)
params.compact = -1 // return full peer objects (used for websocket responses)
params.left = Number(params.left) || Infinity
params.numwant = Math.min(
Number(params.offers && params.offers.length) || 0, // no default - explicit only
common.MAX_ANNOUNCE_PEERS
)
params.compact = -1 // return full peer objects (used for websocket responses)
} else if (params.action === 'scrape') {
params.action = common.ACTIONS.SCRAPE

if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ]
if (Array.isArray(params.info_hash)) {
params.info_hash = params.info_hash.map(function (binaryInfoHash) {
if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) {
throw new Error('invalid info_hash')
}
return common.binaryToHex(binaryInfoHash)
})
}
} else {
throw new Error('invalid action in WS request: ' + params.action)
}

params.ip = opts.trustProxy
? socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bittorrent-tracker",
"description": "Simple, robust, BitTorrent tracker (client & server) implementation",
"version": "7.4.0",
"version": "7.5.2",
"author": {
"name": "Feross Aboukhadijeh",
"email": "[email protected]",
Expand Down Expand Up @@ -42,6 +42,7 @@
"xtend": "^4.0.0"
},
"devDependencies": {
"electron-webrtc": "^0.1.0",
"magnet-uri": "^5.0.0",
"parse-torrent": "^5.0.0",
"standard": "^6.0.4",
Expand Down
23 changes: 13 additions & 10 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ Server.prototype._onWebSocketRequest = function (socket, opts, params) {
self._onRequest(params, function (err, response) {
if (err) {
socket.send(JSON.stringify({
action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape',
'failure reason': err.message,
info_hash: common.hexToBinary(params.info_hash)
}), socket.onSend)
Expand All @@ -336,23 +337,24 @@ Server.prototype._onWebSocketRequest = function (socket, opts, params) {
}
if (self.destroyed) return

if (socket.infoHashes.indexOf(params.info_hash) === -1) {
socket.infoHashes.push(params.info_hash)
}
response.action = params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape'

var peers = response.peers
delete response.peers
var peers
if (response.action === 'announce') {
peers = response.peers
delete response.peers

// WebSocket tracker should have a shorter interval – default: 2 minutes
response.interval = Math.ceil(self.intervalMs / 1000 / 5)
response.info_hash = common.hexToBinary(params.info_hash)

response.info_hash = common.hexToBinary(params.info_hash)
// WebSocket tracker should have a shorter interval – default: 2 minutes
response.interval = Math.ceil(self.intervalMs / 1000 / 5)
}

socket.send(JSON.stringify(response), socket.onSend)
debug('sent response %s to %s', JSON.stringify(response), params.peer_id)

if (params.numwant) {
debug('got offers %s from %s', JSON.stringify(params.offers), params.peer_id)
if (params.offers) {
debug('got offers %o from %s', params.offers, params.peer_id)
debug('got %s peers from swarm %s', peers.length, params.info_hash)
peers.forEach(function (peer, i) {
peer.socket.send(JSON.stringify({
Expand Down Expand Up @@ -597,6 +599,7 @@ Server.prototype._onWebSocketClose = function (socket) {
var swarm = self.torrents[infoHash]
if (swarm) {
swarm.announce({
type: 'ws',
event: 'stopped',
numwant: 0,
peer_id: socket.peerId
Expand Down
Loading