diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64f6689e0..0bb7ae248 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: os: - ubuntu-latest node: - - '14' + - '18' steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5302563f..61c6902cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,9 +17,9 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 18 - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 546af60ae..2b2bab3bc 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?' diff --git a/AUTHORS.md b/AUTHORS.md index e6d57d90f..3b930ae6f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -62,5 +62,13 @@ - Lookis (lookisliu@gmail.com) - Paul Sharypov (pavloniym@gmail.com) - Cas (6506529+ThaUnknown@users.noreply.github.com) +- Tom Snelling (tomsnelling8@gmail.com) +- Cas_ (6506529+ThaUnknown@users.noreply.github.com) +- Arsène Fougerouse (arsene582@gmail.com) +- Brad Marsden (silentbot1@gmail.com) +- krazak (krazak@vt.edu) +- Chocobozzz (chocobozzz@cpy.re) +- uriva (uriva@users.noreply.github.com) +- Subin Siby (mail@subinsb.com) #### Generated by tools/update-authors.sh. diff --git a/CHANGELOG.md b/CHANGELOG.md index c99cb6f63..6b00b4dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,171 @@ +## [11.2.2](https://github.com/webtorrent/bittorrent-tracker/compare/v11.2.1...v11.2.2) (2025-09-06) + + +### Bug Fixes + +* export WebSocketTracker ([#558](https://github.com/webtorrent/bittorrent-tracker/issues/558)) ([1571551](https://github.com/webtorrent/bittorrent-tracker/commit/15715518decfed77d7888ba21d6ab592fa91cc85)) + +## [11.2.1](https://github.com/webtorrent/bittorrent-tracker/compare/v11.2.0...v11.2.1) (2025-01-19) + + +### Bug Fixes + +* http announce no left ([#548](https://github.com/webtorrent/bittorrent-tracker/issues/548)) ([3cd77f3](https://github.com/webtorrent/bittorrent-tracker/commit/3cd77f3e6f5b52f5d58adaf004b333cd2061a4da)) + +# [11.2.0](https://github.com/webtorrent/bittorrent-tracker/compare/v11.1.2...v11.2.0) (2024-12-28) + + +### Features + +* release [#539](https://github.com/webtorrent/bittorrent-tracker/issues/539) and [#535](https://github.com/webtorrent/bittorrent-tracker/issues/535) ([#544](https://github.com/webtorrent/bittorrent-tracker/issues/544)) ([e7de90c](https://github.com/webtorrent/bittorrent-tracker/commit/e7de90c0cbcfb41c9c53c5caf69cc37c6d3ef1e8)) + +## [11.1.2](https://github.com/webtorrent/bittorrent-tracker/compare/v11.1.1...v11.1.2) (2024-08-13) + + +### Bug Fixes + +* statuscode ([#526](https://github.com/webtorrent/bittorrent-tracker/issues/526)) ([e9d8f8c](https://github.com/webtorrent/bittorrent-tracker/commit/e9d8f8cd754ba26d86f32f9b8da0c0c4a3dcd646)) + +## [11.1.1](https://github.com/webtorrent/bittorrent-tracker/compare/v11.1.0...v11.1.1) (2024-07-01) + + +### Performance Improvements + +* drop clone ([#523](https://github.com/webtorrent/bittorrent-tracker/issues/523)) ([83a24ce](https://github.com/webtorrent/bittorrent-tracker/commit/83a24ce77fb1a96b7fe4c383ce92d7c28fc165a7)) + +# [11.1.0](https://github.com/webtorrent/bittorrent-tracker/compare/v11.0.2...v11.1.0) (2024-05-22) + + +### Bug Fixes + +* semantic release ([#520](https://github.com/webtorrent/bittorrent-tracker/issues/520)) ([428fb22](https://github.com/webtorrent/bittorrent-tracker/commit/428fb224f5666731332738032649f4448b2e1e4f)) + + +### Features + +* updated webrtc implementation ([#519](https://github.com/webtorrent/bittorrent-tracker/issues/519)) ([633d68a](https://github.com/webtorrent/bittorrent-tracker/commit/633d68a32c2c143fec0182317a9801dd1b64faef)) + +## [11.0.2](https://github.com/webtorrent/bittorrent-tracker/compare/v11.0.1...v11.0.2) (2024-03-12) + + +### Bug Fixes + +* **parse-http:** ignore announcements from peers with invalid announcement ports. ([#513](https://github.com/webtorrent/bittorrent-tracker/issues/513)) ([fe75272](https://github.com/webtorrent/bittorrent-tracker/commit/fe75272d51653e626583689081afb0b7aeadb84f)) + +## [11.0.1](https://github.com/webtorrent/bittorrent-tracker/compare/v11.0.0...v11.0.1) (2024-01-16) + + +### Bug Fixes + +* update build badge url ([#506](https://github.com/webtorrent/bittorrent-tracker/issues/506)) ([3f59b58](https://github.com/webtorrent/bittorrent-tracker/commit/3f59b58a020ea8c0926be135471a6666fe8e8b21)) + +# [11.0.0](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.12...v11.0.0) (2023-10-31) + + +### Features + +* **major:** drop simple-get ([#443](https://github.com/webtorrent/bittorrent-tracker/issues/443)) ([bce64e1](https://github.com/webtorrent/bittorrent-tracker/commit/bce64e155df6ff9fa605898cbf7498bf76188d8b)) + + +### BREAKING CHANGES + +* **major:** drop simple-get + +* perf: drop simple-get + +* feat: undici agent and socks + +* fix: undici as dev dependency + +* feat: require user passed proxy objects for http and ws + +* chore: include undici for tests + +## [10.0.12](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.11...v10.0.12) (2023-08-09) + + +### Bug Fixes + +* **deps:** update dependency bencode to v4 ([#487](https://github.com/webtorrent/bittorrent-tracker/issues/487)) ([aeccf9c](https://github.com/webtorrent/bittorrent-tracker/commit/aeccf9c1c4b9115fd23b4fe1a0ab990b5add0f17)) + +## [10.0.11](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.10...v10.0.11) (2023-08-01) + + +### Bug Fixes + +* mangled scrape infohashes ([#486](https://github.com/webtorrent/bittorrent-tracker/issues/486)) ([11cce83](https://github.com/webtorrent/bittorrent-tracker/commit/11cce83ddd858813f5684da8a116de4bee6e518b)) + +## [10.0.10](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.9...v10.0.10) (2023-06-16) + + +### Performance Improvements + +* use simple-peer/lite ([#475](https://github.com/webtorrent/bittorrent-tracker/issues/475)) ([5b8db06](https://github.com/webtorrent/bittorrent-tracker/commit/5b8db067e48cc81796728ff538d7ff6efafc59b8)) + +## [10.0.9](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.8...v10.0.9) (2023-06-16) + + +### Performance Improvements + +* use peer/lite ([#474](https://github.com/webtorrent/bittorrent-tracker/issues/474)) ([7c845f0](https://github.com/webtorrent/bittorrent-tracker/commit/7c845f030d07b1bf7060ab880b790ee85a8c7ac0)) + +## [10.0.8](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.7...v10.0.8) (2023-06-07) + + +### Bug Fixes + +* bigInt ([#472](https://github.com/webtorrent/bittorrent-tracker/issues/472)) ([d7061f7](https://github.com/webtorrent/bittorrent-tracker/commit/d7061f73b2ebff072e064971a5960749a7335bae)) + +## [10.0.7](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.6...v10.0.7) (2023-06-05) + + +### Bug Fixes + +* imports ([#471](https://github.com/webtorrent/bittorrent-tracker/issues/471)) ([a12022a](https://github.com/webtorrent/bittorrent-tracker/commit/a12022ac2c81d7fa3ecb81163852161e64199cf4)) + +## [10.0.6](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.5...v10.0.6) (2023-05-27) + + +### Bug Fixes + +* replace simple-peer with maintained one ([#466](https://github.com/webtorrent/bittorrent-tracker/issues/466)) ([3b2dedb](https://github.com/webtorrent/bittorrent-tracker/commit/3b2dedb4151615831ca12d3d0a830354b1c04e68)) + +## [10.0.5](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.4...v10.0.5) (2023-05-27) + + +### Bug Fixes + +* only stringify views ([#467](https://github.com/webtorrent/bittorrent-tracker/issues/467)) ([52f5502](https://github.com/webtorrent/bittorrent-tracker/commit/52f55020f38894e4d45e12c87184540d8b0acad3)) + +## [10.0.4](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.3...v10.0.4) (2023-05-26) + + +### Bug Fixes + +* drop buffer ([#465](https://github.com/webtorrent/bittorrent-tracker/issues/465)) ([c99eb89](https://github.com/webtorrent/bittorrent-tracker/commit/c99eb892088ef3c67ea5bf014dfdd86799251a7e)) + +## [10.0.3](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.2...v10.0.3) (2023-05-25) + + +### Performance Improvements + +* replace simple websocket with maintained one ([#464](https://github.com/webtorrent/bittorrent-tracker/issues/464)) ([3f01c29](https://github.com/webtorrent/bittorrent-tracker/commit/3f01c29122efd726d805673da82f43ce5592b793)) + +## [10.0.2](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.1...v10.0.2) (2023-02-01) + + +### Bug Fixes + +* **deps:** update dependency ws to v8 ([#448](https://github.com/webtorrent/bittorrent-tracker/issues/448)) ([2209d4f](https://github.com/webtorrent/bittorrent-tracker/commit/2209d4f21bdee10e575c1728c3accf7bd34380c9)), closes [#449](https://github.com/webtorrent/bittorrent-tracker/issues/449) + +## [10.0.1](https://github.com/webtorrent/bittorrent-tracker/compare/v10.0.0...v10.0.1) (2022-12-07) + + +### Bug Fixes + +* **deps:** update dependency bencode to v3 ([#434](https://github.com/webtorrent/bittorrent-tracker/issues/434)) [skip ci] ([926ceee](https://github.com/webtorrent/bittorrent-tracker/commit/926ceee0bac6dfe49877566aaa3cf645689492d1)) +* **deps:** update dependency string2compact to v2 ([#437](https://github.com/webtorrent/bittorrent-tracker/issues/437)) ([9be843c](https://github.com/webtorrent/bittorrent-tracker/commit/9be843c5e46ac2ab518187bf0d348e1e69e8633d)) + # [10.0.0](https://github.com/webtorrent/bittorrent-tracker/compare/v9.19.0...v10.0.0) (2022-12-05) diff --git a/README.md b/README.md index 1f2a791cb..db0eba161 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bittorrent-tracker [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] -[ci-image]: https://img.shields.io/github/workflow/status/webtorrent/bittorrent-tracker/ci/master +[ci-image]: https://img.shields.io/github/actions/workflow/status/webtorrent/bittorrent-tracker/ci.yml [ci-url]: https://github.com/webtorrent/bittorrent-tracker/actions [npm-image]: https://img.shields.io/npm/v/bittorrent-tracker.svg [npm-url]: https://npmjs.org/package/bittorrent-tracker @@ -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 @@ -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! @@ -182,7 +159,8 @@ 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' +// Or import Server from 'bittorrent-tracker/server' const server = new Server({ udp: true, // enable udp server? [default=true] @@ -289,7 +267,9 @@ 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' +// Or import Client from 'bittorrent-tracker/client' + Client.scrape({ announce: announceUrl, infoHash: [ infoHash1, infoHash2 ]}, function (err, results) { results[infoHash1].announce results[infoHash1].infoHash diff --git a/bin/cmd.js b/bin/cmd.js index fb7e06536..c57eef55b 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -86,22 +86,22 @@ const server = new Server({ }) server.on('error', err => { - if (!argv.silent) console.error(`ERROR: ${err.message}`) + if (!argv.silent) console.error(`${new Date().toISOString()} ERROR: ${err.message}`) }) server.on('warning', err => { - if (!argv.quiet) console.log(`WARNING: ${err.message}`) + if (!argv.quiet) console.log(`${new Date().toISOString()} WARNING: ${err.message}`) }) server.on('update', addr => { - if (!argv.quiet) console.log(`update: ${addr}`) + if (!argv.quiet) console.log(`${new Date().toISOString()} update: ${addr}`) }) server.on('complete', addr => { - if (!argv.quiet) console.log(`complete: ${addr}`) + if (!argv.quiet) console.log(`${new Date().toISOString()} complete: ${addr}`) }) server.on('start', addr => { - if (!argv.quiet) console.log(`start: ${addr}`) + if (!argv.quiet) console.log(`${new Date().toISOString()} start: ${addr}`) }) server.on('stop', addr => { - if (!argv.quiet) console.log(`stop: ${addr}`) + if (!argv.quiet) console.log(`${new Date().toISOString()} stop: ${addr}`) }) const hostname = { @@ -115,30 +115,30 @@ server.listen(argv.port, hostname, () => { const httpAddr = server.http.address() const httpHost = httpAddr.address !== '::' ? httpAddr.address : 'localhost' const httpPort = httpAddr.port - console.log(`HTTP tracker: http://${httpHost}:${httpPort}/announce`) + console.log(`${new Date().toISOString()} HTTP tracker: http://${httpHost}:${httpPort}/announce`) } if (server.udp && !argv.quiet) { const udpAddr = server.udp.address() const udpHost = udpAddr.address const udpPort = udpAddr.port - console.log(`UDP tracker: udp://${udpHost}:${udpPort}`) + console.log(`${new Date().toISOString()} UDP tracker: udp://${udpHost}:${udpPort}`) } if (server.udp6 && !argv.quiet) { const udp6Addr = server.udp6.address() const udp6Host = udp6Addr.address !== '::' ? udp6Addr.address : 'localhost' const udp6Port = udp6Addr.port - console.log(`UDP6 tracker: udp://${udp6Host}:${udp6Port}`) + console.log(`${new Date().toISOString()} UDP6 tracker: udp://${udp6Host}:${udp6Port}`) } if (server.ws && !argv.quiet) { const wsAddr = server.http.address() const wsHost = wsAddr.address !== '::' ? wsAddr.address : 'localhost' const wsPort = wsAddr.port - console.log(`WebSocket tracker: ws://${wsHost}:${wsPort}`) + console.log(`${new Date().toISOString()} WebSocket tracker: ws://${wsHost}:${wsPort}`) } if (server.http && argv.stats && !argv.quiet) { const statsAddr = server.http.address() const statsHost = statsAddr.address !== '::' ? statsAddr.address : 'localhost' const statsPort = statsAddr.port - console.log(`Tracker stats: http://${statsHost}:${statsPort}/stats`) + console.log(`${new Date().toISOString()} Tracker stats: http://${statsHost}:${statsPort}/stats`) } }) diff --git a/client.js b/client.js index c90d7f90b..c37345f17 100644 --- a/client.js +++ b/client.js @@ -2,8 +2,9 @@ import Debug from 'debug' import EventEmitter from 'events' import once from 'once' import parallel from 'run-parallel' -import Peer from 'simple-peer' +import Peer from '@thaunknown/simple-peer/lite.js' import queueMicrotask from 'queue-microtask' +import { hex2arr, hex2bin, text2arr, arr2hex, arr2text } from 'uint8-util' import common from './lib/common.js' import HTTPTracker from './lib/client/http-tracker.js' // empty object in browser @@ -18,8 +19,8 @@ const debug = Debug('bittorrent-tracker:client') * Find torrent peers, to help a torrent client participate in a torrent swarm. * * @param {Object} opts options object - * @param {string|Buffer} opts.infoHash torrent info hash - * @param {string|Buffer} opts.peerId peer id + * @param {string|Uint8Array} opts.infoHash torrent info hash + * @param {string|Uint8Array} opts.peerId peer id * @param {string|Array.} opts.announce announce * @param {number} opts.port torrent client listening port * @param {function} opts.getAnnounceOpts callback to provide data to tracker @@ -39,15 +40,15 @@ class Client extends EventEmitter { this.peerId = typeof opts.peerId === 'string' ? opts.peerId - : opts.peerId.toString('hex') - this._peerIdBuffer = Buffer.from(this.peerId, 'hex') - this._peerIdBinary = this._peerIdBuffer.toString('binary') + : arr2hex(opts.peerId) + this._peerIdBuffer = hex2arr(this.peerId) + this._peerIdBinary = hex2bin(this.peerId) this.infoHash = typeof opts.infoHash === 'string' ? opts.infoHash.toLowerCase() - : opts.infoHash.toString('hex') - this._infoHashBuffer = Buffer.from(this.infoHash, 'hex') - this._infoHashBinary = this._infoHashBuffer.toString('binary') + : arr2hex(opts.infoHash) + this._infoHashBuffer = hex2arr(this.infoHash) + this._infoHashBinary = hex2bin(this.infoHash) debug('new client %s', this.infoHash) @@ -69,7 +70,7 @@ class Client extends EventEmitter { // Remove trailing slash from trackers to catch duplicates announce = announce.map(announceUrl => { - announceUrl = announceUrl.toString() + if (ArrayBuffer.isView(announceUrl)) announceUrl = arr2text(announceUrl) if (announceUrl[announceUrl.length - 1] === '/') { announceUrl = announceUrl.substring(0, announceUrl.length - 1) } @@ -260,7 +261,7 @@ Client.scrape = (opts, cb) => { const clientOpts = Object.assign({}, opts, { infoHash: Array.isArray(opts.infoHash) ? opts.infoHash[0] : opts.infoHash, - peerId: Buffer.from('01234567890123456789'), // dummy value + peerId: text2arr('01234567890123456789'), // dummy value port: 6881 // dummy value }) @@ -284,9 +285,6 @@ Client.scrape = (opts, cb) => { } }) - opts.infoHash = Array.isArray(opts.infoHash) - ? opts.infoHash.map(infoHash => Buffer.from(infoHash, 'hex')) - : Buffer.from(opts.infoHash, 'hex') client.scrape({ infoHash: opts.infoHash }) return client } diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js index 8dfe718a1..43464e8eb 100644 --- a/lib/client/http-tracker.js +++ b/lib/client/http-tracker.js @@ -1,9 +1,8 @@ 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' import Tracker from './tracker.js' @@ -12,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) * @@ -48,6 +55,8 @@ class HTTPTracker extends Tracker { peer_id: this.client._peerIdBinary, port: this.client._port }) + + if (params.left !== 0 && !params.left) params.left = 16384 if (this._trackerId) params.trackerid = this._trackerId this._request(this.announceUrl, params, (err, data) => { @@ -65,8 +74,8 @@ class HTTPTracker extends Tracker { } const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(infoHash => infoHash.toString('binary')) - : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + ? opts.infoHash.map(infoHash => hex2bin(infoHash)) + : (opts.infoHash && hex2bin(opts.infoHash)) || this.client._infoHashBinary const params = { info_hash: infoHashes } @@ -111,70 +120,73 @@ 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 || '' + } + }) + if (res.body.on) res.body.on('error', cb) + } 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'] - 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'] - 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.status} 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) { @@ -189,15 +201,15 @@ class HTTPTracker extends Tracker { const response = Object.assign({}, data, { announce: this.announceUrl, - infoHash: common.binaryToHex(data.info_hash) + infoHash: bin2hex(data.info_hash || String(data.info_hash)) }) this.client.emit('update', response) let addrs - if (Buffer.isBuffer(data.peers)) { + if (ArrayBuffer.isView(data.peers)) { // tracker returned compact response try { - addrs = compact2string.multi(data.peers) + addrs = compact2string.multi(Buffer.from(data.peers)) } catch (err) { return this.client.emit('warning', err) } @@ -211,10 +223,10 @@ class HTTPTracker extends Tracker { }) } - if (Buffer.isBuffer(data.peers6)) { + if (ArrayBuffer.isView(data.peers6)) { // tracker returned compact response try { - addrs = compact2string.multi6(data.peers6) + addrs = compact2string.multi6(Buffer.from(data.peers6)) } catch (err) { return this.client.emit('warning', err) } @@ -243,12 +255,14 @@ class HTTPTracker extends Tracker { return } - keys.forEach(infoHash => { + keys.forEach(_infoHash => { // TODO: optionally handle data.flags.min_request_interval // (separate from announce interval) - const response = Object.assign(data[infoHash], { + const infoHash = _infoHash.length !== 20 ? arr2hex(text2arr(_infoHash)) : bin2hex(_infoHash) + + const response = Object.assign(data[_infoHash], { announce: this.announceUrl, - infoHash: common.binaryToHex(infoHash) + infoHash }) this.client.emit('scrape', response) }) diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js index 9162d76fb..b4ac4bdba 100644 --- a/lib/client/udp-tracker.js +++ b/lib/client/udp-tracker.js @@ -1,10 +1,8 @@ import arrayRemove from 'unordered-array-remove' -import BN from 'bn.js' -import clone from 'clone' import Debug from 'debug' import dgram from 'dgram' -import randombytes from 'randombytes' import Socks from 'socks' +import { concat, hex2arr, randomBytes } from 'uint8-util' import common from '../common.js' import Tracker from './tracker.js' @@ -12,6 +10,9 @@ import compact2string from 'compact2string' const debug = Debug('bittorrent-tracker:udp-tracker') +// this was done some many years ago to fix "prevent Socks instances concurrency", and used some bloated package, no clue if it's needed, but this is simpler, #356 +const clone = obj => JSON.parse(JSON.stringify(obj)) + /** * UDP torrent tracker client (for an individual tracker) * @@ -131,7 +132,7 @@ class UDPTracker extends Tracker { }, common.REQUEST_TIMEOUT) if (timeout.unref) timeout.unref() - send(Buffer.concat([ + send(concat([ common.CONNECTION_ID, common.toUInt32(common.ACTIONS.CONNECT), transactionId @@ -175,7 +176,8 @@ class UDPTracker extends Tracker { function onSocketMessage (msg) { if (proxySocket) msg = msg.slice(10) - if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { + const view = new DataView(transactionId.buffer) + if (msg.length < 8 || msg.readUInt32BE(4) !== view.getUint32(0)) { return onError(new Error('tracker sent invalid transaction id')) } @@ -270,14 +272,14 @@ class UDPTracker extends Tracker { function announce (connectionId, opts) { transactionId = genTransactionId() - send(Buffer.concat([ + send(concat([ connectionId, common.toUInt32(common.ACTIONS.ANNOUNCE), transactionId, self.client._infoHashBuffer, self.client._peerIdBuffer, toUInt64(opts.downloaded), - opts.left != null ? toUInt64(opts.left) : Buffer.from('FFFFFFFFFFFFFFFF', 'hex'), + opts.left != null ? toUInt64(opts.left) : hex2arr('ffffffffffffffff'), toUInt64(opts.uploaded), common.toUInt32(common.EVENTS[opts.event] || 0), common.toUInt32(0), // ip address (optional) @@ -291,10 +293,10 @@ class UDPTracker extends Tracker { transactionId = genTransactionId() const infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? Buffer.concat(opts.infoHash) + ? concat(opts.infoHash) : (opts.infoHash || self.client._infoHashBuffer) - send(Buffer.concat([ + send(concat([ connectionId, common.toUInt32(common.ACTIONS.SCRAPE), transactionId, @@ -307,12 +309,13 @@ class UDPTracker extends Tracker { UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes function genTransactionId () { - return randombytes(4) + return randomBytes(4) } function toUInt16 (n) { - const buf = Buffer.allocUnsafe(2) - buf.writeUInt16BE(n, 0) + const buf = new Uint8Array(2) + const view = new DataView(buf.buffer) + view.setUint16(0, n) return buf } @@ -320,13 +323,12 @@ const MAX_UINT = 4294967295 function toUInt64 (n) { if (n > MAX_UINT || typeof n === 'string') { - const bytes = new BN(n).toArray() - while (bytes.length < 8) { - bytes.unshift(0) - } - return Buffer.from(bytes) + const buf = new Uint8Array(8) + const view = new DataView(buf.buffer) + view.setBigUint64(0, BigInt(n)) + return buf } - return Buffer.concat([common.toUInt32(0), common.toUInt32(n)]) + return concat([new Uint8Array(4), common.toUInt32(n)]) } function noop () {} diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index 3799510a8..c4e3e23d7 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -1,9 +1,7 @@ -import clone from 'clone' import Debug from 'debug' -import Peer from 'simple-peer' -import randombytes from 'randombytes' -import Socket from 'simple-websocket' -import Socks from 'socks' +import Peer from '@thaunknown/simple-peer/lite.js' +import Socket from '@thaunknown/simple-websocket' +import { arr2text, arr2hex, hex2bin, bin2hex, randomBytes } from 'uint8-util' import common from '../common.js' import Tracker from './tracker.js' @@ -80,8 +78,8 @@ class WebSocketTracker extends Tracker { } const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(infoHash => infoHash.toString('binary')) - : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + ? opts.infoHash.map(infoHash => hex2bin(infoHash)) + : (opts.infoHash && hex2bin(opts.infoHash)) || this.client._infoHashBinary const params = { action: 'scrape', info_hash: infoHashes @@ -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 }) @@ -214,7 +212,7 @@ class WebSocketTracker extends Tracker { this.expectingResponse = false try { - data = JSON.parse(data) + data = JSON.parse(arr2text(data)) } catch (err) { this.client.emit('warning', new Error('Invalid tracker response')) return @@ -233,7 +231,7 @@ class WebSocketTracker extends Tracker { if (data.info_hash !== this.client._infoHashBinary) { debug( 'ignoring websocket data from %s for %s (looking for %s: reused socket)', - this.announceUrl, common.binaryToHex(data.info_hash), this.client.infoHash + this.announceUrl, bin2hex(data.info_hash), this.client.infoHash ) return } @@ -266,7 +264,7 @@ class WebSocketTracker extends Tracker { if (data.complete != null) { const response = Object.assign({}, data, { announce: this.announceUrl, - infoHash: common.binaryToHex(data.info_hash) + infoHash: bin2hex(data.info_hash) }) this.client.emit('update', response) } @@ -275,7 +273,7 @@ class WebSocketTracker extends Tracker { if (data.offer && data.peer_id) { debug('creating peer (from remote offer)') peer = this._createPeer() - peer.id = common.binaryToHex(data.peer_id) + peer.id = bin2hex(data.peer_id) peer.once('signal', answer => { const params = { action: 'announce', @@ -293,10 +291,10 @@ class WebSocketTracker extends Tracker { } if (data.answer && data.peer_id) { - const offerId = common.binaryToHex(data.offer_id) + const offerId = bin2hex(data.offer_id) peer = this.peers[offerId] if (peer) { - peer.id = common.binaryToHex(data.peer_id) + peer.id = bin2hex(data.peer_id) this.client.emit('peer', peer) peer.signal(data.answer) @@ -323,7 +321,7 @@ class WebSocketTracker extends Tracker { // (separate from announce interval) const response = Object.assign(data[infoHash], { announce: this.announceUrl, - infoHash: common.binaryToHex(infoHash) + infoHash: bin2hex(infoHash) }) this.client.emit('scrape', response) }) @@ -376,13 +374,13 @@ class WebSocketTracker extends Tracker { checkDone() function generateOffer () { - const offerId = randombytes(20).toString('hex') + const offerId = arr2hex(randomBytes(20)) debug('creating peer (from _generateOffers)') const peer = self.peers[offerId] = self._createPeer({ initiator: true }) peer.once('signal', offer => { offers.push({ offer, - offer_id: common.hexToBinary(offerId) + offer_id: hex2bin(offerId) }) checkDone() }) diff --git a/lib/common-node.js b/lib/common-node.js index 18af19c95..9fefa5c4d 100644 --- a/lib/common-node.js +++ b/lib/common-node.js @@ -4,12 +4,13 @@ */ import querystring from 'querystring' +import { concat } from 'uint8-util' export const IPV4_RE = /^[\d.]+$/ export const IPV6_RE = /^[\da-fA-F:]+$/ export const REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ -export const CONNECTION_ID = Buffer.concat([toUInt32(0x417), toUInt32(0x27101980)]) +export const CONNECTION_ID = concat([toUInt32(0x417), toUInt32(0x27101980)]) export const ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } export const EVENTS = { update: 0, completed: 1, started: 2, stopped: 3, paused: 4 } export const EVENT_IDS = { @@ -40,8 +41,9 @@ export const REQUEST_TIMEOUT = 15000 export const DESTROY_TIMEOUT = 1000 export function toUInt32 (n) { - const buf = Buffer.allocUnsafe(4) - buf.writeUInt32BE(n, 0) + const buf = new Uint8Array(4) + const view = new DataView(buf.buffer) + view.setUint32(0, n) return buf } diff --git a/lib/common.js b/lib/common.js index d418a3236..ef888579c 100644 --- a/lib/common.js +++ b/lib/common.js @@ -2,24 +2,11 @@ * Functions/constants needed by both the client and server. */ import * as common from './common-node.js' +export * from './common-node.js' export const DEFAULT_ANNOUNCE_PEERS = 50 export const MAX_ANNOUNCE_PEERS = 82 -export const binaryToHex = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'binary').toString('hex') -} - -export const hexToBinary = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'hex').toString('binary') -} - // HACK: Fix for WHATWG URL object not parsing non-standard URL schemes like // 'udp:'. Just replace it with 'http:' since we only need a few properties. // @@ -49,8 +36,6 @@ export const parseUrl = str => { export default { DEFAULT_ANNOUNCE_PEERS, MAX_ANNOUNCE_PEERS, - binaryToHex, - hexToBinary, parseUrl, ...common } diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js index 5847251f4..97b46e307 100644 --- a/lib/server/parse-http.js +++ b/lib/server/parse-http.js @@ -1,8 +1,8 @@ -import common from '../common.js' +import { bin2hex } from 'uint8-util' -export default parseHttpRequest +import common from '../common.js' -function parseHttpRequest (req, opts) { +export default function (req, opts) { if (!opts) opts = {} const s = req.url.split('?') const params = common.querystringParse(s[1]) @@ -14,15 +14,15 @@ function parseHttpRequest (req, opts) { if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { throw new Error('invalid info_hash') } - params.info_hash = common.binaryToHex(params.info_hash) + params.info_hash = bin2hex(params.info_hash) if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { throw new Error('invalid peer_id') } - params.peer_id = common.binaryToHex(params.peer_id) + params.peer_id = bin2hex(params.peer_id) params.port = Number(params.port) - if (!params.port) throw new Error('invalid port') + if (!params.port || params.port <= 0 || params.port > 65535) throw new Error('invalid port') params.left = Number(params.left) if (Number.isNaN(params.left)) params.left = Infinity @@ -33,9 +33,17 @@ function parseHttpRequest (req, opts) { common.MAX_ANNOUNCE_PEERS ) - params.ip = opts.trustProxy - ? req.headers['x-forwarded-for'] || req.connection.remoteAddress - : req.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + if (opts.trustProxy) { + if (req.headers['x-forwarded-for']) { + const [realIp] = req.headers['x-forwarded-for'].split(',') + params.ip = realIp.trim() + } else { + params.ip = req.connection.remoteAddress + } + } else { + params.ip = req.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + } + params.addr = `${common.IPV6_RE.test(params.ip) ? `[${params.ip}]` : params.ip}:${params.port}` params.headers = req.headers @@ -48,7 +56,7 @@ function parseHttpRequest (req, opts) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { throw new Error('invalid info_hash') } - return common.binaryToHex(binaryInfoHash) + return bin2hex(binaryInfoHash) }) } } else { diff --git a/lib/server/parse-udp.js b/lib/server/parse-udp.js index 935420cd3..f73b64917 100644 --- a/lib/server/parse-udp.js +++ b/lib/server/parse-udp.js @@ -1,9 +1,8 @@ import ipLib from 'ip' import common from '../common.js' +import { equal } from 'uint8-util' -export default parseUdpRequest - -function parseUdpRequest (msg, rinfo) { +export default function (msg, rinfo) { if (msg.length < 16) throw new Error('received packet is too short') const params = { @@ -13,7 +12,7 @@ function parseUdpRequest (msg, rinfo) { type: 'udp' } - if (!common.CONNECTION_ID.equals(params.connectionId)) { + if (!equal(common.CONNECTION_ID, params.connectionId)) { throw new Error('received packet with invalid connection id') } diff --git a/lib/server/parse-websocket.js b/lib/server/parse-websocket.js index 5aeba9e16..744a3c30b 100644 --- a/lib/server/parse-websocket.js +++ b/lib/server/parse-websocket.js @@ -1,8 +1,8 @@ -import common from '../common.js' +import { bin2hex } from 'uint8-util' -export default parseWebSocketRequest +import common from '../common.js' -function parseWebSocketRequest (socket, opts, params) { +export default function (socket, opts, params) { if (!opts) opts = {} params = JSON.parse(params) // may throw @@ -14,18 +14,18 @@ function parseWebSocketRequest (socket, opts, params) { if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { throw new Error('invalid info_hash') } - params.info_hash = common.binaryToHex(params.info_hash) + params.info_hash = bin2hex(params.info_hash) if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { throw new Error('invalid peer_id') } - params.peer_id = common.binaryToHex(params.peer_id) + params.peer_id = bin2hex(params.peer_id) if (params.answer) { if (typeof params.to_peer_id !== 'string' || params.to_peer_id.length !== 20) { throw new Error('invalid `to_peer_id` (required with `answer`)') } - params.to_peer_id = common.binaryToHex(params.to_peer_id) + params.to_peer_id = bin2hex(params.to_peer_id) } params.left = Number(params.left) @@ -45,7 +45,7 @@ function parseWebSocketRequest (socket, opts, params) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { throw new Error('invalid info_hash') } - return common.binaryToHex(binaryInfoHash) + return bin2hex(binaryInfoHash) }) } } else { @@ -55,9 +55,17 @@ function parseWebSocketRequest (socket, opts, params) { // On first parse, save important data from `socket.upgradeReq` and delete it // to reduce memory usage. if (socket.upgradeReq) { - socket.ip = opts.trustProxy - ? socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress - : socket.upgradeReq.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + if (opts.trustProxy) { + if (socket.upgradeReq.headers['x-forwarded-for']) { + const [realIp] = socket.upgradeReq.headers['x-forwarded-for'].split(',') + socket.ip = realIp.trim() + } else { + socket.ip = socket.upgradeReq.connection.remoteAddress + } + } else { + socket.ip = socket.upgradeReq.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 + } + socket.port = socket.upgradeReq.connection.remotePort if (socket.port) { socket.addr = `${common.IPV6_RE.test(socket.ip) ? `[${socket.ip}]` : socket.ip}:${socket.port}` diff --git a/package.json b/package.json index 5807bff96..ca9cebd6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bittorrent-tracker", "description": "Simple, robust, BitTorrent tracker (client & server) implementation", - "version": "10.0.0", + "version": "11.2.2", "author": { "name": "WebTorrent LLC", "email": "feross@webtorrent.io", @@ -27,44 +27,55 @@ }, "type": "module", "dependencies": { - "bencode": "^2.0.1", - "bittorrent-peerid": "^1.3.3", - "bn.js": "^5.2.0", + "@thaunknown/simple-peer": "^10.0.8", + "@thaunknown/simple-websocket": "^9.1.3", + "bencode": "^4.0.0", + "bittorrent-peerid": "^1.3.6", "chrome-dgram": "^3.0.6", - "clone": "^2.0.0", "compact2string": "^1.4.1", - "debug": "^4.1.1", - "ip": "^1.1.5", + "cross-fetch-ponyfill": "^1.0.3", + "debug": "^4.3.4", + "ip": "^2.0.1", "lru": "^3.1.0", - "minimist": "^1.2.5", + "minimist": "^1.2.8", "once": "^1.4.0", "queue-microtask": "^1.2.3", "random-iterate": "^1.0.1", - "randombytes": "^2.1.0", "run-parallel": "^1.2.0", "run-series": "^1.1.9", - "simple-get": "^4.0.0", - "simple-peer": "^9.11.0", - "simple-websocket": "^9.1.0", - "socks": "^2.0.0", - "string2compact": "^1.3.0", + "socks": "^2.8.3", + "string2compact": "^2.0.1", + "uint8-util": "^2.2.5", "unordered-array-remove": "^1.0.2", - "ws": "^7.4.5" + "ws": "^8.17.0" }, "devDependencies": { - "@webtorrent/semantic-release-config": "1.0.8", - "magnet-uri": "6.2.0", - "semantic-release": "18.0.1", + "@mapbox/node-pre-gyp": "1.0.11", + "@webtorrent/semantic-release-config": "1.0.10", + "magnet-uri": "7.0.7", + "semantic-release": "21.1.2", "standard": "*", - "tape": "5.6.1", - "webtorrent-fixtures": "1.7.5", - "wrtc": "0.4.7" + "tape": "5.9.0", + "undici": "^6.16.1", + "webrtc-polyfill": "^1.1.5", + "webtorrent-fixtures": "2.0.2" }, "engines": { - "node": ">=12.20.0" + "node": ">=16.0.0" }, "exports": { - "import": "./index.js" + ".": { + "import": "./index.js" + }, + "./client": { + "import": "./client.js" + }, + "./server": { + "import": "./server.js" + }, + "./websocket-tracker": { + "import": "./lib/client/websocket-tracker.js" + } }, "keywords": [ "bittorrent", @@ -79,8 +90,8 @@ "license": "MIT", "main": "index.js", "optionalDependencies": { - "bufferutil": "^4.0.3", - "utf-8-validate": "^5.0.5" + "bufferutil": "^4.0.8", + "utf-8-validate": "^6.0.4" }, "repository": { "type": "git", diff --git a/server.js b/server.js index 3ef4349c3..f7023d99f 100644 --- a/server.js +++ b/server.js @@ -6,7 +6,8 @@ import http from 'http' import peerid from 'bittorrent-peerid' import series from 'run-series' import string2compact from 'string2compact' -import ws from 'ws' +import { WebSocketServer } from 'ws' +import { hex2bin } from 'uint8-util' import common from './lib/common.js' import Swarm from './lib/server/swarm.js' @@ -14,7 +15,6 @@ import parseHttpRequest from './lib/server/parse-http.js' import parseUdpRequest from './lib/server/parse-udp.js' import parseWebSocketRequest from './lib/server/parse-websocket.js' -const { Server: WebSocketServer } = ws const debug = Debug('bittorrent-tracker:server') const hasOwnProperty = Object.prototype.hasOwnProperty @@ -351,7 +351,7 @@ class Server extends EventEmitter { } createSwarm (infoHash, cb) { - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') + if (ArrayBuffer.isView(infoHash)) infoHash = infoHash.toString('hex') process.nextTick(() => { const swarm = this.torrents[infoHash] = new Server.Swarm(infoHash, this) @@ -360,7 +360,7 @@ class Server extends EventEmitter { } getSwarm (infoHash, cb) { - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') + if (ArrayBuffer.isView(infoHash)) infoHash = infoHash.toString('hex') process.nextTick(() => { cb(null, this.torrents[infoHash]) @@ -489,7 +489,7 @@ class Server extends EventEmitter { socket.send(JSON.stringify({ action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', 'failure reason': err.message, - info_hash: common.hexToBinary(params.info_hash) + info_hash: hex2bin(params.info_hash) }), socket.onSend) this.emit('warning', err) @@ -507,7 +507,7 @@ class Server extends EventEmitter { socket.infoHashes.push(params.info_hash) } - response.info_hash = common.hexToBinary(params.info_hash) + response.info_hash = hex2bin(params.info_hash) // WebSocket tracker should have a shorter interval – default: 2 minutes response.interval = Math.ceil(this.intervalMs / 1000 / 5) @@ -527,8 +527,8 @@ class Server extends EventEmitter { action: 'announce', offer: params.offers[i].offer, offer_id: params.offers[i].offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) + peer_id: hex2bin(params.peer_id), + info_hash: hex2bin(params.info_hash) }), peer.socket.onSend) debug('sent offer to %s from %s', peer.peerId, params.peer_id) }) @@ -560,8 +560,8 @@ class Server extends EventEmitter { action: 'announce', answer: params.answer, offer_id: params.offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) + peer_id: hex2bin(params.peer_id), + info_hash: hex2bin(params.info_hash) }), toPeer.socket.onSend) debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) @@ -686,7 +686,7 @@ class Server extends EventEmitter { } else if (params.compact === 0) { // IPv6 peers are not separate for non-compact responses response.peers = response.peers.map(peer => ({ - 'peer id': common.hexToBinary(peer.peerId), + 'peer id': hex2bin(peer.peerId), ip: peer.ip, port: peer.port })) @@ -730,7 +730,7 @@ class Server extends EventEmitter { } results.forEach(result => { - response.files[common.hexToBinary(result.infoHash)] = { + response.files[hex2bin(result.infoHash)] = { complete: result.complete || 0, incomplete: result.incomplete || 0, downloaded: result.complete || 0 // TODO: this only provides a lower-bound diff --git a/test/client.js b/test/client.js index c579058cf..d81ee55f0 100644 --- a/test/client.js +++ b/test/client.js @@ -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') @@ -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, diff --git a/test/evict.js b/test/evict.js index 76967fd30..385fae2a6 100644 --- a/test/evict.js +++ b/test/evict.js @@ -1,7 +1,7 @@ import Client from '../index.js' import common from './common.js' import test from 'tape' -import wrtc from 'wrtc' +import wrtc from 'webrtc-polyfill' const infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' const peerId = Buffer.from('01234567890123456789') diff --git a/test/request-handler.js b/test/request-handler.js index b24110fae..6532cbbb8 100644 --- a/test/request-handler.js +++ b/test/request-handler.js @@ -47,7 +47,7 @@ function testRequestHandler (t, serverType) { client1.once('update', data => { t.equal(data.complete, 246) - t.equal(data.extraData.toString(), 'hi') + t.equal(Buffer.from(data.extraData).toString(), 'hi') client1.destroy(() => { t.pass('client1 destroyed') diff --git a/test/scrape.js b/test/scrape.js index b702fc684..c0c101a5f 100644 --- a/test/scrape.js +++ b/test/scrape.js @@ -3,8 +3,9 @@ import Client from '../index.js' import common from './common.js' import commonLib from '../lib/common.js' import fixtures from 'webtorrent-fixtures' -import get from 'simple-get' +import fetch from 'cross-fetch-ponyfill' import test from 'tape' +import { hex2bin } from 'uint8-util' const peerId = Buffer.from('01234567890123456789') @@ -150,46 +151,49 @@ test('udp: MULTI scrape using Client.scrape static method', t => { }) test('server: multiple info_hash scrape (manual http request)', t => { - t.plan(13) + t.plan(12) - const binaryInfoHash1 = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) - const binaryInfoHash2 = commonLib.hexToBinary(fixtures.alice.parsedTorrent.infoHash) + const binaryInfoHash1 = hex2bin(fixtures.leaves.parsedTorrent.infoHash) + const binaryInfoHash2 = hex2bin(fixtures.alice.parsedTorrent.infoHash) - common.createServer(t, 'http', (server, announceUrl) => { + common.createServer(t, 'http', async (server, announceUrl) => { const scrapeUrl = announceUrl.replace('/announce', '/scrape') const url = `${scrapeUrl}?${commonLib.querystringStringify({ info_hash: [binaryInfoHash1, binaryInfoHash2] })}` - - get.concat(url, (err, res, data) => { + let res + try { + res = await fetch(url) + } catch (err) { t.error(err) + } + let data = Buffer.from(await res.arrayBuffer()) - t.equal(res.statusCode, 200) + t.equal(res.status, 200) - data = bencode.decode(data) - t.ok(data.files) - t.equal(Object.keys(data.files).length, 2) + data = bencode.decode(data) + t.ok(data.files) + t.equal(Object.keys(data.files).length, 2) - t.ok(data.files[binaryInfoHash1]) - t.equal(typeof data.files[binaryInfoHash1].complete, 'number') - t.equal(typeof data.files[binaryInfoHash1].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash1].downloaded, 'number') + t.ok(data.files[binaryInfoHash1]) + t.equal(typeof data.files[binaryInfoHash1].complete, 'number') + t.equal(typeof data.files[binaryInfoHash1].incomplete, 'number') + t.equal(typeof data.files[binaryInfoHash1].downloaded, 'number') - t.ok(data.files[binaryInfoHash2]) - t.equal(typeof data.files[binaryInfoHash2].complete, 'number') - t.equal(typeof data.files[binaryInfoHash2].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash2].downloaded, 'number') + t.ok(data.files[binaryInfoHash2]) + t.equal(typeof data.files[binaryInfoHash2].complete, 'number') + t.equal(typeof data.files[binaryInfoHash2].incomplete, 'number') + t.equal(typeof data.files[binaryInfoHash2].downloaded, 'number') - server.close(() => { t.pass('server closed') }) - }) + server.close(() => { t.pass('server closed') }) }) }) test('server: all info_hash scrape (manual http request)', t => { - t.plan(10) + t.plan(9) - const binaryInfoHash = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) + const binaryInfoHash = hex2bin(fixtures.leaves.parsedTorrent.infoHash) common.createServer(t, 'http', (server, announceUrl) => { const scrapeUrl = announceUrl.replace('/announce', '/scrape') @@ -206,24 +210,28 @@ test('server: all info_hash scrape (manual http request)', t => { client.start() - server.once('start', () => { + server.once('start', async () => { // now do a scrape of everything by omitting the info_hash param - get.concat(scrapeUrl, (err, res, data) => { + let res + try { + res = await fetch(scrapeUrl) + } catch (err) { t.error(err) + } + let data = Buffer.from(await res.arrayBuffer()) - t.equal(res.statusCode, 200) - data = bencode.decode(data) - t.ok(data.files) - t.equal(Object.keys(data.files).length, 1) + t.equal(res.status, 200) + data = bencode.decode(data) + t.ok(data.files) + t.equal(Object.keys(data.files).length, 1) - t.ok(data.files[binaryInfoHash]) - t.equal(typeof data.files[binaryInfoHash].complete, 'number') - t.equal(typeof data.files[binaryInfoHash].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash].downloaded, 'number') + t.ok(data.files[binaryInfoHash]) + t.equal(typeof data.files[binaryInfoHash].complete, 'number') + t.equal(typeof data.files[binaryInfoHash].incomplete, 'number') + t.equal(typeof data.files[binaryInfoHash].downloaded, 'number') - client.destroy(() => { t.pass('client destroyed') }) - server.close(() => { t.pass('server closed') }) - }) + client.destroy(() => { t.pass('client destroyed') }) + server.close(() => { t.pass('server closed') }) }) }) }) diff --git a/test/server.js b/test/server.js index cb1095e11..d4cf493e4 100644 --- a/test/server.js +++ b/test/server.js @@ -1,7 +1,7 @@ import Client from '../index.js' import common from './common.js' import test from 'tape' -import wrtc from 'wrtc' +import wrtc from 'webrtc-polyfill' const infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' const peerId = Buffer.from('01234567890123456789') diff --git a/test/stats.js b/test/stats.js index 3ffa3fe3f..7223da014 100644 --- a/test/stats.js +++ b/test/stats.js @@ -1,7 +1,7 @@ import Client from '../index.js' import commonTest from './common.js' import fixtures from 'webtorrent-fixtures' -import get from 'simple-get' +import fetch from 'cross-fetch-ponyfill' import test from 'tape' const peerId = Buffer.from('-WW0091-4ea5886ce160') @@ -30,89 +30,94 @@ function parseHtml (html) { } test('server: get empty stats', t => { - t.plan(11) + t.plan(10) - commonTest.createServer(t, 'http', (server, announceUrl) => { + commonTest.createServer(t, 'http', async (server, announceUrl) => { const url = announceUrl.replace('/announce', '/stats') - get.concat(url, (err, res, data) => { + let res + try { + res = await fetch(url) + } catch (err) { t.error(err) - - const stats = parseHtml(data.toString()) - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(() => { t.pass('server closed') }) - }) + } + const data = Buffer.from(await res.arrayBuffer()) + + const stats = parseHtml(data.toString()) + t.equal(res.status, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(() => { t.pass('server closed') }) }) }) test('server: get empty stats with json header', t => { - t.plan(11) + t.plan(10) - commonTest.createServer(t, 'http', (server, announceUrl) => { + commonTest.createServer(t, 'http', async (server, announceUrl) => { const opts = { url: announceUrl.replace('/announce', '/stats'), headers: { accept: 'application/json' - }, - json: true + } } - - get.concat(opts, (err, res, stats) => { + let res + try { + res = await fetch(announceUrl.replace('/announce', '/stats'), opts) + } catch (err) { t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(() => { t.pass('server closed') }) - }) + } + const stats = await res.json() + + t.equal(res.status, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(() => { t.pass('server closed') }) }) }) test('server: get empty stats on stats.json', t => { - t.plan(11) + t.plan(10) - commonTest.createServer(t, 'http', (server, announceUrl) => { - const opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true - } - - get.concat(opts, (err, res, stats) => { + commonTest.createServer(t, 'http', async (server, announceUrl) => { + let res + try { + res = await fetch(announceUrl.replace('/announce', '/stats.json')) + } catch (err) { t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(() => { t.pass('server closed') }) - }) + } + const stats = await res.json() + + t.equal(res.status, 200) + t.equal(stats.torrents, 0) + t.equal(stats.activeTorrents, 0) + t.equal(stats.peersAll, 0) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 0) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.peersIPv4, 0) + t.equal(stats.peersIPv6, 0) + + server.close(() => { t.pass('server closed') }) }) }) test('server: get leecher stats.json', t => { - t.plan(11) + t.plan(10) commonTest.createServer(t, 'http', (server, announceUrl) => { // announce a torrent to the tracker @@ -127,33 +132,32 @@ test('server: get leecher stats.json', t => { client.start() - server.once('start', () => { - const opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true + server.once('start', async () => { + let res + try { + res = await fetch(announceUrl.replace('/announce', '/stats.json')) + } catch (err) { + t.error(err) } + const stats = await res.json() - get.concat(opts, (err, res, stats) => { - t.error(err) + t.equal(res.status, 200) + t.equal(stats.torrents, 1) + t.equal(stats.activeTorrents, 1) + t.equal(stats.peersAll, 1) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 1) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.clients.WebTorrent['0.91'], 1) - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 1) - t.equal(stats.activeTorrents, 1) - t.equal(stats.peersAll, 1) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 1) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients.WebTorrent['0.91'], 1) - - client.destroy(() => { t.pass('client destroyed') }) - server.close(() => { t.pass('server closed') }) - }) + client.destroy(() => { t.pass('client destroyed') }) + server.close(() => { t.pass('server closed') }) }) }) }) test('server: get leecher stats.json (unknown peerId)', t => { - t.plan(11) + t.plan(10) commonTest.createServer(t, 'http', (server, announceUrl) => { // announce a torrent to the tracker @@ -168,27 +172,26 @@ test('server: get leecher stats.json (unknown peerId)', t => { client.start() - server.once('start', () => { - const opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true + server.once('start', async () => { + let res + try { + res = await fetch(announceUrl.replace('/announce', '/stats.json')) + } catch (err) { + t.error(err) } + const stats = await res.json() - get.concat(opts, (err, res, stats) => { - t.error(err) + t.equal(res.status, 200) + t.equal(stats.torrents, 1) + t.equal(stats.activeTorrents, 1) + t.equal(stats.peersAll, 1) + t.equal(stats.peersSeederOnly, 0) + t.equal(stats.peersLeecherOnly, 1) + t.equal(stats.peersSeederAndLeecher, 0) + t.equal(stats.clients.unknown['01234567'], 1) - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 1) - t.equal(stats.activeTorrents, 1) - t.equal(stats.peersAll, 1) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 1) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients.unknown['01234567'], 1) - - client.destroy(() => { t.pass('client destroyed') }) - server.close(() => { t.pass('server closed') }) - }) + client.destroy(() => { t.pass('client destroyed') }) + server.close(() => { t.pass('server closed') }) }) }) })