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
61 changes: 19 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ npm install bittorrent-tracker
To connect to a tracker, just do this:

```js
var Client = require('bittorrent-tracker')
import Client from 'bittorrent-tracker'

var requiredOpts = {
const requiredOpts = {
infoHash: new Buffer('012345678901234567890'), // hex string or Buffer
peerId: new Buffer('01234567890123456789'), // hex string or Buffer
announce: [], // list of tracker server urls
port: 6881 // torrent client port, (in browser, optional)
}

var optionalOpts = {
const optionalOpts = {
// RTCPeerConnection config object (only used in browser)
rtcConfig: {},
// User-Agent header for http requests
Expand All @@ -81,47 +81,24 @@ var optionalOpts = {
customParam: 'blah' // custom parameters supported
}
},
// Proxy config object
// Proxy options (used to proxy requests in node)
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: {}
// For WSS trackers this is always a http.Agent
// For UDP trackers this is an object of options for the Socks Connection
// For HTTP trackers this is either an undici Agent if using Node16 or later, or http.Agent if using versions prior to Node 16, ex:
// import Socks from 'socks'
// proxyOpts.socksProxy = new Socks.Agent(optionsObject, isHttps)
// or if using Node 16 or later
// import { socksDispatcher } from 'fetch-socks'
// proxyOpts.socksProxy = socksDispatcher(optionsObject)
socksProxy: new SocksProxy(socksOptionsObject),
// Populated with socksProxy if it's provided
httpAgent: new http.Agent(agentOptionsObject),
httpsAgent: new https.Agent(agentOptionsObject)
},
}

var client = new Client(requiredOpts)
const client = new Client(requiredOpts)

client.on('error', function (err) {
// fatal client error!
Expand Down Expand Up @@ -182,7 +159,7 @@ client.on('scrape', function (data) {
To start a BitTorrent tracker server to track swarms of peers:

```js
const Server = require('bittorrent-tracker').Server
import { Server } from 'bittorrent-tracker'

const server = new Server({
udp: true, // enable udp server? [default=true]
Expand Down Expand Up @@ -289,7 +266,7 @@ The http server will handle requests for the following paths: `/announce`, `/scr
Scraping multiple torrent info is possible with a static `Client.scrape` method:

```js
var Client = require('bittorrent-tracker')
import Client from 'bittorrent-tracker'
Client.scrape({ announce: announceUrl, infoHash: [ infoHash1, infoHash2 ]}, function (err, results) {
results[infoHash1].announce
results[infoHash1].infoHash
Expand Down
108 changes: 58 additions & 50 deletions lib/client/http-tracker.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import arrayRemove from 'unordered-array-remove'
import bencode from 'bencode'
import clone from 'clone'
import Debug from 'debug'
import get from 'simple-get'
import Socks from 'socks'
import fetch from 'cross-fetch-ponyfill'
import { bin2hex, hex2bin, arr2text, text2arr, arr2hex } from 'uint8-util'

import common from '../common.js'
Expand All @@ -13,6 +11,14 @@ import compact2string from 'compact2string'
const debug = Debug('bittorrent-tracker:http-tracker')
const HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/

function abortTimeout (ms) {
const controller = new AbortController()
setTimeout(() => {
controller.abort()
}, ms).unref?.()
return controller
}

/**
* HTTP torrent tracker client (for an individual tracker)
*
Expand Down Expand Up @@ -112,70 +118,72 @@ class HTTPTracker extends Tracker {
}
}

_request (requestUrl, params, cb) {
const self = this
async _request (requestUrl, params, cb) {
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:'))
agent = this.client._proxyOpts.socksProxy
}
}

this.cleanupFns.push(cleanup)

let request = get.concat({
url: parsedUrl.toString(),
agent,
timeout: common.REQUEST_TIMEOUT,
headers: {
'user-agent': this.client._userAgent || ''
}
}, onResponse)

function cleanup () {
if (request) {
arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup))
request.abort()
request = null
const cleanup = () => {
if (!controller.signal.aborted) {
arrayRemove(this.cleanupFns, this.cleanupFns.indexOf(cleanup))
controller.abort()
controller = null
}
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
if (this.maybeDestroyCleanup) this.maybeDestroyCleanup()
}

function onResponse (err, res, data) {
cleanup()
if (self.destroyed) return
this.cleanupFns.push(cleanup)

let res
let controller = abortTimeout(common.REQUEST_TIMEOUT)
try {
res = await fetch(parsedUrl.toString(), {
agent,
signal: controller.signal,
dispatcher: agent,
headers: {
'user-agent': this.client._userAgent || ''
}
})
} catch (err) {
if (err) return cb(err)
if (res.statusCode !== 200) {
return cb(new Error(`Non-200 response code ${res.statusCode} from ${self.announceUrl}`))
}
if (!data || data.length === 0) {
return cb(new Error(`Invalid tracker response from${self.announceUrl}`))
}

try {
data = bencode.decode(data)
} catch (err) {
return cb(new Error(`Error decoding tracker response: ${err.message}`))
}
const failure = data['failure reason'] && arr2text(data['failure reason'])
if (failure) {
debug(`failure from ${requestUrl} (${failure})`)
return cb(new Error(failure))
}
}
let data = new Uint8Array(await res.arrayBuffer())
cleanup()
if (this.destroyed) return

const warning = data['warning message'] && arr2text(data['warning message'])
if (warning) {
debug(`warning from ${requestUrl} (${warning})`)
self.client.emit('warning', new Error(warning))
}
if (res.status !== 200) {
return cb(new Error(`Non-200 response code ${res.statusCode} from ${this.announceUrl}`))
}
if (!data || data.length === 0) {
return cb(new Error(`Invalid tracker response from${this.announceUrl}`))
}

debug(`response from ${requestUrl}`)
try {
data = bencode.decode(data)
} catch (err) {
return cb(new Error(`Error decoding tracker response: ${err.message}`))
}
const failure = data['failure reason'] && arr2text(data['failure reason'])
if (failure) {
debug(`failure from ${requestUrl} (${failure})`)
return cb(new Error(failure))
}

cb(null, data)
const warning = data['warning message'] && arr2text(data['warning message'])
if (warning) {
debug(`warning from ${requestUrl} (${warning})`)
this.client.emit('warning', new Error(warning))
}

debug(`response from ${requestUrl}`)

cb(null, data)
}

_onAnnounceResponse (data) {
Expand Down
4 changes: 1 addition & 3 deletions lib/client/websocket-tracker.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import clone from 'clone'
import Debug from 'debug'
import Peer from '@thaunknown/simple-peer/lite.js'
import Socket from '@thaunknown/simple-websocket'
import Socks from 'socks'
import { arr2text, arr2hex, hex2bin, bin2hex, randomBytes } from 'uint8-util'

import common from '../common.js'
Expand Down Expand Up @@ -185,7 +183,7 @@ class WebSocketTracker extends Tracker {
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:'))
agent = this.client._proxyOpts.socksProxy
}
}
this.socket = socketPool[this.announceUrl] = new Socket({ url: this.announceUrl, agent })
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"chrome-dgram": "^3.0.6",
"clone": "^2.0.0",
"compact2string": "^1.4.1",
"cross-fetch-ponyfill": "^1.0.1",
"debug": "^4.1.1",
"ip": "^1.1.5",
"lru": "^3.1.0",
Expand All @@ -43,7 +44,6 @@
"random-iterate": "^1.0.1",
"run-parallel": "^1.2.0",
"run-series": "^1.1.9",
"simple-get": "^4.0.0",
"socks": "^2.0.0",
"string2compact": "^2.0.0",
"uint8-util": "^2.1.9",
Expand All @@ -57,6 +57,7 @@
"semantic-release": "21.1.2",
"standard": "*",
"tape": "5.7.2",
"undici": "^5.27.0",
"webtorrent-fixtures": "2.0.2",
"wrtc": "0.4.7"
},
Expand Down
28 changes: 23 additions & 5 deletions test/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import http from 'http'
import fixtures from 'webtorrent-fixtures'
import net from 'net'
import test from 'tape'
import undici from 'undici'

const peerId1 = Buffer.from('01234567890123456789')
const peerId2 = Buffer.from('12345678901234567890')
Expand Down Expand Up @@ -572,12 +573,29 @@ 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)
let agent
if (global.fetch && serverType !== 'ws') {
const connector = undici.buildConnector({ rejectUnauthorized: false })
agent = new undici.Agent({
connect (opts, cb) {
agentUsed = true
connector(opts, (err, socket) => {
if (err) {
cb(err, null)
} else {
cb(null, socket)
}
})
}
})
} else {
agent = new http.Agent()
agent.createConnection = function (opts, fn) {
agentUsed = true
return net.createConnection(opts, fn)
}
}
let agentUsed = false
const client = new Client({
infoHash: fixtures.leaves.parsedTorrent.infoHash,
announce: announceUrl,
Expand Down
Loading