diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 822d21cc9..0bb7ae248 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,10 @@ jobs: os: - ubuntu-latest node: - - '14' + - '18' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - run: npm install diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6098b8e66..61c6902cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,15 +11,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 18 - name: Cache - uses: actions/cache@v2 + 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 72597d8e3..2b2bab3bc 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v4 + - 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 e9a486a8f..3b930ae6f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -57,5 +57,18 @@ - semantic-release-bot (semantic-release-bot@martynus.net) - renovate[bot] (29139614+renovate[bot]@users.noreply.github.com) - Jocelyn Liu (yrliou@gmail.com) +- Bruce Hopkins (behopkinsjr@gmail.com) +- Ryan Finnie (ryan@finnie.org) +- 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 b5f55ecc1..6b00b4dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,215 @@ +## [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) + + +### Features + +* esm ([#431](https://github.com/webtorrent/bittorrent-tracker/issues/431)) ([e6d3189](https://github.com/webtorrent/bittorrent-tracker/commit/e6d3189edf1a170197a799b97d84c632692b394f)) + + +### BREAKING CHANGES + +* ESM only + +* feat: esm + +* fix: linter oops + +# [9.19.0](https://github.com/webtorrent/bittorrent-tracker/compare/v9.18.6...v9.19.0) (2022-06-01) + + +### Features + +* **events:** Support of `paused` client event ([#411](https://github.com/webtorrent/bittorrent-tracker/issues/411)) ([ef76b3f](https://github.com/webtorrent/bittorrent-tracker/commit/ef76b3f3b6beee87f57d74addd0ca2ef2c517b6d)) + +## [9.18.6](https://github.com/webtorrent/bittorrent-tracker/compare/v9.18.5...v9.18.6) (2022-05-11) + + +### Bug Fixes + +* revert [#420](https://github.com/webtorrent/bittorrent-tracker/issues/420) ([8d54938](https://github.com/webtorrent/bittorrent-tracker/commit/8d54938f164347d57a7991268d191e44b752de7f)) + +## [9.18.5](https://github.com/webtorrent/bittorrent-tracker/compare/v9.18.4...v9.18.5) (2022-03-25) + + +### Bug Fixes + +* connection leaks ([#420](https://github.com/webtorrent/bittorrent-tracker/issues/420)) ([f7928cf](https://github.com/webtorrent/bittorrent-tracker/commit/f7928cfcc646cd95556549b64e61228892314682)) + +## [9.18.4](https://github.com/webtorrent/bittorrent-tracker/compare/v9.18.3...v9.18.4) (2022-03-06) + + +### Bug Fixes + +* typo in ws example ([#417](https://github.com/webtorrent/bittorrent-tracker/issues/417)) ([023afb9](https://github.com/webtorrent/bittorrent-tracker/commit/023afb9a3228d60392a18e70f85cdb6af5fa79fb)) + ## [9.18.3](https://github.com/webtorrent/bittorrent-tracker/compare/v9.18.2...v9.18.3) (2021-10-29) diff --git a/README.md b/README.md index 022610010..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,14 +159,15 @@ client.on('scrape', function (data) { To start a BitTorrent tracker server to track swarms of peers: ```js -var Server = require('bittorrent-tracker').Server +import { Server } from 'bittorrent-tracker' +// Or import Server from 'bittorrent-tracker/server' -var server = new Server({ +const server = new Server({ udp: true, // enable udp server? [default=true] http: true, // enable http server? [default=true] ws: true, // enable websocket server? [default=true] stats: true, // enable web-based statistics? [default=true] - trustProxy: false // enable trusting x-forwarded-for header for remote IP [default=false] + trustProxy: false, // enable trusting x-forwarded-for header for remote IP [default=false] filter: function (infoHash, params, cb) { // Blacklist/whitelist function for allowing/disallowing torrents. If this option is // omitted, all torrents are allowed. It is possible to interface with a database or @@ -201,7 +179,7 @@ var server = new Server({ // This example only allows one torrent. - var allowed = (infoHash === 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa') + const allowed = (infoHash === 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa') if (allowed) { // If the callback is passed `null`, the torrent will be allowed. cb(null) @@ -230,12 +208,34 @@ server.on('warning', function (err) { server.on('listening', function () { // fired when all requested servers are listening - console.log('listening on http port:' + server.http.address().port) - console.log('listening on udp port:' + server.udp.address().port) + + // HTTP + const httpAddr = server.http.address() + const httpHost = httpAddr.address !== '::' ? httpAddr.address : 'localhost' + const httpPort = httpAddr.port + console.log(`HTTP tracker: http://${httpHost}:${httpPort}/announce`) + + // UDP + const udpAddr = server.udp.address() + const udpHost = udpAddr.address + const udpPort = udpAddr.port + console.log(`UDP tracker: udp://${udpHost}:${udpPort}`) + + // WS + const wsAddr = server.ws.address() + const wsHost = wsAddr.address !== '::' ? wsAddr.address : 'localhost' + const wsPort = wsAddr.port + console.log(`WebSocket tracker: ws://${wsHost}:${wsPort}`) + }) + // start tracker server listening! Use 0 to listen on a random free port. -server.listen(port, hostname, onlistening) +const port = 0 +const hostname = "localhost" +server.listen(port, hostname, () => { + // Do something on listening... +}) // listen for individual tracker messages from peers: @@ -267,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 965e00f71..c57eef55b 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -const minimist = require('minimist') -const Server = require('../').Server +import minimist from 'minimist' +import { Server } from '../index.js' const argv = minimist(process.argv.slice(2), { alias: { @@ -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 c091c319f..c37345f17 100644 --- a/client.js +++ b/client.js @@ -1,14 +1,17 @@ -const debug = require('debug')('bittorrent-tracker:client') -const EventEmitter = require('events') -const once = require('once') -const parallel = require('run-parallel') -const Peer = require('simple-peer') -const queueMicrotask = require('queue-microtask') - -const common = require('./lib/common') -const HTTPTracker = require('./lib/client/http-tracker') // empty object in browser -const UDPTracker = require('./lib/client/udp-tracker') // empty object in browser -const WebSocketTracker = require('./lib/client/websocket-tracker') +import Debug from 'debug' +import EventEmitter from 'events' +import once from 'once' +import parallel from 'run-parallel' +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 +import UDPTracker from './lib/client/udp-tracker.js' // empty object in browser +import WebSocketTracker from './lib/client/websocket-tracker.js' + +const debug = Debug('bittorrent-tracker:client') /** * BitTorrent tracker client. @@ -16,8 +19,8 @@ const WebSocketTracker = require('./lib/client/websocket-tracker') * 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 @@ -37,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) @@ -67,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) } @@ -258,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 }) @@ -282,11 +285,8 @@ 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 } -module.exports = Client +export default Client diff --git a/examples/express-embed/server.js b/examples/express-embed/server.js index cfdbbec82..947dcbfbe 100755 --- a/examples/express-embed/server.js +++ b/examples/express-embed/server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -const Server = require('../..').Server -const express = require('express') +import { Server } from '../../index.js' +import express from 'express' const app = express() // https://wiki.theory.org/BitTorrentSpecification#peer_id diff --git a/index.js b/index.js index de5d73c71..e812c41c8 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ /*! bittorrent-tracker. MIT License. WebTorrent LLC */ -const Client = require('./client') -const Server = require('./server') +import Client from './client.js' +import Server from './server.js' -module.exports = Client -module.exports.Client = Client -module.exports.Server = Server +export default Client +export { Client, Server } diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js index 6fcea8d74..43464e8eb 100644 --- a/lib/client/http-tracker.js +++ b/lib/client/http-tracker.js @@ -1,16 +1,24 @@ -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') +import arrayRemove from 'unordered-array-remove' +import bencode from 'bencode' +import Debug from 'debug' +import fetch from 'cross-fetch-ponyfill' +import { bin2hex, hex2bin, arr2text, text2arr, arr2hex } from 'uint8-util' -const common = require('../common') -const Tracker = require('./tracker') +import common from '../common.js' +import Tracker from './tracker.js' +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) * @@ -47,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) => { @@ -64,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 } @@ -110,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) { @@ -188,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) } @@ -210,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) } @@ -242,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) }) @@ -256,4 +271,4 @@ class HTTPTracker extends Tracker { HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes -module.exports = HTTPTracker +export default HTTPTracker diff --git a/lib/client/tracker.js b/lib/client/tracker.js index cbbd23dee..1129ecd84 100644 --- a/lib/client/tracker.js +++ b/lib/client/tracker.js @@ -1,4 +1,4 @@ -const EventEmitter = require('events') +import EventEmitter from 'events' class Tracker extends EventEmitter { constructor (client, announceUrl) { @@ -25,4 +25,4 @@ class Tracker extends EventEmitter { } } -module.exports = Tracker +export default Tracker diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js index 65d97e406..b4ac4bdba 100644 --- a/lib/client/udp-tracker.js +++ b/lib/client/udp-tracker.js @@ -1,14 +1,17 @@ -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') +import arrayRemove from 'unordered-array-remove' +import Debug from 'debug' +import dgram from 'dgram' +import Socks from 'socks' +import { concat, hex2arr, randomBytes } from 'uint8-util' + +import common from '../common.js' +import Tracker from './tracker.js' +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) @@ -129,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 @@ -173,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')) } @@ -268,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) @@ -289,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, @@ -305,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 } @@ -318,15 +323,14 @@ 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 () {} -module.exports = UDPTracker +export default UDPTracker diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index 1573cfd25..c4e3e23d7 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -1,12 +1,12 @@ -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') +import Debug from 'debug' +import Peer from '@thaunknown/simple-peer/lite.js' +import Socket from '@thaunknown/simple-websocket' +import { arr2text, arr2hex, hex2bin, bin2hex, randomBytes } from 'uint8-util' -const common = require('../common') -const Tracker = require('./tracker') +import common from '../common.js' +import Tracker from './tracker.js' + +const debug = Debug('bittorrent-tracker:websocket-tracker') // Use a socket pool, so tracker clients share WebSocket objects for the same server. // In practice, WebSockets are pretty slow to establish, so this gives a nice performance @@ -78,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 @@ -183,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 }) @@ -212,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 @@ -231,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 } @@ -264,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) } @@ -273,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', @@ -291,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) @@ -321,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) }) @@ -374,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() }) @@ -439,4 +439,4 @@ WebSocketTracker._socketPool = socketPool function noop () {} -module.exports = WebSocketTracker +export default WebSocketTracker diff --git a/lib/common-node.js b/lib/common-node.js index b570f2b13..9fefa5c4d 100644 --- a/lib/common-node.js +++ b/lib/common-node.js @@ -3,46 +3,49 @@ * These are separate from common.js so they can be skipped when bundling for the browser. */ -const querystring = require('querystring') +import querystring from 'querystring' +import { concat } from 'uint8-util' -exports.IPV4_RE = /^[\d.]+$/ -exports.IPV6_RE = /^[\da-fA-F:]+$/ -exports.REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ +export const IPV4_RE = /^[\d.]+$/ +export const IPV6_RE = /^[\da-fA-F:]+$/ +export const REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ -exports.CONNECTION_ID = Buffer.concat([toUInt32(0x417), toUInt32(0x27101980)]) -exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } -exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 } -exports.EVENT_IDS = { +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 = { 0: 'update', 1: 'completed', 2: 'started', - 3: 'stopped' + 3: 'stopped', + 4: 'paused' } -exports.EVENT_NAMES = { +export const EVENT_NAMES = { update: 'update', completed: 'complete', started: 'start', - stopped: 'stop' + stopped: 'stop', + paused: 'pause' } /** * Client request timeout. How long to wait before considering a request to a * tracker server to have timed out. */ -exports.REQUEST_TIMEOUT = 15000 +export const REQUEST_TIMEOUT = 15000 /** * Client destroy timeout. How long to wait before forcibly cleaning up all * pending requests, open sockets, etc. */ -exports.DESTROY_TIMEOUT = 1000 +export const DESTROY_TIMEOUT = 1000 -function toUInt32 (n) { - const buf = Buffer.allocUnsafe(4) - buf.writeUInt32BE(n, 0) +export function toUInt32 (n) { + const buf = new Uint8Array(4) + const view = new DataView(buf.buffer) + view.setUint32(0, n) return buf } -exports.toUInt32 = toUInt32 /** * `querystring.parse` using `unescape` instead of decodeURIComponent, since bittorrent @@ -50,7 +53,7 @@ exports.toUInt32 = toUInt32 * @param {string} q * @return {Object} */ -exports.querystringParse = q => querystring.parse(q, null, null, { decodeURIComponent: unescape }) +export const querystringParse = q => querystring.parse(q, null, null, { decodeURIComponent: unescape }) /** * `querystring.stringify` using `escape` instead of encodeURIComponent, since bittorrent @@ -58,7 +61,7 @@ exports.querystringParse = q => querystring.parse(q, null, null, { decodeURIComp * @param {Object} obj * @return {string} */ -exports.querystringStringify = obj => { +export const querystringStringify = obj => { let ret = querystring.stringify(obj, null, null, { encodeURIComponent: escape }) ret = ret.replace(/[@*/+]/g, char => // `escape` doesn't encode the characters @*/+ so we do it manually `%${char.charCodeAt(0).toString(16).toUpperCase()}`) diff --git a/lib/common.js b/lib/common.js index 0a2026bc9..ef888579c 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,23 +1,11 @@ /** * Functions/constants needed by both the client and server. */ +import * as common from './common-node.js' +export * from './common-node.js' -exports.DEFAULT_ANNOUNCE_PEERS = 50 -exports.MAX_ANNOUNCE_PEERS = 82 - -exports.binaryToHex = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'binary').toString('hex') -} - -exports.hexToBinary = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'hex').toString('binary') -} +export const DEFAULT_ANNOUNCE_PEERS = 50 +export const MAX_ANNOUNCE_PEERS = 82 // 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. @@ -31,7 +19,7 @@ exports.hexToBinary = str => { // Bug reports: // - Chrome: https://bugs.chromium.org/p/chromium/issues/detail?id=734880 // - Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1374505 -exports.parseUrl = str => { +export const parseUrl = str => { const url = new URL(str.replace(/^udp:/, 'http:')) if (str.match(/^udp:/)) { @@ -45,5 +33,9 @@ exports.parseUrl = str => { return url } -const config = require('./common-node') -Object.assign(exports, config) +export default { + DEFAULT_ANNOUNCE_PEERS, + MAX_ANNOUNCE_PEERS, + parseUrl, + ...common +} diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js index 7b2d5e2ca..97b46e307 100644 --- a/lib/server/parse-http.js +++ b/lib/server/parse-http.js @@ -1,8 +1,8 @@ -module.exports = parseHttpRequest +import { bin2hex } from 'uint8-util' -const common = require('../common') +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 939bcb5de..f73b64917 100644 --- a/lib/server/parse-udp.js +++ b/lib/server/parse-udp.js @@ -1,9 +1,8 @@ -module.exports = parseUdpRequest +import ipLib from 'ip' +import common from '../common.js' +import { equal } from 'uint8-util' -const ipLib = require('ip') -const common = require('../common') - -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 21b48fcb9..744a3c30b 100644 --- a/lib/server/parse-websocket.js +++ b/lib/server/parse-websocket.js @@ -1,8 +1,8 @@ -module.exports = parseWebSocketRequest +import { bin2hex } from 'uint8-util' -const common = require('../common') +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/lib/server/swarm.js b/lib/server/swarm.js index 8194270f8..9e3a0a204 100644 --- a/lib/server/swarm.js +++ b/lib/server/swarm.js @@ -1,7 +1,9 @@ -const arrayRemove = require('unordered-array-remove') -const debug = require('debug')('bittorrent-tracker:swarm') -const LRU = require('lru') -const randomIterate = require('random-iterate') +import arrayRemove from 'unordered-array-remove' +import Debug from 'debug' +import LRU from 'lru' +import randomIterate from 'random-iterate' + +const debug = Debug('bittorrent-tracker:swarm') // Regard this as the default implementation of an interface that you // need to support when overriding Server.createSwarm() and Server.getSwarm() @@ -47,6 +49,8 @@ class Swarm { self._onAnnounceCompleted(params, peer, id) } else if (params.event === 'update') { self._onAnnounceUpdate(params, peer, id) + } else if (params.event === 'paused') { + self._onAnnouncePaused(params, peer, id) } else { cb(new Error('invalid event')) return @@ -132,6 +136,15 @@ class Swarm { this.peers.set(id, peer) } + _onAnnouncePaused (params, peer, id) { + if (!peer) { + debug('unexpected `paused` event from peer that is not in swarm') + return this._onAnnounceStarted(params, peer, id) // treat as a start + } + + this._onAnnounceUpdate(params, peer, id) + } + _getPeers (numwant, ownPeerId, isWebRTC) { const peers = [] const ite = randomIterate(this.peers.keys) @@ -148,4 +161,4 @@ class Swarm { } } -module.exports = Swarm +export default Swarm diff --git a/package.json b/package.json index 2b11c9323..ca9cebd6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bittorrent-tracker", "description": "Simple, robust, BitTorrent tracker (client & server) implementation", - "version": "9.18.3", + "version": "11.2.2", "author": { "name": "WebTorrent LLC", "email": "feross@webtorrent.io", @@ -25,42 +25,57 @@ "bugs": { "url": "https://github.com/webtorrent/bittorrent-tracker/issues" }, + "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.7", - "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.4.0", - "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" + "node": ">=16.0.0" + }, + "exports": { + ".": { + "import": "./index.js" + }, + "./client": { + "import": "./client.js" + }, + "./server": { + "import": "./server.js" + }, + "./websocket-tracker": { + "import": "./lib/client/websocket-tracker.js" + } }, "keywords": [ "bittorrent", @@ -75,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 3bdbea71f..f7023d99f 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,21 @@ -const bencode = require('bencode') -const debug = require('debug')('bittorrent-tracker:server') -const dgram = require('dgram') -const EventEmitter = require('events') -const http = require('http') -const peerid = require('bittorrent-peerid') -const series = require('run-series') -const string2compact = require('string2compact') -const WebSocketServer = require('ws').Server - -const common = require('./lib/common') -const Swarm = require('./lib/server/swarm') -const parseHttpRequest = require('./lib/server/parse-http') -const parseUdpRequest = require('./lib/server/parse-udp') -const parseWebSocketRequest = require('./lib/server/parse-websocket') - +import bencode from 'bencode' +import Debug from 'debug' +import dgram from 'dgram' +import EventEmitter from 'events' +import http from 'http' +import peerid from 'bittorrent-peerid' +import series from 'run-series' +import string2compact from 'string2compact' +import { WebSocketServer } from 'ws' +import { hex2bin } from 'uint8-util' + +import common from './lib/common.js' +import Swarm from './lib/server/swarm.js' +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 debug = Debug('bittorrent-tracker:server') const hasOwnProperty = Object.prototype.hasOwnProperty /** @@ -349,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) @@ -358,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]) @@ -487,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) @@ -505,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) @@ -525,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) }) @@ -558,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) @@ -684,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 })) @@ -728,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 @@ -805,4 +807,4 @@ function toNumber (x) { function noop () {} -module.exports = Server +export default Server diff --git a/test/client-large-torrent.js b/test/client-large-torrent.js index a72f9cb05..db0d0fbf9 100644 --- a/test/client-large-torrent.js +++ b/test/client-large-torrent.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import test from 'tape' const peerId = Buffer.from('01234567890123456789') diff --git a/test/client-magnet.js b/test/client-magnet.js index 7138cbd98..a6268b3fc 100644 --- a/test/client-magnet.js +++ b/test/client-magnet.js @@ -1,8 +1,8 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const magnet = require('magnet-uri') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import magnet from 'magnet-uri' +import test from 'tape' const peerId = Buffer.from('01234567890123456789') diff --git a/test/client-ws-socket-pool.js b/test/client-ws-socket-pool.js index 4c041c2a2..6d3fe1a3f 100644 --- a/test/client-ws-socket-pool.js +++ b/test/client-ws-socket-pool.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import test from 'tape' const peerId = Buffer.from('01234567890123456789') const port = 6681 diff --git a/test/client.js b/test/client.js index 4ad16f853..d81ee55f0 100644 --- a/test/client.js +++ b/test/client.js @@ -1,9 +1,10 @@ -const Client = require('../') -const common = require('./common') -const http = require('http') -const fixtures = require('webtorrent-fixtures') -const net = require('net') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +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/common.js b/test/common.js index 9bd098ed0..5ff6ce39f 100644 --- a/test/common.js +++ b/test/common.js @@ -1,6 +1,6 @@ -const Server = require('../').Server +import { Server } from '../index.js' -exports.createServer = (t, opts, cb) => { +export const createServer = (t, opts, cb) => { if (typeof opts === 'string') opts = { serverType: opts } opts.http = (opts.serverType === 'http') @@ -27,7 +27,7 @@ exports.createServer = (t, opts, cb) => { }) } -exports.mockWebsocketTracker = client => { +export const mockWebsocketTracker = client => { client._trackers[0]._generateOffers = (numwant, cb) => { const offers = [] for (let i = 0; i < numwant; i++) { @@ -38,3 +38,5 @@ exports.mockWebsocketTracker = client => { }) } } + +export default { mockWebsocketTracker, createServer } diff --git a/test/destroy.js b/test/destroy.js index 27d476053..e9d7af3a4 100644 --- a/test/destroy.js +++ b/test/destroy.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import test from 'tape' const peerId = Buffer.from('01234567890123456789') const port = 6881 diff --git a/test/evict.js b/test/evict.js index a256a9169..385fae2a6 100644 --- a/test/evict.js +++ b/test/evict.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const test = require('tape') -const wrtc = require('wrtc') +import Client from '../index.js' +import common from './common.js' +import test from 'tape' +import wrtc from 'webrtc-polyfill' const infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' const peerId = Buffer.from('01234567890123456789') diff --git a/test/filter.js b/test/filter.js index fee7dd779..4dc3da02d 100644 --- a/test/filter.js +++ b/test/filter.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const test = require('tape') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import test from 'tape' const peerId = Buffer.from('01234567890123456789') diff --git a/test/querystring.js b/test/querystring.js index 2ce20fe2b..85e14e544 100644 --- a/test/querystring.js +++ b/test/querystring.js @@ -1,5 +1,5 @@ -const common = require('../lib/common') -const test = require('tape') +import common from '../lib/common.js' +import test from 'tape' // https://github.com/webtorrent/webtorrent/issues/196 test('encode special chars +* in http tracker urls', t => { diff --git a/test/request-handler.js b/test/request-handler.js index b60f96745..6532cbbb8 100644 --- a/test/request-handler.js +++ b/test/request-handler.js @@ -1,8 +1,8 @@ -const Client = require('../') -const common = require('./common') -const fixtures = require('webtorrent-fixtures') -const test = require('tape') -const Server = require('../server') +import Client from '../index.js' +import common from './common.js' +import fixtures from 'webtorrent-fixtures' +import test from 'tape' +import Server from '../server.js' const peerId = Buffer.from('01234567890123456789') @@ -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 047d24953..c0c101a5f 100644 --- a/test/scrape.js +++ b/test/scrape.js @@ -1,16 +1,16 @@ -const bencode = require('bencode') -const Client = require('../') -const common = require('./common') -const commonLib = require('../lib/common') -const commonTest = require('./common') -const fixtures = require('webtorrent-fixtures') -const get = require('simple-get') -const test = require('tape') +import bencode from 'bencode' +import Client from '../index.js' +import common from './common.js' +import commonLib from '../lib/common.js' +import fixtures from 'webtorrent-fixtures' +import fetch from 'cross-fetch-ponyfill' +import test from 'tape' +import { hex2bin } from 'uint8-util' const peerId = Buffer.from('01234567890123456789') function testSingle (t, serverType) { - commonTest.createServer(t, serverType, (server, announceUrl) => { + common.createServer(t, serverType, (server, announceUrl) => { const client = new Client({ infoHash: fixtures.leaves.parsedTorrent.infoHash, announce: announceUrl, @@ -52,7 +52,7 @@ test('ws: single info_hash scrape', t => { }) function clientScrapeStatic (t, serverType) { - commonTest.createServer(t, serverType, (server, announceUrl) => { + common.createServer(t, serverType, (server, announceUrl) => { const client = Client.scrape({ announce: announceUrl, infoHash: fixtures.leaves.parsedTorrent.infoHash, @@ -116,7 +116,7 @@ function clientScrapeMulti (t, serverType) { const infoHash1 = fixtures.leaves.parsedTorrent.infoHash const infoHash2 = fixtures.alice.parsedTorrent.infoHash - commonTest.createServer(t, serverType, (server, announceUrl) => { + common.createServer(t, serverType, (server, announceUrl) => { Client.scrape({ infoHash: [infoHash1, infoHash2], announce: announceUrl @@ -151,48 +151,51 @@ 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) - commonTest.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) - commonTest.createServer(t, 'http', (server, announceUrl) => { + common.createServer(t, 'http', (server, announceUrl) => { const scrapeUrl = announceUrl.replace('/announce', '/scrape') // announce a torrent to the tracker @@ -207,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 0acfabd9c..d4cf493e4 100644 --- a/test/server.js +++ b/test/server.js @@ -1,7 +1,7 @@ -const Client = require('../') -const common = require('./common') -const test = require('tape') -const wrtc = require('wrtc') +import Client from '../index.js' +import common from './common.js' +import test from 'tape' +import wrtc from 'webrtc-polyfill' const infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' const peerId = Buffer.from('01234567890123456789') diff --git a/test/stats.js b/test/stats.js index 74128c2fb..7223da014 100644 --- a/test/stats.js +++ b/test/stats.js @@ -1,8 +1,8 @@ -const Client = require('../') -const commonTest = require('./common') -const fixtures = require('webtorrent-fixtures') -const get = require('simple-get') -const test = require('tape') +import Client from '../index.js' +import commonTest from './common.js' +import fixtures from 'webtorrent-fixtures' +import fetch from 'cross-fetch-ponyfill' +import test from 'tape' const peerId = Buffer.from('-WW0091-4ea5886ce160') const unknownPeerId = Buffer.from('01234567890123456789') @@ -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') }) }) }) })