Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,44 @@ var optionalOpts = {
left: 0,
customParam: 'blah' // custom parameters supported
}
},

proxyOpts: {
// Socks proxy options (used to proxy requests in node)
socksProxy: {
// Configuration from socks module (https://github.com/JoshGlazebrook/socks)
proxy: {
// IP Address of Proxy (Required)
ipaddress: "1.2.3.4",
// TCP Port of Proxy (Required)
port: 1080,
// Proxy Type [4, 5] (Required)
// Note: 4 works for both 4 and 4a.
// Type 4 does not support UDP association relay
type: 5,

// SOCKS 4 Specific:

// UserId used when making a SOCKS 4/4a request. (Optional)
userid: "someuserid",

// SOCKS 5 Specific:

// Authentication used for SOCKS 5 (when it's required) (Optional)
authentication: {
username: "Josh",
password: "somepassword"
}
},

// Amount of time to wait for a connection to be established. (Optional)
// - defaults to 10000ms (10 seconds)
timeout: 10000
},
// NodeJS HTTP agents (used to proxy HTTP and Websocket requests in node)
// Populated with Socks.Agent if socksProxy is provided
httpAgent: {},
httpsAgent: {}
}
}

Expand Down
2 changes: 2 additions & 0 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ inherits(Client, EventEmitter)
* @param {function} opts.getAnnounceOpts callback to provide data to tracker
* @param {number} opts.rtcConfig RTCPeerConnection configuration object
* @param {number} opts.wrtc custom webrtc impl (useful in node.js)
* @param {object} opts.proxyOpts proxy options (useful in node.js)
*/
function Client (opts) {
var self = this
Expand Down Expand Up @@ -63,6 +64,7 @@ function Client (opts) {
self._rtcConfig = opts.rtcConfig
self._wrtc = opts.wrtc
self._getAnnounceOpts = opts.getAnnounceOpts
self._proxyOpts = opts.proxyOpts

debug('new client %s', self.infoHash)

Expand Down
18 changes: 16 additions & 2 deletions lib/client/http-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ module.exports = HTTPTracker

var bencode = require('bencode')
var compact2string = require('compact2string')
var clone = require('clone')
var debug = require('debug')('bittorrent-tracker:http-tracker')
var extend = require('xtend')
var get = require('simple-get')
var inherits = require('inherits')
var Socks = require('socks')
var url = require('url')

var common = require('../common')
var Tracker = require('./tracker')
Expand Down Expand Up @@ -87,8 +90,19 @@ HTTPTracker.prototype.destroy = function (cb) {

HTTPTracker.prototype._request = function (requestUrl, params, cb) {
var self = this
var u = requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') +
common.querystringStringify(params)
var parsedUrl = url.parse(requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + common.querystringStringify(params))
var agent
if (self.client._proxyOpts) {
agent = parsedUrl.protocol === 'https:'
? self.client._proxyOpts.httpsAgent : self.client._proxyOpts.httpAgent
if (!agent && self.client._proxyOpts.socksProxy) {
agent = new Socks.Agent(clone(self.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'https:'))
}
}
var u = {
url: parsedUrl,
agent: agent
}

get.concat(u, function (err, res, data) {
if (self.destroyed) return
Expand Down
95 changes: 73 additions & 22 deletions lib/client/udp-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ module.exports = UDPTracker

var BN = require('bn.js')
var Buffer = require('safe-buffer').Buffer
var clone = require('clone')
var compact2string = require('compact2string')
var debug = require('debug')('bittorrent-tracker:udp-tracker')
var dgram = require('dgram')
var inherits = require('inherits')
var randombytes = require('randombytes')
var Socks = require('socks')
var url = require('url')

var common = require('../common')
Expand Down Expand Up @@ -63,8 +65,42 @@ UDPTracker.prototype._request = function (opts) {
var self = this
if (!opts) opts = {}
var parsedUrl = url.parse(self.announceUrl)
if (!parsedUrl.port) {
parsedUrl.port = 80
}
var timeout
// Socket used to connect to the socks server to create a relay, null if socks is disabled
var proxySocket
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a comment here that explains the difference between socket and proxySocket

  • If socks is enabled, what do they both do?
  • If socks is disabled, is proxySocket null? etc

// Socket used to connect to the tracker or to the socks relay if socks is enabled
var socket
// Contains the host/port of the socks relay
var relay
var transactionId = genTransactionId()
var socket = dgram.createSocket('udp4')

var proxyOpts = self.client._proxyOpts && clone(self.client._proxyOpts.socksProxy)
if (proxyOpts) {
if (!proxyOpts.proxy) {
proxyOpts.proxy = {}
}
// UDP requests uses the associate command
proxyOpts.proxy.command = 'associate'
if (!proxyOpts.target) {
// This should contain client IP and port but can be set to 0 if we don't have this information
proxyOpts.target = {
host: '0.0.0.0',
port: 0
}
}

if (proxyOpts.proxy.type === 5) {
Socks.createConnection(proxyOpts, onGotConnection)
} else {
debug('Ignoring Socks proxy for UDP request because type 5 is required')
onGotConnection(null)
}
} else {
onGotConnection(null)
}

var cleanup = function () {
if (!socket) return
Expand All @@ -78,29 +114,42 @@ UDPTracker.prototype._request = function (opts) {
socket.on('error', noop) // ignore all future errors
try { socket.close() } catch (err) {}
socket = null
if (proxySocket) {
try { proxySocket.close() } catch (err) {}
proxySocket = null
}
}
self.cleanupFns.push(cleanup)

// does not matter if `stopped` event arrives, so supress errors & cleanup after timeout
var ms = opts.event === 'stopped' ? TIMEOUT / 10 : TIMEOUT
var timeout = setTimeout(function () {
timeout = null
if (opts.event === 'stopped') cleanup()
else onError(new Error('tracker request timed out (' + opts.event + ')'))
}, ms)
if (timeout.unref) timeout.unref()
function onGotConnection (err, s, info) {
if (err) return onError(err)

proxySocket = s
socket = dgram.createSocket('udp4')
relay = info

// does not matter if `stopped` event arrives, so supress errors & cleanup after timeout
var ms = opts.event === 'stopped' ? TIMEOUT / 10 : TIMEOUT
timeout = setTimeout(function () {
timeout = null
if (opts.event === 'stopped') cleanup()
else onError(new Error('tracker request timed out (' + opts.event + ')'))
}, ms)
if (timeout.unref) timeout.unref()

send(Buffer.concat([
common.CONNECTION_ID,
common.toUInt32(common.ACTIONS.CONNECT),
transactionId
]))
send(Buffer.concat([
common.CONNECTION_ID,
common.toUInt32(common.ACTIONS.CONNECT),
transactionId
]), relay)

socket.on('error', onError)
socket.on('message', onSocketMessage)
socket.on('error', onError)
socket.on('message', onSocketMessage)
}

function onSocketMessage (msg) {
if (self.destroyed) return
if (proxySocket) msg = msg.slice(10)
if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
return onError(new Error('tracker sent invalid transaction id'))
}
Expand Down Expand Up @@ -180,11 +229,13 @@ UDPTracker.prototype._request = function (opts) {
self.client.emit('warning', err)
}

function send (message) {
if (!parsedUrl.port) {
parsedUrl.port = 80
function send (message, proxyInfo) {
if (proxyInfo) {
var pack = Socks.createUDPFrame({ host: parsedUrl.hostname, port: parsedUrl.port }, message)
socket.send(pack, 0, pack.length, proxyInfo.port, proxyInfo.host)
} else {
socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname)
}
socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname)
}

function announce (connectionId, opts) {
Expand All @@ -204,7 +255,7 @@ UDPTracker.prototype._request = function (opts) {
common.toUInt32(0), // key (optional)
common.toUInt32(opts.numwant),
toUInt16(self.client._port)
]))
]), relay)
}

function scrape (connectionId) {
Expand All @@ -219,7 +270,7 @@ UDPTracker.prototype._request = function (opts) {
common.toUInt32(common.ACTIONS.SCRAPE),
transactionId,
infoHash
]))
]), relay)
}
}

Expand Down
14 changes: 13 additions & 1 deletion lib/client/websocket-tracker.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
module.exports = WebSocketTracker

var clone = require('clone')
var debug = require('debug')('bittorrent-tracker:websocket-tracker')
var extend = require('xtend')
var inherits = require('inherits')
var Peer = require('simple-peer')
var randombytes = require('randombytes')
var Socket = require('simple-websocket')
var Socks = require('socks')
var url = require('url')

var common = require('../common')
var Tracker = require('./tracker')
Expand Down Expand Up @@ -167,7 +170,16 @@ WebSocketTracker.prototype._openSocket = function () {
if (self.socket) {
socketPool[self.announceUrl].consumers += 1
} else {
self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl)
var parsedUrl = url.parse(self.announceUrl)
var agent
if (self.client._proxyOpts) {
agent = parsedUrl.protocol === 'wss:'
? self.client._proxyOpts.httpsAgent : self.client._proxyOpts.httpAgent
if (!agent && self.client._proxyOpts.socksProxy) {
agent = new Socks.Agent(clone(self.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'wss:'))
}
}
self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl, { agent: agent })
self.socket.consumers = 1
self.socket.on('connect', self._onSocketConnectBound)
}
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"./lib/common-node.js": false,
"./lib/client/http-tracker.js": false,
"./lib/client/udp-tracker.js": false,
"./server.js": false
"./server.js": false,
"socks": false
},
"bugs": {
"url": "https://github.com/feross/bittorrent-tracker/issues"
Expand All @@ -23,6 +24,7 @@
"bencode": "^0.10.0",
"bittorrent-peerid": "^1.0.2",
"bn.js": "^4.4.0",
"clone": "^1.0.2",
"compact2string": "^1.2.0",
"debug": "^2.0.0",
"inherits": "^2.0.1",
Expand All @@ -38,6 +40,7 @@
"simple-get": "^2.0.0",
"simple-peer": "^6.0.0",
"simple-websocket": "^4.0.0",
"socks": "^1.1.9",
"string2compact": "^1.1.1",
"uniq": "^1.0.1",
"ws": "^1.0.0",
Expand Down
55 changes: 55 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ var Buffer = require('safe-buffer').Buffer
var Client = require('../')
var common = require('./common')
var fixtures = require('webtorrent-fixtures')
var http = require('http')
var net = require('net')
var test = require('tape')

var peerId1 = Buffer.from('01234567890123456789')
Expand Down Expand Up @@ -380,3 +382,56 @@ test('http: client announce with numwant', function (t) {
test('udp: client announce with numwant', function (t) {
testClientAnnounceWithNumWant(t, 'udp')
})

function testClientStartHttpAgent (t, serverType) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, socks proxy is hard to test...

t.plan(5)

common.createServer(t, serverType, function (server, announceUrl) {
var agent = new http.Agent()
var agentUsed = false
agent.createConnection = function (opts, fn) {
agentUsed = true
return net.createConnection(opts, fn)
}
var client = new Client({
infoHash: fixtures.leaves.parsedTorrent.infoHash,
announce: announceUrl,
peerId: peerId1,
port: port,
wrtc: {},
proxyOpts: {
httpAgent: agent
}
})

if (serverType === 'ws') common.mockWebsocketTracker(client)
client.on('error', function (err) { t.error(err) })
client.on('warning', function (err) { t.error(err) })

client.once('update', function (data) {
t.equal(data.announce, announceUrl)
t.equal(typeof data.complete, 'number')
t.equal(typeof data.incomplete, 'number')

t.ok(agentUsed)

client.stop()

client.once('update', function () {
t.pass('got response to stop')
server.close()
client.destroy()
})
})

client.start()
})
}

test('http: client.start(httpAgent)', function (t) {
testClientStartHttpAgent(t, 'http')
})

test('ws: client.start(httpAgent)', function (t) {
testClientStartHttpAgent(t, 'ws')
})