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 = {
|
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 () {
|
getAnnounceOpts: function () {
|
||||||
// Provide a callback that will be called whenever announce() is called
|
// Provide a callback that will be called whenever announce() is called
|
||||||
// internally (on timer), or by the user
|
// internally (on timer), or by the user
|
||||||
@ -75,12 +81,44 @@ var optionalOpts = {
|
|||||||
customParam: 'blah' // custom parameters supported
|
customParam: 'blah' // custom parameters supported
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// RTCPeerConnection config object (only used in browser)
|
// Proxy config object
|
||||||
rtcConfig: {},
|
proxyOpts: {
|
||||||
// User-Agent header for http requests
|
// Socks proxy options (used to proxy requests in node)
|
||||||
userAgent: '',
|
socksProxy: {
|
||||||
// Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc)
|
// Configuration from socks module (https://github.com/JoshGlazebrook/socks)
|
||||||
wrtc: {},
|
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)
|
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.rtcConfig RTCPeerConnection configuration object
|
||||||
* @param {number} opts.userAgent User-Agent header for http requests
|
* @param {number} opts.userAgent User-Agent header for http requests
|
||||||
* @param {number} opts.wrtc custom webrtc impl (useful in node.js)
|
* @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 {
|
class Client extends EventEmitter {
|
||||||
constructor (opts = {}) {
|
constructor (opts = {}) {
|
||||||
@ -54,6 +55,7 @@ class Client extends EventEmitter {
|
|||||||
this._getAnnounceOpts = opts.getAnnounceOpts
|
this._getAnnounceOpts = opts.getAnnounceOpts
|
||||||
this._rtcConfig = opts.rtcConfig
|
this._rtcConfig = opts.rtcConfig
|
||||||
this._userAgent = opts.userAgent
|
this._userAgent = opts.userAgent
|
||||||
|
this._proxyOpts = opts.proxyOpts
|
||||||
|
|
||||||
// Support lazy 'wrtc' module initialization
|
// Support lazy 'wrtc' module initialization
|
||||||
// See: https://github.com/webtorrent/webtorrent-hybrid/issues/46
|
// See: https://github.com/webtorrent/webtorrent-hybrid/issues/46
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
const arrayRemove = require('unordered-array-remove')
|
const arrayRemove = require('unordered-array-remove')
|
||||||
const bencode = require('bencode')
|
const bencode = require('bencode')
|
||||||
|
const clone = require('clone')
|
||||||
const compact2string = require('compact2string')
|
const compact2string = require('compact2string')
|
||||||
const debug = require('debug')('bittorrent-tracker:http-tracker')
|
const debug = require('debug')('bittorrent-tracker:http-tracker')
|
||||||
const get = require('simple-get')
|
const get = require('simple-get')
|
||||||
|
const Socks = require('socks')
|
||||||
|
|
||||||
const common = require('../common')
|
const common = require('../common')
|
||||||
const Tracker = require('./tracker')
|
const Tracker = require('./tracker')
|
||||||
@ -110,13 +112,20 @@ class HTTPTracker extends Tracker {
|
|||||||
|
|
||||||
_request (requestUrl, params, cb) {
|
_request (requestUrl, params, cb) {
|
||||||
const self = this
|
const self = this
|
||||||
const u = requestUrl + (!requestUrl.includes('?') ? '?' : '&') +
|
const parsedUrl = new URL(requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + common.querystringStringify(params))
|
||||||
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)
|
this.cleanupFns.push(cleanup)
|
||||||
|
|
||||||
let request = get.concat({
|
let request = get.concat({
|
||||||
url: u,
|
url: parsedUrl.toString(),
|
||||||
|
agent: agent,
|
||||||
timeout: common.REQUEST_TIMEOUT,
|
timeout: common.REQUEST_TIMEOUT,
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': this.client._userAgent || ''
|
'user-agent': this.client._userAgent || ''
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
const arrayRemove = require('unordered-array-remove')
|
const arrayRemove = require('unordered-array-remove')
|
||||||
const BN = require('bn.js')
|
const BN = require('bn.js')
|
||||||
|
const clone = require('clone')
|
||||||
const compact2string = require('compact2string')
|
const compact2string = require('compact2string')
|
||||||
const debug = require('debug')('bittorrent-tracker:udp-tracker')
|
const debug = require('debug')('bittorrent-tracker:udp-tracker')
|
||||||
const dgram = require('dgram')
|
const dgram = require('dgram')
|
||||||
const randombytes = require('randombytes')
|
const randombytes = require('randombytes')
|
||||||
|
const Socks = require('socks')
|
||||||
|
|
||||||
const common = require('../common')
|
const common = require('../common')
|
||||||
const Tracker = require('./tracker')
|
const Tracker = require('./tracker')
|
||||||
@ -77,27 +79,65 @@ class UDPTracker extends Tracker {
|
|||||||
let { hostname, port } = common.parseUrl(this.announceUrl)
|
let { hostname, port } = common.parseUrl(this.announceUrl)
|
||||||
if (port === '') port = 80
|
if (port === '') port = 80
|
||||||
|
|
||||||
let transactionId = genTransactionId()
|
let timeout
|
||||||
let socket = dgram.createSocket('udp4')
|
// 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()
|
||||||
// does not matter if `stopped` event arrives, so supress errors
|
|
||||||
if (opts.event === 'stopped') cleanup()
|
const proxyOpts = this.client._proxyOpts && clone(this.client._proxyOpts.socksProxy)
|
||||||
else onError(new Error(`tracker request timed out (${opts.event})`))
|
if (proxyOpts) {
|
||||||
timeout = null
|
if (!proxyOpts.proxy) proxyOpts.proxy = {}
|
||||||
}, common.REQUEST_TIMEOUT)
|
// UDP requests uses the associate command
|
||||||
if (timeout.unref) timeout.unref()
|
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)
|
this.cleanupFns.push(cleanup)
|
||||||
|
|
||||||
send(Buffer.concat([
|
function onGotConnection (err, s, info) {
|
||||||
common.CONNECTION_ID,
|
if (err) return onError(err)
|
||||||
common.toUInt32(common.ACTIONS.CONNECT),
|
|
||||||
transactionId
|
|
||||||
]))
|
|
||||||
|
|
||||||
socket.once('error', onError)
|
proxySocket = s
|
||||||
socket.on('message', onSocketMessage)
|
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 () {
|
function cleanup () {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@ -111,6 +151,10 @@ class UDPTracker extends Tracker {
|
|||||||
socket.on('error', noop) // ignore all future errors
|
socket.on('error', noop) // ignore all future errors
|
||||||
try { socket.close() } catch (err) {}
|
try { socket.close() } catch (err) {}
|
||||||
socket = null
|
socket = null
|
||||||
|
if (proxySocket) {
|
||||||
|
try { proxySocket.close() } catch (err) {}
|
||||||
|
proxySocket = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
|
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
|
||||||
}
|
}
|
||||||
@ -128,6 +172,7 @@ class UDPTracker extends Tracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSocketMessage (msg) {
|
function onSocketMessage (msg) {
|
||||||
|
if (proxySocket) msg = msg.slice(10)
|
||||||
if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
|
if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
|
||||||
return onError(new Error('tracker sent invalid transaction id'))
|
return onError(new Error('tracker sent invalid transaction id'))
|
||||||
}
|
}
|
||||||
@ -211,8 +256,13 @@ class UDPTracker extends Tracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function send (message) {
|
function send (message, proxyInfo) {
|
||||||
socket.send(message, 0, message.length, port, hostname)
|
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) {
|
function announce (connectionId, opts) {
|
||||||
@ -232,7 +282,7 @@ class UDPTracker extends Tracker {
|
|||||||
common.toUInt32(0), // key (optional)
|
common.toUInt32(0), // key (optional)
|
||||||
common.toUInt32(opts.numwant),
|
common.toUInt32(opts.numwant),
|
||||||
toUInt16(self.client._port)
|
toUInt16(self.client._port)
|
||||||
]))
|
]), relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrape (connectionId) {
|
function scrape (connectionId) {
|
||||||
@ -247,7 +297,7 @@ class UDPTracker extends Tracker {
|
|||||||
common.toUInt32(common.ACTIONS.SCRAPE),
|
common.toUInt32(common.ACTIONS.SCRAPE),
|
||||||
transactionId,
|
transactionId,
|
||||||
infoHash
|
infoHash
|
||||||
]))
|
]), relay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
const clone = require('clone')
|
||||||
const debug = require('debug')('bittorrent-tracker:websocket-tracker')
|
const debug = require('debug')('bittorrent-tracker:websocket-tracker')
|
||||||
const Peer = require('simple-peer')
|
const Peer = require('simple-peer')
|
||||||
const randombytes = require('randombytes')
|
const randombytes = require('randombytes')
|
||||||
const Socket = require('simple-websocket')
|
const Socket = require('simple-websocket')
|
||||||
|
const Socks = require('socks')
|
||||||
|
|
||||||
const common = require('../common')
|
const common = require('../common')
|
||||||
const Tracker = require('./tracker')
|
const Tracker = require('./tracker')
|
||||||
@ -176,7 +178,15 @@ class WebSocketTracker extends Tracker {
|
|||||||
this._onSocketConnectBound()
|
this._onSocketConnectBound()
|
||||||
}
|
}
|
||||||
} else {
|
} 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.consumers = 1
|
||||||
this.socket.once('connect', this._onSocketConnectBound)
|
this.socket.once('connect', this._onSocketConnectBound)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
"./lib/common-node.js": false,
|
"./lib/common-node.js": false,
|
||||||
"./lib/client/http-tracker.js": false,
|
"./lib/client/http-tracker.js": false,
|
||||||
"./lib/client/udp-tracker.js": false,
|
"./lib/client/udp-tracker.js": false,
|
||||||
"./server.js": false
|
"./server.js": false,
|
||||||
|
"socks": false
|
||||||
},
|
},
|
||||||
"chromeapp": {
|
"chromeapp": {
|
||||||
"./server.js": false,
|
"./server.js": false,
|
||||||
@ -28,6 +29,7 @@
|
|||||||
"bittorrent-peerid": "^1.3.3",
|
"bittorrent-peerid": "^1.3.3",
|
||||||
"bn.js": "^5.2.0",
|
"bn.js": "^5.2.0",
|
||||||
"chrome-dgram": "^3.0.6",
|
"chrome-dgram": "^3.0.6",
|
||||||
|
"clone": "^1.0.2",
|
||||||
"compact2string": "^1.4.1",
|
"compact2string": "^1.4.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"ip": "^1.1.5",
|
"ip": "^1.1.5",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"simple-get": "^4.0.0",
|
"simple-get": "^4.0.0",
|
||||||
"simple-peer": "^9.11.0",
|
"simple-peer": "^9.11.0",
|
||||||
"simple-websocket": "^9.1.0",
|
"simple-websocket": "^9.1.0",
|
||||||
|
"socks": "^1.1.9",
|
||||||
"string2compact": "^1.3.0",
|
"string2compact": "^1.3.0",
|
||||||
"unordered-array-remove": "^1.0.2",
|
"unordered-array-remove": "^1.0.2",
|
||||||
"ws": "^7.4.5"
|
"ws": "^7.4.5"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
const Client = require('../')
|
const Client = require('../')
|
||||||
const common = require('./common')
|
const common = require('./common')
|
||||||
|
const http = require('http')
|
||||||
const fixtures = require('webtorrent-fixtures')
|
const fixtures = require('webtorrent-fixtures')
|
||||||
|
const net = require('net')
|
||||||
const test = require('tape')
|
const test = require('tape')
|
||||||
|
|
||||||
const peerId1 = Buffer.from('01234567890123456789')
|
const peerId1 = Buffer.from('01234567890123456789')
|
||||||
@ -565,3 +567,56 @@ test('ws: invalid tracker url', t => {
|
|||||||
test('ws: invalid tracker url with slash', t => {
|
test('ws: invalid tracker url with slash', t => {
|
||||||
testUnsupportedTracker(t, 'ws://')
|
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