Skip to content
Merged
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
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')
})