Skip to content

Commit

Permalink
feat: add proxy support for tracker clients (#356)
Browse files Browse the repository at this point in the history
* Add a httpAgent options to http and websocket client trackers.

* Add a socks proxy to udp client trackers.

* Update http agent mock to node 5+

* Bugfix in socks configuration

* Use new socket to connect to the proxy relay and slice the proxy header from the message

* Add documentation for proxy

* Provide http and https agents for proxy.
Change proxy options structure and auto populate socks HTTP agents.

* Update documentation

* Check socks version for UDP proxy

* Clone proxy settings to prevent Socks instances concurrency

* Generate socks http agents on the fly (reuse is not working)

* Use clone to deepcopy socks opts

* Dont create agent for now since we cannot reuse it between requests.

* Removed unused require

* Add .gitignore

* Fix merge conflict

* Fix URL toString

* Fix new Socket constructor

Co-authored-by: Yoann Ciabaud <yoann@sonora.io>
  • Loading branch information
alxhotel and yciabaud committed Aug 20, 2021
1 parent 71deb99 commit ad64dc3
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 30 deletions.
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ var requiredOpts = {
}

var optionalOpts = {
// RTCPeerConnection config object (only used in browser)
rtcConfig: {},
// User-Agent header for http requests
userAgent: '',
// Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc)
wrtc: {},
getAnnounceOpts: function () {
// Provide a callback that will be called whenever announce() is called
// internally (on timer), or by the user
Expand All @@ -75,12 +81,44 @@ var optionalOpts = {
customParam: 'blah' // custom parameters supported
}
},
// RTCPeerConnection config object (only used in browser)
rtcConfig: {},
// User-Agent header for http requests
userAgent: '',
// Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc)
wrtc: {},
// Proxy config object
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: {}
},
}

var client = new Client(requiredOpts)
Expand Down
2 changes: 2 additions & 0 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const WebSocketTracker = require('./lib/client/websocket-tracker')
* @param {number} opts.rtcConfig RTCPeerConnection configuration object
* @param {number} opts.userAgent User-Agent header for http requests
* @param {number} opts.wrtc custom webrtc impl (useful in node.js)
* @param {object} opts.proxyOpts proxy options (useful in node.js)
*/
class Client extends EventEmitter {
constructor (opts = {}) {
Expand Down Expand Up @@ -54,6 +55,7 @@ class Client extends EventEmitter {
this._getAnnounceOpts = opts.getAnnounceOpts
this._rtcConfig = opts.rtcConfig
this._userAgent = opts.userAgent
this._proxyOpts = opts.proxyOpts

// Support lazy 'wrtc' module initialization
// See: https://github.com/webtorrent/webtorrent-hybrid/issues/46
Expand Down
15 changes: 12 additions & 3 deletions lib/client/http-tracker.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const arrayRemove = require('unordered-array-remove')
const bencode = require('bencode')
const clone = require('clone')
const compact2string = require('compact2string')
const debug = require('debug')('bittorrent-tracker:http-tracker')
const get = require('simple-get')
const Socks = require('socks')

const common = require('../common')
const Tracker = require('./tracker')
Expand Down Expand Up @@ -110,13 +112,20 @@ class HTTPTracker extends Tracker {

_request (requestUrl, params, cb) {
const self = this
const u = requestUrl + (!requestUrl.includes('?') ? '?' : '&') +
common.querystringStringify(params)
const parsedUrl = new URL(requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + common.querystringStringify(params))
let agent
if (this.client._proxyOpts) {
agent = parsedUrl.protocol === 'https:' ? this.client._proxyOpts.httpsAgent : this.client._proxyOpts.httpAgent
if (!agent && this.client._proxyOpts.socksProxy) {
agent = new Socks.Agent(clone(this.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'https:'))
}
}

this.cleanupFns.push(cleanup)

let request = get.concat({
url: u,
url: parsedUrl.toString(),
agent: agent,
timeout: common.REQUEST_TIMEOUT,
headers: {
'user-agent': this.client._userAgent || ''
Expand Down
88 changes: 69 additions & 19 deletions lib/client/udp-tracker.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const arrayRemove = require('unordered-array-remove')
const BN = require('bn.js')
const clone = require('clone')
const compact2string = require('compact2string')
const debug = require('debug')('bittorrent-tracker:udp-tracker')
const dgram = require('dgram')
const randombytes = require('randombytes')
const Socks = require('socks')

const common = require('../common')
const Tracker = require('./tracker')
Expand Down Expand Up @@ -77,27 +79,65 @@ class UDPTracker extends Tracker {
let { hostname, port } = common.parseUrl(this.announceUrl)
if (port === '') port = 80

let timeout
// Socket used to connect to the socks server to create a relay, null if socks is disabled
let proxySocket
// Socket used to connect to the tracker or to the socks relay if socks is enabled
let socket
// Contains the host/port of the socks relay
let relay

let transactionId = genTransactionId()
let socket = dgram.createSocket('udp4')

let timeout = setTimeout(() => {
// does not matter if `stopped` event arrives, so supress errors
if (opts.event === 'stopped') cleanup()
else onError(new Error(`tracker request timed out (${opts.event})`))
timeout = null
}, common.REQUEST_TIMEOUT)
if (timeout.unref) timeout.unref()
const proxyOpts = this.client._proxyOpts && clone(this.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)
}

this.cleanupFns.push(cleanup)

send(Buffer.concat([
common.CONNECTION_ID,
common.toUInt32(common.ACTIONS.CONNECT),
transactionId
]))
function onGotConnection (err, s, info) {
if (err) return onError(err)

socket.once('error', onError)
socket.on('message', onSocketMessage)
proxySocket = s
socket = dgram.createSocket('udp4')
relay = info

timeout = setTimeout(() => {
// does not matter if `stopped` event arrives, so supress errors
if (opts.event === 'stopped') cleanup()
else onError(new Error(`tracker request timed out (${opts.event})`))
timeout = null
}, common.REQUEST_TIMEOUT)
if (timeout.unref) timeout.unref()

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

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

function cleanup () {
if (timeout) {
Expand All @@ -111,6 +151,10 @@ class UDPTracker extends Tracker {
socket.on('error', noop) // ignore all future errors
try { socket.close() } catch (err) {}
socket = null
if (proxySocket) {
try { proxySocket.close() } catch (err) {}
proxySocket = null
}
}
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
}
Expand All @@ -128,6 +172,7 @@ class UDPTracker extends Tracker {
}

function onSocketMessage (msg) {
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 @@ -211,8 +256,13 @@ class UDPTracker extends Tracker {
}
}

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

function announce (connectionId, opts) {
Expand All @@ -232,7 +282,7 @@ class UDPTracker extends Tracker {
common.toUInt32(0), // key (optional)
common.toUInt32(opts.numwant),
toUInt16(self.client._port)
]))
]), relay)
}

function scrape (connectionId) {
Expand All @@ -247,7 +297,7 @@ class UDPTracker extends Tracker {
common.toUInt32(common.ACTIONS.SCRAPE),
transactionId,
infoHash
]))
]), relay)
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion lib/client/websocket-tracker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const clone = require('clone')
const debug = require('debug')('bittorrent-tracker:websocket-tracker')
const Peer = require('simple-peer')
const randombytes = require('randombytes')
const Socket = require('simple-websocket')
const Socks = require('socks')

const common = require('../common')
const Tracker = require('./tracker')
Expand Down Expand Up @@ -176,7 +178,15 @@ class WebSocketTracker extends Tracker {
this._onSocketConnectBound()
}
} else {
this.socket = socketPool[this.announceUrl] = new Socket(this.announceUrl)
const parsedUrl = new URL(this.announceUrl)
let agent
if (this.client._proxyOpts) {
agent = parsedUrl.protocol === 'wss:' ? this.client._proxyOpts.httpsAgent : this.client._proxyOpts.httpAgent
if (!agent && this.client._proxyOpts.socksProxy) {
agent = new Socks.Agent(clone(this.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'wss:'))
}
}
this.socket = socketPool[this.announceUrl] = new Socket({ url: this.announceUrl, agent: agent })
this.socket.consumers = 1
this.socket.once('connect', this._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
},
"chromeapp": {
"./server.js": false,
Expand All @@ -28,6 +29,7 @@
"bittorrent-peerid": "^1.3.3",
"bn.js": "^5.2.0",
"chrome-dgram": "^3.0.6",
"clone": "^1.0.2",
"compact2string": "^1.4.1",
"debug": "^4.1.1",
"ip": "^1.1.5",
Expand All @@ -42,6 +44,7 @@
"simple-get": "^4.0.0",
"simple-peer": "^9.11.0",
"simple-websocket": "^9.1.0",
"socks": "^1.1.9",
"string2compact": "^1.3.0",
"unordered-array-remove": "^1.0.2",
"ws": "^7.4.5"
Expand Down
55 changes: 55 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const Client = require('../')
const common = require('./common')
const http = require('http')
const fixtures = require('webtorrent-fixtures')
const net = require('net')
const test = require('tape')

const peerId1 = Buffer.from('01234567890123456789')
Expand Down Expand Up @@ -565,3 +567,56 @@ test('ws: invalid tracker url', t => {
test('ws: invalid tracker url with slash', t => {
testUnsupportedTracker(t, 'ws://')
})

function testClientStartHttpAgent (t, serverType) {
t.plan(5)

common.createServer(t, serverType, function (server, announceUrl) {
const agent = new http.Agent()
let agentUsed = false
agent.createConnection = function (opts, fn) {
agentUsed = true
return net.createConnection(opts, fn)
}
const 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')
})

0 comments on commit ad64dc3

Please sign in to comment.