Skip to content

Commit ad64dc3

Browse files
alxhotelyciabaud
andauthored
feat: add proxy support for tracker clients (#356)
* 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 <[email protected]>
1 parent 71deb99 commit ad64dc3

File tree

7 files changed

+197
-30
lines changed

7 files changed

+197
-30
lines changed

README.md

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ var requiredOpts = {
6565
}
6666

6767
var optionalOpts = {
68+
// RTCPeerConnection config object (only used in browser)
69+
rtcConfig: {},
70+
// User-Agent header for http requests
71+
userAgent: '',
72+
// Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc)
73+
wrtc: {},
6874
getAnnounceOpts: function () {
6975
// Provide a callback that will be called whenever announce() is called
7076
// internally (on timer), or by the user
@@ -75,12 +81,44 @@ var optionalOpts = {
7581
customParam: 'blah' // custom parameters supported
7682
}
7783
},
78-
// RTCPeerConnection config object (only used in browser)
79-
rtcConfig: {},
80-
// User-Agent header for http requests
81-
userAgent: '',
82-
// Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc)
83-
wrtc: {},
84+
// Proxy config object
85+
proxyOpts: {
86+
// Socks proxy options (used to proxy requests in node)
87+
socksProxy: {
88+
// Configuration from socks module (https://github.com/JoshGlazebrook/socks)
89+
proxy: {
90+
// IP Address of Proxy (Required)
91+
ipaddress: "1.2.3.4",
92+
// TCP Port of Proxy (Required)
93+
port: 1080,
94+
// Proxy Type [4, 5] (Required)
95+
// Note: 4 works for both 4 and 4a.
96+
// Type 4 does not support UDP association relay
97+
type: 5,
98+
99+
// SOCKS 4 Specific:
100+
101+
// UserId used when making a SOCKS 4/4a request. (Optional)
102+
userid: "someuserid",
103+
104+
// SOCKS 5 Specific:
105+
106+
// Authentication used for SOCKS 5 (when it's required) (Optional)
107+
authentication: {
108+
username: "Josh",
109+
password: "somepassword"
110+
}
111+
},
112+
113+
// Amount of time to wait for a connection to be established. (Optional)
114+
// - defaults to 10000ms (10 seconds)
115+
timeout: 10000
116+
},
117+
// NodeJS HTTP agents (used to proxy HTTP and Websocket requests in node)
118+
// Populated with Socks.Agent if socksProxy is provided
119+
httpAgent: {},
120+
httpsAgent: {}
121+
},
84122
}
85123

86124
var client = new Client(requiredOpts)

client.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const WebSocketTracker = require('./lib/client/websocket-tracker')
2424
* @param {number} opts.rtcConfig RTCPeerConnection configuration object
2525
* @param {number} opts.userAgent User-Agent header for http requests
2626
* @param {number} opts.wrtc custom webrtc impl (useful in node.js)
27+
* @param {object} opts.proxyOpts proxy options (useful in node.js)
2728
*/
2829
class Client extends EventEmitter {
2930
constructor (opts = {}) {
@@ -54,6 +55,7 @@ class Client extends EventEmitter {
5455
this._getAnnounceOpts = opts.getAnnounceOpts
5556
this._rtcConfig = opts.rtcConfig
5657
this._userAgent = opts.userAgent
58+
this._proxyOpts = opts.proxyOpts
5759

5860
// Support lazy 'wrtc' module initialization
5961
// See: https://github.com/webtorrent/webtorrent-hybrid/issues/46

lib/client/http-tracker.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
const arrayRemove = require('unordered-array-remove')
22
const bencode = require('bencode')
3+
const clone = require('clone')
34
const compact2string = require('compact2string')
45
const debug = require('debug')('bittorrent-tracker:http-tracker')
56
const get = require('simple-get')
7+
const Socks = require('socks')
68

79
const common = require('../common')
810
const Tracker = require('./tracker')
@@ -110,13 +112,20 @@ class HTTPTracker extends Tracker {
110112

111113
_request (requestUrl, params, cb) {
112114
const self = this
113-
const u = requestUrl + (!requestUrl.includes('?') ? '?' : '&') +
114-
common.querystringStringify(params)
115+
const parsedUrl = new URL(requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + common.querystringStringify(params))
116+
let agent
117+
if (this.client._proxyOpts) {
118+
agent = parsedUrl.protocol === 'https:' ? this.client._proxyOpts.httpsAgent : this.client._proxyOpts.httpAgent
119+
if (!agent && this.client._proxyOpts.socksProxy) {
120+
agent = new Socks.Agent(clone(this.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'https:'))
121+
}
122+
}
115123

116124
this.cleanupFns.push(cleanup)
117125

118126
let request = get.concat({
119-
url: u,
127+
url: parsedUrl.toString(),
128+
agent: agent,
120129
timeout: common.REQUEST_TIMEOUT,
121130
headers: {
122131
'user-agent': this.client._userAgent || ''

lib/client/udp-tracker.js

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const arrayRemove = require('unordered-array-remove')
22
const BN = require('bn.js')
3+
const clone = require('clone')
34
const compact2string = require('compact2string')
45
const debug = require('debug')('bittorrent-tracker:udp-tracker')
56
const dgram = require('dgram')
67
const randombytes = require('randombytes')
8+
const Socks = require('socks')
79

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

82+
let timeout
83+
// Socket used to connect to the socks server to create a relay, null if socks is disabled
84+
let proxySocket
85+
// Socket used to connect to the tracker or to the socks relay if socks is enabled
86+
let socket
87+
// Contains the host/port of the socks relay
88+
let relay
89+
8090
let transactionId = genTransactionId()
81-
let socket = dgram.createSocket('udp4')
8291

83-
let timeout = setTimeout(() => {
84-
// does not matter if `stopped` event arrives, so supress errors
85-
if (opts.event === 'stopped') cleanup()
86-
else onError(new Error(`tracker request timed out (${opts.event})`))
87-
timeout = null
88-
}, common.REQUEST_TIMEOUT)
89-
if (timeout.unref) timeout.unref()
92+
const proxyOpts = this.client._proxyOpts && clone(this.client._proxyOpts.socksProxy)
93+
if (proxyOpts) {
94+
if (!proxyOpts.proxy) proxyOpts.proxy = {}
95+
// UDP requests uses the associate command
96+
proxyOpts.proxy.command = 'associate'
97+
if (!proxyOpts.target) {
98+
// This should contain client IP and port but can be set to 0 if we don't have this information
99+
proxyOpts.target = {
100+
host: '0.0.0.0',
101+
port: 0
102+
}
103+
}
104+
105+
if (proxyOpts.proxy.type === 5) {
106+
Socks.createConnection(proxyOpts, onGotConnection)
107+
} else {
108+
debug('Ignoring Socks proxy for UDP request because type 5 is required')
109+
onGotConnection(null)
110+
}
111+
} else {
112+
onGotConnection(null)
113+
}
90114

91115
this.cleanupFns.push(cleanup)
92116

93-
send(Buffer.concat([
94-
common.CONNECTION_ID,
95-
common.toUInt32(common.ACTIONS.CONNECT),
96-
transactionId
97-
]))
117+
function onGotConnection (err, s, info) {
118+
if (err) return onError(err)
98119

99-
socket.once('error', onError)
100-
socket.on('message', onSocketMessage)
120+
proxySocket = s
121+
socket = dgram.createSocket('udp4')
122+
relay = info
123+
124+
timeout = setTimeout(() => {
125+
// does not matter if `stopped` event arrives, so supress errors
126+
if (opts.event === 'stopped') cleanup()
127+
else onError(new Error(`tracker request timed out (${opts.event})`))
128+
timeout = null
129+
}, common.REQUEST_TIMEOUT)
130+
if (timeout.unref) timeout.unref()
131+
132+
send(Buffer.concat([
133+
common.CONNECTION_ID,
134+
common.toUInt32(common.ACTIONS.CONNECT),
135+
transactionId
136+
]), relay)
137+
138+
socket.once('error', onError)
139+
socket.on('message', onSocketMessage)
140+
}
101141

102142
function cleanup () {
103143
if (timeout) {
@@ -111,6 +151,10 @@ class UDPTracker extends Tracker {
111151
socket.on('error', noop) // ignore all future errors
112152
try { socket.close() } catch (err) {}
113153
socket = null
154+
if (proxySocket) {
155+
try { proxySocket.close() } catch (err) {}
156+
proxySocket = null
157+
}
114158
}
115159
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
116160
}
@@ -128,6 +172,7 @@ class UDPTracker extends Tracker {
128172
}
129173

130174
function onSocketMessage (msg) {
175+
if (proxySocket) msg = msg.slice(10)
131176
if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
132177
return onError(new Error('tracker sent invalid transaction id'))
133178
}
@@ -211,8 +256,13 @@ class UDPTracker extends Tracker {
211256
}
212257
}
213258

214-
function send (message) {
215-
socket.send(message, 0, message.length, port, hostname)
259+
function send (message, proxyInfo) {
260+
if (proxyInfo) {
261+
const pack = Socks.createUDPFrame({ host: hostname, port: port }, message)
262+
socket.send(pack, 0, pack.length, proxyInfo.port, proxyInfo.host)
263+
} else {
264+
socket.send(message, 0, message.length, port, hostname)
265+
}
216266
}
217267

218268
function announce (connectionId, opts) {
@@ -232,7 +282,7 @@ class UDPTracker extends Tracker {
232282
common.toUInt32(0), // key (optional)
233283
common.toUInt32(opts.numwant),
234284
toUInt16(self.client._port)
235-
]))
285+
]), relay)
236286
}
237287

238288
function scrape (connectionId) {
@@ -247,7 +297,7 @@ class UDPTracker extends Tracker {
247297
common.toUInt32(common.ACTIONS.SCRAPE),
248298
transactionId,
249299
infoHash
250-
]))
300+
]), relay)
251301
}
252302
}
253303
}

lib/client/websocket-tracker.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
const clone = require('clone')
12
const debug = require('debug')('bittorrent-tracker:websocket-tracker')
23
const Peer = require('simple-peer')
34
const randombytes = require('randombytes')
45
const Socket = require('simple-websocket')
6+
const Socks = require('socks')
57

68
const common = require('../common')
79
const Tracker = require('./tracker')
@@ -176,7 +178,15 @@ class WebSocketTracker extends Tracker {
176178
this._onSocketConnectBound()
177179
}
178180
} else {
179-
this.socket = socketPool[this.announceUrl] = new Socket(this.announceUrl)
181+
const parsedUrl = new URL(this.announceUrl)
182+
let agent
183+
if (this.client._proxyOpts) {
184+
agent = parsedUrl.protocol === 'wss:' ? this.client._proxyOpts.httpsAgent : this.client._proxyOpts.httpAgent
185+
if (!agent && this.client._proxyOpts.socksProxy) {
186+
agent = new Socks.Agent(clone(this.client._proxyOpts.socksProxy), (parsedUrl.protocol === 'wss:'))
187+
}
188+
}
189+
this.socket = socketPool[this.announceUrl] = new Socket({ url: this.announceUrl, agent: agent })
180190
this.socket.consumers = 1
181191
this.socket.once('connect', this._onSocketConnectBound)
182192
}

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"./lib/common-node.js": false,
1515
"./lib/client/http-tracker.js": false,
1616
"./lib/client/udp-tracker.js": false,
17-
"./server.js": false
17+
"./server.js": false,
18+
"socks": false
1819
},
1920
"chromeapp": {
2021
"./server.js": false,
@@ -28,6 +29,7 @@
2829
"bittorrent-peerid": "^1.3.3",
2930
"bn.js": "^5.2.0",
3031
"chrome-dgram": "^3.0.6",
32+
"clone": "^1.0.2",
3133
"compact2string": "^1.4.1",
3234
"debug": "^4.1.1",
3335
"ip": "^1.1.5",
@@ -42,6 +44,7 @@
4244
"simple-get": "^4.0.0",
4345
"simple-peer": "^9.11.0",
4446
"simple-websocket": "^9.1.0",
47+
"socks": "^1.1.9",
4548
"string2compact": "^1.3.0",
4649
"unordered-array-remove": "^1.0.2",
4750
"ws": "^7.4.5"

test/client.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const Client = require('../')
22
const common = require('./common')
3+
const http = require('http')
34
const fixtures = require('webtorrent-fixtures')
5+
const net = require('net')
46
const test = require('tape')
57

68
const peerId1 = Buffer.from('01234567890123456789')
@@ -565,3 +567,56 @@ test('ws: invalid tracker url', t => {
565567
test('ws: invalid tracker url with slash', t => {
566568
testUnsupportedTracker(t, 'ws://')
567569
})
570+
571+
function testClientStartHttpAgent (t, serverType) {
572+
t.plan(5)
573+
574+
common.createServer(t, serverType, function (server, announceUrl) {
575+
const agent = new http.Agent()
576+
let agentUsed = false
577+
agent.createConnection = function (opts, fn) {
578+
agentUsed = true
579+
return net.createConnection(opts, fn)
580+
}
581+
const client = new Client({
582+
infoHash: fixtures.leaves.parsedTorrent.infoHash,
583+
announce: announceUrl,
584+
peerId: peerId1,
585+
port: port,
586+
wrtc: {},
587+
proxyOpts: {
588+
httpAgent: agent
589+
}
590+
})
591+
592+
if (serverType === 'ws') common.mockWebsocketTracker(client)
593+
client.on('error', function (err) { t.error(err) })
594+
client.on('warning', function (err) { t.error(err) })
595+
596+
client.once('update', function (data) {
597+
t.equal(data.announce, announceUrl)
598+
t.equal(typeof data.complete, 'number')
599+
t.equal(typeof data.incomplete, 'number')
600+
601+
t.ok(agentUsed)
602+
603+
client.stop()
604+
605+
client.once('update', function () {
606+
t.pass('got response to stop')
607+
server.close()
608+
client.destroy()
609+
})
610+
})
611+
612+
client.start()
613+
})
614+
}
615+
616+
test('http: client.start(httpAgent)', function (t) {
617+
testClientStartHttpAgent(t, 'http')
618+
})
619+
620+
test('ws: client.start(httpAgent)', function (t) {
621+
testClientStartHttpAgent(t, 'ws')
622+
})

0 commit comments

Comments
 (0)