mirror of
https://github.com/logos-storage/bittorrent-tracker.git
synced 2026-01-04 05:53:06 +00:00
add support for webtorrent (websocket) trackers
This commit is contained in:
parent
d759456d7a
commit
1ff5769054
18
client.js
18
client.js
@ -7,8 +7,8 @@ var once = require('once')
|
|||||||
var url = require('url')
|
var url = require('url')
|
||||||
|
|
||||||
var common = require('./lib/common')
|
var common = require('./lib/common')
|
||||||
var HTTPTracker = require('./lib/http-tracker')
|
var HTTPTracker = require('./lib/http-tracker') // empty object in browser
|
||||||
var UDPTracker = require('./lib/udp-tracker')
|
var UDPTracker = require('./lib/udp-tracker') // empty object in browser
|
||||||
var WebSocketTracker = require('./lib/websocket-tracker')
|
var WebSocketTracker = require('./lib/websocket-tracker')
|
||||||
|
|
||||||
inherits(Client, EventEmitter)
|
inherits(Client, EventEmitter)
|
||||||
@ -47,24 +47,24 @@ function Client (peerId, port, torrent, opts) {
|
|||||||
debug('new client %s', self._infoHash.toString('hex'))
|
debug('new client %s', self._infoHash.toString('hex'))
|
||||||
|
|
||||||
if (typeof torrent.announce === 'string') torrent.announce = [ torrent.announce ]
|
if (typeof torrent.announce === 'string') torrent.announce = [ torrent.announce ]
|
||||||
|
if (torrent.announce == null) torrent.announce = []
|
||||||
|
|
||||||
self._trackers = (torrent.announce || [])
|
self._trackers = torrent.announce
|
||||||
.filter(function (announceUrl) {
|
|
||||||
var protocol = url.parse(announceUrl).protocol
|
|
||||||
return [ 'udp:', 'http:', 'https:', 'ws:', 'wss:' ].indexOf(protocol) !== -1
|
|
||||||
})
|
|
||||||
.map(function (announceUrl) {
|
.map(function (announceUrl) {
|
||||||
var trackerOpts = { interval: self._intervalMs }
|
var trackerOpts = { interval: self._intervalMs }
|
||||||
var protocol = url.parse(announceUrl).protocol
|
var protocol = url.parse(announceUrl).protocol
|
||||||
|
|
||||||
if (protocol === 'http:' || protocol === 'https:') {
|
if ((protocol === 'http:' || protocol === 'https:') &&
|
||||||
|
typeof HTTPTracker === 'function') {
|
||||||
return new HTTPTracker(self, announceUrl, trackerOpts)
|
return new HTTPTracker(self, announceUrl, trackerOpts)
|
||||||
} else if (protocol === 'udp:') {
|
} else if (protocol === 'udp:' && typeof UDPTracker === 'function') {
|
||||||
return new UDPTracker(self, announceUrl, trackerOpts)
|
return new UDPTracker(self, announceUrl, trackerOpts)
|
||||||
} else if (protocol === 'ws:' || protocol === 'wss:') {
|
} else if (protocol === 'ws:' || protocol === 'wss:') {
|
||||||
return new WebSocketTracker(self, announceUrl, trackerOpts)
|
return new WebSocketTracker(self, announceUrl, trackerOpts)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
64
lib/common-node.js
Normal file
64
lib/common-node.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Functions/constants needed by both the client and server (but only in node).
|
||||||
|
* These are separate from common.js so they can be skipped when bundling for the browser.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var querystring = require('querystring')
|
||||||
|
|
||||||
|
exports.IPV4_RE = /^[\d\.]+$/
|
||||||
|
exports.IPV6_RE = /^[\da-fA-F:]+$/
|
||||||
|
|
||||||
|
exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ])
|
||||||
|
exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 }
|
||||||
|
exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 }
|
||||||
|
exports.EVENT_IDS = {
|
||||||
|
0: 'update',
|
||||||
|
1: 'completed',
|
||||||
|
2: 'started',
|
||||||
|
3: 'stopped'
|
||||||
|
}
|
||||||
|
exports.EVENT_NAMES = {
|
||||||
|
update: 'update',
|
||||||
|
completed: 'complete',
|
||||||
|
started: 'start',
|
||||||
|
stopped: 'stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUInt32 (n) {
|
||||||
|
var buf = new Buffer(4)
|
||||||
|
buf.writeUInt32BE(n, 0)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
exports.toUInt32 = toUInt32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `querystring.parse` using `unescape` instead of decodeURIComponent, since bittorrent
|
||||||
|
* clients send non-UTF8 querystrings
|
||||||
|
* @param {string} q
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
exports.querystringParse = function (q) {
|
||||||
|
var saved = querystring.unescape
|
||||||
|
querystring.unescape = unescape // global
|
||||||
|
var ret = querystring.parse(q)
|
||||||
|
querystring.unescape = saved
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `querystring.stringify` using `escape` instead of encodeURIComponent, since bittorrent
|
||||||
|
* clients send non-UTF8 querystrings
|
||||||
|
* @param {Object} obj
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
exports.querystringStringify = function (obj) {
|
||||||
|
var saved = querystring.escape
|
||||||
|
querystring.escape = escape // global
|
||||||
|
var ret = querystring.stringify(obj)
|
||||||
|
ret = ret.replace(/[\@\*\/\+]/g, function (char) {
|
||||||
|
// `escape` doesn't encode the characters @*/+ so we do it manually
|
||||||
|
return '%' + char.charCodeAt(0).toString(16).toUpperCase()
|
||||||
|
})
|
||||||
|
querystring.escape = saved
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@ -2,39 +2,13 @@
|
|||||||
* Functions/constants needed by both the client and server.
|
* Functions/constants needed by both the client and server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var querystring = require('querystring')
|
var extend = require('xtend/mutable')
|
||||||
|
|
||||||
exports.IPV4_RE = /^[\d\.]+$/
|
|
||||||
exports.IPV6_RE = /^[\da-fA-F:]+$/
|
|
||||||
|
|
||||||
exports.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes
|
exports.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes
|
||||||
|
|
||||||
exports.DEFAULT_ANNOUNCE_PEERS = 50
|
exports.DEFAULT_ANNOUNCE_PEERS = 50
|
||||||
exports.MAX_ANNOUNCE_PEERS = 82
|
exports.MAX_ANNOUNCE_PEERS = 82
|
||||||
|
|
||||||
exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ])
|
|
||||||
exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 }
|
|
||||||
exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 }
|
|
||||||
exports.EVENT_IDS = {
|
|
||||||
0: 'update',
|
|
||||||
1: 'completed',
|
|
||||||
2: 'started',
|
|
||||||
3: 'stopped'
|
|
||||||
}
|
|
||||||
exports.EVENT_NAMES = {
|
|
||||||
update: 'update',
|
|
||||||
completed: 'complete',
|
|
||||||
started: 'start',
|
|
||||||
stopped: 'stop'
|
|
||||||
}
|
|
||||||
|
|
||||||
function toUInt32 (n) {
|
|
||||||
var buf = new Buffer(4)
|
|
||||||
buf.writeUInt32BE(n, 0)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
exports.toUInt32 = toUInt32
|
|
||||||
|
|
||||||
exports.binaryToHex = function (str) {
|
exports.binaryToHex = function (str) {
|
||||||
return new Buffer(str, 'binary').toString('hex')
|
return new Buffer(str, 'binary').toString('hex')
|
||||||
}
|
}
|
||||||
@ -43,34 +17,5 @@ exports.hexToBinary = function (str) {
|
|||||||
return new Buffer(str, 'hex').toString('binary')
|
return new Buffer(str, 'hex').toString('binary')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
var config = require('./common-node')
|
||||||
* `querystring.parse` using `unescape` instead of decodeURIComponent, since bittorrent
|
extend(exports, config)
|
||||||
* clients send non-UTF8 querystrings
|
|
||||||
* @param {string} q
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
exports.querystringParse = function (q) {
|
|
||||||
var saved = querystring.unescape
|
|
||||||
querystring.unescape = unescape // global
|
|
||||||
var ret = querystring.parse(q)
|
|
||||||
querystring.unescape = saved
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `querystring.stringify` using `escape` instead of encodeURIComponent, since bittorrent
|
|
||||||
* clients send non-UTF8 querystrings
|
|
||||||
* @param {Object} obj
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
exports.querystringStringify = function (obj) {
|
|
||||||
var saved = querystring.escape
|
|
||||||
querystring.escape = escape // global
|
|
||||||
var ret = querystring.stringify(obj)
|
|
||||||
ret = ret.replace(/[\@\*\/\+]/g, function (char) {
|
|
||||||
// `escape` doesn't encode the characters @*/+ so we do it manually
|
|
||||||
return '%' + char.charCodeAt(0).toString(16).toUpperCase()
|
|
||||||
})
|
|
||||||
querystring.escape = saved
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|||||||
@ -88,8 +88,14 @@ HTTPTracker.prototype._request = function (requestUrl, opts, cb) {
|
|||||||
|
|
||||||
get.concat(u, function (err, data, res) {
|
get.concat(u, function (err, data, res) {
|
||||||
if (err) return self.client.emit('warning', err)
|
if (err) return self.client.emit('warning', err)
|
||||||
if (res.statusCode !== 200) return self.client.emit('warning', new Error('Non-200 response code ' + res.statusCode + ' from ' + self.a))
|
if (res.statusCode !== 200) {
|
||||||
if (!data || data.length === 0) return
|
return self.client.emit('warning', new Error('Non-200 response code ' +
|
||||||
|
res.statusCode + ' from ' + self._announceUrl))
|
||||||
|
}
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return self.client.emit('warning', new Error('Invalid tracker response from' +
|
||||||
|
self._announceUrl))
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = bencode.decode(data)
|
data = bencode.decode(data)
|
||||||
|
|||||||
@ -0,0 +1,189 @@
|
|||||||
|
// TODO: destroy the websocket
|
||||||
|
|
||||||
|
module.exports = WebSocketTracker
|
||||||
|
|
||||||
|
var debug = require('debug')('bittorrent-tracker:http-tracker')
|
||||||
|
var EventEmitter = require('events').EventEmitter
|
||||||
|
var hat = require('hat')
|
||||||
|
var inherits = require('inherits')
|
||||||
|
var Peer = require('simple-peer')
|
||||||
|
var Socket = require('simple-websocket')
|
||||||
|
|
||||||
|
var common = require('./common')
|
||||||
|
|
||||||
|
// It turns out that you can't open multiple websockets to the same server within one
|
||||||
|
// browser tab, so let's reuse them.
|
||||||
|
var socketPool = {}
|
||||||
|
|
||||||
|
inherits(WebSocketTracker, EventEmitter)
|
||||||
|
|
||||||
|
function WebSocketTracker (client, announceUrl, opts) {
|
||||||
|
var self = this
|
||||||
|
EventEmitter.call(self)
|
||||||
|
debug('new websocket tracker %s', announceUrl)
|
||||||
|
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
self._announceUrl = announceUrl
|
||||||
|
self._peers = {} // peers (offer id -> peer)
|
||||||
|
self._ready = false
|
||||||
|
self._socket = null
|
||||||
|
self._intervalMs = self.client._intervalMs // use client interval initially
|
||||||
|
self._interval = null
|
||||||
|
|
||||||
|
if (socketPool[announceUrl]) self._socket = socketPool[announceUrl]
|
||||||
|
else self._socket = socketPool[announceUrl] = new Socket(announceUrl)
|
||||||
|
|
||||||
|
self._socket.on('warning', self._onSocketWarning.bind(self))
|
||||||
|
self._socket.on('error', self._onSocketWarning.bind(self)) // TODO: handle error
|
||||||
|
self._socket.on('message', self._onSocketMessage.bind(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype.announce = function (opts) {
|
||||||
|
var self = this
|
||||||
|
if (!self._socket.ready) return self._socket.on('ready', self.announce.bind(self, opts))
|
||||||
|
|
||||||
|
opts.info_hash = self.client._infoHash.toString('binary')
|
||||||
|
opts.peer_id = self.client._peerId.toString('binary')
|
||||||
|
|
||||||
|
self._generateOffers(opts.numWant, function (offers) {
|
||||||
|
opts.offers = offers
|
||||||
|
|
||||||
|
if (self._trackerId) {
|
||||||
|
opts.trackerid = self._trackerId
|
||||||
|
}
|
||||||
|
self._send(opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype.scrape = function (opts) {
|
||||||
|
var self = this
|
||||||
|
self.client.emit('error', new Error('scrape not supported ' + self._announceUrl))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Improve this interface
|
||||||
|
WebSocketTracker.prototype.setInterval = function (intervalMs) {
|
||||||
|
var self = this
|
||||||
|
clearInterval(self._interval)
|
||||||
|
|
||||||
|
self._intervalMs = intervalMs
|
||||||
|
if (intervalMs) {
|
||||||
|
// HACK
|
||||||
|
var update = self.announce.bind(self, self.client._defaultAnnounceOpts())
|
||||||
|
self._interval = setInterval(update, self._intervalMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype._onSocketWarning = function (err) {
|
||||||
|
debug('tracker warning %s', err.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype._onSocketMessage = function (data) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
if (!(typeof data === 'object' && data !== null)) {
|
||||||
|
return self.client.emit('warning', new Error('Invalid tracker response'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.info_hash !== self.client._infoHash.toString('binary')) return
|
||||||
|
|
||||||
|
debug('received %s from %s', JSON.stringify(data), self._announceUrl)
|
||||||
|
|
||||||
|
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']
|
||||||
|
if (interval && !self._opts.interval && self._intervalMs !== 0) {
|
||||||
|
// use the interval the tracker recommends, UNLESS the user manually specifies an
|
||||||
|
// interval they want to use
|
||||||
|
self.setInterval(interval * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
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', {
|
||||||
|
announce: self._announceUrl,
|
||||||
|
complete: data.complete,
|
||||||
|
incomplete: data.incomplete
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer
|
||||||
|
if (data.offer) {
|
||||||
|
peer = new Peer({ trickle: false, config: self._opts.rtcConfig })
|
||||||
|
peer.id = common.binaryToHex(data.peer_id)
|
||||||
|
peer.once('signal', function (answer) {
|
||||||
|
var opts = {
|
||||||
|
info_hash: self.client._infoHash.toString('binary'),
|
||||||
|
peer_id: self.client._peerId.toString('binary'),
|
||||||
|
to_peer_id: data.peer_id,
|
||||||
|
answer: answer,
|
||||||
|
offer_id: data.offer_id
|
||||||
|
}
|
||||||
|
if (self._trackerId) opts.trackerid = self._trackerId
|
||||||
|
self._send(opts)
|
||||||
|
})
|
||||||
|
peer.signal(data.offer)
|
||||||
|
self.client.emit('peer', peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.answer) {
|
||||||
|
peer = self._peers[data.offer_id]
|
||||||
|
if (peer) {
|
||||||
|
peer.id = common.binaryToHex(data.peer_id)
|
||||||
|
peer.signal(data.answer)
|
||||||
|
self.client.emit('peer', peer)
|
||||||
|
} else {
|
||||||
|
debug('got unexpected answer: ' + JSON.stringify(data.answer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype._send = function (opts) {
|
||||||
|
var self = this
|
||||||
|
debug('send %s', JSON.stringify(opts))
|
||||||
|
self._socket.send(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketTracker.prototype._generateOffers = function (numWant, cb) {
|
||||||
|
var self = this
|
||||||
|
var offers = []
|
||||||
|
debug('generating %s offers', numWant)
|
||||||
|
|
||||||
|
// TODO: cleanup dead peers and peers that never get a return offer, from self._peers
|
||||||
|
for (var i = 0; i < numWant; ++i) {
|
||||||
|
generateOffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateOffer () {
|
||||||
|
var offerId = hat(160)
|
||||||
|
var peer = self._peers[offerId] = new Peer({
|
||||||
|
initiator: true,
|
||||||
|
trickle: false,
|
||||||
|
config: self._opts.rtcConfig
|
||||||
|
})
|
||||||
|
peer.once('signal', function (offer) {
|
||||||
|
offers.push({
|
||||||
|
offer: offer,
|
||||||
|
offer_id: offerId
|
||||||
|
})
|
||||||
|
checkDone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDone () {
|
||||||
|
if (offers.length === numWant) {
|
||||||
|
debug('generated %s offers', numWant)
|
||||||
|
cb(offers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
package.json
14
package.json
@ -7,12 +7,15 @@
|
|||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
"url": "http://feross.org/"
|
"url": "http://feross.org/"
|
||||||
},
|
},
|
||||||
"browser": {
|
|
||||||
"./server.js": false
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"bittorrent-tracker": "./bin/cmd.js"
|
"bittorrent-tracker": "./bin/cmd.js"
|
||||||
},
|
},
|
||||||
|
"browser": {
|
||||||
|
"./lib/common-node": false,
|
||||||
|
"./lib/http-tracker": false,
|
||||||
|
"./lib/udp-tracker": false,
|
||||||
|
"./server": false
|
||||||
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/feross/bittorrent-tracker/issues"
|
"url": "https://github.com/feross/bittorrent-tracker/issues"
|
||||||
},
|
},
|
||||||
@ -29,7 +32,10 @@
|
|||||||
"once": "^1.3.0",
|
"once": "^1.3.0",
|
||||||
"run-series": "^1.0.2",
|
"run-series": "^1.0.2",
|
||||||
"simple-get": "^1.3.0",
|
"simple-get": "^1.3.0",
|
||||||
"string2compact": "^1.1.1"
|
"simple-peer": "4.0.4",
|
||||||
|
"simple-websocket": "1.0.4",
|
||||||
|
"string2compact": "^1.1.1",
|
||||||
|
"xtend": "4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"magnet-uri": "^4.0.0",
|
"magnet-uri": "^4.0.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user