mirror of
https://github.com/webtorrent/bittorrent-tracker.git
synced 2025-01-18 12:11:36 +00:00
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 <yoann@sonora.io>
This commit is contained in:
parent
71deb99dca
commit
ad64dc3a68
50
README.md
50
README.md
@ -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
|
||||
@ -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)
|
||||
|
@ -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 = {}) {
|
||||
@ -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
|
||||
|
@ -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')
|
||||
@ -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 || ''
|
||||
|
@ -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')
|
||||
@ -77,10 +79,49 @@ class UDPTracker extends Tracker {
|
||||
let { hostname, port } = common.parseUrl(this.announceUrl)
|
||||
if (port === '') port = 80
|
||||
|
||||
let transactionId = genTransactionId()
|
||||
let socket = dgram.createSocket('udp4')
|
||||
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 timeout = setTimeout(() => {
|
||||
let transactionId = genTransactionId()
|
||||
|
||||
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)
|
||||
|
||||
function onGotConnection (err, s, info) {
|
||||
if (err) return onError(err)
|
||||
|
||||
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})`))
|
||||
@ -88,16 +129,15 @@ class UDPTracker extends Tracker {
|
||||
}, common.REQUEST_TIMEOUT)
|
||||
if (timeout.unref) timeout.unref()
|
||||
|
||||
this.cleanupFns.push(cleanup)
|
||||
|
||||
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) {
|
||||
@ -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()
|
||||
}
|
||||
@ -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'))
|
||||
}
|
||||
@ -211,9 +256,14 @@ class UDPTracker extends Tracker {
|
||||
}
|
||||
}
|
||||
|
||||
function send (message) {
|
||||
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) {
|
||||
transactionId = genTransactionId()
|
||||
@ -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) {
|
||||
@ -247,7 +297,7 @@ class UDPTracker extends Tracker {
|
||||
common.toUInt32(common.ACTIONS.SCRAPE),
|
||||
transactionId,
|
||||
infoHash
|
||||
]))
|
||||
]), relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
@ -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",
|
||||
@ -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"
|
||||
|
@ -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')
|
||||
@ -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')
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user