From 93a9095d9999165c3388ed03de56c4a0f8f20c79 Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Tue, 19 Sep 2023 19:52:35 +0300 Subject: [PATCH] Add eslint (#364) * Add initial prettier and eslint configs * Modify prettierrc * Run eslint on everything * Actually remove prettier * Fix some eslints * Remove label in gs2 * Update CHANGELOG * Update eslintrc to specify es2021 --- .eslintrc.json | 13 + .gitattributes | 1 + CHANGELOG.md | 539 +++++----- bin/gamedig.js | 135 ++- bin/genreadme.js | 26 +- lib/DnsResolver.js | 156 +-- lib/GameResolver.js | 236 +++-- lib/GlobalUdpSocket.js | 138 +-- lib/HexUtil.js | 41 +- lib/Logger.js | 88 +- lib/Promises.js | 36 +- lib/ProtocolResolver.js | 15 +- lib/QueryRunner.js | 192 ++-- lib/Results.js | 85 +- lib/index.js | 47 +- lib/reader.js | 344 +++---- package-lock.json | 1730 +++++++++++++++++++++++++++++++++ package.json | 33 +- protocols/armagetron.js | 128 +-- protocols/ase.js | 90 +- protocols/assettocorsa.js | 76 +- protocols/battlefield.js | 323 +++--- protocols/buildandshoot.js | 110 +-- protocols/core.js | 699 ++++++------- protocols/cs2d.js | 132 ++- protocols/discord.js | 58 +- protocols/doom3.js | 296 +++--- protocols/eco.js | 38 +- protocols/ffow.js | 75 +- protocols/fivem.js | 66 +- protocols/gamespy1.js | 358 +++---- protocols/gamespy2.js | 281 +++--- protocols/gamespy3.js | 390 ++++---- protocols/geneshift.js | 92 +- protocols/goldsrc.js | 16 +- protocols/hexen2.js | 27 +- protocols/index.js | 108 +- protocols/jc2mp.js | 37 +- protocols/kspdmp.js | 54 +- protocols/mafia2mp.js | 82 +- protocols/mafia2online.js | 18 +- protocols/minecraft.js | 198 ++-- protocols/minecraftbedrock.js | 144 +-- protocols/minecraftvanilla.js | 161 +-- protocols/mumble.js | 78 +- protocols/mumbleping.js | 48 +- protocols/nadeo.js | 170 ++-- protocols/openttd.js | 254 ++--- protocols/quake1.js | 18 +- protocols/quake2.js | 172 ++-- protocols/quake3.js | 46 +- protocols/rfactor.js | 143 ++- protocols/samp.js | 215 ++-- protocols/savage2.js | 54 +- protocols/starmade.js | 134 +-- protocols/starsiege.js | 20 +- protocols/teamspeak2.js | 140 +-- protocols/teamspeak3.js | 134 +-- protocols/terraria.js | 48 +- protocols/tribes1.js | 303 +++--- protocols/tribes1master.js | 159 +-- protocols/unreal2.js | 300 +++--- protocols/ut3.js | 90 +- protocols/valve.js | 1216 +++++++++++------------ protocols/vcmp.js | 20 +- protocols/ventrilo.js | 471 ++++----- protocols/warsow.js | 26 +- 67 files changed, 6960 insertions(+), 5211 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .gitattributes diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..783db16 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "env": { + "browser": false, + "es2021": true + }, + "extends": "standard", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module" + }, + "rules": { + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7dc058d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +text=auto diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d5a45e..7a2e6e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,269 +1,270 @@ - -### To Be Released... -#### Breaking Changes -* NodeJS 14.17 is now required (from 14). -* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)). -* Updated `CS2D` protocol (by @ernestpasnik) - -#### Other changes -* Replaced usage of deprecated `substr` with `substring`. -* Moved the library a `module`. -* CLI: Resolved incorrect error message when querying with a non-existent protocol name. -* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package. -* Eco (2018) - Added support (requested by @dgibbs64). -* Core Keeper (2022) - Added support (by @dgibbs64). - -### 4.1.0 -* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. -* Sons Of The Forest (2023) - Added support -* Red Dead Redemption 2 - RedM (2018) - Added support -* Creativerse (2017) - Added support -* The Isle (2015) - Added support - -### 4.0.7 -* Updated some dependencies to solve vulnerabilities -* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers. -* Risk of Rain 2 (2019) - Added support -* Survive the Nights (2017) - Added support -* V Rising (2022) - Added support -* Day of Dragons (2019) - Added support -* Onset (2019) - Added support -* Don't Starve Together (2016) - Added support -* Chivalry: Medieval Warfare (2012) - Added support -* Avorion (2020) - Added support -* Black Mesa (2020) - Added support -* Ballistic Overkill (2017) - Added support -* Codename CURE (2017) - Added support -* Colony Survival (2017) - Added support -* Rising World (2014) - Added support -* BrainBread 2 (2016) - Added support - -### 4.0.6 -* Fixed ping returned by minecraft queries -* Added ipFamily option to query only ipv4 or only ipv6 dns records - -### 4.0.5 -* Fixed filtering out fake "Max Players" player on CSGO -* Removed moment dependency - -### 4.0.4 -* Updated dependencies - -### 4.0.3 -* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4) -* Ground Breach (2018) - Added support -* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser -* Barotrauma (2019) - Added support - -### 4.0.2 -* Counter-Strike 1.5 - Fixed support - -### 4.0.1 -* Rust - Fixed maxplayers >255 -* dayZ - Fixed tag info not parsing when queryRules wasn't set - -### 4.0.0 - -#### Breaking Changes -* NodeJS 14 is now required - -#### Other changes -* Dependencies are updated -* Node 14 is now required due to new requirement in `got` dependency - -### 3.0.9 -* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup) -* Adds Project Zomboid support (Thanks xhip) -* Adds Post Scriptum support (Thanks arkuar) -* Adds some more DayZ info to state.raw (Thanks podrivo) -* Updates to README regarding DayZ (Thanks podrivo) -* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy) -* Fixes ping always being 0 for minecraft servers -* Adds README documentation about teamspeakQueryPort - -### 3.0.8 -* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync) -* Fixes minecraft protocol not using player list from bedrock protocol in some cases - -### 3.0.7 -* Fixes corrupted dayzMods when packet overflow is present - -### 3.0.6 -* raw.tags for valve servers is now an array rather than a string -* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true -* DayZ queue length, day and night acceleration are now parsed into raw as well - -### 3.0.5 -* Add support for `listenUdpPort` to specify a fixed bind port. -* Improved udp bind failure detection. - -### 3.0.4 -* Add support for Discord widget - -### 3.0.3 -* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support. - -### 3.0.2 -* Fix player name extraction for Unreal Tournament (1999) and possibly - other gamespy1 games. - -### 3.0.1 -* Clarified that nodejs 12 is now required for gamedig 3 -* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2` - -### 3.0.0 -Major Changes: -* **NodeJS 12 is now required** -* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string. -* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent - response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig. -* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait - for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules, - you may pass the `requestRules: true` option to re-enable them. -* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`. - -### 2.0.28 -* Added Valheim (2021) - -### 2.0.27 -* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock - -### 2.0.26 -* Added support for the native minecraft bedrock protocol, since some -bedrock servers apparently do not respond to the gamespy3 protocol. - -### 2.0.25 -* Support challenges in A2S_INFO (upcoming change to valve protocol) - -### 2.0.24 -* Add Savage 2: A Tortured Soul (2008) - -### 2.0.23 -* Fix Conan Exiles and other games which don't respond to the valve player query -* Add givenPortOnly query option for users that require extreme optimization - -### 2.0.22 -* Updated dependencies - -### 2.0.21 -* Added Assetto Corsa (2014) -* Fixed password flag for Squad -* Added Mordhau (2019) -* Fixed player count being incorrect in minecraftvanilla protocol in some cases -* Updated dependencies -* Replaced deprecated Request http library with Got - -### 2.0.20 -* Fixed minecraft protocol never throwing exceptions - -### 2.0.19 -* Added Days of War (2017) -* Added The Forrest (2014) -* Added Just Cause 3 Multiplayer (2017) -* Added Project Reality: Battlefield 2 (2005) -* Added Quake Live (2010) -* Added Contagion (2011) -* Added Empyrion: Galactic Survival (2015) -* Added PixARK (2018) - -### 2.0.16, 2.0.17, 2.0.18 -* Various improvements to killing floor / unreal2 protocol - -### 2.0.15 -* Added Hell Let Loose -* Added Rising Storm 2: Vietnam -* Added Squad -* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string -* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord) -* Updated dependencies - -### 2.0.14 -* Node 8 compatibility fixes - -### 2.0.13 -* Improved logging - -### 2.0.12 -* Servers are now limited to 10000 players to prevent OOM -* Improvements to Starmade (2012) -* Added Atlas (2018) - -### 2.0.11 -* Added Acra Sim Racing -* Added Mafia 2: Online - -### 2.0.10 -* Added rFactor - -### 2.0.9 -* Added Vice City: Multiplayer - -### 2.0.8 -* Improve out-of-order packet handling for gamespy1 protocol -* Work-around for buggy duplicate player reporting from bf1942 servers -* Report team names rather than IDs when possible for gamespy1 protocol - -### 2.0.7 -* Prevent tcp socket errors from dumping straight to console - -### 2.0.6 -* Added support for host domains requiring Punycode encoding (special characters) - -### 2.0.5 -* Added support for Counter-Strike: 2D - -### 2.0.4 -* Added details about new 2.0 reponse fields to the README. - -### 2.0.3 -* Added support for Insurgency: Sandstorm - -### 2.0.2 -* Added support for Starsiege 2009 (starsiege) - -### 2.0.1 -* Updated readme games list for 2.0 -* Fixed csgo default port - -### 2.0.0 - -##### Breaking API changes -* **Node 8 is now required** -* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and -GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in -unusual cases, as otherwise it must be automatically derived from the game port. -* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue -using callbacks, you can use node's `util.callbackify` function to convert the method to callback format. -* Removed `query` field from response object, as it was poorly documented and unstable. -* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed. - -##### Minor Changes -* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified -by using async/await across the codebase, eliminating callback chains and the 'async' dependency. -* Replaced `--output pretty` cli parameter with `--pretty`. -* You can now query from CLI using shorthand syntax: `gamedig --type [:]` -* UDP socket is only opened if needed by a query. -* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a -game port or query port by querying twice: once to the port provided, and once to the port including the game's query -port offset (if available). -* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's -game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url -if an IP:Port is not appropriate. -* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support -for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket -during the query. -* Improved debug logging across all parts of GameDig -* Removed global `Gamedig.debug`. `debug` is now an option on each query. - -##### Protocol Changes -* Added support for games using older versions of battlefield protocol. -* Simplified detection of BC2 when using battlefield protocol. -* Fixed buildandshoot not reading player list -* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically. -* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically. -* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically. -* Improved valve protocol challenge key retry process - -### 1.0.0 -* First official release -* Node.js 6 is now required + +### To Be Released... +#### Breaking Changes +* NodeJS 14.17 is now required (from 14). +* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)). +* Updated `CS2D` protocol (by @ernestpasnik) + +#### Other changes +* Replaced usage of deprecated `substr` with `substring`. +* Moved the library a `module`. +* CLI: Resolved incorrect error message when querying with a non-existent protocol name. +* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package. +* Eco (2018) - Added support (requested by @dgibbs64). +* Core Keeper (2022) - Added support (by @dgibbs64). +* Added eslint which spotted some unused variables and other lints. + +### 4.1.0 +* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. +* Sons Of The Forest (2023) - Added support +* Red Dead Redemption 2 - RedM (2018) - Added support +* Creativerse (2017) - Added support +* The Isle (2015) - Added support + +### 4.0.7 +* Updated some dependencies to solve vulnerabilities +* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers. +* Risk of Rain 2 (2019) - Added support +* Survive the Nights (2017) - Added support +* V Rising (2022) - Added support +* Day of Dragons (2019) - Added support +* Onset (2019) - Added support +* Don't Starve Together (2016) - Added support +* Chivalry: Medieval Warfare (2012) - Added support +* Avorion (2020) - Added support +* Black Mesa (2020) - Added support +* Ballistic Overkill (2017) - Added support +* Codename CURE (2017) - Added support +* Colony Survival (2017) - Added support +* Rising World (2014) - Added support +* BrainBread 2 (2016) - Added support + +### 4.0.6 +* Fixed ping returned by minecraft queries +* Added ipFamily option to query only ipv4 or only ipv6 dns records + +### 4.0.5 +* Fixed filtering out fake "Max Players" player on CSGO +* Removed moment dependency + +### 4.0.4 +* Updated dependencies + +### 4.0.3 +* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4) +* Ground Breach (2018) - Added support +* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser +* Barotrauma (2019) - Added support + +### 4.0.2 +* Counter-Strike 1.5 - Fixed support + +### 4.0.1 +* Rust - Fixed maxplayers >255 +* dayZ - Fixed tag info not parsing when queryRules wasn't set + +### 4.0.0 + +#### Breaking Changes +* NodeJS 14 is now required + +#### Other changes +* Dependencies are updated +* Node 14 is now required due to new requirement in `got` dependency + +### 3.0.9 +* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup) +* Adds Project Zomboid support (Thanks xhip) +* Adds Post Scriptum support (Thanks arkuar) +* Adds some more DayZ info to state.raw (Thanks podrivo) +* Updates to README regarding DayZ (Thanks podrivo) +* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy) +* Fixes ping always being 0 for minecraft servers +* Adds README documentation about teamspeakQueryPort + +### 3.0.8 +* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync) +* Fixes minecraft protocol not using player list from bedrock protocol in some cases + +### 3.0.7 +* Fixes corrupted dayzMods when packet overflow is present + +### 3.0.6 +* raw.tags for valve servers is now an array rather than a string +* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true +* DayZ queue length, day and night acceleration are now parsed into raw as well + +### 3.0.5 +* Add support for `listenUdpPort` to specify a fixed bind port. +* Improved udp bind failure detection. + +### 3.0.4 +* Add support for Discord widget + +### 3.0.3 +* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support. + +### 3.0.2 +* Fix player name extraction for Unreal Tournament (1999) and possibly + other gamespy1 games. + +### 3.0.1 +* Clarified that nodejs 12 is now required for gamedig 3 +* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2` + +### 3.0.0 +Major Changes: +* **NodeJS 12 is now required** +* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string. +* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent + response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig. +* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait + for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules, + you may pass the `requestRules: true` option to re-enable them. +* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`. + +### 2.0.28 +* Added Valheim (2021) + +### 2.0.27 +* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock + +### 2.0.26 +* Added support for the native minecraft bedrock protocol, since some +bedrock servers apparently do not respond to the gamespy3 protocol. + +### 2.0.25 +* Support challenges in A2S_INFO (upcoming change to valve protocol) + +### 2.0.24 +* Add Savage 2: A Tortured Soul (2008) + +### 2.0.23 +* Fix Conan Exiles and other games which don't respond to the valve player query +* Add givenPortOnly query option for users that require extreme optimization + +### 2.0.22 +* Updated dependencies + +### 2.0.21 +* Added Assetto Corsa (2014) +* Fixed password flag for Squad +* Added Mordhau (2019) +* Fixed player count being incorrect in minecraftvanilla protocol in some cases +* Updated dependencies +* Replaced deprecated Request http library with Got + +### 2.0.20 +* Fixed minecraft protocol never throwing exceptions + +### 2.0.19 +* Added Days of War (2017) +* Added The Forrest (2014) +* Added Just Cause 3 Multiplayer (2017) +* Added Project Reality: Battlefield 2 (2005) +* Added Quake Live (2010) +* Added Contagion (2011) +* Added Empyrion: Galactic Survival (2015) +* Added PixARK (2018) + +### 2.0.16, 2.0.17, 2.0.18 +* Various improvements to killing floor / unreal2 protocol + +### 2.0.15 +* Added Hell Let Loose +* Added Rising Storm 2: Vietnam +* Added Squad +* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string +* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord) +* Updated dependencies + +### 2.0.14 +* Node 8 compatibility fixes + +### 2.0.13 +* Improved logging + +### 2.0.12 +* Servers are now limited to 10000 players to prevent OOM +* Improvements to Starmade (2012) +* Added Atlas (2018) + +### 2.0.11 +* Added Acra Sim Racing +* Added Mafia 2: Online + +### 2.0.10 +* Added rFactor + +### 2.0.9 +* Added Vice City: Multiplayer + +### 2.0.8 +* Improve out-of-order packet handling for gamespy1 protocol +* Work-around for buggy duplicate player reporting from bf1942 servers +* Report team names rather than IDs when possible for gamespy1 protocol + +### 2.0.7 +* Prevent tcp socket errors from dumping straight to console + +### 2.0.6 +* Added support for host domains requiring Punycode encoding (special characters) + +### 2.0.5 +* Added support for Counter-Strike: 2D + +### 2.0.4 +* Added details about new 2.0 reponse fields to the README. + +### 2.0.3 +* Added support for Insurgency: Sandstorm + +### 2.0.2 +* Added support for Starsiege 2009 (starsiege) + +### 2.0.1 +* Updated readme games list for 2.0 +* Fixed csgo default port + +### 2.0.0 + +##### Breaking API changes +* **Node 8 is now required** +* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and +GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in +unusual cases, as otherwise it must be automatically derived from the game port. +* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue +using callbacks, you can use node's `util.callbackify` function to convert the method to callback format. +* Removed `query` field from response object, as it was poorly documented and unstable. +* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed. + +##### Minor Changes +* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified +by using async/await across the codebase, eliminating callback chains and the 'async' dependency. +* Replaced `--output pretty` cli parameter with `--pretty`. +* You can now query from CLI using shorthand syntax: `gamedig --type [:]` +* UDP socket is only opened if needed by a query. +* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a +game port or query port by querying twice: once to the port provided, and once to the port including the game's query +port offset (if available). +* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's +game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url +if an IP:Port is not appropriate. +* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support +for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket +during the query. +* Improved debug logging across all parts of GameDig +* Removed global `Gamedig.debug`. `debug` is now an option on each query. + +##### Protocol Changes +* Added support for games using older versions of battlefield protocol. +* Simplified detection of BC2 when using battlefield protocol. +* Fixed buildandshoot not reading player list +* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically. +* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically. +* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically. +* Improved valve protocol challenge key retry process + +### 1.0.0 +* First official release +* Node.js 6 is now required diff --git a/bin/gamedig.js b/bin/gamedig.js index c01d875..c0e35bb 100755 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -1,68 +1,67 @@ -#!/usr/bin/env node - -import Minimist from 'minimist'; -import GameDig from './../lib/index.js'; - -const argv = Minimist(process.argv.slice(2), { - boolean: ['pretty','debug','givenPortOnly','requestRules'], - string: ['guildId','listenUdpPort','ipFamily'] -}); - -const debug = argv.debug; -delete argv.debug; -const pretty = !!argv.pretty || debug; -delete argv.pretty; -const givenPortOnly = argv.givenPortOnly; -delete argv.givenPortOnly; - -let options = {}; -for(const key of Object.keys(argv)) { - const value = argv[key]; - - if(key === '_' || key.charAt(0) === '$') - continue; - - options[key] = value; -} - -if (argv._.length >= 1) { - const target = argv._[0]; - const split = target.split(':'); - options.host = split[0]; - if (split.length >= 2) { - options.port = split[1]; - } -} -if (debug) { - options.debug = true; -} -if (givenPortOnly) { - options.givenPortOnly = true; -} - -const printOnPretty = (object) => { - if(pretty) { - console.log(JSON.stringify(object,null,' ')); - } else { - console.log(JSON.stringify(object)); - } -} - -const gamedig = new GameDig(options); -gamedig.query(options) - .then(printOnPretty) - .catch((error) => { - if (debug) { - if (error instanceof Error) { - console.log(error.stack); - } else { - console.log(error); - } - } else { - if (error instanceof Error) { - error = error.message; - } - - printOnPretty({error: error}); - } - }); +#!/usr/bin/env node + +import Minimist from 'minimist' +import GameDig from './../lib/index.js' + +const argv = Minimist(process.argv.slice(2), { + boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules'], + string: ['guildId', 'listenUdpPort', 'ipFamily'] +}) + +const debug = argv.debug +delete argv.debug +const pretty = !!argv.pretty || debug +delete argv.pretty +const givenPortOnly = argv.givenPortOnly +delete argv.givenPortOnly + +const options = {} +for (const key of Object.keys(argv)) { + const value = argv[key] + + if (key === '_' || key.charAt(0) === '$') { continue } + + options[key] = value +} + +if (argv._.length >= 1) { + const target = argv._[0] + const split = target.split(':') + options.host = split[0] + if (split.length >= 2) { + options.port = split[1] + } +} +if (debug) { + options.debug = true +} +if (givenPortOnly) { + options.givenPortOnly = true +} + +const printOnPretty = (object) => { + if (pretty) { + console.log(JSON.stringify(object, null, ' ')) + } else { + console.log(JSON.stringify(object)) + } +} + +const gamedig = new GameDig(options) +gamedig.query(options) + .then(printOnPretty) + .catch((error) => { + if (debug) { + if (error instanceof Error) { + console.log(error.stack) + } else { + console.log(error) + } + } else { + if (error instanceof Error) { + error = error.message + } + + printOnPretty({ error }) + } + }) diff --git a/bin/genreadme.js b/bin/genreadme.js index c44d51c..53cf374 100644 --- a/bin/genreadme.js +++ b/bin/genreadme.js @@ -1,21 +1,21 @@ #!/usr/bin/env node -import * as fs from 'fs'; -import GameResolver from "../lib/GameResolver"; +import * as fs from 'fs' +import GameResolver from '../lib/GameResolver' -const gameResolver = new GameResolver(); +const gameResolver = new GameResolver() -const generated = gameResolver.printReadme(); +const generated = gameResolver.printReadme() -const readmeFilename = __dirname+'/../README.md'; -const readme = fs.readFileSync(readmeFilename, {encoding:'utf8'}); +const readmeFilename = __dirname + '/../README.md' +const readme = fs.readFileSync(readmeFilename, { encoding: 'utf8' }) -const marker_top = ''; -const marker_bottom = ''; +const marker_top = '' +const marker_bottom = '' -let start = readme.indexOf(marker_top); -start += marker_top.length; -const end = readme.indexOf(marker_bottom); +let start = readme.indexOf(marker_top) +start += marker_top.length +const end = readme.indexOf(marker_bottom) -const updated = readme.substring(0,start)+"\n\n"+generated+"\n"+readme.substring(end); -fs.writeFileSync(readmeFilename, updated); +const updated = readme.substring(0, start) + '\n\n' + generated + '\n' + readme.substring(end) +fs.writeFileSync(readmeFilename, updated) diff --git a/lib/DnsResolver.js b/lib/DnsResolver.js index d4f474a..f515948 100644 --- a/lib/DnsResolver.js +++ b/lib/DnsResolver.js @@ -1,78 +1,78 @@ -import * as dns from 'dns'; -import punycode from "punycode/punycode.js"; -import { promisify } from "util"; - -const dnsLookupAsync = promisify(dns.lookup); -const dnsResolveAsync = promisify(dns.resolve); - -export default class DnsResolver { - /** - * @param {Logger} logger - */ - constructor(logger) { - this.logger = logger; - } - - isIp(host) { - return !!host.match(/\d+\.\d+\.\d+\.\d+/); - } - - /** - * Response port will only be present if srv record was involved. - * @param {string} host - * @param {number} ipFamily - * @param {string=} srvRecordPrefix - * @returns {Promise<{address:string, port:number=}>} - */ - async resolve(host, ipFamily, srvRecordPrefix) { - this.logger.debug("DNS Lookup: " + host); - - if(this.isIp(host)) { - this.logger.debug("Raw IP Address: " + host); - return {address: host}; - } - - const asciiForm = punycode.toASCII(host); - if (asciiForm !== host) { - this.logger.debug("Encoded punycode: " + host + " -> " + asciiForm); - host = asciiForm; - } - - if (srvRecordPrefix) { - this.logger.debug("SRV Resolve: " + srvRecordPrefix + '.' + host); - let records; - try { - records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV'); - if (records.length >= 1) { - this.logger.debug("Found SRV Records: ", records); - const record = records[0]; - const srvPort = record.port; - const srvHost = record.name; - if (srvHost === host) { - throw new Error('Loop in DNS SRV records'); - } - return { - port: srvPort, - ...await this.resolve(srvHost, ipFamily, srvRecordPrefix) - }; - } - this.logger.debug("No SRV Record"); - } catch (e) { - this.logger.debug(e); - } - } - - this.logger.debug("Standard Resolve: " + host); - const dnsResult = await dnsLookupAsync(host, ipFamily); - // For some reason, this sometimes returns a string address rather than an object. - // I haven't been able to reproduce, but it's been reported on the issue tracker. - let address; - if (typeof dnsResult === 'string') { - address = dnsResult; - } else { - address = dnsResult.address; - } - this.logger.debug("Found address: " + address); - return {address: address}; - } -} +import * as dns from 'dns' +import punycode from 'punycode/punycode.js' +import { promisify } from 'util' + +const dnsLookupAsync = promisify(dns.lookup) +const dnsResolveAsync = promisify(dns.resolve) + +export default class DnsResolver { + /** + * @param {Logger} logger + */ + constructor (logger) { + this.logger = logger + } + + isIp (host) { + return !!host.match(/\d+\.\d+\.\d+\.\d+/) + } + + /** + * Response port will only be present if srv record was involved. + * @param {string} host + * @param {number} ipFamily + * @param {string=} srvRecordPrefix + * @returns {Promise<{address:string, port:number=}>} + */ + async resolve (host, ipFamily, srvRecordPrefix) { + this.logger.debug('DNS Lookup: ' + host) + + if (this.isIp(host)) { + this.logger.debug('Raw IP Address: ' + host) + return { address: host } + } + + const asciiForm = punycode.toASCII(host) + if (asciiForm !== host) { + this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm) + host = asciiForm + } + + if (srvRecordPrefix) { + this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host) + let records + try { + records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV') + if (records.length >= 1) { + this.logger.debug('Found SRV Records: ', records) + const record = records[0] + const srvPort = record.port + const srvHost = record.name + if (srvHost === host) { + throw new Error('Loop in DNS SRV records') + } + return { + port: srvPort, + ...await this.resolve(srvHost, ipFamily, srvRecordPrefix) + } + } + this.logger.debug('No SRV Record') + } catch (e) { + this.logger.debug(e) + } + } + + this.logger.debug('Standard Resolve: ' + host) + const dnsResult = await dnsLookupAsync(host, ipFamily) + // For some reason, this sometimes returns a string address rather than an object. + // I haven't been able to reproduce, but it's been reported on the issue tracker. + let address + if (typeof dnsResult === 'string') { + address = dnsResult + } else { + address = dnsResult.address + } + this.logger.debug('Found address: ' + address) + return { address } + } +} diff --git a/lib/GameResolver.js b/lib/GameResolver.js index 2c5ec42..149d9c6 100644 --- a/lib/GameResolver.js +++ b/lib/GameResolver.js @@ -1,122 +1,114 @@ -import * as path from 'path'; -import { fileURLToPath } from "url"; -import * as fs from 'fs'; - -export default class GameResolver { - constructor() { - const loaded = this._readGames(); - this.gamesByKey = loaded.gamesByKey; - this.games = loaded.games; - } - - lookup(type) { - if(!type) - throw Error('No game specified'); - - if(type.substring(0,9) === 'protocol-') { - return { - protocol: type.substring(9) - }; - } - - const game = this.gamesByKey.get(type); - - if(!game) - throw Error('Invalid game: '+type); - - return game.options; - } - - printReadme() { - let out = ''; - out += '| GameDig Type ID | Name | See Also\n'; - out += '|---|---|---\n'; - - const sorted = this.games - .filter(game => game.pretty) - .sort((a,b) => { - return a.pretty.localeCompare(b.pretty); - }); - for(const game of sorted) { - let keysOut = game.keys.map(key => '`'+key+'`').join('
'); - out += "| " + keysOut.padEnd(10, " ") + " " - + "| " + game.pretty; - let notes = []; - if(game.extra.doc_notes) { - notes.push("[Notes](#" + game.extra.doc_notes + ")"); - } - if(game.options.protocol === 'valve') { - notes.push('[Valve Protocol](#valve)'); - } - if(notes.length) { - out += " | " + notes.join(', '); - } - out += "\n"; - } - return out; - } - - _readGames() { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const gamesFile = path.normalize(__dirname+'/../games.txt'); - const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); - - const gamesByKey = new Map(); - const games = []; - - for (let line of lines) { - // strip comments - const comment = line.indexOf('#'); - if(comment !== -1) line = line.substring(0,comment); - line = line.trim(); - if(!line) continue; - - const split = line.split('|'); - const keys = split[0].trim().split(','); - const name = split[1].trim(); - const options = this._parseList(split[3]); - options.protocol = split[2].trim(); - const extra = this._parseList(split[4]); - - const game = { - keys: keys, - pretty: name, - options: options, - extra: extra - }; - - for (const key of keys) { - gamesByKey.set(key, game); - } - - games.push(game); - } - return { gamesByKey, games }; - } - - _parseList(str) { - if(!str) - return {}; - - let out = {}; - for (const one of str.split(',')) { - const equals = one.indexOf('='); - const key = equals === -1 ? one : one.substring(0, equals); - - /** @type {string|number|boolean} */ - let value = equals === -1 ? '' : one.substring(equals + 1); - - if(value === 'true' || value === '') - value = true; - else if(value === 'false') - value = false; - else if(!isNaN(parseInt(value))) - value = parseInt(value); - - out[key] = value; - } - - return out; - } -} +import * as path from 'path' +import { fileURLToPath } from 'url' +import * as fs from 'fs' + +export default class GameResolver { + constructor () { + const loaded = this._readGames() + this.gamesByKey = loaded.gamesByKey + this.games = loaded.games + } + + lookup (type) { + if (!type) { throw Error('No game specified') } + + if (type.substring(0, 9) === 'protocol-') { + return { + protocol: type.substring(9) + } + } + + const game = this.gamesByKey.get(type) + + if (!game) { throw Error('Invalid game: ' + type) } + + return game.options + } + + printReadme () { + let out = '' + out += '| GameDig Type ID | Name | See Also\n' + out += '|---|---|---\n' + + const sorted = this.games + .filter(game => game.pretty) + .sort((a, b) => { + return a.pretty.localeCompare(b.pretty) + }) + for (const game of sorted) { + const keysOut = game.keys.map(key => '`' + key + '`').join('
') + out += '| ' + keysOut.padEnd(10, ' ') + ' ' + + '| ' + game.pretty + const notes = [] + if (game.extra.doc_notes) { + notes.push('[Notes](#' + game.extra.doc_notes + ')') + } + if (game.options.protocol === 'valve') { + notes.push('[Valve Protocol](#valve)') + } + if (notes.length) { + out += ' | ' + notes.join(', ') + } + out += '\n' + } + return out + } + + _readGames () { + const __filename = fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename) + const gamesFile = path.normalize(__dirname + '/../games.txt') + const lines = fs.readFileSync(gamesFile, 'utf8').split('\n') + + const gamesByKey = new Map() + const games = [] + + for (let line of lines) { + // strip comments + const comment = line.indexOf('#') + if (comment !== -1) line = line.substring(0, comment) + line = line.trim() + if (!line) continue + + const split = line.split('|') + const keys = split[0].trim().split(',') + const name = split[1].trim() + const options = this._parseList(split[3]) + options.protocol = split[2].trim() + const extra = this._parseList(split[4]) + + const game = { + keys, + pretty: name, + options, + extra + } + + for (const key of keys) { + gamesByKey.set(key, game) + } + + games.push(game) + } + return { gamesByKey, games } + } + + _parseList (str) { + if (!str) { return {} } + + const out = {} + for (const one of str.split(',')) { + const equals = one.indexOf('=') + const key = equals === -1 ? one : one.substring(0, equals) + + /** @type {string|number|boolean} */ + let value = equals === -1 ? '' : one.substring(equals + 1) + + if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) } + + out[key] = value + } + + return out + } +} diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js index c1938ac..793367b 100644 --- a/lib/GlobalUdpSocket.js +++ b/lib/GlobalUdpSocket.js @@ -1,69 +1,69 @@ - -import { createSocket } from "dgram"; -import { debugDump } from "./HexUtil.js"; -import { promisify } from "util"; -import Logger from "./Logger.js"; - -export default class GlobalUdpSocket { - constructor({port}) { - this.socket = null; - this.callbacks = new Set(); - this.debuggingCallbacks = new Set(); - this.logger = new Logger(); - this.port = port; - } - - async _getSocket() { - if (!this.socket) { - const udpSocket = createSocket({ - type: 'udp4', - reuseAddr: true - }); - udpSocket.unref(); - udpSocket.on('message', (buffer, rinfo) => { - const fromAddress = rinfo.address; - const fromPort = rinfo.port; - this.logger.debug(log => { - log(fromAddress + ':' + fromPort + " <--UDP(" + this.port + ")"); - log(debugDump(buffer)); - }); - for (const callback of this.callbacks) { - callback(fromAddress, fromPort, buffer); - } - }); - udpSocket.on('error', e => { - this.logger.debug("UDP ERROR:", e); - }); - await promisify(udpSocket.bind).bind(udpSocket)(this.port); - this.port = udpSocket.address().port; - this.socket = udpSocket; - } - return this.socket; - } - - async send(buffer, address, port, debug) { - const socket = await this._getSocket(); - - if (debug) { - this.logger._print(log => { - log(address + ':' + port + " UDP(" + this.port + ")-->"); - log(debugDump(buffer)); - }); - } - - await promisify(socket.send).bind(socket)(buffer,0,buffer.length,port,address); - } - - addCallback(callback, debug) { - this.callbacks.add(callback); - if (debug) { - this.debuggingCallbacks.add(callback); - this.logger.debugEnabled = true; - } - } - removeCallback(callback) { - this.callbacks.delete(callback); - this.debuggingCallbacks.delete(callback); - this.logger.debugEnabled = this.debuggingCallbacks.size > 0; - } -} +import { createSocket } from 'dgram' +import { debugDump } from './HexUtil.js' +import { promisify } from 'util' +import Logger from './Logger.js' + +export default class GlobalUdpSocket { + constructor ({ port }) { + this.socket = null + this.callbacks = new Set() + this.debuggingCallbacks = new Set() + this.logger = new Logger() + this.port = port + } + + async _getSocket () { + if (!this.socket) { + const udpSocket = createSocket({ + type: 'udp4', + reuseAddr: true + }) + udpSocket.unref() + udpSocket.on('message', (buffer, rinfo) => { + const fromAddress = rinfo.address + const fromPort = rinfo.port + this.logger.debug(log => { + log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')') + log(debugDump(buffer)) + }) + for (const callback of this.callbacks) { + callback(fromAddress, fromPort, buffer) + } + }) + udpSocket.on('error', e => { + this.logger.debug('UDP ERROR:', e) + }) + await promisify(udpSocket.bind).bind(udpSocket)(this.port) + this.port = udpSocket.address().port + this.socket = udpSocket + } + return this.socket + } + + async send (buffer, address, port, debug) { + const socket = await this._getSocket() + + if (debug) { + this.logger._print(log => { + log(address + ':' + port + ' UDP(' + this.port + ')-->') + log(debugDump(buffer)) + }) + } + + await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address) + } + + addCallback (callback, debug) { + this.callbacks.add(callback) + if (debug) { + this.debuggingCallbacks.add(callback) + this.logger.debugEnabled = true + } + } + + removeCallback (callback) { + this.callbacks.delete(callback) + this.debuggingCallbacks.delete(callback) + this.logger.debugEnabled = this.debuggingCallbacks.size > 0 + } +} diff --git a/lib/HexUtil.js b/lib/HexUtil.js index aa555f1..eedd38a 100644 --- a/lib/HexUtil.js +++ b/lib/HexUtil.js @@ -1,21 +1,20 @@ - -/** @param {Buffer} buffer */ -export const debugDump = (buffer) => { - let hexLine = ''; - let chrLine = ''; - let out = ''; - out += "Buffer length: " + buffer.length + " bytes\n"; - for(let i = 0; i < buffer.length; i++) { - const sliced = buffer.slice(i,i+1); - hexLine += sliced.toString('hex')+' '; - let chr = sliced.toString(); - if(chr < ' ' || chr > '~') chr = ' '; - chrLine += chr+' '; - if(hexLine.length > 60 || i === buffer.length - 1) { - out += hexLine + '\n'; - out += chrLine + '\n'; - hexLine = chrLine = ''; - } - } - return out; -} +/** @param {Buffer} buffer */ +export const debugDump = (buffer) => { + let hexLine = '' + let chrLine = '' + let out = '' + out += 'Buffer length: ' + buffer.length + ' bytes\n' + for (let i = 0; i < buffer.length; i++) { + const sliced = buffer.slice(i, i + 1) + hexLine += sliced.toString('hex') + ' ' + let chr = sliced.toString() + if (chr < ' ' || chr > '~') chr = ' ' + chrLine += chr + ' ' + if (hexLine.length > 60 || i === buffer.length - 1) { + out += hexLine + '\n' + out += chrLine + '\n' + hexLine = chrLine = '' + } + } + return out +} diff --git a/lib/Logger.js b/lib/Logger.js index f9a584d..c570821 100644 --- a/lib/Logger.js +++ b/lib/Logger.js @@ -1,44 +1,44 @@ -import {debugDump} from './HexUtil.js'; - -export default class Logger { - constructor() { - this.debugEnabled = false; - this.prefix = ''; - } - - debug(...args) { - if (!this.debugEnabled) return; - this._print(...args); - } - - _print(...args) { - try { - const strings = this._convertArgsToStrings(...args); - if (strings.length) { - if (this.prefix) { - strings.unshift(this.prefix); - } - console.log(...strings); - } - } catch(e) { - console.log("Error while logging: " + e); - } - } - - _convertArgsToStrings(...args) { - const out = []; - for (const arg of args) { - if (arg instanceof Error) { - out.push(arg.stack); - } else if (arg instanceof Buffer) { - out.push(debugDump(arg)); - } else if (typeof arg == 'function') { - const result = arg.call(undefined, (...args) => this._print(...args)); - if (result !== undefined) out.push(...this._convertArgsToStrings(result)); - } else { - out.push(arg); - } - } - return out; - } -} +import { debugDump } from './HexUtil.js' + +export default class Logger { + constructor () { + this.debugEnabled = false + this.prefix = '' + } + + debug (...args) { + if (!this.debugEnabled) return + this._print(...args) + } + + _print (...args) { + try { + const strings = this._convertArgsToStrings(...args) + if (strings.length) { + if (this.prefix) { + strings.unshift(this.prefix) + } + console.log(...strings) + } + } catch (e) { + console.log('Error while logging: ' + e) + } + } + + _convertArgsToStrings (...args) { + const out = [] + for (const arg of args) { + if (arg instanceof Error) { + out.push(arg.stack) + } else if (arg instanceof Buffer) { + out.push(debugDump(arg)) + } else if (typeof arg === 'function') { + const result = arg.call(undefined, (...args) => this._print(...args)) + if (result !== undefined) out.push(...this._convertArgsToStrings(result)) + } else { + out.push(arg) + } + } + return out + } +} diff --git a/lib/Promises.js b/lib/Promises.js index 7d4f0d6..eb27f32 100644 --- a/lib/Promises.js +++ b/lib/Promises.js @@ -1,18 +1,18 @@ -export default class Promises { - static createTimeout(timeoutMs, timeoutMsg) { - let cancel = null; - const wrapped = new Promise((res, rej) => { - const timeout = setTimeout( - () => { - rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")); - }, - timeoutMs - ); - cancel = () => { - clearTimeout(timeout); - }; - }); - wrapped.cancel = cancel; - return wrapped; - } -} +export default class Promises { + static createTimeout (timeoutMs, timeoutMsg) { + let cancel = null + const wrapped = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => { + reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms')) + }, + timeoutMs + ) + cancel = () => { + clearTimeout(timeout) + } + }) + wrapped.cancel = cancel + return wrapped + } +} diff --git a/lib/ProtocolResolver.js b/lib/ProtocolResolver.js index 8e796c9..6696e62 100644 --- a/lib/ProtocolResolver.js +++ b/lib/ProtocolResolver.js @@ -1,8 +1,7 @@ -import * as Protocols from '../protocols/index.js' - -export const getProtocol = (protocolId) => { - if(!(protocolId in Protocols)) - throw Error('Protocol definition file missing: ' + protocolId); - - return new Protocols[protocolId]; -} +import * as Protocols from '../protocols/index.js' + +export const getProtocol = (protocolId) => { + if (!(protocolId in Protocols)) { throw Error('Protocol definition file missing: ' + protocolId) } + + return new Protocols[protocolId]() +} diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js index 6e4187b..07fdf30 100644 --- a/lib/QueryRunner.js +++ b/lib/QueryRunner.js @@ -1,97 +1,95 @@ -import GameResolver from "./GameResolver.js"; -import {getProtocol} from './ProtocolResolver.js'; -import GlobalUdpSocket from "./GlobalUdpSocket.js"; - -const defaultOptions = { - socketTimeout: 2000, - attemptTimeout: 10000, - maxAttempts: 1, - ipFamily: 0 -}; - -export default class QueryRunner { - constructor(runnerOpts = {}) { - this.udpSocket = new GlobalUdpSocket({ - port: runnerOpts.listenUdpPort - }); - this.gameResolver = new GameResolver(); - } - - async run(userOptions) { - for (const key of Object.keys(userOptions)) { - const value = userOptions[key]; - if (['port', 'ipFamily'].includes(key)) { - userOptions[key] = parseInt(value); - } - } - - const { - port_query: gameQueryPort, - port_query_offset: gameQueryPortOffset, - ...gameOptions - } = this.gameResolver.lookup(userOptions.type); - let attempts = []; - - const optionsCollection = { - ...defaultOptions, - ...gameOptions, - ...userOptions - }; - - const addAttemptWithPort = port => { - attempts.push({ - ...optionsCollection, - port - }); - } - - if (userOptions.port) { - if(!userOptions.givenPortOnly) { - if (gameQueryPortOffset) - addAttemptWithPort(userOptions.port + gameQueryPortOffset); - - if (userOptions.port === gameOptions.port && gameQueryPort) - addAttemptWithPort(gameQueryPort); - } - - attempts.push(optionsCollection); - } else if (gameQueryPort) { - addAttemptWithPort(gameQueryPort); - } else if (gameOptions.port) { - addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0)); - } else { - // Hopefully the request doesn't need a port. If it does, it'll fail when making the request. - attempts.push(optionsCollection); - } - - const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts; - - let attemptNum = 0; - const errors = []; - for (const attempt of attempts) { - for (let retry = 0; retry < numRetries; retry++) { - attemptNum++; - try { - return await this._attempt(attempt); - } catch (e) { - e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack; - errors.push(e); - } - } - } - - const err = new Error('Failed all ' + errors.length + ' attempts'); - for (const e of errors) { - err.stack += '\n' + e.stack; - } - - throw err; - } - - async _attempt(options) { - const core = getProtocol(options.protocol); - core.options = options; - core.udpSocket = this.udpSocket; - return await core.runOnceSafe(); - } -} +import GameResolver from './GameResolver.js' +import { getProtocol } from './ProtocolResolver.js' +import GlobalUdpSocket from './GlobalUdpSocket.js' + +const defaultOptions = { + socketTimeout: 2000, + attemptTimeout: 10000, + maxAttempts: 1, + ipFamily: 0 +} + +export default class QueryRunner { + constructor (runnerOpts = {}) { + this.udpSocket = new GlobalUdpSocket({ + port: runnerOpts.listenUdpPort + }) + this.gameResolver = new GameResolver() + } + + async run (userOptions) { + for (const key of Object.keys(userOptions)) { + const value = userOptions[key] + if (['port', 'ipFamily'].includes(key)) { + userOptions[key] = parseInt(value) + } + } + + const { + port_query: gameQueryPort, + port_query_offset: gameQueryPortOffset, + ...gameOptions + } = this.gameResolver.lookup(userOptions.type) + const attempts = [] + + const optionsCollection = { + ...defaultOptions, + ...gameOptions, + ...userOptions + } + + const addAttemptWithPort = port => { + attempts.push({ + ...optionsCollection, + port + }) + } + + if (userOptions.port) { + if (!userOptions.givenPortOnly) { + if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) } + + if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) } + } + + attempts.push(optionsCollection) + } else if (gameQueryPort) { + addAttemptWithPort(gameQueryPort) + } else if (gameOptions.port) { + addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0)) + } else { + // Hopefully the request doesn't need a port. If it does, it'll fail when making the request. + attempts.push(optionsCollection) + } + + const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts + + let attemptNum = 0 + const errors = [] + for (const attempt of attempts) { + for (let retry = 0; retry < numRetries; retry++) { + attemptNum++ + try { + return await this._attempt(attempt) + } catch (e) { + e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack + errors.push(e) + } + } + } + + const err = new Error('Failed all ' + errors.length + ' attempts') + for (const e of errors) { + err.stack += '\n' + e.stack + } + + throw err + } + + async _attempt (options) { + const core = getProtocol(options.protocol) + core.options = options + core.udpSocket = this.udpSocket + return await core.runOnceSafe() + } +} diff --git a/lib/Results.js b/lib/Results.js index 71908d0..fd76ebd 100644 --- a/lib/Results.js +++ b/lib/Results.js @@ -1,43 +1,42 @@ - -export class Player { - name = ''; - raw = {}; - - constructor(data) { - if (typeof data === 'string') { - this.name = data; - } else { - const {name, ...raw} = data; - if (name) this.name = name; - if (raw) this.raw = raw; - } - } -} - -export class Players extends Array { - setNum(num) { - // If the server specified some ridiculous number of players (billions), we don't want to - // run out of ram allocating these objects. - num = Math.min(num, 10000); - - while(this.length < num) { - this.push({}); - } - } - - push(data) { - super.push(new Player(data)); - } -} - -export class Results { - name = ''; - map = ''; - password = false; - - raw = {}; - - maxplayers = 0; - players = new Players(); - bots = new Players(); -} +export class Player { + name = '' + raw = {} + + constructor (data) { + if (typeof data === 'string') { + this.name = data + } else { + const { name, ...raw } = data + if (name) this.name = name + if (raw) this.raw = raw + } + } +} + +export class Players extends Array { + setNum (num) { + // If the server specified some ridiculous number of players (billions), we don't want to + // run out of ram allocating these objects. + num = Math.min(num, 10000) + + while (this.length < num) { + this.push({}) + } + } + + push (data) { + super.push(new Player(data)) + } +} + +export class Results { + name = '' + map = '' + password = false + + raw = {} + + maxplayers = 0 + players = new Players() + bots = new Players() +} diff --git a/lib/index.js b/lib/index.js index 71da89d..f77f041 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,24 +1,23 @@ -import QueryRunner from './QueryRunner.js'; - -let singleton = null; - -export default class Gamedig { - constructor(runnerOpts) { - this.queryRunner = new QueryRunner(runnerOpts); - } - - async query(userOptions) { - return await this.queryRunner.run(userOptions); - } - - static getInstance() { - if (!singleton) - singleton = new Gamedig(); - - return singleton; - } - - static async query(...args) { - return await Gamedig.getInstance().query(...args); - } -} +import QueryRunner from './QueryRunner.js' + +let singleton = null + +export default class Gamedig { + constructor (runnerOpts) { + this.queryRunner = new QueryRunner(runnerOpts) + } + + async query (userOptions) { + return await this.queryRunner.run(userOptions) + } + + static getInstance () { + if (!singleton) { singleton = new Gamedig() } + + return singleton + } + + static async query (...args) { + return await Gamedig.getInstance().query(...args) + } +} diff --git a/lib/reader.js b/lib/reader.js index 17801b9..b706e64 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -1,172 +1,172 @@ -import Iconv from "iconv-lite"; -import Long from 'long'; -import {Buffer} from "buffer"; -import Varint from 'varint'; - -function readUInt64BE(buffer,offset) { - const high = buffer.readUInt32BE(offset); - const low = buffer.readUInt32BE(offset+4); - return new Long(low,high,true); -} -function readUInt64LE(buffer,offset) { - const low = buffer.readUInt32LE(offset); - const high = buffer.readUInt32LE(offset+4); - return new Long(low,high,true); -} - -export default class Reader { - /** - * @param {Core} query - * @param {Buffer} buffer - **/ - constructor(query,buffer) { - this.defaultEncoding = query.options.encoding || query.encoding; - this.defaultDelimiter = query.delimiter; - this.defaultByteOrder = query.byteorder; - this.buffer = buffer; - this.i = 0; - } - - setOffset(offset) { - this.i = offset; - } - - offset() { - return this.i; - } - - skip(i) { - this.i += i; - } - - pascalString(bytesForSize, adjustment=0) { - const length = this.uint(bytesForSize) + adjustment; - return this.string(length); - } - - string(arg) { - let encoding = this.defaultEncoding; - let length = null; - let delimiter = this.defaultDelimiter; - - if(typeof arg === 'string') delimiter = arg; - else if(typeof arg === 'number') length = arg; - else if(typeof arg === 'object') { - if ('encoding' in arg) encoding = arg.encoding; - if ('length' in arg) length = arg.length; - if ('delimiter' in arg) delimiter = arg.delimiter; - } - - if(encoding === 'latin1') encoding = 'win1252'; - - const start = this.i; - let end = start; - if(length === null) { - // terminated by the delimiter - let delim = delimiter; - if (typeof delim === 'string') delim = delim.charCodeAt(0); - while (true) { - if (end >= this.buffer.length) { - end = this.buffer.length; - break; - } - if (this.buffer.readUInt8(end) === delim) break; - end++; - } - this.i = end + 1; - } else if (length <= 0) { - return ''; - } else { - end = start+length; - if(end >= this.buffer.length) { - end = this.buffer.length; - } - this.i = end; - } - - const slice = this.buffer.slice(start, end); - const enc = encoding; - if(enc === 'utf8' || enc === 'ucs2' || enc === 'binary') { - return slice.toString(enc); - } else { - return Iconv.decode(slice,enc); - } - } - - int(bytes) { - let r = 0; - if(this.remaining() >= bytes) { - if(this.defaultByteOrder === 'be') { - if(bytes === 1) r = this.buffer.readInt8(this.i); - else if(bytes === 2) r = this.buffer.readInt16BE(this.i); - else if(bytes === 4) r = this.buffer.readInt32BE(this.i); - } else { - if(bytes === 1) r = this.buffer.readInt8(this.i); - else if(bytes === 2) r = this.buffer.readInt16LE(this.i); - else if(bytes === 4) r = this.buffer.readInt32LE(this.i); - } - } - this.i += bytes; - return r; - } - - /** @returns {number} */ - uint(bytes) { - let r = 0; - if(this.remaining() >= bytes) { - if(this.defaultByteOrder === 'be') { - if(bytes === 1) r = this.buffer.readUInt8(this.i); - else if(bytes === 2) r = this.buffer.readUInt16BE(this.i); - else if(bytes === 4) r = this.buffer.readUInt32BE(this.i); - else if(bytes === 8) r = readUInt64BE(this.buffer,this.i); - } else { - if(bytes === 1) r = this.buffer.readUInt8(this.i); - else if(bytes === 2) r = this.buffer.readUInt16LE(this.i); - else if(bytes === 4) r = this.buffer.readUInt32LE(this.i); - else if(bytes === 8) r = readUInt64LE(this.buffer,this.i); - } - } - this.i += bytes; - return r; - } - - float() { - let r = 0; - if(this.remaining() >= 4) { - if(this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i); - else r = this.buffer.readFloatLE(this.i); - } - this.i += 4; - return r; - } - - varint() { - const out = Varint.decode(this.buffer, this.i); - this.i += Varint.decode.bytes; - return out; - } - - /** @returns Buffer */ - part(bytes) { - let r; - if(this.remaining() >= bytes) { - r = this.buffer.slice(this.i,this.i+bytes); - } else { - r = Buffer.from([]); - } - this.i += bytes; - return r; - } - - remaining() { - return this.buffer.length-this.i; - } - - rest() { - return this.buffer.slice(this.i); - } - - done() { - return this.i >= this.buffer.length; - } -} +import Iconv from 'iconv-lite' +import Long from 'long' +import { Buffer } from 'buffer' +import Varint from 'varint' + +function readUInt64BE (buffer, offset) { + const high = buffer.readUInt32BE(offset) + const low = buffer.readUInt32BE(offset + 4) + return new Long(low, high, true) +} +function readUInt64LE (buffer, offset) { + const low = buffer.readUInt32LE(offset) + const high = buffer.readUInt32LE(offset + 4) + return new Long(low, high, true) +} + +export default class Reader { + /** + * @param {Core} query + * @param {Buffer} buffer + **/ + constructor (query, buffer) { + this.defaultEncoding = query.options.encoding || query.encoding + this.defaultDelimiter = query.delimiter + this.defaultByteOrder = query.byteorder + this.buffer = buffer + this.i = 0 + } + + setOffset (offset) { + this.i = offset + } + + offset () { + return this.i + } + + skip (i) { + this.i += i + } + + pascalString (bytesForSize, adjustment = 0) { + const length = this.uint(bytesForSize) + adjustment + return this.string(length) + } + + string (arg) { + let encoding = this.defaultEncoding + let length = null + let delimiter = this.defaultDelimiter + + if (typeof arg === 'string') delimiter = arg + else if (typeof arg === 'number') length = arg + else if (typeof arg === 'object') { + if ('encoding' in arg) encoding = arg.encoding + if ('length' in arg) length = arg.length + if ('delimiter' in arg) delimiter = arg.delimiter + } + + if (encoding === 'latin1') encoding = 'win1252' + + const start = this.i + let end = start + if (length === null) { + // terminated by the delimiter + let delim = delimiter + if (typeof delim === 'string') delim = delim.charCodeAt(0) + while (true) { + if (end >= this.buffer.length) { + end = this.buffer.length + break + } + if (this.buffer.readUInt8(end) === delim) break + end++ + } + this.i = end + 1 + } else if (length <= 0) { + return '' + } else { + end = start + length + if (end >= this.buffer.length) { + end = this.buffer.length + } + this.i = end + } + + const slice = this.buffer.slice(start, end) + const enc = encoding + if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') { + return slice.toString(enc) + } else { + return Iconv.decode(slice, enc) + } + } + + int (bytes) { + let r = 0 + if (this.remaining() >= bytes) { + if (this.defaultByteOrder === 'be') { + if (bytes === 1) r = this.buffer.readInt8(this.i) + else if (bytes === 2) r = this.buffer.readInt16BE(this.i) + else if (bytes === 4) r = this.buffer.readInt32BE(this.i) + } else { + if (bytes === 1) r = this.buffer.readInt8(this.i) + else if (bytes === 2) r = this.buffer.readInt16LE(this.i) + else if (bytes === 4) r = this.buffer.readInt32LE(this.i) + } + } + this.i += bytes + return r + } + + /** @returns {number} */ + uint (bytes) { + let r = 0 + if (this.remaining() >= bytes) { + if (this.defaultByteOrder === 'be') { + if (bytes === 1) r = this.buffer.readUInt8(this.i) + else if (bytes === 2) r = this.buffer.readUInt16BE(this.i) + else if (bytes === 4) r = this.buffer.readUInt32BE(this.i) + else if (bytes === 8) r = readUInt64BE(this.buffer, this.i) + } else { + if (bytes === 1) r = this.buffer.readUInt8(this.i) + else if (bytes === 2) r = this.buffer.readUInt16LE(this.i) + else if (bytes === 4) r = this.buffer.readUInt32LE(this.i) + else if (bytes === 8) r = readUInt64LE(this.buffer, this.i) + } + } + this.i += bytes + return r + } + + float () { + let r = 0 + if (this.remaining() >= 4) { + if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i) + else r = this.buffer.readFloatLE(this.i) + } + this.i += 4 + return r + } + + varint () { + const out = Varint.decode(this.buffer, this.i) + this.i += Varint.decode.bytes + return out + } + + /** @returns Buffer */ + part (bytes) { + let r + if (this.remaining() >= bytes) { + r = this.buffer.slice(this.i, this.i + bytes) + } else { + r = Buffer.from([]) + } + this.i += bytes + return r + } + + remaining () { + return this.buffer.length - this.i + } + + rest () { + return this.buffer.slice(this.i) + } + + done () { + return this.i >= this.buffer.length + } +} diff --git a/package-lock.json b/package-lock.json index 6f151d2..ff0bc13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,99 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@sindresorhus/is": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", @@ -31,17 +124,155 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "@types/node": { "version": "14.18.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.13.tgz", "integrity": "sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==", "dev": true }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + } + }, + "array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "barse": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz", @@ -55,6 +286,36 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "requires": { + "semver": "^7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -74,6 +335,32 @@ "responselike": "^3.0.0" } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, "cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -100,11 +387,43 @@ "domutils": "^2.7.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "css-select": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", @@ -122,6 +441,15 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==" }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -137,11 +465,48 @@ } } }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, + "define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -180,16 +545,474 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" }, + "es-abstract": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.11" + } + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + } + }, + "eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.13.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "requires": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "event-to-promise": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz", "integrity": "sha1-ywffzUGNoiIdkPd+q3E7wjXiCQ8=" }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "requires": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, "form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, "gbxremote": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz", @@ -202,11 +1025,83 @@ "xmlrpc": "^1.3.1" } }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -225,6 +1120,63 @@ "responselike": "^3.0.0" } }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -258,21 +1210,245 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -281,6 +1457,31 @@ "json-buffer": "3.0.1" } }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "long": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", @@ -291,16 +1492,46 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "normalize-url": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", @@ -314,11 +1545,119 @@ "boolbase": "^1.0.0" } }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, "p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -332,6 +1671,36 @@ "parse5": "^6.0.1" } }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -342,6 +1711,12 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -358,11 +1733,45 @@ "string_decoder": "~0.10.x" } }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, "responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", @@ -371,11 +1780,66 @@ "lowercase-keys": "^3.0.0" } }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -401,6 +1865,49 @@ } } }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "string-to-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", @@ -439,16 +1946,186 @@ } } }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -459,6 +2136,47 @@ "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", @@ -472,6 +2190,18 @@ "sax": "1.2.x", "xmlbuilder": "8.2.x" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index 0fc9a10..b4cd943 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "gamedig", "description": "Query for the status of any game server in Node.JS", + "scripts": { + "lint:check": "eslint .", + "lint:fix": "eslint --fix ." + }, "keywords": [ "srcds", "query", @@ -35,17 +39,6 @@ "engines": { "node": ">=14.17.0" }, - "dependencies": { - "cheerio": "^1.0.0-rc.10", - "gbxremote": "^0.2.1", - "got": "^12.1.0", - "iconv-lite": "^0.6.3", - "long": "^5.2.0", - "minimist": "^1.2.6", - "punycode": "^2.3.0", - "seek-bzip": "^2.0.0", - "varint": "^6.0.0" - }, "bin": { "gamedig": "bin/gamedig.js" }, @@ -58,8 +51,24 @@ "GAMES_LIST.md", "README.md" ], + "dependencies": { + "cheerio": "^1.0.0-rc.10", + "gbxremote": "^0.2.1", + "got": "^12.1.0", + "iconv-lite": "^0.6.3", + "long": "^5.2.0", + "minimist": "^1.2.6", + "punycode": "^2.3.0", + "seek-bzip": "^2.0.0", + "varint": "^6.0.0" + }, "devDependencies": { "@types/cheerio": "^0.22.31", - "@types/node": "^14.18.13" + "@types/node": "^14.18.13", + "eslint": "^8.49.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-n": "15.7.0", + "eslint-plugin-promise": "^6.1.1" } } diff --git a/protocols/armagetron.js b/protocols/armagetron.js index 626c42e..52ff379 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -1,63 +1,65 @@ -import Core from './core.js'; - -export default class armagetron extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.byteorder = 'be'; - } - - async run(state) { - const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); - - const buffer = await this.udpSend(b,b => b); - const reader = this.reader(buffer); - - reader.skip(6); - - state.gamePort = this.readUInt(reader); - state.raw.hostname = this.readString(reader); - state.name = this.stripColorCodes(this.readString(reader)); - state.raw.numplayers = this.readUInt(reader); - state.raw.versionmin = this.readUInt(reader); - state.raw.versionmax = this.readUInt(reader); - state.raw.version = this.readString(reader); - state.maxplayers = this.readUInt(reader); - - const players = this.readString(reader); - const list = players.split('\n'); - for(const name of list) { - if(!name) continue; - state.players.push({ - name: this.stripColorCodes(name) - }); - } - - state.raw.options = this.stripColorCodes(this.readString(reader)); - state.raw.uri = this.readString(reader); - state.raw.globalids = this.readString(reader); - } - - readUInt(reader) { - const a = reader.uint(2); - const b = reader.uint(2); - return (b<<16) + a; - } - readString(reader) { - const len = reader.uint(2); - if(!len) return ''; - - let out = ''; - for(let i = 0; i < len; i += 2) { - const hi = reader.uint(1); - const lo = reader.uint(1); - if(i+1 b) + const reader = this.reader(buffer) + + reader.skip(6) + + state.gamePort = this.readUInt(reader) + state.raw.hostname = this.readString(reader) + state.name = this.stripColorCodes(this.readString(reader)) + state.raw.numplayers = this.readUInt(reader) + state.raw.versionmin = this.readUInt(reader) + state.raw.versionmax = this.readUInt(reader) + state.raw.version = this.readString(reader) + state.maxplayers = this.readUInt(reader) + + const players = this.readString(reader) + const list = players.split('\n') + for (const name of list) { + if (!name) continue + state.players.push({ + name: this.stripColorCodes(name) + }) + } + + state.raw.options = this.stripColorCodes(this.readString(reader)) + state.raw.uri = this.readString(reader) + state.raw.globalids = this.readString(reader) + } + + readUInt (reader) { + const a = reader.uint(2) + const b = reader.uint(2) + return (b << 16) + a + } + + readString (reader) { + const len = reader.uint(2) + if (!len) return '' + + let out = '' + for (let i = 0; i < len; i += 2) { + const hi = reader.uint(1) + const lo = reader.uint(1) + if (i + 1 < len) out += String.fromCharCode(lo) + if (i + 2 < len) out += String.fromCharCode(hi) + } + + return out + } + + stripColorCodes (str) { + return str.replace(/0x[0-9a-f]{6}/g, '') + } +} diff --git a/protocols/ase.js b/protocols/ase.js index 81f925e..dedec85 100644 --- a/protocols/ase.js +++ b/protocols/ase.js @@ -1,45 +1,45 @@ -import Core from './core.js'; - -export default class ase extends Core { - async run(state) { - const buffer = await this.udpSend('s',(buffer) => { - const reader = this.reader(buffer); - const header = reader.string(4); - if (header === 'EYE1') return reader.rest(); - }); - - const reader = this.reader(buffer); - state.raw.gamename = this.readString(reader); - state.gamePort = parseInt(this.readString(reader)); - state.name = this.readString(reader); - state.raw.gametype = this.readString(reader); - state.map = this.readString(reader); - state.raw.version = this.readString(reader); - state.password = this.readString(reader) === '1'; - state.raw.numplayers = parseInt(this.readString(reader)); - state.maxplayers = parseInt(this.readString(reader)); - - while(!reader.done()) { - const key = this.readString(reader); - if(!key) break; - const value = this.readString(reader); - state.raw[key] = value; - } - - while(!reader.done()) { - const flags = reader.uint(1); - const player = {}; - if(flags & 1) player.name = this.readString(reader); - if(flags & 2) player.team = this.readString(reader); - if(flags & 4) player.skin = this.readString(reader); - if(flags & 8) player.score = parseInt(this.readString(reader)); - if(flags & 16) player.ping = parseInt(this.readString(reader)); - if(flags & 32) player.time = parseInt(this.readString(reader)); - state.players.push(player); - } - } - - readString(reader) { - return reader.pascalString(1, -1); - } -} +import Core from './core.js' + +export default class ase extends Core { + async run (state) { + const buffer = await this.udpSend('s', (buffer) => { + const reader = this.reader(buffer) + const header = reader.string(4) + if (header === 'EYE1') return reader.rest() + }) + + const reader = this.reader(buffer) + state.raw.gamename = this.readString(reader) + state.gamePort = parseInt(this.readString(reader)) + state.name = this.readString(reader) + state.raw.gametype = this.readString(reader) + state.map = this.readString(reader) + state.raw.version = this.readString(reader) + state.password = this.readString(reader) === '1' + state.raw.numplayers = parseInt(this.readString(reader)) + state.maxplayers = parseInt(this.readString(reader)) + + while (!reader.done()) { + const key = this.readString(reader) + if (!key) break + const value = this.readString(reader) + state.raw[key] = value + } + + while (!reader.done()) { + const flags = reader.uint(1) + const player = {} + if (flags & 1) player.name = this.readString(reader) + if (flags & 2) player.team = this.readString(reader) + if (flags & 4) player.skin = this.readString(reader) + if (flags & 8) player.score = parseInt(this.readString(reader)) + if (flags & 16) player.ping = parseInt(this.readString(reader)) + if (flags & 32) player.time = parseInt(this.readString(reader)) + state.players.push(player) + } + } + + readString (reader) { + return reader.pascalString(1, -1) + } +} diff --git a/protocols/assettocorsa.js b/protocols/assettocorsa.js index 480a5fa..a036dda 100644 --- a/protocols/assettocorsa.js +++ b/protocols/assettocorsa.js @@ -1,38 +1,38 @@ -import Core from './core.js'; - -export default class assettocorsa extends Core { - async run(state) { - const serverInfo = await this.request({ - url: `http://${this.options.address}:${this.options.port}/INFO`, - responseType: 'json' - }); - const carInfo = await this.request({ - url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`, - responseType: 'json' - }); - - if (!serverInfo || !carInfo || !carInfo.Cars) { - throw new Error('Query not successful'); - } - - state.maxplayers = serverInfo.maxclients; - state.name = serverInfo.name; - state.map = serverInfo.track; - state.password = serverInfo.pass; - state.gamePort = serverInfo.port; - state.raw.carInfo = carInfo.Cars; - state.raw.serverInfo = serverInfo; - - for (const car of carInfo.Cars) { - if (car.IsConnected) { - state.players.push({ - name: car.DriverName, - car: car.Model, - skin: car.Skin, - nation: car.DriverNation, - team: car.DriverTeam - }); - } - } - } -} +import Core from './core.js' + +export default class assettocorsa extends Core { + async run (state) { + const serverInfo = await this.request({ + url: `http://${this.options.address}:${this.options.port}/INFO`, + responseType: 'json' + }) + const carInfo = await this.request({ + url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`, + responseType: 'json' + }) + + if (!serverInfo || !carInfo || !carInfo.Cars) { + throw new Error('Query not successful') + } + + state.maxplayers = serverInfo.maxclients + state.name = serverInfo.name + state.map = serverInfo.track + state.password = serverInfo.pass + state.gamePort = serverInfo.port + state.raw.carInfo = carInfo.Cars + state.raw.serverInfo = serverInfo + + for (const car of carInfo.Cars) { + if (car.IsConnected) { + state.players.push({ + name: car.DriverName, + car: car.Model, + skin: car.Skin, + nation: car.DriverNation, + team: car.DriverTeam + }) + } + } + } +} diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 8c22ebd..65c336e 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -1,161 +1,162 @@ -import Core from './core.js'; - -export default class battlefield extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - } - - async run(state) { - await this.withTcp(async socket => { - { - const data = await this.query(socket, ['serverInfo']); - state.name = data.shift(); - state.raw.numplayers = parseInt(data.shift()); - state.maxplayers = parseInt(data.shift()); - state.raw.gametype = data.shift(); - state.map = data.shift(); - state.raw.roundsplayed = parseInt(data.shift()); - state.raw.roundstotal = parseInt(data.shift()); - - const teamCount = data.shift(); - state.raw.teams = []; - for (let i = 0; i < teamCount; i++) { - const tickets = parseFloat(data.shift()); - state.raw.teams.push({ - tickets: tickets - }); - } - - state.raw.targetscore = parseInt(data.shift()); - state.raw.status = data.shift(); - - // Seems like the fields end at random places beyond this point - // depending on the server version - - if (data.length) state.raw.ranked = (data.shift() === 'true'); - if (data.length) state.raw.punkbuster = (data.shift() === 'true'); - if (data.length) state.password = (data.shift() === 'true'); - if (data.length) state.raw.uptime = parseInt(data.shift()); - if (data.length) state.raw.roundtime = parseInt(data.shift()); - - const isBadCompany2 = data[0] === 'BC2'; - if (isBadCompany2) { - if (data.length) data.shift(); - if (data.length) data.shift(); - } - if (data.length) { - state.raw.ip = data.shift(); - const split = state.raw.ip.split(':'); - state.gameHost = split[0]; - state.gamePort = split[1]; - } else { - // best guess if the server doesn't tell us what the server port is - // these are just the default game ports for different default query ports - if (this.options.port === 48888) state.gamePort = 7673; - if (this.options.port === 22000) state.gamePort = 25200; - } - if (data.length) state.raw.punkbusterversion = data.shift(); - if (data.length) state.raw.joinqueue = (data.shift() === 'true'); - if (data.length) state.raw.region = data.shift(); - if (data.length) state.raw.pingsite = data.shift(); - if (data.length) state.raw.country = data.shift(); - if (data.length) state.raw.quickmatch = (data.shift() === 'true'); - } - - { - const data = await this.query(socket, ['version']); - data.shift(); - state.raw.version = data.shift(); - } - - { - const data = await this.query(socket, ['listPlayers', 'all']); - const fieldCount = parseInt(data.shift()); - const fields = []; - for (let i = 0; i < fieldCount; i++) { - fields.push(data.shift()); - } - const numplayers = data.shift(); - for (let i = 0; i < numplayers; i++) { - const player = {}; - for (let key of fields) { - let value = data.shift(); - - if (key === 'teamId') key = 'team'; - else if (key === 'squadId') key = 'squad'; - - if ( - key === 'kills' - || key === 'deaths' - || key === 'score' - || key === 'rank' - || key === 'team' - || key === 'squad' - || key === 'ping' - || key === 'type' - ) { - value = parseInt(value); - } - - player[key] = value; - } - state.players.push(player); - } - } - }); - } - - async query(socket, params) { - const outPacket = this.buildPacket(params); - return await this.tcpSend(socket, outPacket, (data) => { - const decoded = this.decodePacket(data); - if(decoded) { - this.logger.debug(decoded); - if(decoded.shift() !== 'OK') throw new Error('Missing OK'); - return decoded; - } - }); - } - - buildPacket(params) { - const paramBuffers = []; - for (const param of params) { - paramBuffers.push(Buffer.from(param,'utf8')); - } - - let totalLength = 12; - for (const paramBuffer of paramBuffers) { - totalLength += paramBuffer.length+1+4; - } - - const b = Buffer.alloc(totalLength); - b.writeUInt32LE(0,0); - b.writeUInt32LE(totalLength,4); - b.writeUInt32LE(params.length,8); - let offset = 12; - for (const paramBuffer of paramBuffers) { - b.writeUInt32LE(paramBuffer.length, offset); offset += 4; - paramBuffer.copy(b, offset); offset += paramBuffer.length; - b.writeUInt8(0, offset); offset += 1; - } - - return b; - } - decodePacket(buffer) { - if(buffer.length < 8) return false; - const reader = this.reader(buffer); - const header = reader.uint(4); - const totalLength = reader.uint(4); - if(buffer.length < totalLength) return false; - this.logger.debug("Expected " + totalLength + " bytes, have " + buffer.length); - - const paramCount = reader.uint(4); - const params = []; - for(let i = 0; i < paramCount; i++) { - params.push(reader.pascalString(4)); - const strNull = reader.uint(1); - } - return params; - } -} +import Core from './core.js' + +export default class battlefield extends Core { + constructor () { + super() + this.encoding = 'latin1' + } + + async run (state) { + await this.withTcp(async socket => { + { + const data = await this.query(socket, ['serverInfo']) + state.name = data.shift() + state.raw.numplayers = parseInt(data.shift()) + state.maxplayers = parseInt(data.shift()) + state.raw.gametype = data.shift() + state.map = data.shift() + state.raw.roundsplayed = parseInt(data.shift()) + state.raw.roundstotal = parseInt(data.shift()) + + const teamCount = data.shift() + state.raw.teams = [] + for (let i = 0; i < teamCount; i++) { + const tickets = parseFloat(data.shift()) + state.raw.teams.push({ + tickets + }) + } + + state.raw.targetscore = parseInt(data.shift()) + state.raw.status = data.shift() + + // Seems like the fields end at random places beyond this point + // depending on the server version + + if (data.length) state.raw.ranked = (data.shift() === 'true') + if (data.length) state.raw.punkbuster = (data.shift() === 'true') + if (data.length) state.password = (data.shift() === 'true') + if (data.length) state.raw.uptime = parseInt(data.shift()) + if (data.length) state.raw.roundtime = parseInt(data.shift()) + + const isBadCompany2 = data[0] === 'BC2' + if (isBadCompany2) { + if (data.length) data.shift() + if (data.length) data.shift() + } + if (data.length) { + state.raw.ip = data.shift() + const split = state.raw.ip.split(':') + state.gameHost = split[0] + state.gamePort = split[1] + } else { + // best guess if the server doesn't tell us what the server port is + // these are just the default game ports for different default query ports + if (this.options.port === 48888) state.gamePort = 7673 + if (this.options.port === 22000) state.gamePort = 25200 + } + if (data.length) state.raw.punkbusterversion = data.shift() + if (data.length) state.raw.joinqueue = (data.shift() === 'true') + if (data.length) state.raw.region = data.shift() + if (data.length) state.raw.pingsite = data.shift() + if (data.length) state.raw.country = data.shift() + if (data.length) state.raw.quickmatch = (data.shift() === 'true') + } + + { + const data = await this.query(socket, ['version']) + data.shift() + state.raw.version = data.shift() + } + + { + const data = await this.query(socket, ['listPlayers', 'all']) + const fieldCount = parseInt(data.shift()) + const fields = [] + for (let i = 0; i < fieldCount; i++) { + fields.push(data.shift()) + } + const numplayers = data.shift() + for (let i = 0; i < numplayers; i++) { + const player = {} + for (let key of fields) { + let value = data.shift() + + if (key === 'teamId') key = 'team' + else if (key === 'squadId') key = 'squad' + + if ( + key === 'kills' || + key === 'deaths' || + key === 'score' || + key === 'rank' || + key === 'team' || + key === 'squad' || + key === 'ping' || + key === 'type' + ) { + value = parseInt(value) + } + + player[key] = value + } + state.players.push(player) + } + } + }) + } + + async query (socket, params) { + const outPacket = this.buildPacket(params) + return await this.tcpSend(socket, outPacket, (data) => { + const decoded = this.decodePacket(data) + if (decoded) { + this.logger.debug(decoded) + if (decoded.shift() !== 'OK') throw new Error('Missing OK') + return decoded + } + }) + } + + buildPacket (params) { + const paramBuffers = [] + for (const param of params) { + paramBuffers.push(Buffer.from(param, 'utf8')) + } + + let totalLength = 12 + for (const paramBuffer of paramBuffers) { + totalLength += paramBuffer.length + 1 + 4 + } + + const b = Buffer.alloc(totalLength) + b.writeUInt32LE(0, 0) + b.writeUInt32LE(totalLength, 4) + b.writeUInt32LE(params.length, 8) + let offset = 12 + for (const paramBuffer of paramBuffers) { + b.writeUInt32LE(paramBuffer.length, offset); offset += 4 + paramBuffer.copy(b, offset); offset += paramBuffer.length + b.writeUInt8(0, offset); offset += 1 + } + + return b + } + + decodePacket (buffer) { + if (buffer.length < 8) return false + const reader = this.reader(buffer) + reader.uint(4) // header + const totalLength = reader.uint(4) + if (buffer.length < totalLength) return false + this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length) + + const paramCount = reader.uint(4) + const params = [] + for (let i = 0; i < paramCount; i++) { + params.push(reader.pascalString(4)) + reader.uint(1) // strNull + } + return params + } +} diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index ba94f67..7b8b64c 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -1,55 +1,55 @@ -import Core from './core.js'; -import * as cheerio from "cheerio"; - -export default class buildandshoot extends Core { - async run(state) { - const body = await this.request({ - url: 'http://'+this.options.address+':'+this.options.port+'/', - }); - - let m; - - m = body.match(/status server for (.*?)\.?[\r\n]/); - if(m) state.name = m[1]; - - m = body.match(/Current uptime: (\d+)/); - if(m) state.raw.uptime = m[1]; - - m = body.match(/currently running (.*?) by /); - if(m) state.map = m[1]; - - m = body.match(/Current players: (\d+)\/(\d+)/); - if(m) { - state.raw.numplayers = m[1]; - state.maxplayers = m[2]; - } - - m = body.match(/aos:\/\/[0-9]+:[0-9]+/); - if (m) { - state.connect = m[0]; - } - - const $ = cheerio.load(body); - $('#playerlist tbody tr').each((i,tr) => { - if (!$(tr).find('td').first().attr('colspan')) { - state.players.push({ - name: $(tr).find('td').eq(2).text(), - ping: $(tr).find('td').eq(3).text().trim(), - team: $(tr).find('td').eq(4).text().toLowerCase(), - score: parseInt($(tr).find('td').eq(5).text()) - }); - } - }); - /* - var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); - if(m) { - var o1 = parseInt(m[1]); - var o2 = parseInt(m[2]); - var o3 = parseInt(m[3]); - var o4 = parseInt(m[4]); - var addr = o1+(o2<<8)+(o3<<16)+(o4<<24); - state.raw.url = 'aos://'+addr; - } - */ - } -} +import Core from './core.js' +import * as cheerio from 'cheerio' + +export default class buildandshoot extends Core { + async run (state) { + const body = await this.request({ + url: 'http://' + this.options.address + ':' + this.options.port + '/' + }) + + let m + + m = body.match(/status server for (.*?)\.?[\r\n]/) + if (m) state.name = m[1] + + m = body.match(/Current uptime: (\d+)/) + if (m) state.raw.uptime = m[1] + + m = body.match(/currently running (.*?) by /) + if (m) state.map = m[1] + + m = body.match(/Current players: (\d+)\/(\d+)/) + if (m) { + state.raw.numplayers = m[1] + state.maxplayers = m[2] + } + + m = body.match(/aos:\/\/[0-9]+:[0-9]+/) + if (m) { + state.connect = m[0] + } + + const $ = cheerio.load(body) + $('#playerlist tbody tr').each((i, tr) => { + if (!$(tr).find('td').first().attr('colspan')) { + state.players.push({ + name: $(tr).find('td').eq(2).text(), + ping: $(tr).find('td').eq(3).text().trim(), + team: $(tr).find('td').eq(4).text().toLowerCase(), + score: parseInt($(tr).find('td').eq(5).text()) + }) + } + }) + /* + var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); + if(m) { + var o1 = parseInt(m[1]); + var o2 = parseInt(m[2]); + var o3 = parseInt(m[3]); + var o4 = parseInt(m[4]); + var addr = o1+(o2<<8)+(o3<<16)+(o4<<24); + state.raw.url = 'aos://'+addr; + } + */ + } +} diff --git a/protocols/core.js b/protocols/core.js index 23261be..74705cd 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -1,349 +1,350 @@ -import {EventEmitter} from "events"; -import * as net from "net"; -import Reader from "../lib/reader.js"; -import {debugDump} from '../lib/HexUtil.js'; -import Logger from "../lib/Logger.js"; -import DnsResolver from "../lib/DnsResolver.js"; -import {Results} from "../lib/Results.js"; -import Promises from "../lib/Promises.js"; - -let uid = 0; - -export default class Core extends EventEmitter { - constructor() { - super(); - this.encoding = 'utf8'; - this.byteorder = 'le'; - this.delimiter = '\0'; - this.srvRecord = null; - this.abortedPromise = null; - this.logger = new Logger(); - this.dnsResolver = new DnsResolver(this.logger); - - // Sent to us by QueryRunner - this.options = null; - /** @type GlobalUdpSocket */ - this.udpSocket = null; - this.shortestRTT = 0; - this.usedTcp = false; - } - - // Runs a single attempt with a timeout and cleans up afterward - async runOnceSafe() { - if (this.options.debug) { - this.logger.debugEnabled = true; - } - this.logger.prefix = 'Q#' + (uid++); - - this.logger.debug("Starting"); - this.logger.debug("Protocol: " + this.constructor.name); - this.logger.debug("Options:", this.options); - - let abortCall = null; - this.abortedPromise = new Promise((resolve,reject) => { - abortCall = () => reject(new Error("Query is finished -- cancelling outstanding promises")); - }).catch(() => { - // Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection - }); - - let timeout; - try { - const promise = this.runOnce(); - timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt"); - const result = await Promise.race([promise, timeout]); - this.logger.debug("Query was successful"); - return result; - } catch(e) { - this.logger.debug("Query failed with error", e); - throw e; - } finally { - timeout && timeout.cancel(); - try { - abortCall(); - } catch(e) { - this.logger.debug("Error during abort cleanup: " + e.stack); - } - } - } - - async runOnce() { - const options = this.options; - if (('host' in options) && !('address' in options)) { - const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord); - options.address = resolved.address; - if (resolved.port) options.port = resolved.port; - } - - const state = new Results(); - - await this.run(state); - - // because lots of servers prefix with spaces to try to appear first - state.name = (state.name || '').trim(); - - if (!('connect' in state)) { - state.connect = '' - + (state.gameHost || this.options.host || this.options.address) - + ':' - + (state.gamePort || this.options.port) - } - state.ping = this.shortestRTT; - delete state.gameHost; - delete state.gamePort; - - this.logger.debug(log => { - log("Size of players array: " + state.players.length); - log("Size of bots array: " + state.bots.length); - }); - - return state; - } - - async run(/** Results */ state) {} - - /** Param can be a time in ms, or a promise (which will be timed) */ - registerRtt(param) { - if (param.then) { - const start = Date.now(); - param.then(() => { - const end = Date.now(); - const rtt = end - start; - this.registerRtt(rtt); - }).catch(() => {}); - } else { - this.logger.debug("Registered RTT: " + param + "ms"); - if (this.shortestRTT === 0 || param < this.shortestRTT) { - this.shortestRTT = param; - } - } - } - - // utils - /** @returns {Reader} */ - reader(buffer) { - return new Reader(this,buffer); - } - translate(obj,trans) { - for(const from of Object.keys(trans)) { - const to = trans[from]; - if(from in obj) { - if(to) obj[to] = obj[from]; - delete obj[from]; - } - } - } - - trueTest(str) { - if(typeof str === 'boolean') return str; - if(typeof str === 'number') return str !== 0; - if(typeof str === 'string') { - if(str.toLowerCase() === 'true') return true; - if(str.toLowerCase() === 'yes') return true; - if(str === '1') return true; - } - return false; - } - - assertValidPort(port) { - if (!port) { - throw new Error("Could not determine port to query. Did you provide a port?"); - } - if (port < 1 || port > 65535) { - throw new Error("Invalid tcp/ip port: " + port); - } - } - - /** - * @template T - * @param {function(NodeJS.Socket):Promise} fn - * @param {number=} port - * @returns {Promise} - */ - async withTcp(fn, port) { - this.usedTcp = true; - const address = this.options.address; - if (!port) port = this.options.port; - this.assertValidPort(port); - - let socket, connectionTimeout; - try { - socket = net.connect(port,address); - socket.setNoDelay(true); - - // Prevent unhandled 'error' events from dumping straight to console - socket.on('error', () => {}); - - this.logger.debug(log => { - this.logger.debug(address+':'+port+" TCP Connecting"); - const writeHook = socket.write; - socket.write = (...args) => { - log(address+':'+port+" TCP-->"); - log(debugDump(args[0])); - writeHook.apply(socket,args); - }; - socket.on('error', e => log('TCP Error:', e)); - socket.on('close', () => log('TCP Closed')); - socket.on('data', (data) => { - log(address+':'+port+" <--TCP"); - log(data); - }); - socket.on('ready', () => log(address+':'+port+" TCP Connected")); - }); - - const connectionPromise = new Promise((resolve,reject) => { - socket.on('ready', resolve); - socket.on('close', () => reject(new Error('TCP Connection Refused'))); - }); - this.registerRtt(connectionPromise); - connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening'); - await Promise.race([ - connectionPromise, - connectionTimeout, - this.abortedPromise - ]); - return await fn(socket); - } finally { - socket && socket.destroy(); - connectionTimeout && connectionTimeout.cancel(); - } - } - - /** - * @template T - * @param {NodeJS.Socket} socket - * @param {Buffer|string} buffer - * @param {function(Buffer):T} ondata - * @returns Promise - */ - async tcpSend(socket,buffer,ondata) { - let timeout; - try { - const promise = new Promise(async (resolve, reject) => { - let received = Buffer.from([]); - const onData = (data) => { - received = Buffer.concat([received, data]); - const result = ondata(received); - if (result !== undefined) { - socket.removeListener('data', onData); - resolve(result); - } - }; - socket.on('data', onData); - socket.write(buffer); - }); - timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP'); - return await Promise.race([promise, timeout, this.abortedPromise]); - } finally { - timeout && timeout.cancel(); - } - } - - /** - * @param {Buffer|string} buffer - * @param {function(Buffer):T=} onPacket - * @param {(function():T)=} onTimeout - * @returns Promise - * @template T - */ - async udpSend(buffer,onPacket,onTimeout) { - const address = this.options.address; - const port = this.options.port; - this.assertValidPort(port); - - if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); - - const socket = this.udpSocket; - await socket.send(buffer, address, port, this.options.debug); - - if (!onPacket && !onTimeout) { - return null; - } - - let socketCallback; - let timeout; - try { - const promise = new Promise((resolve, reject) => { - const start = Date.now(); - let end = null; - socketCallback = (fromAddress, fromPort, buffer) => { - try { - if (fromAddress !== address) return; - if (fromPort !== port) return; - if (end === null) { - end = Date.now(); - const rtt = end-start; - this.registerRtt(rtt); - } - const result = onPacket(buffer); - if (result !== undefined) { - this.logger.debug("UDP send finished by callback"); - resolve(result); - } - } catch(e) { - reject(e); - } - }; - socket.addCallback(socketCallback, this.options.debug); - }); - timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP'); - const wrappedTimeout = new Promise((resolve, reject) => { - timeout.catch((e) => { - this.logger.debug("UDP timeout detected"); - if (onTimeout) { - try { - const result = onTimeout(); - if (result !== undefined) { - this.logger.debug("UDP timeout resolved by callback"); - resolve(result); - return; - } - } catch(e) { - reject(e); - } - } - reject(e); - }); - }); - return await Promise.race([promise, wrappedTimeout, this.abortedPromise]); - } finally { - timeout && timeout.cancel(); - socketCallback && socket.removeCallback(socketCallback); - } - } - - async tcpPing() { - // This will give a much more accurate RTT than using the rtt of an http request. - if (!this.usedTcp) { - await this.withTcp(() => {}); - } - } - - async request(params) { - await this.tcpPing(); - - const got = (await import('got')).got; - - let requestPromise; - try { - requestPromise = got({ - ...params, - timeout: { - request: this.options.socketTimeout - } - }); - this.logger.debug(log => { - log(() => params.url + " HTTP-->"); - requestPromise - .then((response) => log(params.url + " <--HTTP " + response.statusCode)) - .catch(() => {}); - }); - const wrappedPromise = requestPromise.then(response => { - if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode); - return response.body; - }); - return await Promise.race([wrappedPromise, this.abortedPromise]); - } finally { - requestPromise && requestPromise.cancel(); - } - } -} +import { EventEmitter } from 'events' +import * as net from 'net' +import Reader from '../lib/reader.js' +import { debugDump } from '../lib/HexUtil.js' +import Logger from '../lib/Logger.js' +import DnsResolver from '../lib/DnsResolver.js' +import { Results } from '../lib/Results.js' +import Promises from '../lib/Promises.js' + +let uid = 0 + +export default class Core extends EventEmitter { + constructor () { + super() + this.encoding = 'utf8' + this.byteorder = 'le' + this.delimiter = '\0' + this.srvRecord = null + this.abortedPromise = null + this.logger = new Logger() + this.dnsResolver = new DnsResolver(this.logger) + + // Sent to us by QueryRunner + this.options = null + /** @type GlobalUdpSocket */ + this.udpSocket = null + this.shortestRTT = 0 + this.usedTcp = false + } + + // Runs a single attempt with a timeout and cleans up afterward + async runOnceSafe () { + if (this.options.debug) { + this.logger.debugEnabled = true + } + this.logger.prefix = 'Q#' + (uid++) + + this.logger.debug('Starting') + this.logger.debug('Protocol: ' + this.constructor.name) + this.logger.debug('Options:', this.options) + + let abortCall = null + this.abortedPromise = new Promise((resolve, reject) => { + abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises')) + }).catch(() => { + // Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection + }) + + let timeout + try { + const promise = this.runOnce() + timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt') + const result = await Promise.race([promise, timeout]) + this.logger.debug('Query was successful') + return result + } catch (e) { + this.logger.debug('Query failed with error', e) + throw e + } finally { + timeout && timeout.cancel() + try { + abortCall() + } catch (e) { + this.logger.debug('Error during abort cleanup: ' + e.stack) + } + } + } + + async runOnce () { + const options = this.options + if (('host' in options) && !('address' in options)) { + const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord) + options.address = resolved.address + if (resolved.port) options.port = resolved.port + } + + const state = new Results() + + await this.run(state) + + // because lots of servers prefix with spaces to try to appear first + state.name = (state.name || '').trim() + + if (!('connect' in state)) { + state.connect = '' + + (state.gameHost || this.options.host || this.options.address) + + ':' + + (state.gamePort || this.options.port) + } + state.ping = this.shortestRTT + delete state.gameHost + delete state.gamePort + + this.logger.debug(log => { + log('Size of players array: ' + state.players.length) + log('Size of bots array: ' + state.bots.length) + }) + + return state + } + + async run (/** Results */ state) {} + + /** Param can be a time in ms, or a promise (which will be timed) */ + registerRtt (param) { + if (param.then) { + const start = Date.now() + param.then(() => { + const end = Date.now() + const rtt = end - start + this.registerRtt(rtt) + }).catch(() => {}) + } else { + this.logger.debug('Registered RTT: ' + param + 'ms') + if (this.shortestRTT === 0 || param < this.shortestRTT) { + this.shortestRTT = param + } + } + } + + // utils + /** @returns {Reader} */ + reader (buffer) { + return new Reader(this, buffer) + } + + translate (obj, trans) { + for (const from of Object.keys(trans)) { + const to = trans[from] + if (from in obj) { + if (to) obj[to] = obj[from] + delete obj[from] + } + } + } + + trueTest (str) { + if (typeof str === 'boolean') return str + if (typeof str === 'number') return str !== 0 + if (typeof str === 'string') { + if (str.toLowerCase() === 'true') return true + if (str.toLowerCase() === 'yes') return true + if (str === '1') return true + } + return false + } + + assertValidPort (port) { + if (!port) { + throw new Error('Could not determine port to query. Did you provide a port?') + } + if (port < 1 || port > 65535) { + throw new Error('Invalid tcp/ip port: ' + port) + } + } + + /** + * @template T + * @param {function(NodeJS.Socket):Promise} fn + * @param {number=} port + * @returns {Promise} + */ + async withTcp (fn, port) { + this.usedTcp = true + const address = this.options.address + if (!port) port = this.options.port + this.assertValidPort(port) + + let socket, connectionTimeout + try { + socket = net.connect(port, address) + socket.setNoDelay(true) + + // Prevent unhandled 'error' events from dumping straight to console + socket.on('error', () => {}) + + this.logger.debug(log => { + this.logger.debug(address + ':' + port + ' TCP Connecting') + const writeHook = socket.write + socket.write = (...args) => { + log(address + ':' + port + ' TCP-->') + log(debugDump(args[0])) + writeHook.apply(socket, args) + } + socket.on('error', e => log('TCP Error:', e)) + socket.on('close', () => log('TCP Closed')) + socket.on('data', (data) => { + log(address + ':' + port + ' <--TCP') + log(data) + }) + socket.on('ready', () => log(address + ':' + port + ' TCP Connected')) + }) + + const connectionPromise = new Promise((resolve, reject) => { + socket.on('ready', resolve) + socket.on('close', () => reject(new Error('TCP Connection Refused'))) + }) + this.registerRtt(connectionPromise) + connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening') + await Promise.race([ + connectionPromise, + connectionTimeout, + this.abortedPromise + ]) + return await fn(socket) + } finally { + socket && socket.destroy() + connectionTimeout && connectionTimeout.cancel() + } + } + + /** + * @template T + * @param {NodeJS.Socket} socket + * @param {Buffer|string} buffer + * @param {function(Buffer):T} ondata + * @returns Promise + */ + async tcpSend (socket, buffer, ondata) { + let timeout + try { + const promise = new Promise((resolve, reject) => { + let received = Buffer.from([]) + const onData = (data) => { + received = Buffer.concat([received, data]) + const result = ondata(received) + if (result !== undefined) { + socket.removeListener('data', onData) + resolve(result) + } + } + socket.on('data', onData) + socket.write(buffer) + }) + timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP') + return await Promise.race([promise, timeout, this.abortedPromise]) + } finally { + timeout && timeout.cancel() + } + } + + /** + * @param {Buffer|string} buffer + * @param {function(Buffer):T=} onPacket + * @param {(function():T)=} onTimeout + * @returns Promise + * @template T + */ + async udpSend (buffer, onPacket, onTimeout) { + const address = this.options.address + const port = this.options.port + this.assertValidPort(port) + + if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary') + + const socket = this.udpSocket + await socket.send(buffer, address, port, this.options.debug) + + if (!onPacket && !onTimeout) { + return null + } + + let socketCallback + let timeout + try { + const promise = new Promise((resolve, reject) => { + const start = Date.now() + let end = null + socketCallback = (fromAddress, fromPort, buffer) => { + try { + if (fromAddress !== address) return + if (fromPort !== port) return + if (end === null) { + end = Date.now() + const rtt = end - start + this.registerRtt(rtt) + } + const result = onPacket(buffer) + if (result !== undefined) { + this.logger.debug('UDP send finished by callback') + resolve(result) + } + } catch (e) { + reject(e) + } + } + socket.addCallback(socketCallback, this.options.debug) + }) + timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP') + const wrappedTimeout = new Promise((resolve, reject) => { + timeout.catch((e) => { + this.logger.debug('UDP timeout detected') + if (onTimeout) { + try { + const result = onTimeout() + if (result !== undefined) { + this.logger.debug('UDP timeout resolved by callback') + resolve(result) + return + } + } catch (e) { + reject(e) + } + } + reject(e) + }) + }) + return await Promise.race([promise, wrappedTimeout, this.abortedPromise]) + } finally { + timeout && timeout.cancel() + socketCallback && socket.removeCallback(socketCallback) + } + } + + async tcpPing () { + // This will give a much more accurate RTT than using the rtt of an http request. + if (!this.usedTcp) { + await this.withTcp(() => {}) + } + } + + async request (params) { + await this.tcpPing() + + const got = (await import('got')).got + + let requestPromise + try { + requestPromise = got({ + ...params, + timeout: { + request: this.options.socketTimeout + } + }) + this.logger.debug(log => { + log(() => params.url + ' HTTP-->') + requestPromise + .then((response) => log(params.url + ' <--HTTP ' + response.statusCode)) + .catch(() => {}) + }) + const wrappedPromise = requestPromise.then(response => { + if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode) + return response.body + }) + return await Promise.race([wrappedPromise, this.abortedPromise]) + } finally { + requestPromise && requestPromise.cancel() + } + } +} diff --git a/protocols/cs2d.js b/protocols/cs2d.js index 39da950..5343cd0 100644 --- a/protocols/cs2d.js +++ b/protocols/cs2d.js @@ -1,67 +1,65 @@ -import Core from './core.js'; - -export default class cs2d extends Core { - async run(state) { - { - const reader = await this.sendQuery( - Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'), - Buffer.from('\x01\x00\xFB\x01', 'binary') - ); - const flags = reader.uint(1); - state.raw.flags = flags; - state.password = this.readFlag(flags, 0); - state.raw.registeredOnly = this.readFlag(flags, 1); - state.raw.fogOfWar = this.readFlag(flags, 2); - state.raw.friendlyFire = this.readFlag(flags, 3); - state.raw.botsEnabled = this.readFlag(flags, 5); - state.raw.luaScripts = this.readFlag(flags, 6); - state.raw.forceLight = this.readFlag(flags, 7); - state.name = this.readString(reader); - state.map = this.readString(reader); - state.raw.numplayers = reader.uint(1); - state.maxplayers = reader.uint(1); - if (flags & 32) { - state.raw.gamemode = reader.uint(1); - } else { - state.raw.gamemode = 0; - } - state.raw.numbots = reader.uint(1); - const flags2 = reader.uint(1); - state.raw.flags2 = flags2; - state.raw.recoil = this.readFlag(flags2, 0); - state.raw.offScreenDamage = this.readFlag(flags2, 1); - state.raw.hasDownloads = this.readFlag(flags2, 2); - reader.skip(2); - const players = reader.uint(1); - for (let i = 0; i < players; i++) { - const player = {} - player.id = reader.uint(1); - player.name = this.readString(reader); - player.team = reader.uint(1); - player.score = reader.uint(4); - player.deaths = reader.uint(4); - state.players.push(player); - } - } - } - - async sendQuery(request, expectedHeader) { - // Send multiple copies of the request packet, because cs2d likes to just ignore them randomly - await this.udpSend(request); - await this.udpSend(request); - return await this.udpSend(request, (buffer) => { - const reader = this.reader(buffer); - const header = reader.part(4); - if (!header.equals(expectedHeader)) return; - return reader; - }); - } - - readFlag(flags, offset) { - return !!(flags & (1 << offset)); - } - - readString(reader) { - return reader.pascalString(1); - } -} +import Core from './core.js' + +export default class cs2d extends Core { + async run (state) { + const reader = await this.sendQuery( + Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'), + Buffer.from('\x01\x00\xFB\x01', 'binary') + ) + const flags = reader.uint(1) + state.raw.flags = flags + state.password = this.readFlag(flags, 0) + state.raw.registeredOnly = this.readFlag(flags, 1) + state.raw.fogOfWar = this.readFlag(flags, 2) + state.raw.friendlyFire = this.readFlag(flags, 3) + state.raw.botsEnabled = this.readFlag(flags, 5) + state.raw.luaScripts = this.readFlag(flags, 6) + state.raw.forceLight = this.readFlag(flags, 7) + state.name = this.readString(reader) + state.map = this.readString(reader) + state.raw.numplayers = reader.uint(1) + state.maxplayers = reader.uint(1) + if (flags & 32) { + state.raw.gamemode = reader.uint(1) + } else { + state.raw.gamemode = 0 + } + state.raw.numbots = reader.uint(1) + const flags2 = reader.uint(1) + state.raw.flags2 = flags2 + state.raw.recoil = this.readFlag(flags2, 0) + state.raw.offScreenDamage = this.readFlag(flags2, 1) + state.raw.hasDownloads = this.readFlag(flags2, 2) + reader.skip(2) + const players = reader.uint(1) + for (let i = 0; i < players; i++) { + const player = {} + player.id = reader.uint(1) + player.name = this.readString(reader) + player.team = reader.uint(1) + player.score = reader.uint(4) + player.deaths = reader.uint(4) + state.players.push(player) + } + } + + async sendQuery (request, expectedHeader) { + // Send multiple copies of the request packet, because cs2d likes to just ignore them randomly + await this.udpSend(request) + await this.udpSend(request) + return await this.udpSend(request, (buffer) => { + const reader = this.reader(buffer) + const header = reader.part(4) + if (!header.equals(expectedHeader)) return + return reader + }) + } + + readFlag (flags, offset) { + return !!(flags & (1 << offset)) + } + + readString (reader) { + return reader.pascalString(1) + } +} diff --git a/protocols/discord.js b/protocols/discord.js index 9d48d85..4463734 100644 --- a/protocols/discord.js +++ b/protocols/discord.js @@ -1,29 +1,29 @@ -import Core from './core.js'; - -export default class discord extends Core { - async run(state) { - const guildId = this.options.guildId; - if (typeof guildId !== 'string') { - throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' - + " (It's too large of a number for javascript to store without losing precision)"); - } - this.usedTcp = true; - const raw = await this.request({ - url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json', - }); - const json = JSON.parse(raw); - state.name = json.name; - if (json.instant_invite) { - state.connect = json.instant_invite; - } else { - state.connect = 'https://discordapp.com/channels/' + guildId; - } - for (const member of json.members) { - const {username: name, ...rest} = member; - state.players.push({ name, ...rest }); - } - delete json.members; - state.maxplayers = 500000; - state.raw = json; - } -} +import Core from './core.js' + +export default class discord extends Core { + async run (state) { + const guildId = this.options.guildId + if (typeof guildId !== 'string') { + throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' + + " (It's too large of a number for javascript to store without losing precision)") + } + this.usedTcp = true + const raw = await this.request({ + url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json' + }) + const json = JSON.parse(raw) + state.name = json.name + if (json.instant_invite) { + state.connect = json.instant_invite + } else { + state.connect = 'https://discordapp.com/channels/' + guildId + } + for (const member of json.members) { + const { username: name, ...rest } = member + state.players.push({ name, ...rest }) + } + delete json.members + state.maxplayers = 500000 + state.raw = json + } +} diff --git a/protocols/doom3.js b/protocols/doom3.js index e6bd3f8..787c71d 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -1,149 +1,147 @@ -import Core from './core.js'; - -export default class doom3 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - } - async run(state) { - const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => { - const reader = this.reader(packet); - const header = reader.uint(2); - if(header !== 0xffff) return; - const header2 = reader.string(); - if(header2 !== 'infoResponse') return; - const challengePart1 = reader.string(4); - if (challengePart1 !== "PiNG") return; - // some doom3 implementations only return the first 4 bytes of the challenge - const challengePart2 = reader.string(4); - if (challengePart2 !== 'PoNg') reader.skip(-4); - return reader.rest(); - }); - - let reader = this.reader(body); - const protoVersion = reader.uint(4); - state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff); - - // some doom implementations send us a packet size here, some don't (etqw does this) - // we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive) - reader.skip(2); - const packetContainsSize = (reader.uint(2) === 0); - reader.skip(-4); - - if (packetContainsSize) { - const size = reader.uint(4); - this.logger.debug("Received packet size: " + size); - } - - while(!reader.done()) { - const key = reader.string(); - let value = this.stripColors(reader.string()); - if(key === 'si_map') { - value = value.replace('maps/',''); - value = value.replace('.entities',''); - } - if(!key) break; - state.raw[key] = value; - this.logger.debug(key + "=" + value); - } - - const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw'); - - const rest = reader.rest(); - let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false); - if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false); - if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true); - if (!playerResult) { - throw new Error("Unable to find a suitable parse strategy for player list"); - } - let players; - [players,reader] = playerResult; - - for (const player of players) { - if(!player.ping || player.typeflag) - state.bots.push(player); - else - state.players.push(player); - } - - state.raw.osmask = reader.uint(4); - if (isEtqw) { - state.raw.ranked = reader.uint(1); - state.raw.timeleft = reader.uint(4); - state.raw.gamestate = reader.uint(1); - state.raw.servertype = reader.uint(1); - // 0 = regular, 1 = tv - if(state.raw.servertype === 0) { - state.raw.interestedClients = reader.uint(1); - } else if(state.raw.servertype === 1) { - state.raw.connectedClients = reader.uint(4); - state.raw.maxClients = reader.uint(4); - } - } - - if (state.raw.si_name) state.name = state.raw.si_name; - if (state.raw.si_map) state.map = state.raw.si_map; - if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers); - if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers); - if (state.raw.si_usepass === '1') state.password = true; - if (state.raw.si_needPass === '1') state.password = true; - if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port - } - - attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) { - this.logger.debug("starting player parse attempt:"); - this.logger.debug("isEtqw: " + isEtqw); - this.logger.debug("hasClanTag: " + hasClanTag); - this.logger.debug("hasClanTagPos: " + hasClanTagPos); - this.logger.debug("hasTypeFlag: " + hasTypeFlag); - const reader = this.reader(rest); - let lastId = -1; - const players = []; - while(true) { - this.logger.debug("---"); - if (reader.done()) { - this.logger.debug("* aborting attempt, overran buffer *"); - return null; - } - const player = {}; - player.id = reader.uint(1); - this.logger.debug("id: " + player.id); - if (player.id <= lastId || player.id > 0x20) { - this.logger.debug("* aborting attempt, invalid player id *"); - return null; - } - lastId = player.id; - if(player.id === 0x20) { - this.logger.debug("* player parse successful *"); - break; - } - player.ping = reader.uint(2); - this.logger.debug("ping: " + player.ping); - if(!isEtqw) { - player.rate = reader.uint(4); - this.logger.debug("rate: " + player.rate); - } - player.name = this.stripColors(reader.string()); - this.logger.debug("name: " + player.name); - if(hasClanTag) { - if(hasClanTagPos) { - const clanTagPos = reader.uint(1); - this.logger.debug("clanTagPos: " + clanTagPos); - } - player.clantag = this.stripColors(reader.string()); - this.logger.debug("clan tag: " + player.clantag); - } - if(hasTypeFlag) { - player.typeflag = reader.uint(1); - this.logger.debug("type flag: " + player.typeflag); - } - players.push(player); - } - return [players,reader]; - } - - stripColors(str) { - // uses quake 3 color codes - return str.replace(/\^(X.{6}|.)/g,''); - } -} +import Core from './core.js' + +export default class doom3 extends Core { + constructor () { + super() + this.encoding = 'latin1' + } + + async run (state) { + const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => { + const reader = this.reader(packet) + const header = reader.uint(2) + if (header !== 0xffff) return + const header2 = reader.string() + if (header2 !== 'infoResponse') return + const challengePart1 = reader.string(4) + if (challengePart1 !== 'PiNG') return + // some doom3 implementations only return the first 4 bytes of the challenge + const challengePart2 = reader.string(4) + if (challengePart2 !== 'PoNg') reader.skip(-4) + return reader.rest() + }) + + let reader = this.reader(body) + const protoVersion = reader.uint(4) + state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff) + + // some doom implementations send us a packet size here, some don't (etqw does this) + // we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive) + reader.skip(2) + const packetContainsSize = (reader.uint(2) === 0) + reader.skip(-4) + + if (packetContainsSize) { + const size = reader.uint(4) + this.logger.debug('Received packet size: ' + size) + } + + while (!reader.done()) { + const key = reader.string() + let value = this.stripColors(reader.string()) + if (key === 'si_map') { + value = value.replace('maps/', '') + value = value.replace('.entities', '') + } + if (!key) break + state.raw[key] = value + this.logger.debug(key + '=' + value) + } + + const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw') + + const rest = reader.rest() + let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false) + if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false) + if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true) + if (!playerResult) { + throw new Error('Unable to find a suitable parse strategy for player list') + } + let players; + [players, reader] = playerResult + + for (const player of players) { + if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) } + } + + state.raw.osmask = reader.uint(4) + if (isEtqw) { + state.raw.ranked = reader.uint(1) + state.raw.timeleft = reader.uint(4) + state.raw.gamestate = reader.uint(1) + state.raw.servertype = reader.uint(1) + // 0 = regular, 1 = tv + if (state.raw.servertype === 0) { + state.raw.interestedClients = reader.uint(1) + } else if (state.raw.servertype === 1) { + state.raw.connectedClients = reader.uint(4) + state.raw.maxClients = reader.uint(4) + } + } + + if (state.raw.si_name) state.name = state.raw.si_name + if (state.raw.si_map) state.map = state.raw.si_map + if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers) + if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers) + if (state.raw.si_usepass === '1') state.password = true + if (state.raw.si_needPass === '1') state.password = true + if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port + } + + attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) { + this.logger.debug('starting player parse attempt:') + this.logger.debug('isEtqw: ' + isEtqw) + this.logger.debug('hasClanTag: ' + hasClanTag) + this.logger.debug('hasClanTagPos: ' + hasClanTagPos) + this.logger.debug('hasTypeFlag: ' + hasTypeFlag) + const reader = this.reader(rest) + let lastId = -1 + const players = [] + while (true) { + this.logger.debug('---') + if (reader.done()) { + this.logger.debug('* aborting attempt, overran buffer *') + return null + } + const player = {} + player.id = reader.uint(1) + this.logger.debug('id: ' + player.id) + if (player.id <= lastId || player.id > 0x20) { + this.logger.debug('* aborting attempt, invalid player id *') + return null + } + lastId = player.id + if (player.id === 0x20) { + this.logger.debug('* player parse successful *') + break + } + player.ping = reader.uint(2) + this.logger.debug('ping: ' + player.ping) + if (!isEtqw) { + player.rate = reader.uint(4) + this.logger.debug('rate: ' + player.rate) + } + player.name = this.stripColors(reader.string()) + this.logger.debug('name: ' + player.name) + if (hasClanTag) { + if (hasClanTagPos) { + const clanTagPos = reader.uint(1) + this.logger.debug('clanTagPos: ' + clanTagPos) + } + player.clantag = this.stripColors(reader.string()) + this.logger.debug('clan tag: ' + player.clantag) + } + if (hasTypeFlag) { + player.typeflag = reader.uint(1) + this.logger.debug('type flag: ' + player.typeflag) + } + players.push(player) + } + return [players, reader] + } + + stripColors (str) { + // uses quake 3 color codes + return str.replace(/\^(X.{6}|.)/g, '') + } +} diff --git a/protocols/eco.js b/protocols/eco.js index aef0b9c..6bae191 100644 --- a/protocols/eco.js +++ b/protocols/eco.js @@ -1,19 +1,19 @@ -import Core from './core.js'; - -export default class eco extends Core { - async run(state) { - if (!this.options.port) this.options.port = 3001; - - const request = await this.request({ - url: `http://${this.options.address}:${this.options.port}/frontpage`, - responseType: 'json' - }); - const serverInfo = request.Info; - - state.name = serverInfo.Description; - state.maxplayers = serverInfo.TotalPlayers; - state.password = serverInfo.HasPassword; - state.gamePort = serverInfo.GamePort; - state.raw = serverInfo; - } -} +import Core from './core.js' + +export default class eco extends Core { + async run (state) { + if (!this.options.port) this.options.port = 3001 + + const request = await this.request({ + url: `http://${this.options.address}:${this.options.port}/frontpage`, + responseType: 'json' + }) + const serverInfo = request.Info + + state.name = serverInfo.Description + state.maxplayers = serverInfo.TotalPlayers + state.password = serverInfo.HasPassword + state.gamePort = serverInfo.GamePort + state.raw = serverInfo + } +} diff --git a/protocols/ffow.js b/protocols/ffow.js index f0ef852..bfcc64f 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -1,37 +1,38 @@ -import valve from './valve.js'; - -export default class ffow extends valve { - constructor() { - super(); - this.byteorder = 'be'; - this.legacyChallenge = true; - } - async queryInfo(state) { - this.logger.debug("Requesting ffow info ..."); - const b = await this.sendPacket( - 0x46, - 'LSQ', - 0x49 - ); - - const reader = this.reader(b); - state.raw.protocol = reader.uint(1); - state.name = reader.string(); - state.map = reader.string(); - state.raw.mod = reader.string(); - state.raw.gamemode = reader.string(); - state.raw.description = reader.string(); - state.raw.version = reader.string(); - state.gamePort = reader.uint(2); - state.raw.numplayers = reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.listentype = String.fromCharCode(reader.uint(1)); - state.raw.environment = String.fromCharCode(reader.uint(1)); - state.password = !!reader.uint(1); - state.raw.secure = reader.uint(1); - state.raw.averagefps = reader.uint(1); - state.raw.round = reader.uint(1); - state.raw.maxrounds = reader.uint(1); - state.raw.timeleft = reader.uint(2); - } -} +import valve from './valve.js' + +export default class ffow extends valve { + constructor () { + super() + this.byteorder = 'be' + this.legacyChallenge = true + } + + async queryInfo (state) { + this.logger.debug('Requesting ffow info ...') + const b = await this.sendPacket( + 0x46, + 'LSQ', + 0x49 + ) + + const reader = this.reader(b) + state.raw.protocol = reader.uint(1) + state.name = reader.string() + state.map = reader.string() + state.raw.mod = reader.string() + state.raw.gamemode = reader.string() + state.raw.description = reader.string() + state.raw.version = reader.string() + state.gamePort = reader.uint(2) + state.raw.numplayers = reader.uint(1) + state.maxplayers = reader.uint(1) + state.raw.listentype = String.fromCharCode(reader.uint(1)) + state.raw.environment = String.fromCharCode(reader.uint(1)) + state.password = !!reader.uint(1) + state.raw.secure = reader.uint(1) + state.raw.averagefps = reader.uint(1) + state.raw.round = reader.uint(1) + state.raw.maxrounds = reader.uint(1) + state.raw.timeleft = reader.uint(2) + } +} diff --git a/protocols/fivem.js b/protocols/fivem.js index b86391a..10fdefd 100644 --- a/protocols/fivem.js +++ b/protocols/fivem.js @@ -1,33 +1,33 @@ -import quake2 from './quake2.js'; - -export default class fivem extends quake2 { - constructor() { - super(); - this.sendHeader = 'getinfo xxx'; - this.responseHeader = 'infoResponse'; - this.encoding = 'utf8'; - } - - async run(state) { - await super.run(state); - - { - const json = await this.request({ - url: 'http://' + this.options.address + ':' + this.options.port + '/info.json', - responseType: 'json' - }); - state.raw.info = json; - } - - { - const json = await this.request({ - url: 'http://' + this.options.address + ':' + this.options.port + '/players.json', - responseType: 'json' - }); - state.raw.players = json; - for (const player of json) { - state.players.push({name: player.name, ping: player.ping}); - } - } - } -} +import quake2 from './quake2.js' + +export default class fivem extends quake2 { + constructor () { + super() + this.sendHeader = 'getinfo xxx' + this.responseHeader = 'infoResponse' + this.encoding = 'utf8' + } + + async run (state) { + await super.run(state) + + { + const json = await this.request({ + url: 'http://' + this.options.address + ':' + this.options.port + '/info.json', + responseType: 'json' + }) + state.raw.info = json + } + + { + const json = await this.request({ + url: 'http://' + this.options.address + ':' + this.options.port + '/players.json', + responseType: 'json' + }) + state.raw.players = json + for (const player of json) { + state.players.push({ name: player.name, ping: player.ping }) + } + } + } +} diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index 546cc96..c80b1f7 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -1,179 +1,179 @@ -import Core from './core.js'; - -const stringKeys = new Set([ - 'website', - 'gametype', - 'gamemode', - 'player' -]); - -function normalizeEntry([key,value]) { - key = key.toLowerCase(); - const split = key.split('_'); - let keyType = key; - - if (split.length === 2 && !isNaN(Number(split[1]))) { - keyType = split[0]; - } - - if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope" - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (value.length && !isNaN(Number(value))) { - value = Number(value); - } - } - - return [key,value]; -} - -export default class gamespy1 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.byteorder = 'be'; - } - - async run(state) { - const raw = await this.sendPacket('\\status\\xserverquery'); - // Convert all keys to lowercase and normalize value types - const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry))); - state.raw = data; - if ('hostname' in data) state.name = data.hostname; - if ('mapname' in data) state.map = data.mapname; - if (this.trueTest(data.password)) state.password = true; - if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers); - if ('hostport' in data) state.gamePort = Number(data.hostport); - - const teamOffByOne = data.gamename === 'bfield1942'; - const playersById = {}; - const teamNamesById = {}; - for (const ident of Object.keys(data)) { - const split = ident.split('_'); - if (split.length !== 2) continue; - let key = split[0].toLowerCase(); - const id = Number(split[1]); - if (isNaN(id)) continue; - let value = data[ident]; - - delete data[ident]; - - if (key !== 'team' && key.startsWith('team')) { - // Info about a team - if (key === 'teamname') { - teamNamesById[id] = value; - } else { - // other team info which we don't track - } - } else { - // Info about a player - if (!(id in playersById)) playersById[id] = {}; - - if (key === 'playername' || key === 'player') { - key = 'name'; - } - if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed. - key = 'teamId'; - value += teamOffByOne ? -1 : 0; - } - - playersById[id][key] = value; - } - } - state.raw.teams = teamNamesById; - - const players = Object.values(playersById); - - const seenHashes = new Set(); - for (const player of players) { - // Some servers (bf1942) report the same player multiple times (bug?) - // Ignore these duplicates - if (player.keyhash) { - if (seenHashes.has(player.keyhash)) { - this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)"); - continue; - } else { - seenHashes.add(player.keyhash); - } - } - - // Convert player's team ID to team name if possible - if (player.hasOwnProperty('teamId')) { - if (Object.keys(teamNamesById).length) { - player.team = teamNamesById[player.teamId] || ''; - } else { - player.team = player.teamId; - delete player.teamId; - } - } - - state.players.push(player); - } - } - - async sendPacket(type) { - let receivedQueryId; - const output = {}; - const parts = new Set(); - let maxPartNum = 0; - - return await this.udpSend(type, buffer => { - const reader = this.reader(buffer); - const str = reader.string(buffer.length); - const split = str.split('\\'); - split.shift(); - const data = {}; - while(split.length) { - const key = split.shift(); - const value = split.shift() || ''; - data[key] = value; - } - - let queryId, partNum; - const partFinal = ('final' in data); - if (data.queryid) { - const split = data.queryid.split('.'); - if (split.length >= 2) { - partNum = Number(split[1]); - } - queryId = split[0]; - } - delete data.final; - delete data.queryid; - this.logger.debug("Received part num=" + partNum + " queryId=" + queryId + " final=" + partFinal); - - if (queryId) { - if (receivedQueryId && receivedQueryId !== queryId) { - this.logger.debug("Rejected packet (Wrong query ID)"); - return; - } else if (!receivedQueryId) { - receivedQueryId = queryId; - } - } - if (!partNum) { - partNum = parts.size; - this.logger.debug("No part number received (assigned #" + partNum + ")"); - } - if (parts.has(partNum)) { - this.logger.debug("Rejected packet (Duplicate part)"); - return; - } - parts.add(partNum); - if (partFinal) { - maxPartNum = partNum; - } - - this.logger.debug("Received part #" + partNum + " of " + (maxPartNum ? maxPartNum : "?")); - for(const i of Object.keys(data)) { - output[i] = data[i]; - } - if (maxPartNum && parts.size === maxPartNum) { - this.logger.debug("Received all parts"); - this.logger.debug(output); - return output; - } - }); - } -} +import Core from './core.js' + +const stringKeys = new Set([ + 'website', + 'gametype', + 'gamemode', + 'player' +]) + +function normalizeEntry ([key, value]) { + key = key.toLowerCase() + const split = key.split('_') + let keyType = key + + if (split.length === 2 && !isNaN(Number(split[1]))) { + keyType = split[0] + } + + if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope" + if (value.toLowerCase() === 'true') { + value = true + } else if (value.toLowerCase() === 'false') { + value = false + } else if (value.length && !isNaN(Number(value))) { + value = Number(value) + } + } + + return [key, value] +} + +export default class gamespy1 extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.byteorder = 'be' + } + + async run (state) { + const raw = await this.sendPacket('\\status\\xserverquery') + // Convert all keys to lowercase and normalize value types + const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry))) + state.raw = data + if ('hostname' in data) state.name = data.hostname + if ('mapname' in data) state.map = data.mapname + if (this.trueTest(data.password)) state.password = true + if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers) + if ('hostport' in data) state.gamePort = Number(data.hostport) + + const teamOffByOne = data.gamename === 'bfield1942' + const playersById = {} + const teamNamesById = {} + for (const ident of Object.keys(data)) { + const split = ident.split('_') + if (split.length !== 2) continue + let key = split[0].toLowerCase() + const id = Number(split[1]) + if (isNaN(id)) continue + let value = data[ident] + + delete data[ident] + + if (key !== 'team' && key.startsWith('team')) { + // Info about a team + if (key === 'teamname') { + teamNamesById[id] = value + } else { + // other team info which we don't track + } + } else { + // Info about a player + if (!(id in playersById)) playersById[id] = {} + + if (key === 'playername' || key === 'player') { + key = 'name' + } + if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed. + key = 'teamId' + value += teamOffByOne ? -1 : 0 + } + + playersById[id][key] = value + } + } + state.raw.teams = teamNamesById + + const players = Object.values(playersById) + + const seenHashes = new Set() + for (const player of players) { + // Some servers (bf1942) report the same player multiple times (bug?) + // Ignore these duplicates + if (player.keyhash) { + if (seenHashes.has(player.keyhash)) { + this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)') + continue + } else { + seenHashes.add(player.keyhash) + } + } + + // Convert player's team ID to team name if possible + if (Object.prototype.hasOwnProperty.call(player, 'teamId')) { + if (Object.keys(teamNamesById).length) { + player.team = teamNamesById[player.teamId] || '' + } else { + player.team = player.teamId + delete player.teamId + } + } + + state.players.push(player) + } + } + + async sendPacket (type) { + let receivedQueryId + const output = {} + const parts = new Set() + let maxPartNum = 0 + + return await this.udpSend(type, buffer => { + const reader = this.reader(buffer) + const str = reader.string(buffer.length) + const split = str.split('\\') + split.shift() + const data = {} + while (split.length) { + const key = split.shift() + const value = split.shift() || '' + data[key] = value + } + + let queryId, partNum + const partFinal = ('final' in data) + if (data.queryid) { + const split = data.queryid.split('.') + if (split.length >= 2) { + partNum = Number(split[1]) + } + queryId = split[0] + } + delete data.final + delete data.queryid + this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal) + + if (queryId) { + if (receivedQueryId && receivedQueryId !== queryId) { + this.logger.debug('Rejected packet (Wrong query ID)') + return + } else if (!receivedQueryId) { + receivedQueryId = queryId + } + } + if (!partNum) { + partNum = parts.size + this.logger.debug('No part number received (assigned #' + partNum + ')') + } + if (parts.has(partNum)) { + this.logger.debug('Rejected packet (Duplicate part)') + return + } + parts.add(partNum) + if (partFinal) { + maxPartNum = partNum + } + + this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?')) + for (const i of Object.keys(data)) { + output[i] = data[i] + } + if (maxPartNum && parts.size === maxPartNum) { + this.logger.debug('Received all parts') + this.logger.debug(output) + return output + } + }) + } +} diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index 66e87b8..1ad8f30 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -1,140 +1,141 @@ -import Core from './core.js'; - -export default class gamespy2 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.byteorder = 'be'; - } - - async run(state) { - // Parse info - { - const body = await this.sendPacket([0xff, 0, 0]); - const reader = this.reader(body); - while (!reader.done()) { - const key = reader.string(); - const value = reader.string(); - if (!key) break; - state.raw[key] = value; - } - if ('hostname' in state.raw) state.name = state.raw.hostname; - if ('mapname' in state.raw) state.map = state.raw.mapname; - if (this.trueTest(state.raw.password)) state.password = true; - if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); - if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport); - } - - // Parse players - { - const body = await this.sendPacket([0, 0xff, 0]); - const reader = this.reader(body); - for (const rawPlayer of this.readFieldData(reader)) { - state.players.push(rawPlayer); - } - } - - // Parse teams - { - const body = await this.sendPacket([0, 0, 0xff]); - const reader = this.reader(body); - state.raw.teams = this.readFieldData(reader); - } - - // Special case for america's army 1 and 2 - // both use gamename = "armygame" - if (state.raw.gamename === 'armygame') { - const stripColor = (str) => { - // uses unreal 2 color codes - return str.replace(/\x1b...|[\x00-\x1a]/g,''); - }; - state.name = stripColor(state.name); - state.map = stripColor(state.map); - for(const key of Object.keys(state.raw)) { - if(typeof state.raw[key] === 'string') { - state.raw[key] = stripColor(state.raw[key]); - } - } - for(const player of state.players) { - if(!('name' in player)) continue; - player.name = stripColor(player.name); - } - } - } - - async sendPacket(type) { - const request = Buffer.concat([ - Buffer.from([0xfe,0xfd,0x00]), // gamespy2 - Buffer.from([0x00,0x00,0x00,0x01]), // ping ID - Buffer.from(type) - ]); - return await this.udpSend(request, buffer => { - const reader = this.reader(buffer); - const header = reader.uint(1); - if (header !== 0) return; - const pingId = reader.uint(4); - if (pingId !== 1) return; - return reader.rest(); - }); - } - - readFieldData(reader) { - const zero = reader.uint(1); // always 0 - const count = reader.uint(1); // number of rows in this data - - // some games omit the count byte entirely if it's 0 or at random (like americas army) - // Luckily, count should always be <64, and ascii characters will typically be >64, - // so we can detect this. - if (count > 64) { - reader.skip(-1); - this.logger.debug("Detected missing count byte, rewinding by 1"); - } else { - this.logger.debug("Detected row count: " + count); - } - - this.logger.debug(() => "Reading fields, starting at: "+reader.rest()); - - const fields = []; - while(!reader.done()) { - let field = reader.string(); - if(!field) break; - fields.push(field); - this.logger.debug("field:"+field); - } - - if (!fields.length) return []; - - const units = []; - outer: while(!reader.done()) { - const unit = {}; - for(let iField = 0; iField < fields.length; iField++) { - let key = fields[iField]; - let value = reader.string(); - if(!value && iField === 0) break outer; - this.logger.debug("value:"+value); - if(key === 'player_') key = 'name'; - else if(key === 'score_') key = 'score'; - else if(key === 'deaths_') key = 'deaths'; - else if(key === 'ping_') key = 'ping'; - else if(key === 'team_') key = 'team'; - else if(key === 'kills_') key = 'kills'; - else if(key === 'team_t') key = 'name'; - else if(key === 'tickets_t') key = 'tickets'; - - if( - key === 'score' || key === 'deaths' - || key === 'ping' || key === 'team' - || key === 'kills' || key === 'tickets' - ) { - if(value === '') continue; - value = parseInt(value); - } - - unit[key] = value; - } - units.push(unit); - } - - return units; - } -} +import Core from './core.js' + +export default class gamespy2 extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.byteorder = 'be' + } + + async run (state) { + // Parse info + { + const body = await this.sendPacket([0xff, 0, 0]) + const reader = this.reader(body) + while (!reader.done()) { + const key = reader.string() + const value = reader.string() + if (!key) break + state.raw[key] = value + } + if ('hostname' in state.raw) state.name = state.raw.hostname + if ('mapname' in state.raw) state.map = state.raw.mapname + if (this.trueTest(state.raw.password)) state.password = true + if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers) + if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport) + } + + // Parse players + { + const body = await this.sendPacket([0, 0xff, 0]) + const reader = this.reader(body) + for (const rawPlayer of this.readFieldData(reader)) { + state.players.push(rawPlayer) + } + } + + // Parse teams + { + const body = await this.sendPacket([0, 0, 0xff]) + const reader = this.reader(body) + state.raw.teams = this.readFieldData(reader) + } + + // Special case for america's army 1 and 2 + // both use gamename = "armygame" + if (state.raw.gamename === 'armygame') { + const stripColor = (str) => { + // uses unreal 2 color codes + return str.replace(/\x1b...|[\x00-\x1a]/g, '') + } + state.name = stripColor(state.name) + state.map = stripColor(state.map) + for (const key of Object.keys(state.raw)) { + if (typeof state.raw[key] === 'string') { + state.raw[key] = stripColor(state.raw[key]) + } + } + for (const player of state.players) { + if (!('name' in player)) continue + player.name = stripColor(player.name) + } + } + } + + async sendPacket (type) { + const request = Buffer.concat([ + Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2 + Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID + Buffer.from(type) + ]) + return await this.udpSend(request, buffer => { + const reader = this.reader(buffer) + const header = reader.uint(1) + if (header !== 0) return + const pingId = reader.uint(4) + if (pingId !== 1) return + return reader.rest() + }) + } + + readFieldData (reader) { + reader.uint(1) // always 0 + const count = reader.uint(1) // number of rows in this data + + // some games omit the count byte entirely if it's 0 or at random (like americas army) + // Luckily, count should always be <64, and ascii characters will typically be >64, + // so we can detect this. + if (count > 64) { + reader.skip(-1) + this.logger.debug('Detected missing count byte, rewinding by 1') + } else { + this.logger.debug('Detected row count: ' + count) + } + + this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest()) + + const fields = [] + while (!reader.done()) { + const field = reader.string() + if (!field) break + fields.push(field) + this.logger.debug('field:' + field) + } + + if (!fields.length) return [] + + const units = [] + while (!reader.done()) { + const unit = {} + for (let iField = 0; iField < fields.length; iField++) { + let key = fields[iField] + let value = reader.string() + if (!value && iField === 0) return units + + this.logger.debug('value:' + value) + if (key === 'player_') key = 'name' + else if (key === 'score_') key = 'score' + else if (key === 'deaths_') key = 'deaths' + else if (key === 'ping_') key = 'ping' + else if (key === 'team_') key = 'team' + else if (key === 'kills_') key = 'kills' + else if (key === 'team_t') key = 'name' + else if (key === 'tickets_t') key = 'tickets' + + if ( + key === 'score' || key === 'deaths' || + key === 'ping' || key === 'team' || + key === 'kills' || key === 'tickets' + ) { + if (value === '') continue + value = parseInt(value) + } + + unit[key] = value + } + units.push(unit) + } + + return units + } +} diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 0ed408b..3825b35 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -1,196 +1,194 @@ -import Core from './core.js'; - -export default class gamespy3 extends Core { - constructor() { - super(); - this.sessionId = 1; - this.encoding = 'latin1'; - this.byteorder = 'be'; - this.useOnlySingleSplit = false; - this.isJc2mp = false; - } - - async run(state) { - const buffer = await this.sendPacket(9, false, false, false); - const reader = this.reader(buffer); - let challenge = parseInt(reader.string()); - this.logger.debug("Received challenge key: " + challenge); - if (challenge === 0) { - // Some servers send us a 0 if they don't want a challenge key used - // BF2 does this. - challenge = null; - } - - let requestPayload; - if(this.isJc2mp) { - // they completely alter the protocol. because why not. - requestPayload = Buffer.from([0xff,0xff,0xff,0x02]); - } else { - requestPayload = Buffer.from([0xff,0xff,0xff,0x01]); - } - /** @type Buffer[] */ - const packets = await this.sendPacket(0,challenge,requestPayload,true); - - // iterate over the received packets - // the first packet will start off with k/v pairs, followed with data fields - // the following packets will only have data fields - state.raw.playerTeamInfo = {}; - - for(let iPacket = 0; iPacket < packets.length; iPacket++) { - const packet = packets[iPacket]; - const reader = this.reader(packet); - - this.logger.debug("Parsing packet #" + iPacket); - this.logger.debug(packet); - - // Parse raw server key/values - - if(iPacket === 0) { - while(!reader.done()) { - const key = reader.string(); - if(!key) break; - - let value = reader.string(); - while(value.match(/^p[0-9]+$/)) { - // fix a weird ut3 bug where some keys don't have values - value = reader.string(); - } - - state.raw[key] = value; - this.logger.debug(key + " = " + value); - } - } - - // Parse player, team, item array state - - if(this.isJc2mp) { - state.raw.numPlayers2 = reader.uint(2); - while(!reader.done()) { - const player = {}; - player.name = reader.string(); - player.steamid = reader.string(); - player.ping = reader.uint(2); - state.players.push(player); - } - } else { - let firstMode = true; - while(!reader.done()) { - if (reader.uint(1) <= 2) continue; - reader.skip(-1); - let fieldId = reader.string(); - if(!fieldId) continue; - const fieldIdSplit = fieldId.split('_'); - const fieldName = fieldIdSplit[0]; - const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'; - - if(!(itemType in state.raw.playerTeamInfo)) { - state.raw.playerTeamInfo[itemType] = []; - } - const items = state.raw.playerTeamInfo[itemType]; - - let offset = reader.uint(1); - firstMode = false; - - this.logger.debug(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset); - - while(!reader.done()) { - const item = reader.string(); - if(!item) break; - - while(items.length <= offset) { items.push({}); } - items[offset][fieldName] = item; - this.logger.debug("* " + item); - offset++; - } - } - } - } - - // Turn all that raw state into something useful - - if ('hostname' in state.raw) state.name = state.raw.hostname; - else if('servername' in state.raw) state.name = state.raw.servername; - if ('mapname' in state.raw) state.map = state.raw.mapname; - if (state.raw.password === '1') state.password = true; - if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); - if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport); - - if('' in state.raw.playerTeamInfo) { - for (const playerInfo of state.raw.playerTeamInfo['']) { - const player = {}; - for(const from of Object.keys(playerInfo)) { - let key = from; - let value = playerInfo[from]; - - if(key === 'player') key = 'name'; - if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value); - player[key] = value; - } - state.players.push(player); - } - } - } - - async sendPacket(type,challenge,payload,assemble) { - const challengeLength = challenge === null ? 0 : 4; - const payloadLength = payload ? payload.length : 0; - - const b = Buffer.alloc(7 + challengeLength + payloadLength); - b.writeUInt8(0xFE, 0); - b.writeUInt8(0xFD, 1); - b.writeUInt8(type, 2); - b.writeUInt32BE(this.sessionId, 3); - if(challengeLength) b.writeInt32BE(challenge, 7); - if(payloadLength) payload.copy(b, 7+challengeLength); - - let numPackets = 0; - const packets = {}; - return await this.udpSend(b,(buffer) => { - const reader = this.reader(buffer); - const iType = reader.uint(1); - if(iType !== type) { - this.logger.debug('Skipping packet, type mismatch'); - return; - } - const iSessionId = reader.uint(4); - if(iSessionId !== this.sessionId) { - this.logger.debug('Skipping packet, session id mismatch'); - return; - } - - if(!assemble) { - return reader.rest(); - } - if(this.useOnlySingleSplit) { - // has split headers, but they are worthless and only one packet is used - reader.skip(11); - return [reader.rest()]; - } - - reader.skip(9); // filler data -- usually set to 'splitnum\0' - let id = reader.uint(1); - const last = (id & 0x80); - id = id & 0x7f; - if(last) numPackets = id+1; - - reader.skip(1); // "another 'packet number' byte, but isn't understood." - - packets[id] = reader.rest(); - if(this.debug) { - this.logger.debug("Received packet #"+id + (last ? " (last)" : "")); - } - - if(!numPackets || Object.keys(packets).length !== numPackets) return; - - // assemble the parts - const list = []; - for(let i = 0; i < numPackets; i++) { - if(!(i in packets)) { - throw new Error('Missing packet #'+i); - } - list.push(packets[i]); - } - return list; - }); - } -} +import Core from './core.js' + +export default class gamespy3 extends Core { + constructor () { + super() + this.sessionId = 1 + this.encoding = 'latin1' + this.byteorder = 'be' + this.useOnlySingleSplit = false + this.isJc2mp = false + } + + async run (state) { + const buffer = await this.sendPacket(9, false, false, false) + const reader = this.reader(buffer) + let challenge = parseInt(reader.string()) + this.logger.debug('Received challenge key: ' + challenge) + if (challenge === 0) { + // Some servers send us a 0 if they don't want a challenge key used + // BF2 does this. + challenge = null + } + + let requestPayload + if (this.isJc2mp) { + // they completely alter the protocol. because why not. + requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02]) + } else { + requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01]) + } + /** @type Buffer[] */ + const packets = await this.sendPacket(0, challenge, requestPayload, true) + + // iterate over the received packets + // the first packet will start off with k/v pairs, followed with data fields + // the following packets will only have data fields + state.raw.playerTeamInfo = {} + + for (let iPacket = 0; iPacket < packets.length; iPacket++) { + const packet = packets[iPacket] + const reader = this.reader(packet) + + this.logger.debug('Parsing packet #' + iPacket) + this.logger.debug(packet) + + // Parse raw server key/values + + if (iPacket === 0) { + while (!reader.done()) { + const key = reader.string() + if (!key) break + + let value = reader.string() + while (value.match(/^p[0-9]+$/)) { + // fix a weird ut3 bug where some keys don't have values + value = reader.string() + } + + state.raw[key] = value + this.logger.debug(key + ' = ' + value) + } + } + + // Parse player, team, item array state + + if (this.isJc2mp) { + state.raw.numPlayers2 = reader.uint(2) + while (!reader.done()) { + const player = {} + player.name = reader.string() + player.steamid = reader.string() + player.ping = reader.uint(2) + state.players.push(player) + } + } else { + while (!reader.done()) { + if (reader.uint(1) <= 2) continue + reader.skip(-1) + const fieldId = reader.string() + if (!fieldId) continue + const fieldIdSplit = fieldId.split('_') + const fieldName = fieldIdSplit[0] + const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_' + + if (!(itemType in state.raw.playerTeamInfo)) { + state.raw.playerTeamInfo[itemType] = [] + } + const items = state.raw.playerTeamInfo[itemType] + + let offset = reader.uint(1) + + this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset) + + while (!reader.done()) { + const item = reader.string() + if (!item) break + + while (items.length <= offset) { items.push({}) } + items[offset][fieldName] = item + this.logger.debug('* ' + item) + offset++ + } + } + } + } + + // Turn all that raw state into something useful + + if ('hostname' in state.raw) state.name = state.raw.hostname + else if ('servername' in state.raw) state.name = state.raw.servername + if ('mapname' in state.raw) state.map = state.raw.mapname + if (state.raw.password === '1') state.password = true + if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers) + if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport) + + if ('' in state.raw.playerTeamInfo) { + for (const playerInfo of state.raw.playerTeamInfo['']) { + const player = {} + for (const from of Object.keys(playerInfo)) { + let key = from + let value = playerInfo[from] + + if (key === 'player') key = 'name' + if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value) + player[key] = value + } + state.players.push(player) + } + } + } + + async sendPacket (type, challenge, payload, assemble) { + const challengeLength = challenge === null ? 0 : 4 + const payloadLength = payload ? payload.length : 0 + + const b = Buffer.alloc(7 + challengeLength + payloadLength) + b.writeUInt8(0xFE, 0) + b.writeUInt8(0xFD, 1) + b.writeUInt8(type, 2) + b.writeUInt32BE(this.sessionId, 3) + if (challengeLength) b.writeInt32BE(challenge, 7) + if (payloadLength) payload.copy(b, 7 + challengeLength) + + let numPackets = 0 + const packets = {} + return await this.udpSend(b, (buffer) => { + const reader = this.reader(buffer) + const iType = reader.uint(1) + if (iType !== type) { + this.logger.debug('Skipping packet, type mismatch') + return + } + const iSessionId = reader.uint(4) + if (iSessionId !== this.sessionId) { + this.logger.debug('Skipping packet, session id mismatch') + return + } + + if (!assemble) { + return reader.rest() + } + if (this.useOnlySingleSplit) { + // has split headers, but they are worthless and only one packet is used + reader.skip(11) + return [reader.rest()] + } + + reader.skip(9) // filler data -- usually set to 'splitnum\0' + let id = reader.uint(1) + const last = (id & 0x80) + id = id & 0x7f + if (last) numPackets = id + 1 + + reader.skip(1) // "another 'packet number' byte, but isn't understood." + + packets[id] = reader.rest() + if (this.debug) { + this.logger.debug('Received packet #' + id + (last ? ' (last)' : '')) + } + + if (!numPackets || Object.keys(packets).length !== numPackets) return + + // assemble the parts + const list = [] + for (let i = 0; i < numPackets; i++) { + if (!(i in packets)) { + throw new Error('Missing packet #' + i) + } + list.push(packets[i]) + } + return list + }) + } +} diff --git a/protocols/geneshift.js b/protocols/geneshift.js index 92ef780..12f709b 100644 --- a/protocols/geneshift.js +++ b/protocols/geneshift.js @@ -1,46 +1,46 @@ -import Core from './core.js'; - -export default class geneshift extends Core { - async run(state) { - await this.tcpPing(); - - const body = await this.request({ - url: 'http://geneshift.net/game/receiveLobby.php' - }); - - const split = body.split('
'); - let found = null; - for(const line of split) { - const fields = line.split('::'); - const ip = fields[2]; - const port = fields[3]; - if(ip === this.options.address && parseInt(port) === this.options.port) { - found = fields; - break; - } - } - - if(found === null) { - throw new Error('Server not found in list'); - } - - state.raw.countrycode = found[0]; - state.raw.country = found[1]; - state.name = found[4]; - state.map = found[5]; - state.players.setNum(parseInt(found[6])); - state.maxplayers = parseInt(found[7]); - // fields[8] is unknown? - state.raw.rules = found[9]; - state.raw.gamemode = parseInt(found[10]); - state.raw.gangsters = parseInt(found[11]); - state.raw.cashrate = parseInt(found[12]); - state.raw.missions = !!parseInt(found[13]); - state.raw.vehicles = !!parseInt(found[14]); - state.raw.customweapons = !!parseInt(found[15]); - state.raw.friendlyfire = !!parseInt(found[16]); - state.raw.mercs = !!parseInt(found[17]); - // fields[18] is unknown? listen server? - state.raw.version = found[19]; - } -} +import Core from './core.js' + +export default class geneshift extends Core { + async run (state) { + await this.tcpPing() + + const body = await this.request({ + url: 'http://geneshift.net/game/receiveLobby.php' + }) + + const split = body.split('
') + let found = null + for (const line of split) { + const fields = line.split('::') + const ip = fields[2] + const port = fields[3] + if (ip === this.options.address && parseInt(port) === this.options.port) { + found = fields + break + } + } + + if (found === null) { + throw new Error('Server not found in list') + } + + state.raw.countrycode = found[0] + state.raw.country = found[1] + state.name = found[4] + state.map = found[5] + state.players.setNum(parseInt(found[6])) + state.maxplayers = parseInt(found[7]) + // fields[8] is unknown? + state.raw.rules = found[9] + state.raw.gamemode = parseInt(found[10]) + state.raw.gangsters = parseInt(found[11]) + state.raw.cashrate = parseInt(found[12]) + state.raw.missions = !!parseInt(found[13]) + state.raw.vehicles = !!parseInt(found[14]) + state.raw.customweapons = !!parseInt(found[15]) + state.raw.friendlyfire = !!parseInt(found[16]) + state.raw.mercs = !!parseInt(found[17]) + // fields[18] is unknown? listen server? + state.raw.version = found[19] + } +} diff --git a/protocols/goldsrc.js b/protocols/goldsrc.js index ab4a526..33a7b63 100644 --- a/protocols/goldsrc.js +++ b/protocols/goldsrc.js @@ -1,8 +1,8 @@ -import valve from './valve.js'; - -export default class goldsrc extends valve { - constructor() { - super(); - this.goldsrcInfo = true; - } -} +import valve from './valve.js' + +export default class goldsrc extends valve { + constructor () { + super() + this.goldsrcInfo = true + } +} diff --git a/protocols/hexen2.js b/protocols/hexen2.js index 45c8bf7..20b6826 100644 --- a/protocols/hexen2.js +++ b/protocols/hexen2.js @@ -1,13 +1,14 @@ -import quake1 from './quake1.js'; - -export default class hexen2 extends quake1 { - constructor() { - super(); - this.sendHeader = '\xFFstatus\x0a'; - this.responseHeader = '\xffn'; - } - async run(state) { - await super.run(state); - state.gamePort = this.options.port - 50; - } -} +import quake1 from './quake1.js' + +export default class hexen2 extends quake1 { + constructor () { + super() + this.sendHeader = '\xFFstatus\x0a' + this.responseHeader = '\xffn' + } + + async run (state) { + await super.run(state) + state.gamePort = this.options.port - 50 + } +} diff --git a/protocols/index.js b/protocols/index.js index 35f200d..c581d36 100644 --- a/protocols/index.js +++ b/protocols/index.js @@ -1,53 +1,55 @@ -import armagetron from "./armagetron.js"; -import ase from "./ase.js"; -import assettocorsa from "./assettocorsa.js"; -import battlefield from "./battlefield.js"; -import buildandshoot from "./buildandshoot.js"; -import cs2d from "./cs2d.js"; -import discord from "./discord.js"; -import doom3 from "./doom3.js"; -import eco from "./eco.js"; -import ffow from "./ffow.js"; -import fivem from "./fivem.js"; -import gamespy1 from "./gamespy1.js"; -import gamespy2 from "./gamespy2.js"; -import gamespy3 from "./gamespy3.js"; -import geneshift from "./geneshift.js"; -import goldsrc from "./goldsrc.js"; -import hexen2 from "./hexen2.js"; -import jc2mp from "./jc2mp.js"; -import kspdmp from "./kspdmp.js"; -import mafia2mp from "./mafia2mp.js"; -import mafia2online from "./mafia2online.js"; -import minecraft from "./minecraft.js"; -import minecraftbedrock from "./minecraftbedrock.js"; -import minecraftvanilla from "./minecraftvanilla.js"; -import mumble from "./mumble.js"; -import mumbleping from "./mumbleping.js"; -import nadeo from "./nadeo.js"; -import openttd from "./openttd.js"; -import quake1 from "./quake1.js"; -import quake2 from "./quake2.js"; -import quake3 from "./quake3.js"; -import rfactor from "./rfactor.js"; -import samp from "./samp.js"; -import savage2 from "./savage2.js"; -import starmade from "./starmade.js"; -import starsiege from "./starsiege.js"; -import teamspeak2 from "./teamspeak2.js"; -import teamspeak3 from "./teamspeak3.js"; -import terraria from "./terraria.js"; -import tribes1 from "./tribes1.js"; -import tribes1master from "./tribes1master.js"; -import unreal2 from "./unreal2.js"; -import ut3 from "./ut3.js"; -import valve from "./valve.js"; -import vcmp from "./vcmp.js"; -import ventrilo from "./ventrilo.js"; -import warsow from "./warsow.js"; - -export { armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1, - gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, - minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp, - savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve, - vcmp, ventrilo, warsow } +import armagetron from './armagetron.js' +import ase from './ase.js' +import assettocorsa from './assettocorsa.js' +import battlefield from './battlefield.js' +import buildandshoot from './buildandshoot.js' +import cs2d from './cs2d.js' +import discord from './discord.js' +import doom3 from './doom3.js' +import eco from './eco.js' +import ffow from './ffow.js' +import fivem from './fivem.js' +import gamespy1 from './gamespy1.js' +import gamespy2 from './gamespy2.js' +import gamespy3 from './gamespy3.js' +import geneshift from './geneshift.js' +import goldsrc from './goldsrc.js' +import hexen2 from './hexen2.js' +import jc2mp from './jc2mp.js' +import kspdmp from './kspdmp.js' +import mafia2mp from './mafia2mp.js' +import mafia2online from './mafia2online.js' +import minecraft from './minecraft.js' +import minecraftbedrock from './minecraftbedrock.js' +import minecraftvanilla from './minecraftvanilla.js' +import mumble from './mumble.js' +import mumbleping from './mumbleping.js' +import nadeo from './nadeo.js' +import openttd from './openttd.js' +import quake1 from './quake1.js' +import quake2 from './quake2.js' +import quake3 from './quake3.js' +import rfactor from './rfactor.js' +import samp from './samp.js' +import savage2 from './savage2.js' +import starmade from './starmade.js' +import starsiege from './starsiege.js' +import teamspeak2 from './teamspeak2.js' +import teamspeak3 from './teamspeak3.js' +import terraria from './terraria.js' +import tribes1 from './tribes1.js' +import tribes1master from './tribes1master.js' +import unreal2 from './unreal2.js' +import ut3 from './ut3.js' +import valve from './valve.js' +import vcmp from './vcmp.js' +import ventrilo from './ventrilo.js' +import warsow from './warsow.js' + +export { + armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1, + gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, + minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp, + savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve, + vcmp, ventrilo, warsow +} diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 8e69f49..0f60ae4 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -1,18 +1,19 @@ -import gamespy3 from './gamespy3.js'; - -// supposedly, gamespy3 is the "official" query protocol for jcmp, -// but it's broken (requires useOnlySingleSplit), and may not include some player names -export default class jc2mp extends gamespy3 { - constructor() { - super(); - this.useOnlySingleSplit = true; - this.isJc2mp = true; - this.encoding = 'utf8'; - } - async run(state) { - await super.run(state); - if(!state.players.length && parseInt(state.raw.numplayers)) { - state.players.setNum(parseInt(state.raw.numplayers)); - } - } -} +import gamespy3 from './gamespy3.js' + +// supposedly, gamespy3 is the "official" query protocol for jcmp, +// but it's broken (requires useOnlySingleSplit), and may not include some player names +export default class jc2mp extends gamespy3 { + constructor () { + super() + this.useOnlySingleSplit = true + this.isJc2mp = true + this.encoding = 'utf8' + } + + async run (state) { + await super.run(state) + if (!state.players.length && parseInt(state.raw.numplayers)) { + state.players.setNum(parseInt(state.raw.numplayers)) + } + } +} diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index b5af402..8038252 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -1,27 +1,27 @@ -import Core from './core.js'; - -export default class kspdmp extends Core { - async run(state) { - const json = await this.request({ - url: 'http://'+this.options.address+':'+this.options.port, - responseType: 'json' - }); - - for (const one of json.players) { - state.players.push({name:one.nickname,team:one.team}); - } - - for (const key of Object.keys(json)) { - state.raw[key] = json[key]; - } - state.name = json.server_name; - state.maxplayers = json.max_players; - state.gamePort = json.port; - if (json.players) { - const split = json.players.split(', '); - for (const name of split) { - state.players.push({name:name}); - } - } - } -} +import Core from './core.js' + +export default class kspdmp extends Core { + async run (state) { + const json = await this.request({ + url: 'http://' + this.options.address + ':' + this.options.port, + responseType: 'json' + }) + + for (const one of json.players) { + state.players.push({ name: one.nickname, team: one.team }) + } + + for (const key of Object.keys(json)) { + state.raw[key] = json[key] + } + state.name = json.server_name + state.maxplayers = json.max_players + state.gamePort = json.port + if (json.players) { + const split = json.players.split(', ') + for (const name of split) { + state.players.push({ name }) + } + } + } +} diff --git a/protocols/mafia2mp.js b/protocols/mafia2mp.js index 5c63cc7..38c49fa 100644 --- a/protocols/mafia2mp.js +++ b/protocols/mafia2mp.js @@ -1,41 +1,41 @@ -import Core from './core.js'; - -export default class mafia2mp extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.header = 'M2MP'; - this.isMafia2Online = false; - } - - async run(state) { - const body = await this.udpSend(this.header,(buffer) => { - const reader = this.reader(buffer); - const header = reader.string(this.header.length); - if (header !== this.header) return; - return reader.rest(); - }); - - const reader = this.reader(body); - state.name = this.readString(reader); - state.raw.numplayers = this.readString(reader); - state.maxplayers = parseInt(this.readString(reader)); - state.raw.gamemode = this.readString(reader); - state.password = !!reader.uint(1); - state.gamePort = this.options.port - 1; - - while(!reader.done()) { - const player = {}; - player.name = this.readString(reader); - if(!player.name) break; - if (this.isMafia2Online) { - player.ping = parseInt(this.readString(reader)); - } - state.players.push(player); - } - } - - readString(reader) { - return reader.pascalString(1,-1); - } -} +import Core from './core.js' + +export default class mafia2mp extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.header = 'M2MP' + this.isMafia2Online = false + } + + async run (state) { + const body = await this.udpSend(this.header, (buffer) => { + const reader = this.reader(buffer) + const header = reader.string(this.header.length) + if (header !== this.header) return + return reader.rest() + }) + + const reader = this.reader(body) + state.name = this.readString(reader) + state.raw.numplayers = this.readString(reader) + state.maxplayers = parseInt(this.readString(reader)) + state.raw.gamemode = this.readString(reader) + state.password = !!reader.uint(1) + state.gamePort = this.options.port - 1 + + while (!reader.done()) { + const player = {} + player.name = this.readString(reader) + if (!player.name) break + if (this.isMafia2Online) { + player.ping = parseInt(this.readString(reader)) + } + state.players.push(player) + } + } + + readString (reader) { + return reader.pascalString(1, -1) + } +} diff --git a/protocols/mafia2online.js b/protocols/mafia2online.js index 31c49f9..3559cea 100644 --- a/protocols/mafia2online.js +++ b/protocols/mafia2online.js @@ -1,9 +1,9 @@ -import mafia2mp from './mafia2mp.js'; - -export default class mafia2online extends mafia2mp { - constructor() { - super(); - this.header = 'M2Online'; - this.isMafia2Online = true; - } -} +import mafia2mp from './mafia2mp.js' + +export default class mafia2online extends mafia2mp { + constructor () { + super() + this.header = 'M2Online' + this.isMafia2Online = true + } +} diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 5e1b3e8..11024f2 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -1,99 +1,99 @@ -import Core from './core.js'; -import minecraftbedrock from "./minecraftbedrock.js"; -import minecraftvanilla from "./minecraftvanilla.js"; -import Gamespy3 from "./gamespy3.js"; -import {Results} from "../lib/Results.js"; - -/* -Vanilla servers respond to minecraftvanilla only -Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only -Some bedrock servers respond to gamespy3 only -Some bedrock servers respond to minecraftbedrock only -Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock - */ - -export default class minecraft extends Core { - constructor() { - super(); - this.srvRecord = "_minecraft._tcp"; - } - async run(state) { - /** @type {Promise[]} */ - const promises = []; - - const vanillaResolver = new minecraftvanilla(); - vanillaResolver.options = this.options; - vanillaResolver.udpSocket = this.udpSocket; - promises.push((async () => { - try { return await vanillaResolver.runOnceSafe(); } catch(e) {} - })()); - - const gamespyResolver = new Gamespy3(); - gamespyResolver.options = { - ...this.options, - encoding: 'utf8', - }; - gamespyResolver.udpSocket = this.udpSocket; - promises.push((async () => { - try { return await gamespyResolver.runOnceSafe(); } catch(e) {} - })()); - - const bedrockResolver = new minecraftbedrock(); - bedrockResolver.options = this.options; - bedrockResolver.udpSocket = this.udpSocket; - promises.push((async () => { - try { return await bedrockResolver.runOnceSafe(); } catch(e) {} - })()); - - const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises); - - state.raw.vanilla = vanillaState; - state.raw.gamespy = gamespyState; - state.raw.bedrock = bedrockState; - - if (!vanillaState && !gamespyState && !bedrockState) { - throw new Error('No protocols succeeded'); - } - - // Ordered from least worth to most worth (player names / etc) - if (bedrockState) { - if (bedrockState.players.length) state.players = bedrockState.players; - } - if (vanillaState) { - try { - let name = ''; - const description = vanillaState.raw.description; - if (typeof description === 'string') { - name = description; - } - if (!name && typeof description === 'object' && description.text) { - name = description.text; - } - if (!name && typeof description === 'object' && description.extra) { - name = description.extra.map(part => part.text).join(''); - } - state.name = name; - } catch(e) {} - if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers; - if (vanillaState.players.length) state.players = vanillaState.players; - if (vanillaState.ping) this.registerRtt(vanillaState.ping); - } - if (gamespyState) { - if (gamespyState.name) state.name = gamespyState.name; - if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers; - if (gamespyState.players.length) state.players = gamespyState.players; - else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers)); - if (gamespyState.ping) this.registerRtt(gamespyState.ping); - } - if (bedrockState) { - if (bedrockState.name) state.name = bedrockState.name; - if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers; - if (bedrockState.map) state.map = bedrockState.map; - if (bedrockState.ping) this.registerRtt(bedrockState.ping); - } - // remove dupe spaces from name - state.name = state.name.replace(/\s+/g, ' '); - // remove color codes from name - state.name = state.name.replace(/\u00A7./g, ''); - } -} +import Core from './core.js' +import minecraftbedrock from './minecraftbedrock.js' +import minecraftvanilla from './minecraftvanilla.js' +import Gamespy3 from './gamespy3.js' + +/* +Vanilla servers respond to minecraftvanilla only +Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only +Some bedrock servers respond to gamespy3 only +Some bedrock servers respond to minecraftbedrock only +Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock + */ + +export default class minecraft extends Core { + constructor () { + super() + this.srvRecord = '_minecraft._tcp' + } + + async run (state) { + /** @type {Promise[]} */ + const promises = [] + + const vanillaResolver = new minecraftvanilla() + vanillaResolver.options = this.options + vanillaResolver.udpSocket = this.udpSocket + promises.push((async () => { + try { return await vanillaResolver.runOnceSafe() } catch (e) {} + })()) + + const gamespyResolver = new Gamespy3() + gamespyResolver.options = { + ...this.options, + encoding: 'utf8' + } + gamespyResolver.udpSocket = this.udpSocket + promises.push((async () => { + try { return await gamespyResolver.runOnceSafe() } catch (e) {} + })()) + + const bedrockResolver = new minecraftbedrock() + bedrockResolver.options = this.options + bedrockResolver.udpSocket = this.udpSocket + promises.push((async () => { + try { return await bedrockResolver.runOnceSafe() } catch (e) {} + })()) + + const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises) + + state.raw.vanilla = vanillaState + state.raw.gamespy = gamespyState + state.raw.bedrock = bedrockState + + if (!vanillaState && !gamespyState && !bedrockState) { + throw new Error('No protocols succeeded') + } + + // Ordered from least worth to most worth (player names / etc) + if (bedrockState) { + if (bedrockState.players.length) state.players = bedrockState.players + } + if (vanillaState) { + try { + let name = '' + const description = vanillaState.raw.description + if (typeof description === 'string') { + name = description + } + if (!name && typeof description === 'object' && description.text) { + name = description.text + } + if (!name && typeof description === 'object' && description.extra) { + name = description.extra.map(part => part.text).join('') + } + state.name = name + } catch (e) {} + if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers + if (vanillaState.players.length) state.players = vanillaState.players + if (vanillaState.ping) this.registerRtt(vanillaState.ping) + } + if (gamespyState) { + if (gamespyState.name) state.name = gamespyState.name + if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers + if (gamespyState.players.length) state.players = gamespyState.players + else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers)) + if (gamespyState.ping) this.registerRtt(gamespyState.ping) + } + if (bedrockState) { + if (bedrockState.name) state.name = bedrockState.name + if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers + if (bedrockState.map) state.map = bedrockState.map + if (bedrockState.ping) this.registerRtt(bedrockState.ping) + } + // remove dupe spaces from name + state.name = state.name.replace(/\s+/g, ' ') + // remove color codes from name + state.name = state.name.replace(/\u00A7./g, '') + } +} diff --git a/protocols/minecraftbedrock.js b/protocols/minecraftbedrock.js index f85ee57..cd5b0dd 100644 --- a/protocols/minecraftbedrock.js +++ b/protocols/minecraftbedrock.js @@ -1,72 +1,72 @@ -import Core from './core.js'; - -export default class minecraftbedrock extends Core { - constructor() { - super(); - this.byteorder = 'be'; - } - - async run(state) { - const bufs = [ - Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING - Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp - Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic - Buffer.from('0000000000000000', 'hex') // Cliend GUID - ]; - - return await this.udpSend(Buffer.concat(bufs), buffer => { - const reader = this.reader(buffer); - - const messageId = reader.uint(1); - if (messageId !== 0x1c) { - this.logger.debug('Skipping packet, invalid message id'); - return; - } - - const nonce = reader.part(8).toString('hex'); // should match the nonce we sent - this.logger.debug('Nonce: ' + nonce); - if (nonce !== '1122334455667788') { - this.logger.debug('Skipping packet, invalid nonce'); - return; - } - - // These 8 bytes are identical to the serverId string we receive in decimal below - reader.skip(8); - - const magic = reader.part(16).toString('hex'); - this.logger.debug('Magic value: ' + magic); - if (magic !== '00ffff00fefefefefdfdfdfd12345678') { - this.logger.debug('Skipping packet, invalid magic'); - return; - } - - const statusLen = reader.uint(2); - if (reader.remaining() !== statusLen) { - throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen); - } - - const statusStr = reader.rest().toString('utf8'); - this.logger.debug('Raw status str: ' + statusStr); - - const split = statusStr.split(';'); - if (split.length < 6) { - throw new Error('Missing enough chunks in status str'); - } - - state.raw.edition = split.shift(); - state.name = split.shift(); - state.raw.protocolVersion = split.shift(); - state.raw.mcVersion = split.shift(); - state.players.setNum(parseInt(split.shift())); - state.maxplayers = parseInt(split.shift()); - if (split.length) state.raw.serverId = split.shift(); - if (split.length) state.map = split.shift(); - if (split.length) state.raw.gameMode = split.shift(); - if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift()); - if (split.length) state.raw.ipv4Port = split.shift(); - if (split.length) state.raw.ipv6Port = split.shift(); - - return true; - }); - } -} +import Core from './core.js' + +export default class minecraftbedrock extends Core { + constructor () { + super() + this.byteorder = 'be' + } + + async run (state) { + const bufs = [ + Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING + Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp + Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic + Buffer.from('0000000000000000', 'hex') // Cliend GUID + ] + + return await this.udpSend(Buffer.concat(bufs), buffer => { + const reader = this.reader(buffer) + + const messageId = reader.uint(1) + if (messageId !== 0x1c) { + this.logger.debug('Skipping packet, invalid message id') + return + } + + const nonce = reader.part(8).toString('hex') // should match the nonce we sent + this.logger.debug('Nonce: ' + nonce) + if (nonce !== '1122334455667788') { + this.logger.debug('Skipping packet, invalid nonce') + return + } + + // These 8 bytes are identical to the serverId string we receive in decimal below + reader.skip(8) + + const magic = reader.part(16).toString('hex') + this.logger.debug('Magic value: ' + magic) + if (magic !== '00ffff00fefefefefdfdfdfd12345678') { + this.logger.debug('Skipping packet, invalid magic') + return + } + + const statusLen = reader.uint(2) + if (reader.remaining() !== statusLen) { + throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen) + } + + const statusStr = reader.rest().toString('utf8') + this.logger.debug('Raw status str: ' + statusStr) + + const split = statusStr.split(';') + if (split.length < 6) { + throw new Error('Missing enough chunks in status str') + } + + state.raw.edition = split.shift() + state.name = split.shift() + state.raw.protocolVersion = split.shift() + state.raw.mcVersion = split.shift() + state.players.setNum(parseInt(split.shift())) + state.maxplayers = parseInt(split.shift()) + if (split.length) state.raw.serverId = split.shift() + if (split.length) state.map = split.shift() + if (split.length) state.raw.gameMode = split.shift() + if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift()) + if (split.length) state.raw.ipv4Port = split.shift() + if (split.length) state.raw.ipv6Port = split.shift() + + return true + }) + } +} diff --git a/protocols/minecraftvanilla.js b/protocols/minecraftvanilla.js index 6af85e4..cadc294 100644 --- a/protocols/minecraftvanilla.js +++ b/protocols/minecraftvanilla.js @@ -1,80 +1,81 @@ -import Core from './core.js'; -import Varint from "varint"; - -export default class minecraftvanilla extends Core { - async run(state) { - const portBuf = Buffer.alloc(2); - portBuf.writeUInt16BE(this.options.port,0); - - const addressBuf = Buffer.from(this.options.host,'utf8'); - - const bufs = [ - this.varIntBuffer(47), - this.varIntBuffer(addressBuf.length), - addressBuf, - portBuf, - this.varIntBuffer(1) - ]; - - const outBuffer = Buffer.concat([ - this.buildPacket(0,Buffer.concat(bufs)), - this.buildPacket(0) - ]); - - const data = await this.withTcp(async socket => { - return await this.tcpSend(socket, outBuffer, data => { - if(data.length < 10) return; - const reader = this.reader(data); - const length = reader.varint(); - if(data.length < length) return; - return reader.rest(); - }); - }); - - const reader = this.reader(data); - - const packetId = reader.varint(); - this.logger.debug("Packet ID: "+packetId); - - const strLen = reader.varint(); - this.logger.debug("String Length: "+strLen); - - const str = reader.rest().toString('utf8'); - this.logger.debug(str); - - const json = JSON.parse(str); - delete json.favicon; - - state.raw = json; - state.maxplayers = json.players.max; - - if(json.players.sample) { - for(const player of json.players.sample) { - state.players.push({ - id: player.id, - name: player.name - }); - } - } - - // players.sample may not contain all players or no players at all, depending on how many players are online. - // Insert a dummy player object for every online player that is not listed in players.sample. - // Limit player amount to 10.000 players for performance reasons. - for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) { - state.players.push({}); - } - } - - varIntBuffer(num) { - return Buffer.from(Varint.encode(num)); - } - buildPacket(id,data) { - if(!data) data = Buffer.from([]); - const idBuffer = this.varIntBuffer(id); - return Buffer.concat([ - this.varIntBuffer(data.length+idBuffer.length), - idBuffer, - data - ]); - } -} +import Core from './core.js' +import Varint from 'varint' + +export default class minecraftvanilla extends Core { + async run (state) { + const portBuf = Buffer.alloc(2) + portBuf.writeUInt16BE(this.options.port, 0) + + const addressBuf = Buffer.from(this.options.host, 'utf8') + + const bufs = [ + this.varIntBuffer(47), + this.varIntBuffer(addressBuf.length), + addressBuf, + portBuf, + this.varIntBuffer(1) + ] + + const outBuffer = Buffer.concat([ + this.buildPacket(0, Buffer.concat(bufs)), + this.buildPacket(0) + ]) + + const data = await this.withTcp(async socket => { + return await this.tcpSend(socket, outBuffer, data => { + if (data.length < 10) return + const reader = this.reader(data) + const length = reader.varint() + if (data.length < length) return + return reader.rest() + }) + }) + + const reader = this.reader(data) + + const packetId = reader.varint() + this.logger.debug('Packet ID: ' + packetId) + + const strLen = reader.varint() + this.logger.debug('String Length: ' + strLen) + + const str = reader.rest().toString('utf8') + this.logger.debug(str) + + const json = JSON.parse(str) + delete json.favicon + + state.raw = json + state.maxplayers = json.players.max + + if (json.players.sample) { + for (const player of json.players.sample) { + state.players.push({ + id: player.id, + name: player.name + }) + } + } + + // players.sample may not contain all players or no players at all, depending on how many players are online. + // Insert a dummy player object for every online player that is not listed in players.sample. + // Limit player amount to 10.000 players for performance reasons. + for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) { + state.players.push({}) + } + } + + varIntBuffer (num) { + return Buffer.from(Varint.encode(num)) + } + + buildPacket (id, data) { + if (!data) data = Buffer.from([]) + const idBuffer = this.varIntBuffer(id) + return Buffer.concat([ + this.varIntBuffer(data.length + idBuffer.length), + idBuffer, + data + ]) + } +} diff --git a/protocols/mumble.js b/protocols/mumble.js index 312a615..929efcd 100644 --- a/protocols/mumble.js +++ b/protocols/mumble.js @@ -1,39 +1,39 @@ -import Core from './core.js'; - -export default class mumble extends Core { - async run(state) { - const json = await this.withTcp(async socket => { - return await this.tcpSend(socket, 'json', (buffer) => { - if (buffer.length < 10) return; - const str = buffer.toString(); - let json; - try { - json = JSON.parse(str); - } catch (e) { - // probably not all here yet - return; - } - return json; - }); - }); - - state.raw = json; - state.name = json.name; - state.gamePort = json.x_gtmurmur_connectport || 64738; - - let channelStack = [state.raw.root]; - while(channelStack.length) { - const channel = channelStack.shift(); - channel.description = this.cleanComment(channel.description); - channelStack = channelStack.concat(channel.channels); - for(const user of channel.users) { - user.comment = this.cleanComment(user.comment); - state.players.push(user); - } - } - } - - cleanComment(str) { - return str.replace(/<.*>/g,''); - } -} +import Core from './core.js' + +export default class mumble extends Core { + async run (state) { + const json = await this.withTcp(async socket => { + return await this.tcpSend(socket, 'json', (buffer) => { + if (buffer.length < 10) return + const str = buffer.toString() + let json + try { + json = JSON.parse(str) + } catch (e) { + // probably not all here yet + return + } + return json + }) + }) + + state.raw = json + state.name = json.name + state.gamePort = json.x_gtmurmur_connectport || 64738 + + let channelStack = [state.raw.root] + while (channelStack.length) { + const channel = channelStack.shift() + channel.description = this.cleanComment(channel.description) + channelStack = channelStack.concat(channel.channels) + for (const user of channel.users) { + user.comment = this.cleanComment(user.comment) + state.players.push(user) + } + } + } + + cleanComment (str) { + return str.replace(/<.*>/g, '') + } +} diff --git a/protocols/mumbleping.js b/protocols/mumbleping.js index ab1fccb..77e6ab7 100644 --- a/protocols/mumbleping.js +++ b/protocols/mumbleping.js @@ -1,24 +1,24 @@ -import Core from './core.js'; - -export default class mumbleping extends Core { - constructor() { - super(); - this.byteorder = 'be'; - } - - async run(state) { - const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { - if (buffer.length >= 24) return buffer; - }); - - const reader = this.reader(data); - reader.skip(1); - state.raw.versionMajor = reader.uint(1); - state.raw.versionMinor = reader.uint(1); - state.raw.versionPatch = reader.uint(1); - reader.skip(8); - state.players.setNum(reader.uint(4)); - state.maxplayers = reader.uint(4); - state.raw.allowedbandwidth = reader.uint(4); - } -} +import Core from './core.js' + +export default class mumbleping extends Core { + constructor () { + super() + this.byteorder = 'be' + } + + async run (state) { + const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { + if (buffer.length >= 24) return buffer + }) + + const reader = this.reader(data) + reader.skip(1) + state.raw.versionMajor = reader.uint(1) + state.raw.versionMinor = reader.uint(1) + state.raw.versionPatch = reader.uint(1) + reader.skip(8) + state.players.setNum(reader.uint(4)) + state.maxplayers = reader.uint(4) + state.raw.allowedbandwidth = reader.uint(4) + } +} diff --git a/protocols/nadeo.js b/protocols/nadeo.js index e9c9ca6..a996bc8 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -1,85 +1,85 @@ -import Core from './core.js'; -import Promises from "../lib/Promises.js"; -import * as gbxremote from 'gbxremote'; - -export default class nadeo extends Core { - async run(state) { - await this.withClient(async client => { - const start = Date.now(); - await this.query(client, 'Authenticate', this.options.login, this.options.password); - this.registerRtt(Date.now()-start); - - //const data = this.methodCall(client, 'GetStatus'); - - { - const results = await this.query(client, 'GetServerOptions'); - state.name = this.stripColors(results.Name); - state.password = (results.Password !== 'No password'); - state.maxplayers = results.CurrentMaxPlayers; - state.raw.maxspectators = results.CurrentMaxSpectators; - } - - { - const results = await this.query(client, 'GetCurrentMapInfo'); - state.map = this.stripColors(results.Name); - state.raw.mapUid = results.UId; - } - - { - const results = await this.query(client, 'GetCurrentGameInfo'); - let gamemode = ''; - const igm = results.GameMode; - if(igm === 0) gamemode="Rounds"; - if(igm === 1) gamemode="Time Attack"; - if(igm === 2) gamemode="Team"; - if(igm === 3) gamemode="Laps"; - if(igm === 4) gamemode="Stunts"; - if(igm === 5) gamemode="Cup"; - state.raw.gametype = gamemode; - state.raw.mapcount = results.NbChallenge; - } - - { - const results = await this.query(client, 'GetNextMapInfo'); - state.raw.nextmapName = this.stripColors(results.Name); - state.raw.nextmapUid = results.UId; - } - - if (this.options.port === 5000) { - state.gamePort = 2350; - } - - state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0); - for (const player of state.raw.players) { - state.players.push({ - name:this.stripColors(player.Name || player.NickName) - }); - } - }); - } - - async withClient(fn) { - const socket = new gbxremote.Client(this.options.port, this.options.host); - try { - const connectPromise = socket.connect(); - const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening'); - await Promise.race([connectPromise, timeoutPromise, this.abortedPromise]); - return await fn(socket); - } finally { - socket.terminate(); - } - } - - async query(client, ...cmdset) { - const cmd = cmdset[0]; - const params = cmdset.slice(1); - - const sentPromise = client.query(cmd, params); - const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call'); - return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise]); - } - - stripColors(str) { - return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,''); - } -} +import Core from './core.js' +import Promises from '../lib/Promises.js' +import * as gbxremote from 'gbxremote' + +export default class nadeo extends Core { + async run (state) { + await this.withClient(async client => { + const start = Date.now() + await this.query(client, 'Authenticate', this.options.login, this.options.password) + this.registerRtt(Date.now() - start) + + // const data = this.methodCall(client, 'GetStatus'); + + { + const results = await this.query(client, 'GetServerOptions') + state.name = this.stripColors(results.Name) + state.password = (results.Password !== 'No password') + state.maxplayers = results.CurrentMaxPlayers + state.raw.maxspectators = results.CurrentMaxSpectators + } + + { + const results = await this.query(client, 'GetCurrentMapInfo') + state.map = this.stripColors(results.Name) + state.raw.mapUid = results.UId + } + + { + const results = await this.query(client, 'GetCurrentGameInfo') + let gamemode = '' + const igm = results.GameMode + if (igm === 0) gamemode = 'Rounds' + if (igm === 1) gamemode = 'Time Attack' + if (igm === 2) gamemode = 'Team' + if (igm === 3) gamemode = 'Laps' + if (igm === 4) gamemode = 'Stunts' + if (igm === 5) gamemode = 'Cup' + state.raw.gametype = gamemode + state.raw.mapcount = results.NbChallenge + } + + { + const results = await this.query(client, 'GetNextMapInfo') + state.raw.nextmapName = this.stripColors(results.Name) + state.raw.nextmapUid = results.UId + } + + if (this.options.port === 5000) { + state.gamePort = 2350 + } + + state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0) + for (const player of state.raw.players) { + state.players.push({ + name: this.stripColors(player.Name || player.NickName) + }) + } + }) + } + + async withClient (fn) { + const socket = new gbxremote.Client(this.options.port, this.options.host) + try { + const connectPromise = socket.connect() + const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening') + await Promise.race([connectPromise, timeoutPromise, this.abortedPromise]) + return await fn(socket) + } finally { + socket.terminate() + } + } + + async query (client, ...cmdset) { + const cmd = cmdset[0] + const params = cmdset.slice(1) + + const sentPromise = client.query(cmd, params) + const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call') + return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise]) + } + + stripColors (str) { + return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '') + } +} diff --git a/protocols/openttd.js b/protocols/openttd.js index 8795a40..418efc6 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -1,127 +1,127 @@ -import Core from './core.js'; - -export default class openttd extends Core { - async run(state) { - { - const [reader, version] = await this.query(0, 1, 1, 4); - if (version >= 4) { - const numGrf = reader.uint(1); - state.raw.grfs = []; - for (let i = 0; i < numGrf; i++) { - const grf = {}; - grf.id = reader.part(4).toString('hex'); - grf.md5 = reader.part(16).toString('hex'); - state.raw.grfs.push(grf); - } - } - if (version >= 3) { - state.raw.date_current = this.readDate(reader); - state.raw.date_start = this.readDate(reader); - } - if (version >= 2) { - state.raw.maxcompanies = reader.uint(1); - state.raw.numcompanies = reader.uint(1); - state.raw.maxspectators = reader.uint(1); - } - - state.name = reader.string(); - state.raw.version = reader.string(); - - state.raw.language = this.decode( - reader.uint(1), - ['any', 'en', 'de', 'fr'] - ); - - state.password = !!reader.uint(1); - state.maxplayers = reader.uint(1); - state.players.setNum(reader.uint(1)); - state.raw.numspectators = reader.uint(1); - state.map = reader.string(); - state.raw.map_width = reader.uint(2); - state.raw.map_height = reader.uint(2); - - state.raw.landscape = this.decode( - reader.uint(1), - ['temperate', 'arctic', 'desert', 'toyland'] - ); - - state.raw.dedicated = !!reader.uint(1); - } - - { - const [reader,version] = await this.query(2,3,-1,-1); - // we don't know how to deal with companies outside version 6 - if(version === 6) { - state.raw.companies = []; - const numCompanies = reader.uint(1); - for (let iCompany = 0; iCompany < numCompanies; iCompany++) { - const company = {}; - company.id = reader.uint(1); - company.name = reader.string(); - company.year_start = reader.uint(4); - company.value = reader.uint(8).toString(); - company.money = reader.uint(8).toString(); - company.income = reader.uint(8).toString(); - company.performance = reader.uint(2); - company.password = !!reader.uint(1); - - const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship']; - const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock']; - - company.vehicles = {}; - for (const type of vehicle_types) { - company.vehicles[type] = reader.uint(2); - } - company.stations = {}; - for (const type of station_types) { - company.stations[type] = reader.uint(2); - } - - company.clients = reader.string(); - state.raw.companies.push(company); - } - } - } - } - - async query(type,expected,minver,maxver) { - const b = Buffer.from([0x03,0x00,type]); - return await this.udpSend(b,(buffer) => { - const reader = this.reader(buffer); - - const packetLen = reader.uint(2); - if(packetLen !== buffer.length) { - this.logger.debug('Invalid reported packet length: '+packetLen+' '+buffer.length); - return; - } - - const packetType = reader.uint(1); - if(packetType !== expected) { - this.logger.debug('Unexpected response packet type: '+packetType); - return; - } - - const protocolVersion = reader.uint(1); - if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { - throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); - } - - return [reader,protocolVersion]; - }); - } - - readDate(reader) { - const daysSinceZero = reader.uint(4); - const temp = new Date(0,0,1); - temp.setFullYear(0); - temp.setDate(daysSinceZero+1); - return temp.toISOString().split('T')[0]; - } - - decode(num,arr) { - if(num < 0 || num >= arr.length) { - return num; - } - return arr[num]; - } -} +import Core from './core.js' + +export default class openttd extends Core { + async run (state) { + { + const [reader, version] = await this.query(0, 1, 1, 4) + if (version >= 4) { + const numGrf = reader.uint(1) + state.raw.grfs = [] + for (let i = 0; i < numGrf; i++) { + const grf = {} + grf.id = reader.part(4).toString('hex') + grf.md5 = reader.part(16).toString('hex') + state.raw.grfs.push(grf) + } + } + if (version >= 3) { + state.raw.date_current = this.readDate(reader) + state.raw.date_start = this.readDate(reader) + } + if (version >= 2) { + state.raw.maxcompanies = reader.uint(1) + state.raw.numcompanies = reader.uint(1) + state.raw.maxspectators = reader.uint(1) + } + + state.name = reader.string() + state.raw.version = reader.string() + + state.raw.language = this.decode( + reader.uint(1), + ['any', 'en', 'de', 'fr'] + ) + + state.password = !!reader.uint(1) + state.maxplayers = reader.uint(1) + state.players.setNum(reader.uint(1)) + state.raw.numspectators = reader.uint(1) + state.map = reader.string() + state.raw.map_width = reader.uint(2) + state.raw.map_height = reader.uint(2) + + state.raw.landscape = this.decode( + reader.uint(1), + ['temperate', 'arctic', 'desert', 'toyland'] + ) + + state.raw.dedicated = !!reader.uint(1) + } + + { + const [reader, version] = await this.query(2, 3, -1, -1) + // we don't know how to deal with companies outside version 6 + if (version === 6) { + state.raw.companies = [] + const numCompanies = reader.uint(1) + for (let iCompany = 0; iCompany < numCompanies; iCompany++) { + const company = {} + company.id = reader.uint(1) + company.name = reader.string() + company.year_start = reader.uint(4) + company.value = reader.uint(8).toString() + company.money = reader.uint(8).toString() + company.income = reader.uint(8).toString() + company.performance = reader.uint(2) + company.password = !!reader.uint(1) + + const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship'] + const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock'] + + company.vehicles = {} + for (const type of vehicleTypes) { + company.vehicles[type] = reader.uint(2) + } + company.stations = {} + for (const type of stationTypes) { + company.stations[type] = reader.uint(2) + } + + company.clients = reader.string() + state.raw.companies.push(company) + } + } + } + } + + async query (type, expected, minver, maxver) { + const b = Buffer.from([0x03, 0x00, type]) + return await this.udpSend(b, (buffer) => { + const reader = this.reader(buffer) + + const packetLen = reader.uint(2) + if (packetLen !== buffer.length) { + this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length) + return + } + + const packetType = reader.uint(1) + if (packetType !== expected) { + this.logger.debug('Unexpected response packet type: ' + packetType) + return + } + + const protocolVersion = reader.uint(1) + if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { + throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver) + } + + return [reader, protocolVersion] + }) + } + + readDate (reader) { + const daysSinceZero = reader.uint(4) + const temp = new Date(0, 0, 1) + temp.setFullYear(0) + temp.setDate(daysSinceZero + 1) + return temp.toISOString().split('T')[0] + } + + decode (num, arr) { + if (num < 0 || num >= arr.length) { + return num + } + return arr[num] + } +} diff --git a/protocols/quake1.js b/protocols/quake1.js index e1c5137..9cf11a5 100644 --- a/protocols/quake1.js +++ b/protocols/quake1.js @@ -1,9 +1,9 @@ -import quake2 from './quake2.js'; - -export default class quake1 extends quake2 { - constructor() { - super(); - this.responseHeader = 'n'; - this.isQuake1 = true; - } -} +import quake2 from './quake2.js' + +export default class quake1 extends quake2 { + constructor () { + super() + this.responseHeader = 'n' + this.isQuake1 = true + } +} diff --git a/protocols/quake2.js b/protocols/quake2.js index 8f01b98..afcd9ea 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -1,86 +1,86 @@ -import Core from './core.js'; - -export default class quake2 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.delimiter = '\n'; - this.sendHeader = 'status'; - this.responseHeader = 'print'; - this.isQuake1 = false; - } - - async run(state) { - const body = await this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', packet => { - const reader = this.reader(packet); - const header = reader.string({length: 4, encoding: 'latin1'}); - if (header !== '\xff\xff\xff\xff') return; - let type; - if (this.isQuake1) { - type = reader.string(this.responseHeader.length); - } else { - type = reader.string({encoding: 'latin1'}); - } - if (type !== this.responseHeader) return; - return reader.rest(); - }); - - const reader = this.reader(body); - const info = reader.string().split('\\'); - if(info[0] === '') info.shift(); - - while(true) { - const key = info.shift(); - const value = info.shift(); - if(typeof value === 'undefined') break; - state.raw[key] = value; - } - - while(!reader.done()) { - const line = reader.string(); - if(!line || line.charAt(0) === '\0') break; - - const args = []; - const split = line.split('"'); - split.forEach((part,i) => { - const inQuote = (i%2 === 1); - if(inQuote) { - args.push(part); - } else { - const splitSpace = part.split(' '); - for (const subpart of splitSpace) { - if(subpart) args.push(subpart); - } - } - }); - - const player = {}; - if(this.isQuake1) { - player.id = parseInt(args.shift()); - player.score = parseInt(args.shift()); - player.time = parseInt(args.shift()); - player.ping = parseInt(args.shift()); - player.name = args.shift(); - player.skin = args.shift(); - player.color1 = parseInt(args.shift()); - player.color2 = parseInt(args.shift()); - } else { - player.frags = parseInt(args.shift()); - player.ping = parseInt(args.shift()); - player.name = args.shift() || ''; - if (!player.name) delete player.name; - player.address = args.shift() || ''; - if (!player.address) delete player.address; - } - - (player.ping ? state.players : state.bots).push(player); - } - - if('g_needpass' in state.raw) state.password = state.raw.g_needpass; - if('mapname' in state.raw) state.map = state.raw.mapname; - if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients; - if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients; - if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname; - if('hostname' in state.raw) state.name = state.raw.hostname; - } -} +import Core from './core.js' + +export default class quake2 extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.delimiter = '\n' + this.sendHeader = 'status' + this.responseHeader = 'print' + this.isQuake1 = false + } + + async run (state) { + const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => { + const reader = this.reader(packet) + const header = reader.string({ length: 4, encoding: 'latin1' }) + if (header !== '\xff\xff\xff\xff') return + let type + if (this.isQuake1) { + type = reader.string(this.responseHeader.length) + } else { + type = reader.string({ encoding: 'latin1' }) + } + if (type !== this.responseHeader) return + return reader.rest() + }) + + const reader = this.reader(body) + const info = reader.string().split('\\') + if (info[0] === '') info.shift() + + while (true) { + const key = info.shift() + const value = info.shift() + if (typeof value === 'undefined') break + state.raw[key] = value + } + + while (!reader.done()) { + const line = reader.string() + if (!line || line.charAt(0) === '\0') break + + const args = [] + const split = line.split('"') + split.forEach((part, i) => { + const inQuote = (i % 2 === 1) + if (inQuote) { + args.push(part) + } else { + const splitSpace = part.split(' ') + for (const subpart of splitSpace) { + if (subpart) args.push(subpart) + } + } + }) + + const player = {} + if (this.isQuake1) { + player.id = parseInt(args.shift()) + player.score = parseInt(args.shift()) + player.time = parseInt(args.shift()) + player.ping = parseInt(args.shift()) + player.name = args.shift() + player.skin = args.shift() + player.color1 = parseInt(args.shift()) + player.color2 = parseInt(args.shift()) + } else { + player.frags = parseInt(args.shift()) + player.ping = parseInt(args.shift()) + player.name = args.shift() || '' + if (!player.name) delete player.name + player.address = args.shift() || '' + if (!player.address) delete player.address + } + + (player.ping ? state.players : state.bots).push(player) + } + + if ('g_needpass' in state.raw) state.password = state.raw.g_needpass + if ('mapname' in state.raw) state.map = state.raw.mapname + if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients + if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients + if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname + if ('hostname' in state.raw) state.name = state.raw.hostname + } +} diff --git a/protocols/quake3.js b/protocols/quake3.js index 866b5b0..cc7eab5 100644 --- a/protocols/quake3.js +++ b/protocols/quake3.js @@ -1,22 +1,24 @@ -import quake2 from './quake2.js'; - -export default class quake3 extends quake2 { - constructor() { - super(); - this.sendHeader = 'getstatus'; - this.responseHeader = 'statusResponse'; - } - async run(state) { - await super.run(state); - state.name = this.stripColors(state.name); - for(const key of Object.keys(state.raw)) { - state.raw[key] = this.stripColors(state.raw[key]); - } - for(const player of state.players) { - player.name = this.stripColors(player.name); - } - } - stripColors(str) { - return str.replace(/\^(X.{6}|.)/g,''); - } -} +import quake2 from './quake2.js' + +export default class quake3 extends quake2 { + constructor () { + super() + this.sendHeader = 'getstatus' + this.responseHeader = 'statusResponse' + } + + async run (state) { + await super.run(state) + state.name = this.stripColors(state.name) + for (const key of Object.keys(state.raw)) { + state.raw[key] = this.stripColors(state.raw[key]) + } + for (const player of state.players) { + player.name = this.stripColors(player.name) + } + } + + stripColors (str) { + return str.replace(/\^(X.{6}|.)/g, '') + } +} diff --git a/protocols/rfactor.js b/protocols/rfactor.js index 80bbc3e..868a8f8 100644 --- a/protocols/rfactor.js +++ b/protocols/rfactor.js @@ -1,74 +1,69 @@ -import Core from './core.js'; - -export default class rfactor extends Core { - constructor() { - super(); - //this.byteorder = 'be'; - } - - async run(state) { - const buffer = await this.udpSend('rF_S',b => b); - const reader = this.reader(buffer); - - state.raw.gamename = this.readString(reader, 8); - state.raw.fullUpdate = reader.uint(1); - state.raw.region = reader.uint(2); - state.raw.ip = reader.part(4); - state.raw.size = reader.uint(2); - state.raw.version = reader.uint(2); - state.raw.versionRaceCast = reader.uint(2); - state.gamePort = reader.uint(2); - state.raw.queryPort = reader.uint(2); - state.raw.game = this.readString(reader, 20); - state.name = this.readString(reader, 28); - state.map = this.readString(reader, 32); - state.raw.motd = this.readString(reader, 96); - state.raw.packedAids = reader.uint(2); - state.raw.ping = reader.uint(2); - state.raw.packedFlags = reader.uint(1); - state.raw.rate = reader.uint(1); - state.players.setNum(reader.uint(1)); - state.maxplayers = reader.uint(1); - state.raw.bots = reader.uint(1); - state.raw.packedSpecial = reader.uint(1); - state.raw.damage = reader.uint(1); - state.raw.packedRules = reader.uint(2); - state.raw.credits1 = reader.uint(1); - state.raw.credits2 = reader.uint(2); - this.logger.debug(reader.offset()); - state.raw.time = reader.uint(2); - state.raw.laps = reader.uint(2) / 16; - reader.skip(3); - state.raw.vehicles = reader.string(); - - state.password = !!(state.raw.packedSpecial & 2); - state.raw.raceCast = !!(state.raw.packedSpecial & 4); - state.raw.fixedSetups = !!(state.raw.packedSpecial & 16); - - const aids = [ - 'TractionControl', - 'AntiLockBraking', - 'StabilityControl', - 'AutoShifting', - 'AutoClutch', - 'Invulnerability', - 'OppositeLock', - 'SteeringHelp', - 'BrakingHelp', - 'SpinRecovery', - 'AutoPitstop' - ]; - state.raw.aids = []; - for (let offset = 0; offset < aids.length; offset++) { - if (state.packedAids && (1 << offset)) { - state.raw.aids.push(aids[offset]); - } - } - } - - // Consumes bytesToConsume, but only returns string up to the first null - readString(reader, bytesToConsume) { - const consumed = reader.part(bytesToConsume); - return this.reader(consumed).string(); - } -} +import Core from './core.js' + +export default class rfactor extends Core { + async run (state) { + const buffer = await this.udpSend('rF_S', b => b) + const reader = this.reader(buffer) + + state.raw.gamename = this.readString(reader, 8) + state.raw.fullUpdate = reader.uint(1) + state.raw.region = reader.uint(2) + state.raw.ip = reader.part(4) + state.raw.size = reader.uint(2) + state.raw.version = reader.uint(2) + state.raw.versionRaceCast = reader.uint(2) + state.gamePort = reader.uint(2) + state.raw.queryPort = reader.uint(2) + state.raw.game = this.readString(reader, 20) + state.name = this.readString(reader, 28) + state.map = this.readString(reader, 32) + state.raw.motd = this.readString(reader, 96) + state.raw.packedAids = reader.uint(2) + state.raw.ping = reader.uint(2) + state.raw.packedFlags = reader.uint(1) + state.raw.rate = reader.uint(1) + state.players.setNum(reader.uint(1)) + state.maxplayers = reader.uint(1) + state.raw.bots = reader.uint(1) + state.raw.packedSpecial = reader.uint(1) + state.raw.damage = reader.uint(1) + state.raw.packedRules = reader.uint(2) + state.raw.credits1 = reader.uint(1) + state.raw.credits2 = reader.uint(2) + this.logger.debug(reader.offset()) + state.raw.time = reader.uint(2) + state.raw.laps = reader.uint(2) / 16 + reader.skip(3) + state.raw.vehicles = reader.string() + + state.password = !!(state.raw.packedSpecial & 2) + state.raw.raceCast = !!(state.raw.packedSpecial & 4) + state.raw.fixedSetups = !!(state.raw.packedSpecial & 16) + + const aids = [ + 'TractionControl', + 'AntiLockBraking', + 'StabilityControl', + 'AutoShifting', + 'AutoClutch', + 'Invulnerability', + 'OppositeLock', + 'SteeringHelp', + 'BrakingHelp', + 'SpinRecovery', + 'AutoPitstop' + ] + state.raw.aids = [] + for (let offset = 0; offset < aids.length; offset++) { + if (state.packedAids && (1 << offset)) { + state.raw.aids.push(aids[offset]) + } + } + } + + // Consumes bytesToConsume, but only returns string up to the first null + readString (reader, bytesToConsume) { + const consumed = reader.part(bytesToConsume) + return this.reader(consumed).string() + } +} diff --git a/protocols/samp.js b/protocols/samp.js index 96f6937..03a13f2 100644 --- a/protocols/samp.js +++ b/protocols/samp.js @@ -1,107 +1,108 @@ -import Core from './core.js'; - -export default class samp extends Core { - constructor() { - super(); - this.encoding = 'win1252'; - this.magicHeader = 'SAMP'; - this.responseMagicHeader = null; - this.isVcmp = false; - } - - async run(state) { - // read info - { - const reader = await this.sendPacket('i'); - if (this.isVcmp) { - const consumed = reader.part(12); - state.raw.version = this.reader(consumed).string(); - } - state.password = !!reader.uint(1); - state.raw.numplayers = reader.uint(2); - state.maxplayers = reader.uint(2); - state.name = reader.pascalString(4); - state.raw.gamemode = reader.pascalString(4); - state.raw.map = reader.pascalString(4); - } - - // read rules - if (!this.isVcmp) { - const reader = await this.sendPacket('r'); - const ruleCount = reader.uint(2); - state.raw.rules = {}; - for(let i = 0; i < ruleCount; i++) { - const key = reader.pascalString(1); - const value = reader.pascalString(1); - state.raw.rules[key] = value; - } - } - - // read players - // don't even bother if > 100 players, because the server won't respond - let gotPlayerData = false; - if (state.raw.numplayers < 100) { - if (this.isVcmp) { - const reader = await this.sendPacket('c', true); - if (reader !== null) { - gotPlayerData = true; - const playerCount = reader.uint(2); - for(let i = 0; i < playerCount; i++) { - const player = {}; - player.name = reader.pascalString(1); - state.players.push(player); - } - } - } else { - const reader = await this.sendPacket('d', true); - if (reader !== null) { - gotPlayerData = true; - const playerCount = reader.uint(2); - for(let i = 0; i < playerCount; i++) { - const player = {}; - player.id = reader.uint(1); - player.name = reader.pascalString(1); - player.score = reader.int(4); - player.ping = reader.uint(4); - state.players.push(player); - } - } - } - } - if (!gotPlayerData) { - state.players.setNum(state.raw.numplayers); - } - } - async sendPacket(type,allowTimeout) { - const outBuffer = Buffer.alloc(11); - outBuffer.write(this.magicHeader,0, 4); - const ipSplit = this.options.address.split('.'); - outBuffer.writeUInt8(parseInt(ipSplit[0]),4); - outBuffer.writeUInt8(parseInt(ipSplit[1]),5); - outBuffer.writeUInt8(parseInt(ipSplit[2]),6); - outBuffer.writeUInt8(parseInt(ipSplit[3]),7); - outBuffer.writeUInt16LE(this.options.port,8); - outBuffer.writeUInt8(type.charCodeAt(0),10); - - const checkBuffer = Buffer.from(outBuffer); - if (this.responseMagicHeader) { - checkBuffer.write(this.responseMagicHeader, 0, 4); - } - - return await this.udpSend( - outBuffer, - (buffer) => { - const reader = this.reader(buffer); - for(let i = 0; i < checkBuffer.length; i++) { - if(checkBuffer.readUInt8(i) !== reader.uint(1)) return; - } - return reader; - }, - () => { - if(allowTimeout) { - return null; - } - } - ); - } -} +import Core from './core.js' + +export default class samp extends Core { + constructor () { + super() + this.encoding = 'win1252' + this.magicHeader = 'SAMP' + this.responseMagicHeader = null + this.isVcmp = false + } + + async run (state) { + // read info + { + const reader = await this.sendPacket('i') + if (this.isVcmp) { + const consumed = reader.part(12) + state.raw.version = this.reader(consumed).string() + } + state.password = !!reader.uint(1) + state.raw.numplayers = reader.uint(2) + state.maxplayers = reader.uint(2) + state.name = reader.pascalString(4) + state.raw.gamemode = reader.pascalString(4) + state.raw.map = reader.pascalString(4) + } + + // read rules + if (!this.isVcmp) { + const reader = await this.sendPacket('r') + const ruleCount = reader.uint(2) + state.raw.rules = {} + for (let i = 0; i < ruleCount; i++) { + const key = reader.pascalString(1) + const value = reader.pascalString(1) + state.raw.rules[key] = value + } + } + + // read players + // don't even bother if > 100 players, because the server won't respond + let gotPlayerData = false + if (state.raw.numplayers < 100) { + if (this.isVcmp) { + const reader = await this.sendPacket('c', true) + if (reader !== null) { + gotPlayerData = true + const playerCount = reader.uint(2) + for (let i = 0; i < playerCount; i++) { + const player = {} + player.name = reader.pascalString(1) + state.players.push(player) + } + } + } else { + const reader = await this.sendPacket('d', true) + if (reader !== null) { + gotPlayerData = true + const playerCount = reader.uint(2) + for (let i = 0; i < playerCount; i++) { + const player = {} + player.id = reader.uint(1) + player.name = reader.pascalString(1) + player.score = reader.int(4) + player.ping = reader.uint(4) + state.players.push(player) + } + } + } + } + if (!gotPlayerData) { + state.players.setNum(state.raw.numplayers) + } + } + + async sendPacket (type, allowTimeout) { + const outBuffer = Buffer.alloc(11) + outBuffer.write(this.magicHeader, 0, 4) + const ipSplit = this.options.address.split('.') + outBuffer.writeUInt8(parseInt(ipSplit[0]), 4) + outBuffer.writeUInt8(parseInt(ipSplit[1]), 5) + outBuffer.writeUInt8(parseInt(ipSplit[2]), 6) + outBuffer.writeUInt8(parseInt(ipSplit[3]), 7) + outBuffer.writeUInt16LE(this.options.port, 8) + outBuffer.writeUInt8(type.charCodeAt(0), 10) + + const checkBuffer = Buffer.from(outBuffer) + if (this.responseMagicHeader) { + checkBuffer.write(this.responseMagicHeader, 0, 4) + } + + return await this.udpSend( + outBuffer, + (buffer) => { + const reader = this.reader(buffer) + for (let i = 0; i < checkBuffer.length; i++) { + if (checkBuffer.readUInt8(i) !== reader.uint(1)) return + } + return reader + }, + () => { + if (allowTimeout) { + return null + } + } + ) + } +} diff --git a/protocols/savage2.js b/protocols/savage2.js index b53688c..959ff73 100644 --- a/protocols/savage2.js +++ b/protocols/savage2.js @@ -1,29 +1,25 @@ -import Core from './core.js'; - -export default class savage2 extends Core { - constructor() { - super(); - } - - async run(state) { - const buffer = await this.udpSend('\x01',b => b); - const reader = this.reader(buffer); - - reader.skip(12); - state.name = this.stripColorCodes(reader.string()); - state.players.setNum(reader.uint(1)); - state.maxplayers = reader.uint(1); - state.raw.time = reader.string(); - state.map = reader.string(); - state.raw.nextmap = reader.string(); - state.raw.location = reader.string(); - state.raw.minplayers = reader.uint(1); - state.raw.gametype = reader.string(); - state.raw.version = reader.string(); - state.raw.minlevel = reader.uint(1); - } - - stripColorCodes(str) { - return str.replace(/\^./g,''); - } -} +import Core from './core.js' + +export default class savage2 extends Core { + async run (state) { + const buffer = await this.udpSend('\x01', b => b) + const reader = this.reader(buffer) + + reader.skip(12) + state.name = this.stripColorCodes(reader.string()) + state.players.setNum(reader.uint(1)) + state.maxplayers = reader.uint(1) + state.raw.time = reader.string() + state.map = reader.string() + state.raw.nextmap = reader.string() + state.raw.location = reader.string() + state.raw.minplayers = reader.uint(1) + state.raw.gametype = reader.string() + state.raw.version = reader.string() + state.raw.minlevel = reader.uint(1) + } + + stripColorCodes (str) { + return str.replace(/\^./g, '') + } +} diff --git a/protocols/starmade.js b/protocols/starmade.js index b8c054b..31cb972 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -1,67 +1,67 @@ -import Core from './core.js'; - -export default class starmade extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.byteorder = 'be'; - } - - async run(state) { - const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]); - - const payload = await this.withTcp(async socket => { - return await this.tcpSend(socket, b, buffer => { - if (buffer.length < 12) return; - const reader = this.reader(buffer); - const packetLength = reader.uint(4); - this.logger.debug("Received packet length: " + packetLength); - const timestamp = reader.uint(8).toString(); - this.logger.debug("Received timestamp: " + timestamp); - if (reader.remaining() < packetLength || reader.remaining() < 5) return; - - const checkId = reader.uint(1); - const packetId = reader.uint(2); - const commandId = reader.uint(1); - const type = reader.uint(1); - - this.logger.debug("checkId=" + checkId + " packetId=" + packetId + " commandId=" + commandId + " type=" + type); - if (checkId !== 0x2a) return; - - return reader.rest(); - }); - }); - - const reader = this.reader(payload); - - const data = []; - state.raw.data = data; - - while(!reader.done()) { - const mark = reader.uint(1); - if(mark === 1) { - // signed int - data.push(reader.int(4)); - } else if(mark === 3) { - // float - data.push(reader.float()); - } else if(mark === 4) { - // string - data.push(reader.pascalString(2)); - } else if(mark === 6) { - // byte - data.push(reader.uint(1)); - } - } - - this.logger.debug("Received raw data array", data); - - if(typeof data[0] === 'number') state.raw.infoVersion = data[0]; - if(typeof data[1] === 'number') state.raw.version = data[1]; - if(typeof data[2] === 'string') state.name = data[2]; - if(typeof data[3] === 'string') state.raw.description = data[3]; - if(typeof data[4] === 'number') state.raw.startTime = data[4]; - if(typeof data[5] === 'number') state.players.setNum(data[5]); - if(typeof data[6] === 'number') state.maxplayers = data[6]; - } -} +import Core from './core.js' + +export default class starmade extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.byteorder = 'be' + } + + async run (state) { + const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00]) + + const payload = await this.withTcp(async socket => { + return await this.tcpSend(socket, b, buffer => { + if (buffer.length < 12) return + const reader = this.reader(buffer) + const packetLength = reader.uint(4) + this.logger.debug('Received packet length: ' + packetLength) + const timestamp = reader.uint(8).toString() + this.logger.debug('Received timestamp: ' + timestamp) + if (reader.remaining() < packetLength || reader.remaining() < 5) return + + const checkId = reader.uint(1) + const packetId = reader.uint(2) + const commandId = reader.uint(1) + const type = reader.uint(1) + + this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type) + if (checkId !== 0x2a) return + + return reader.rest() + }) + }) + + const reader = this.reader(payload) + + const data = [] + state.raw.data = data + + while (!reader.done()) { + const mark = reader.uint(1) + if (mark === 1) { + // signed int + data.push(reader.int(4)) + } else if (mark === 3) { + // float + data.push(reader.float()) + } else if (mark === 4) { + // string + data.push(reader.pascalString(2)) + } else if (mark === 6) { + // byte + data.push(reader.uint(1)) + } + } + + this.logger.debug('Received raw data array', data) + + if (typeof data[0] === 'number') state.raw.infoVersion = data[0] + if (typeof data[1] === 'number') state.raw.version = data[1] + if (typeof data[2] === 'string') state.name = data[2] + if (typeof data[3] === 'string') state.raw.description = data[3] + if (typeof data[4] === 'number') state.raw.startTime = data[4] + if (typeof data[5] === 'number') state.players.setNum(data[5]) + if (typeof data[6] === 'number') state.maxplayers = data[6] + } +} diff --git a/protocols/starsiege.js b/protocols/starsiege.js index dcb0d61..2bdc921 100644 --- a/protocols/starsiege.js +++ b/protocols/starsiege.js @@ -1,10 +1,10 @@ -import tribes1 from "./tribes1.js"; - -export default class starsiege extends tribes1 { - constructor() { - super(); - this.encoding = 'latin1'; - this.requestByte = 0x72; - this.responseByte = 0x73; - } -} +import tribes1 from './tribes1.js' + +export default class starsiege extends tribes1 { + constructor () { + super() + this.encoding = 'latin1' + this.requestByte = 0x72 + this.responseByte = 0x73 + } +} diff --git a/protocols/teamspeak2.js b/protocols/teamspeak2.js index 153d781..e70ccd0 100644 --- a/protocols/teamspeak2.js +++ b/protocols/teamspeak2.js @@ -1,70 +1,70 @@ -import Core from './core.js'; - -export default class teamspeak2 extends Core { - async run(state) { - const queryPort = this.options.teamspeakQueryPort || 51234; - - await this.withTcp(async socket => { - { - const data = await this.sendCommand(socket, 'sel '+this.options.port); - if(data !== '[TS]') throw new Error('Invalid header'); - } - - { - const data = await this.sendCommand(socket, 'si'); - for (const line of data.split('\r\n')) { - const equals = line.indexOf('='); - const key = equals === -1 ? line : line.substring(0,equals); - const value = equals === -1 ? '' : line.substring(equals+1); - state.raw[key] = value; - } - } - - { - const data = await this.sendCommand(socket, 'pl'); - const split = data.split('\r\n'); - const fields = split.shift().split('\t'); - for (const line of split) { - const split2 = line.split('\t'); - const player = {}; - split2.forEach((value,i) => { - let key = fields[i]; - if(!key) return; - if(key === 'nick') key = 'name'; - const m = value.match(/^"(.*)"$/); - if(m) value = m[1]; - player[key] = value; - }); - state.players.push(player); - } - } - - { - const data = await this.sendCommand(socket, 'cl'); - const split = data.split('\r\n'); - const fields = split.shift().split('\t'); - state.raw.channels = []; - for (const line of split) { - const split2 = line.split('\t'); - const channel = {}; - split2.forEach((value,i) => { - const key = fields[i]; - if(!key) return; - const m = value.match(/^"(.*)"$/); - if(m) value = m[1]; - channel[key] = value; - }); - state.raw.channels.push(channel); - } - } - }, queryPort); - } - - async sendCommand(socket,cmd) { - return await this.tcpSend(socket, cmd+'\x0A', buffer => { - if(buffer.length < 6) return; - if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return; - return buffer.slice(0,-6).toString(); - }); - } -} +import Core from './core.js' + +export default class teamspeak2 extends Core { + async run (state) { + const queryPort = this.options.teamspeakQueryPort || 51234 + + await this.withTcp(async socket => { + { + const data = await this.sendCommand(socket, 'sel ' + this.options.port) + if (data !== '[TS]') throw new Error('Invalid header') + } + + { + const data = await this.sendCommand(socket, 'si') + for (const line of data.split('\r\n')) { + const equals = line.indexOf('=') + const key = equals === -1 ? line : line.substring(0, equals) + const value = equals === -1 ? '' : line.substring(equals + 1) + state.raw[key] = value + } + } + + { + const data = await this.sendCommand(socket, 'pl') + const split = data.split('\r\n') + const fields = split.shift().split('\t') + for (const line of split) { + const split2 = line.split('\t') + const player = {} + split2.forEach((value, i) => { + let key = fields[i] + if (!key) return + if (key === 'nick') key = 'name' + const m = value.match(/^"(.*)"$/) + if (m) value = m[1] + player[key] = value + }) + state.players.push(player) + } + } + + { + const data = await this.sendCommand(socket, 'cl') + const split = data.split('\r\n') + const fields = split.shift().split('\t') + state.raw.channels = [] + for (const line of split) { + const split2 = line.split('\t') + const channel = {} + split2.forEach((value, i) => { + const key = fields[i] + if (!key) return + const m = value.match(/^"(.*)"$/) + if (m) value = m[1] + channel[key] = value + }) + state.raw.channels.push(channel) + } + } + }, queryPort) + } + + async sendCommand (socket, cmd) { + return await this.tcpSend(socket, cmd + '\x0A', buffer => { + if (buffer.length < 6) return + if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return + return buffer.slice(0, -6).toString() + }) + } +} diff --git a/protocols/teamspeak3.js b/protocols/teamspeak3.js index a132e26..d079124 100644 --- a/protocols/teamspeak3.js +++ b/protocols/teamspeak3.js @@ -1,66 +1,68 @@ -import Core from './core.js'; - -export default class teamspeak3 extends Core { - async run(state) { - const queryPort = this.options.teamspeakQueryPort || 10011; - - await this.withTcp(async socket => { - { - const data = await this.sendCommand(socket, 'use port='+this.options.port, true); - const split = data.split('\n\r'); - if(split[0] !== 'TS3') throw new Error('Invalid header'); - } - - { - const data = await this.sendCommand(socket, 'serverinfo'); - state.raw = data[0]; - if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name; - if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients; - } - - { - const list = await this.sendCommand(socket, 'clientlist'); - for (const client of list) { - client.name = client.client_nickname; - delete client.client_nickname; - if(client.client_type === '0') { - state.players.push(client); - } - } - } - - { - const data = await this.sendCommand(socket, 'channellist -topic'); - state.raw.channels = data; - } - }, queryPort); - } - - async sendCommand(socket,cmd,raw) { - const body = await this.tcpSend(socket, cmd+'\x0A', (buffer) => { - if (buffer.length < 21) return; - if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return; - return buffer.slice(0, -21).toString(); - }); - - if(raw) { - return body; - } else { - const segments = body.split('|'); - const out = []; - for (const line of segments) { - const split = line.split(' '); - const unit = {}; - for (const field of split) { - const equals = field.indexOf('='); - const key = equals === -1 ? field : field.substring(0, equals); - const value = equals === -1 ? '' : field.substring(equals + 1) - .replace(/\\s/g,' ').replace(/\\\//g,'/'); - unit[key] = value; - } - out.push(unit); - } - return out; - } - } -} +import Core from './core.js' + +export default class teamspeak3 extends Core { + async run (state) { + const queryPort = this.options.teamspeakQueryPort || 10011 + + await this.withTcp(async socket => { + { + const data = await this.sendCommand(socket, 'use port=' + this.options.port, true) + const split = data.split('\n\r') + if (split[0] !== 'TS3') throw new Error('Invalid header') + } + + { + const data = await this.sendCommand(socket, 'serverinfo') + state.raw = data[0] + if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name + if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients + } + + { + const list = await this.sendCommand(socket, 'clientlist') + for (const client of list) { + client.name = client.client_nickname + delete client.client_nickname + if (client.client_type === '0') { + state.players.push(client) + } + } + } + + { + const data = await this.sendCommand(socket, 'channellist -topic') + state.raw.channels = data + } + }, queryPort) + } + + async sendCommand (socket, cmd, raw) { + const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => { + if (buffer.length < 21) return + if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return + return buffer.slice(0, -21).toString() + }) + + if (raw) { + return body + } else { + const segments = body.split('|') + const out = [] + for (const line of segments) { + const split = line.split(' ') + const unit = {} + for (const field of split) { + const equals = field.indexOf('=') + const key = equals === -1 ? field : field.substring(0, equals) + const value = equals === -1 + ? '' + : field.substring(equals + 1) + .replace(/\\s/g, ' ').replace(/\\\//g, '/') + unit[key] = value + } + out.push(unit) + } + return out + } + } +} diff --git a/protocols/terraria.js b/protocols/terraria.js index 5ea8486..85dff63 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -1,24 +1,24 @@ -import Core from './core.js'; - -export default class terraria extends Core { - async run(state) { - const json = await this.request({ - url: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status', - searchParams: { - players: 'true', - token: this.options.token - }, - responseType: 'json' - }); - - if(json.status !== '200') throw new Error('Invalid status'); - - for (const one of json.players) { - state.players.push({name:one.nickname,team:one.team}); - } - - state.name = json.name; - state.gamePort = json.port; - state.raw.numplayers = json.playercount; - } -} +import Core from './core.js' + +export default class terraria extends Core { + async run (state) { + const json = await this.request({ + url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status', + searchParams: { + players: 'true', + token: this.options.token + }, + responseType: 'json' + }) + + if (json.status !== '200') throw new Error('Invalid status') + + for (const one of json.players) { + state.players.push({ name: one.nickname, team: one.team }) + } + + state.name = json.name + state.gamePort = json.port + state.raw.numplayers = json.playercount + } +} diff --git a/protocols/tribes1.js b/protocols/tribes1.js index 4bea176..5c34cfb 100644 --- a/protocols/tribes1.js +++ b/protocols/tribes1.js @@ -1,150 +1,153 @@ -import Core from './core.js'; - -export default class tribes1 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - this.requestByte = 0x62; - this.responseByte = 0x63; - this.challenge = 0x01; - } - - async run(state) { - const query = Buffer.alloc(3); - query.writeUInt8(this.requestByte, 0); - query.writeUInt16LE(this.challenge, 1); - const reader = await this.udpSend(query,(buffer) => { - const reader = this.reader(buffer); - const responseByte = reader.uint(1); - if (responseByte !== this.responseByte) { - this.logger.debug('Unexpected response byte'); - return; - } - const challenge = reader.uint(2); - if (challenge !== this.challenge) { - this.logger.debug('Unexpected challenge'); - return; - } - const requestByte = reader.uint(1); - if (requestByte !== this.requestByte) { - this.logger.debug('Unexpected request byte'); - return; - } - return reader; - }); - - state.raw.gametype = this.readString(reader); - const isStarsiege2009 = state.raw.gametype === 'Starsiege'; - state.raw.version = this.readString(reader); - state.name = this.readString(reader); - - if (isStarsiege2009) { - state.password = !!reader.uint(1); - state.raw.dedicated = !!reader.uint(1); - state.raw.dropInProgress = !!reader.uint(1); - state.raw.gameInProgress = !!reader.uint(1); - state.raw.playerCount = reader.uint(4); - state.maxplayers = reader.uint(4); - state.raw.teamPlay = reader.uint(1); - state.map = this.readString(reader); - state.raw.cpuSpeed = reader.uint(2); - state.raw.factoryVeh = reader.uint(1); - state.raw.allowTecmix = reader.uint(1); - state.raw.spawnLimit = reader.uint(4); - state.raw.fragLimit = reader.uint(4); - state.raw.timeLimit = reader.uint(4); - state.raw.techLimit = reader.uint(4); - state.raw.combatLimit = reader.uint(4); - state.raw.massLimit = reader.uint(4); - state.raw.playersSent = reader.uint(4); - const teams = {1:'yellow', 2:'blue', 4:'red', 8:'purple'}; - while (!reader.done()) { - const player = {}; - player.name = this.readString(reader); - const teamId = reader.uint(1); - const team = teams[teamId]; - if (team) player.team = teams[teamId]; - } - return; - } - - state.raw.dedicated = !!reader.uint(1); - state.password = !!reader.uint(1); - state.raw.playerCount = reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.cpuSpeed = reader.uint(2); - state.raw.mod = this.readString(reader); - state.raw.type = this.readString(reader); - state.map = this.readString(reader); - state.raw.motd = this.readString(reader); - state.raw.teamCount = reader.uint(1); - - const teamFields = this.readFieldList(reader); - const playerFields = this.readFieldList(reader); - - state.raw.teams = []; - for(let i = 0; i < state.raw.teamCount; i++) { - const teamName = this.readString(reader); - const teamValues = this.readValues(reader); - - const teamInfo = {}; - for (let i = 0; i < teamValues.length && i < teamFields.length; i++) { - let key = teamFields[i]; - let value = teamValues[i]; - if (key === 'ultra_base') key = 'name'; - if (value === '%t') value = teamName; - if (['score','players'].includes(key)) value = parseInt(value); - teamInfo[key] = value; - } - state.raw.teams.push(teamInfo); - } - - for(let i = 0; i < state.raw.playerCount; i++) { - const ping = reader.uint(1) * 4; - const packetLoss = reader.uint(1); - const teamNum = reader.uint(1); - const name = this.readString(reader); - const playerValues = this.readValues(reader); - - const playerInfo = {}; - for (let i = 0; i < playerValues.length && i < playerFields.length; i++) { - let key = playerFields[i]; - let value = playerValues[i]; - if (value === '%p') value = ping; - if (value === '%l') value = packetLoss; - if (value === '%t') value = teamNum; - if (value === '%n') value = name; - if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value); - if (key === 'team') { - const teamId = parseInt(value); - if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) { - value = state.raw.teams[teamId].name; - } else { - continue; - } - } - playerInfo[key] = value; - } - state.players.push(playerInfo); - } - } - readFieldList(reader) { - const str = this.readString(reader); - if (!str) return []; - return ('?'+str) - .split('\t') - .map((a) => a.substring(1).trim().toLowerCase()) - .map((a) => a === 'team name' ? 'name' : a) - .map((a) => a === 'player name' ? 'name' : a); - } - readValues(reader) { - const str = this.readString(reader); - if (!str) return []; - return str - .split('\t') - .map((a) => a.trim()); - } - readString(reader) { - return reader.pascalString(1); - } -} +import Core from './core.js' + +export default class tribes1 extends Core { + constructor () { + super() + this.encoding = 'latin1' + this.requestByte = 0x62 + this.responseByte = 0x63 + this.challenge = 0x01 + } + + async run (state) { + const query = Buffer.alloc(3) + query.writeUInt8(this.requestByte, 0) + query.writeUInt16LE(this.challenge, 1) + const reader = await this.udpSend(query, (buffer) => { + const reader = this.reader(buffer) + const responseByte = reader.uint(1) + if (responseByte !== this.responseByte) { + this.logger.debug('Unexpected response byte') + return + } + const challenge = reader.uint(2) + if (challenge !== this.challenge) { + this.logger.debug('Unexpected challenge') + return + } + const requestByte = reader.uint(1) + if (requestByte !== this.requestByte) { + this.logger.debug('Unexpected request byte') + return + } + return reader + }) + + state.raw.gametype = this.readString(reader) + const isStarsiege2009 = state.raw.gametype === 'Starsiege' + state.raw.version = this.readString(reader) + state.name = this.readString(reader) + + if (isStarsiege2009) { + state.password = !!reader.uint(1) + state.raw.dedicated = !!reader.uint(1) + state.raw.dropInProgress = !!reader.uint(1) + state.raw.gameInProgress = !!reader.uint(1) + state.raw.playerCount = reader.uint(4) + state.maxplayers = reader.uint(4) + state.raw.teamPlay = reader.uint(1) + state.map = this.readString(reader) + state.raw.cpuSpeed = reader.uint(2) + state.raw.factoryVeh = reader.uint(1) + state.raw.allowTecmix = reader.uint(1) + state.raw.spawnLimit = reader.uint(4) + state.raw.fragLimit = reader.uint(4) + state.raw.timeLimit = reader.uint(4) + state.raw.techLimit = reader.uint(4) + state.raw.combatLimit = reader.uint(4) + state.raw.massLimit = reader.uint(4) + state.raw.playersSent = reader.uint(4) + const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' } + while (!reader.done()) { + const player = {} + player.name = this.readString(reader) + const teamId = reader.uint(1) + const team = teams[teamId] + if (team) player.team = teams[teamId] + } + return + } + + state.raw.dedicated = !!reader.uint(1) + state.password = !!reader.uint(1) + state.raw.playerCount = reader.uint(1) + state.maxplayers = reader.uint(1) + state.raw.cpuSpeed = reader.uint(2) + state.raw.mod = this.readString(reader) + state.raw.type = this.readString(reader) + state.map = this.readString(reader) + state.raw.motd = this.readString(reader) + state.raw.teamCount = reader.uint(1) + + const teamFields = this.readFieldList(reader) + const playerFields = this.readFieldList(reader) + + state.raw.teams = [] + for (let i = 0; i < state.raw.teamCount; i++) { + const teamName = this.readString(reader) + const teamValues = this.readValues(reader) + + const teamInfo = {} + for (let i = 0; i < teamValues.length && i < teamFields.length; i++) { + let key = teamFields[i] + let value = teamValues[i] + if (key === 'ultra_base') key = 'name' + if (value === '%t') value = teamName + if (['score', 'players'].includes(key)) value = parseInt(value) + teamInfo[key] = value + } + state.raw.teams.push(teamInfo) + } + + for (let i = 0; i < state.raw.playerCount; i++) { + const ping = reader.uint(1) * 4 + const packetLoss = reader.uint(1) + const teamNum = reader.uint(1) + const name = this.readString(reader) + const playerValues = this.readValues(reader) + + const playerInfo = {} + for (let i = 0; i < playerValues.length && i < playerFields.length; i++) { + const key = playerFields[i] + let value = playerValues[i] + if (value === '%p') value = ping + if (value === '%l') value = packetLoss + if (value === '%t') value = teamNum + if (value === '%n') value = name + if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value) + if (key === 'team') { + const teamId = parseInt(value) + if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) { + value = state.raw.teams[teamId].name + } else { + continue + } + } + playerInfo[key] = value + } + state.players.push(playerInfo) + } + } + + readFieldList (reader) { + const str = this.readString(reader) + if (!str) return [] + return ('?' + str) + .split('\t') + .map((a) => a.substring(1).trim().toLowerCase()) + .map((a) => a === 'team name' ? 'name' : a) + .map((a) => a === 'player name' ? 'name' : a) + } + + readValues (reader) { + const str = this.readString(reader) + if (!str) return [] + return str + .split('\t') + .map((a) => a.trim()) + } + + readString (reader) { + return reader.pascalString(1) + } +} diff --git a/protocols/tribes1master.js b/protocols/tribes1master.js index ec7dd68..f7b2bbd 100644 --- a/protocols/tribes1master.js +++ b/protocols/tribes1master.js @@ -1,79 +1,80 @@ -import Core from './core.js'; - -/** Unsupported -- use at your own risk!! */ - -export default class tribes1master extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - } - - async run(state) { - const queryBuffer = Buffer.from([ - 0x10, // standard header - 0x03, // dump servers - 0xff, // ask for all packets - 0x00, // junk - 0x01, 0x02, // challenge - ]); - - let parts = new Map(); - let total = 0; - const full = await this.udpSend(queryBuffer,(buffer) => { - const reader = this.reader(buffer); - const header = reader.uint(2); - if (header !== 0x0610) { - this.logger.debug('Header response does not match: ' + header.toString(16)); - return; - } - const num = reader.uint(1); - const t = reader.uint(1); - if (t <= 0 || (total > 0 && t !== total)) { - throw new Error('Conflicting packet total: ' + t); - } - total = t; - - if (num < 1 || num > total) { - this.logger.debug('Invalid packet number: ' + num + ' ' + total); - return; - } - if (parts.has(num)) { - this.logger.debug('Duplicate part: ' + num); - return; - } - - reader.skip(2); // challenge (0x0201) - reader.skip(2); // always 0x6600 - parts.set(num, reader.rest()); - - if (parts.size === total) { - const ordered = []; - for (let i = 1; i <= total; i++) ordered.push(parts.get(i)); - return Buffer.concat(ordered); - } - }); - - const fullReader = this.reader(full); - state.raw.name = this.readString(fullReader); - state.raw.motd = this.readString(fullReader); - - state.raw.servers = []; - while (!fullReader.done()) { - fullReader.skip(1); // junk ? - const count = fullReader.uint(1); - for (let i = 0; i < count; i++) { - const six = fullReader.uint(1); - if (six !== 6) { - throw new Error('Expecting 6'); - } - const ip = fullReader.uint(4); - const port = fullReader.uint(2); - const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24); - state.raw.servers.push(ipStr+":"+port); - } - } - } - readString(reader) { - return reader.pascalString(1); - } -} +import Core from './core.js' + +/** Unsupported -- use at your own risk!! */ + +export default class tribes1master extends Core { + constructor () { + super() + this.encoding = 'latin1' + } + + async run (state) { + const queryBuffer = Buffer.from([ + 0x10, // standard header + 0x03, // dump servers + 0xff, // ask for all packets + 0x00, // junk + 0x01, 0x02 // challenge + ]) + + const parts = new Map() + let total = 0 + const full = await this.udpSend(queryBuffer, (buffer) => { + const reader = this.reader(buffer) + const header = reader.uint(2) + if (header !== 0x0610) { + this.logger.debug('Header response does not match: ' + header.toString(16)) + return + } + const num = reader.uint(1) + const t = reader.uint(1) + if (t <= 0 || (total > 0 && t !== total)) { + throw new Error('Conflicting packet total: ' + t) + } + total = t + + if (num < 1 || num > total) { + this.logger.debug('Invalid packet number: ' + num + ' ' + total) + return + } + if (parts.has(num)) { + this.logger.debug('Duplicate part: ' + num) + return + } + + reader.skip(2) // challenge (0x0201) + reader.skip(2) // always 0x6600 + parts.set(num, reader.rest()) + + if (parts.size === total) { + const ordered = [] + for (let i = 1; i <= total; i++) ordered.push(parts.get(i)) + return Buffer.concat(ordered) + } + }) + + const fullReader = this.reader(full) + state.raw.name = this.readString(fullReader) + state.raw.motd = this.readString(fullReader) + + state.raw.servers = [] + while (!fullReader.done()) { + fullReader.skip(1) // junk ? + const count = fullReader.uint(1) + for (let i = 0; i < count; i++) { + const six = fullReader.uint(1) + if (six !== 6) { + throw new Error('Expecting 6') + } + const ip = fullReader.uint(4) + const port = fullReader.uint(2) + const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24) + state.raw.servers.push(ipStr + ':' + port) + } + } + } + + readString (reader) { + return reader.pascalString(1) + } +} diff --git a/protocols/unreal2.js b/protocols/unreal2.js index d85f586..15a6ed9 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -1,150 +1,150 @@ -import Core from './core.js'; - -export default class unreal2 extends Core { - constructor() { - super(); - this.encoding = 'latin1'; - } - async run(state) { - let extraInfoReader; - { - const b = await this.sendPacket(0, true); - const reader = this.reader(b); - state.raw.serverid = reader.uint(4); - state.raw.ip = this.readUnrealString(reader); - state.gamePort = reader.uint(4); - state.raw.queryport = reader.uint(4); - state.name = this.readUnrealString(reader, true); - state.map = this.readUnrealString(reader, true); - state.raw.gametype = this.readUnrealString(reader, true); - state.raw.numplayers = reader.uint(4); - state.maxplayers = reader.uint(4); - this.logger.debug(log => { - log("UNREAL2 EXTRA INFO", reader.buffer.slice(reader.i)); - }); - extraInfoReader = reader; - } - - { - const b = await this.sendPacket(1,true); - const reader = this.reader(b); - state.raw.mutators = []; - state.raw.rules = {}; - while(!reader.done()) { - const key = this.readUnrealString(reader,true); - const value = this.readUnrealString(reader,true); - this.logger.debug(key+'='+value); - if(key === 'Mutator' || key === 'mutator') { - state.raw.mutators.push(value); - } else if (key || value) { - if (state.raw.rules.hasOwnProperty(key)) { - state.raw.rules[key] += ',' + value; - } else { - state.raw.rules[key] = value; - } - } - } - if('GamePassword' in state.raw.rules) - state.password = state.raw.rules.GamePassword !== 'True'; - } - - if (state.raw.mutators.includes('KillingFloorMut') - || state.raw.rules['Num trader weapons'] - || state.raw.rules['Server Version'] === '1065' - ) { - // Killing Floor - state.raw.wavecurrent = extraInfoReader.uint(4); - state.raw.wavetotal = extraInfoReader.uint(4); - state.raw.ping = extraInfoReader.uint(4); - state.raw.flags = extraInfoReader.uint(4); - state.raw.skillLevel = this.readUnrealString(extraInfoReader, true); - } else { - state.raw.ping = extraInfoReader.uint(4); - // These fields were added in later revisions of unreal engine - if (extraInfoReader.remaining() >= 8) { - state.raw.flags = extraInfoReader.uint(4); - state.raw.skill = this.readUnrealString(extraInfoReader, true); - } - } - - { - const b = await this.sendPacket(2,false); - const reader = this.reader(b); - - state.raw.scoreboard = {}; - while(!reader.done()) { - const player = {}; - player.id = reader.uint(4); - player.name = this.readUnrealString(reader,true); - player.ping = reader.uint(4); - player.score = reader.int(4); - player.statsId = reader.uint(4); - this.logger.debug(player); - - if (!player.id) { - state.raw.scoreboard[player.name] = player.score; - } else if (!player.ping) { - state.bots.push(player); - } else { - state.players.push(player); - } - } - } - } - - readUnrealString(reader, stripColor) { - let length = reader.uint(1), ucs2 = false; - if(length >= 0x80) { - // This is flagged as a UCS-2 String - length = (length&0x7f)*2; - ucs2 = true; - - // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, - // not included in the length. Skip it if present (hopefully this never happens legitimately) - const peek = reader.uint(1); - if (peek !== 1) reader.skip(-1); - - this.logger.debug(log => { - log("UCS2 STRING"); - log("UCS2 Length: " + length); - log(reader.buffer.slice(reader.i,reader.i+length)); - }); - } - - let out = ''; - if (ucs2) { - out = reader.string({encoding:'ucs2',length:length}); - this.logger.debug("UCS2 String decoded: " + out); - } else if (length > 0) { - out = reader.string(); - } - - // Sometimes the string has a null at the end (included with the length) - // Strip it if present - if(out.charCodeAt(out.length-1) === 0) { - out = out.substring(0, out.length - 1); - } - - if(stripColor) { - out = out.replace(/\x1b...|[\x00-\x1a]/gus,''); - } - - return out; - } - - async sendPacket(type,required) { - const outbuffer = Buffer.from([0x79,0,0,0,type]); - - const packets = []; - return await this.udpSend(outbuffer,(buffer) => { - const reader = this.reader(buffer); - const header = reader.uint(4); - const iType = reader.uint(1); - if(iType !== type) return; - packets.push(reader.rest()); - }, () => { - if(!packets.length && required) return; - return Buffer.concat(packets); - }); - } -} +import Core from './core.js' + +export default class unreal2 extends Core { + constructor () { + super() + this.encoding = 'latin1' + } + + async run (state) { + let extraInfoReader + { + const b = await this.sendPacket(0, true) + const reader = this.reader(b) + state.raw.serverid = reader.uint(4) + state.raw.ip = this.readUnrealString(reader) + state.gamePort = reader.uint(4) + state.raw.queryport = reader.uint(4) + state.name = this.readUnrealString(reader, true) + state.map = this.readUnrealString(reader, true) + state.raw.gametype = this.readUnrealString(reader, true) + state.raw.numplayers = reader.uint(4) + state.maxplayers = reader.uint(4) + this.logger.debug(log => { + log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i)) + }) + extraInfoReader = reader + } + + { + const b = await this.sendPacket(1, true) + const reader = this.reader(b) + state.raw.mutators = [] + state.raw.rules = {} + while (!reader.done()) { + const key = this.readUnrealString(reader, true) + const value = this.readUnrealString(reader, true) + this.logger.debug(key + '=' + value) + if (key === 'Mutator' || key === 'mutator') { + state.raw.mutators.push(value) + } else if (key || value) { + if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) { + state.raw.rules[key] += ',' + value + } else { + state.raw.rules[key] = value + } + } + } + if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' } + } + + if (state.raw.mutators.includes('KillingFloorMut') || + state.raw.rules['Num trader weapons'] || + state.raw.rules['Server Version'] === '1065' + ) { + // Killing Floor + state.raw.wavecurrent = extraInfoReader.uint(4) + state.raw.wavetotal = extraInfoReader.uint(4) + state.raw.ping = extraInfoReader.uint(4) + state.raw.flags = extraInfoReader.uint(4) + state.raw.skillLevel = this.readUnrealString(extraInfoReader, true) + } else { + state.raw.ping = extraInfoReader.uint(4) + // These fields were added in later revisions of unreal engine + if (extraInfoReader.remaining() >= 8) { + state.raw.flags = extraInfoReader.uint(4) + state.raw.skill = this.readUnrealString(extraInfoReader, true) + } + } + + { + const b = await this.sendPacket(2, false) + const reader = this.reader(b) + + state.raw.scoreboard = {} + while (!reader.done()) { + const player = {} + player.id = reader.uint(4) + player.name = this.readUnrealString(reader, true) + player.ping = reader.uint(4) + player.score = reader.int(4) + player.statsId = reader.uint(4) + this.logger.debug(player) + + if (!player.id) { + state.raw.scoreboard[player.name] = player.score + } else if (!player.ping) { + state.bots.push(player) + } else { + state.players.push(player) + } + } + } + } + + readUnrealString (reader, stripColor) { + let length = reader.uint(1); let ucs2 = false + if (length >= 0x80) { + // This is flagged as a UCS-2 String + length = (length & 0x7f) * 2 + ucs2 = true + + // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, + // not included in the length. Skip it if present (hopefully this never happens legitimately) + const peek = reader.uint(1) + if (peek !== 1) reader.skip(-1) + + this.logger.debug(log => { + log('UCS2 STRING') + log('UCS2 Length: ' + length) + log(reader.buffer.slice(reader.i, reader.i + length)) + }) + } + + let out = '' + if (ucs2) { + out = reader.string({ encoding: 'ucs2', length }) + this.logger.debug('UCS2 String decoded: ' + out) + } else if (length > 0) { + out = reader.string() + } + + // Sometimes the string has a null at the end (included with the length) + // Strip it if present + if (out.charCodeAt(out.length - 1) === 0) { + out = out.substring(0, out.length - 1) + } + + if (stripColor) { + out = out.replace(/\x1b...|[\x00-\x1a]/gus, '') + } + + return out + } + + async sendPacket (type, required) { + const outbuffer = Buffer.from([0x79, 0, 0, 0, type]) + + const packets = [] + return await this.udpSend(outbuffer, (buffer) => { + const reader = this.reader(buffer) + reader.uint(4) // header + const iType = reader.uint(1) + if (iType !== type) return + packets.push(reader.rest()) + }, () => { + if (!packets.length && required) return + return Buffer.concat(packets) + }) + } +} diff --git a/protocols/ut3.js b/protocols/ut3.js index aed697e..bbbc7fc 100644 --- a/protocols/ut3.js +++ b/protocols/ut3.js @@ -1,45 +1,45 @@ -import gamespy3 from './gamespy3.js'; - -export default class ut3 extends gamespy3 { - async run(state) { - await super.run(state); - - this.translate(state.raw,{ - 'mapname': false, - 'p1073741825': 'map', - 'p1073741826': 'gametype', - 'p1073741827': 'servername', - 'p1073741828': 'custom_mutators', - 'gamemode': 'joininprogress', - 's32779': 'gamemode', - 's0': 'bot_skill', - 's6': 'pure_server', - 's7': 'password', - 's8': 'vs_bots', - 's10': 'force_respawn', - 'p268435704': 'frag_limit', - 'p268435705': 'time_limit', - 'p268435703': 'numbots', - 'p268435717': 'stock_mutators', - 'p1073741829': 'stock_mutators', - 's1': false, - 's9': false, - 's11': false, - 's12': false, - 's13': false, - 's14': false, - 'p268435706': false, - 'p268435968': false, - 'p268435969': false - }); - - const split = (a) => { - let s = a.split('\x1c'); - s = s.filter((e) => { return e }); - return s; - }; - if('custom_mutators' in state.raw) state.raw['custom_mutators'] = split(state.raw['custom_mutators']); - if('stock_mutators' in state.raw) state.raw['stock_mutators'] = split(state.raw['stock_mutators']); - if('map' in state.raw) state.map = state.raw.map; - } -} +import gamespy3 from './gamespy3.js' + +export default class ut3 extends gamespy3 { + async run (state) { + await super.run(state) + + this.translate(state.raw, { + mapname: false, + p1073741825: 'map', + p1073741826: 'gametype', + p1073741827: 'servername', + p1073741828: 'custom_mutators', + gamemode: 'joininprogress', + s32779: 'gamemode', + s0: 'bot_skill', + s6: 'pure_server', + s7: 'password', + s8: 'vs_bots', + s10: 'force_respawn', + p268435704: 'frag_limit', + p268435705: 'time_limit', + p268435703: 'numbots', + p268435717: 'stock_mutators', + p1073741829: 'stock_mutators', + s1: false, + s9: false, + s11: false, + s12: false, + s13: false, + s14: false, + p268435706: false, + p268435968: false, + p268435969: false + }) + + const split = (a) => { + let s = a.split('\x1c') + s = s.filter((e) => { return e }) + return s + } + if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators) + if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators) + if ('map' in state.raw) state.map = state.raw.map + } +} diff --git a/protocols/valve.js b/protocols/valve.js index 357c6e0..1c09aaa 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -1,606 +1,610 @@ -import Bzip2 from 'seek-bzip'; -import Core from './core.js'; - -const AppId = { - Squad: 393380, - Bat1944: 489940, - Ship: 2400, - DayZ: 221100, - Rust: 252490, - CSGO: 730, - CS_Source: 240, - EternalSilence: 17550, - Insurgency_MIC: 17700, - Source_SDK_Base_2006: 215 -}; - -export default class valve extends Core { - constructor() { - super(); - - // legacy goldsrc info response -- basically not used by ANYTHING now, - // as most (all?) goldsrc servers respond with the source info reponse - // delete in a few years if nothing ends up using it anymore - this.goldsrcInfo = false; - - // unfortunately, the split format from goldsrc is still around, but we - // can detect that during the query - this.goldsrcSplits = false; - - // some mods require a challenge, but don't provide them in the new format - // at all, use the old dedicated challenge query if needed - this.legacyChallenge = false; - - // 2006 engines don't pass packet switching size in split packet header - // while all others do, this need is detected automatically - this._skipSizeInSplitHeader = false; - - this._challenge = ''; - } - - async run(state) { - if (!this.options.port) this.options.port = 27015; - await this.queryInfo(state); - await this.queryChallenge(); - await this.queryPlayers(state); - await this.queryRules(state); - await this.cleanup(state); - } - - async queryInfo(/** Results */ state) { - this.logger.debug("Requesting info ..."); - const b = await this.sendPacket( - this.goldsrcInfo ? undefined : 0x54, - this.goldsrcInfo ? 'details' : 'Source Engine Query\0', - this.goldsrcInfo ? 0x6D : 0x49, - false - ); - - const reader = this.reader(b); - - if(this.goldsrcInfo) state.raw.address = reader.string(); - else state.raw.protocol = reader.uint(1); - - state.name = reader.string(); - state.map = reader.string(); - state.raw.folder = reader.string(); - state.raw.game = reader.string(); - if(!this.goldsrcInfo) state.raw.appId = reader.uint(2); - state.raw.numplayers = reader.uint(1); - state.maxplayers = reader.uint(1); - - if(this.goldsrcInfo) state.raw.protocol = reader.uint(1); - else state.raw.numbots = reader.uint(1); - - state.raw.listentype = String.fromCharCode(reader.uint(1)); - state.raw.environment = String.fromCharCode(reader.uint(1)); - - state.password = !!reader.uint(1); - if(this.goldsrcInfo) { - state.raw.ismod = reader.uint(1); - if(state.raw.ismod) { - state.raw.modlink = reader.string(); - state.raw.moddownload = reader.string(); - reader.skip(1); - state.raw.modversion = reader.uint(4); - state.raw.modsize = reader.uint(4); - state.raw.modtype = reader.uint(1); - state.raw.moddll = reader.uint(1); - } - } else { - state.raw.secure = reader.uint(1); - if(state.raw.appId === AppId.Ship) { - state.raw.shipmode = reader.uint(1); - state.raw.shipwitnesses = reader.uint(1); - state.raw.shipduration = reader.uint(1); - } - state.raw.version = reader.string(); - const extraFlag = reader.uint(1); - if(extraFlag & 0x80) state.gamePort = reader.uint(2); - if(extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString(); - if(extraFlag & 0x40) { - state.raw.sourcetvport = reader.uint(2); - state.raw.sourcetvname = reader.string(); - } - if(extraFlag & 0x20) state.raw.tags = reader.string().split(','); - if(extraFlag & 0x01) { - const gameId = reader.uint(8); - const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff; - if (betterAppId) { - state.raw.appId = betterAppId; - } - } - } - - const appId = state.raw.appId; - - // from https://developer.valvesoftware.com/wiki/Server_queries - if( - state.raw.protocol === 7 && ( - state.raw.appId === AppId.Source_SDK_Base_2006 - || state.raw.appId === AppId.EternalSilence - || state.raw.appId === AppId.Insurgency_MIC - || state.raw.appId === AppId.CS_Source - ) - ) { - this._skipSizeInSplitHeader = true; - } - this.logger.debug("INFO: ", state.raw); - if(state.raw.protocol === 48) { - this.logger.debug("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); - this.goldsrcSplits = true; - } - - // DayZ embeds some of the server information inside the tags attribute - if (appId === AppId.DayZ) { - if (state.raw.tags) { - state.raw.dlcEnabled = false - state.raw.firstPerson = false - for (const tag of state.raw.tags) { - if (tag.startsWith('lqs')) { - const value = parseInt(tag.replace('lqs', '')); - if (!isNaN(value)) { - state.raw.queue = value; - } - } - if (tag.includes('no3rd')) { - state.raw.firstPerson = true; - } - if (tag.includes('isDLC')) { - state.raw.dlcEnabled = true; - } - if (tag.includes(':')) { - state.raw.time = tag; - } - if (tag.startsWith('etm')) { - const value = parseInt(tag.replace('etm', '')); - if (!isNaN(value)) { - state.raw.dayAcceleration = value; - } - } - if (tag.startsWith('entm')) { - const value = parseInt(tag.replace('entm', '')); - if (!isNaN(value)) { - state.raw.nightAcceleration = value; - } - } - } - } - } - - if (appId === AppId.Rust) { - if (state.raw.tags) { - for (const tag of state.raw.tags) { - if (tag.startsWith('mp')) { - const value = parseInt(tag.replace('mp', '')); - if (!isNaN(value)) { - state.maxplayers = value; - } - } - } - } - } - } - - async queryChallenge() { - if(this.legacyChallenge) { - // sendPacket will catch the response packet and - // save the challenge for us - this.logger.debug("Requesting legacy challenge key ..."); - await this.sendPacket( - 0x57, - null, - 0x41, - false - ); - } - } - - async queryPlayers(/** Results */ state) { - state.raw.players = []; - - this.logger.debug("Requesting player list ..."); - const b = await this.sendPacket( - this.goldsrcInfo ? undefined : 0x55, - this.goldsrcInfo ? 'players' : null, - 0x44, - true - ); - - if (b === null) { - // Player query timed out - // CSGO doesn't respond to player query if host_players_show is not 2 - // Conan Exiles never responds to player query - // Just skip it, and we'll fill with dummy objects in cleanup() - return; - } - - const reader = this.reader(b); - const num = reader.uint(1); - for(let i = 0; i < num; i++) { - reader.skip(1); - const name = reader.string(); - const score = reader.int(4); - const time = reader.float(); - - this.logger.debug("Found player: "+name+" "+score+" "+time); - - // connecting players don't count as players. - if(!name) continue; - - // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2 - if (state.raw.appId === AppId.CSGO && name === 'Max Players') continue; - - state.raw.players.push({ - name:name, score:score, time:time - }); - } - } - - async queryRules(/** Results */ state) { - const appId = state.raw.appId; - if (appId === AppId.Squad - || appId === AppId.Bat1944 - || this.options.requestRules) { - // let's get 'em - } else { - return; - } - - const rules = {}; - state.raw.rules = rules; - const dayZPayload = []; - - this.logger.debug("Requesting rules ..."); - - if (this.goldsrcInfo) { - const b = await this.udpSend('\xff\xff\xff\xffrules', b=>b, ()=>null); - if (b === null) return; // timed out - the server probably has rules disabled - const reader = this.reader(b); - while (!reader.done()) { - const key = reader.string(); - const value = reader.string(); - rules[key] = value; - } - } else { - const b = await this.sendPacket(0x56,null,0x45,true); - if (b === null) return; // timed out - the server probably has rules disabled - - let dayZPayloadEnded = false; - - const reader = this.reader(b); - const num = reader.uint(2); - for (let i = 0; i < num; i++) { - if (appId === AppId.DayZ && !dayZPayloadEnded) { - const one = reader.uint(1); - const two = reader.uint(1); - const three = reader.uint(1); - if (one !== 0 && two !== 0 && three === 0) { - while (true) { - const byte = reader.uint(1); - if (byte === 0) break; - dayZPayload.push(byte); - } - continue; - } else { - reader.skip(-3); - dayZPayloadEnded = true; - } - } - - const key = reader.string(); - const value = reader.string(); - rules[key] = value; - } - } - - // Battalion 1944 puts its info into rules fields for some reason - if (appId === AppId.Bat1944) { - if ('bat_name_s' in rules) { - state.name = rules.bat_name_s; - delete rules.bat_name_s; - if ('bat_player_count_s' in rules) { - state.raw.numplayers = parseInt(rules.bat_player_count_s); - delete rules.bat_player_count_s; - } - if ('bat_max_players_i' in rules) { - state.maxplayers = parseInt(rules.bat_max_players_i); - delete rules.bat_max_players_i; - } - if ('bat_has_password_s' in rules) { - state.password = rules.bat_has_password_s === 'Y'; - delete rules.bat_has_password_s; - } - // apparently map is already right, and this var is often wrong - delete rules.bat_map_s; - } - } - - // Squad keeps its password in a separate field - if (appId === AppId.Squad) { - if (rules.Password_b === "true") { - state.password = true; - } - } - - if (appId === AppId.DayZ) { - state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload)); - } - } - - readDayzMods(/** Buffer */ buffer) { - if (!buffer.length) { - return {}; - } - - this.logger.debug("DAYZ BUFFER"); - this.logger.debug(buffer); - - const reader = this.reader(buffer); - const version = this.readDayzByte(reader); - const overflow = this.readDayzByte(reader); - const dlc1 = this.readDayzByte(reader); - const dlc2 = this.readDayzByte(reader); - this.logger.debug("version " + version); - this.logger.debug("overflow " + overflow); - this.logger.debug("dlc1 " + dlc1); - this.logger.debug("dlc2 " + dlc2); - if (dlc1) { - const unknown = this.readDayzUint(reader, 4); // ? - this.logger.debug("unknown " + unknown); - } - if (dlc2) { - const unknown = this.readDayzUint(reader, 4); // ? - this.logger.debug("unknown " + unknown); - } - const mods = []; - mods.push(...this.readDayzModsSection(reader, true)); - mods.push(...this.readDayzModsSection(reader, false)); - this.logger.debug("dayz buffer rest:", reader.rest()); - return mods; - } - readDayzModsSection(/** Reader */ reader, withHeader) { - const out = []; - const count = this.readDayzByte(reader); - this.logger.debug("dayz mod section withHeader:" + withHeader + " count:" + count); - for(let i = 0; i < count; i++) { - if (reader.done()) break; - const mod = {}; - if (withHeader) { - mod.unknown = this.readDayzUint(reader, 4); // ? - - // For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes? - const offset = reader.offset(); - const flag = this.readDayzByte(reader); - if (flag !== 4) reader.setOffset(offset); - - mod.workshopId = this.readDayzUint(reader, 4); - } - mod.title = this.readDayzString(reader); - this.logger.debug(mod); - out.push(mod); - } - return out; - } - readDayzUint(reader, bytes) { - const out = []; - for (let i = 0; i < bytes; i++) { - out.push(this.readDayzByte(reader)); - } - const buf = Buffer.from(out); - const r2 = this.reader(buf); - return r2.uint(bytes); - } - readDayzByte(reader) { - const byte = reader.uint(1); - if (byte === 1) { - const byte2 = reader.uint(1); - if (byte2 === 1) return 1; - if (byte2 === 2) return 0; - if (byte2 === 3) return 0xff; - return 0; // ? - } - return byte; - } - readDayzString(reader) { - const length = this.readDayzByte(reader); - const out = []; - for (let i = 0; i < length; i++) { - out.push(this.readDayzByte(reader)); - } - return Buffer.from(out).toString('utf8'); - } - - async cleanup(/** Results */ state) { - // Organize players / hidden players into player / bot arrays - const botProbability = (p) => { - if (p.time === -1) return Number.MAX_VALUE; - return p.time; - }; - const sortedPlayers = state.raw.players.sort((a,b) => { - return botProbability(a) - botProbability(b); - }); - delete state.raw.players; - const numBots = state.raw.numbots || 0; - const numPlayers = state.raw.numplayers - numBots; - while(state.bots.length < numBots) { - if (sortedPlayers.length) state.bots.push(sortedPlayers.pop()); - else state.bots.push({}); - } - while(state.players.length < numPlayers || sortedPlayers.length) { - if (sortedPlayers.length) state.players.push(sortedPlayers.pop()); - else state.players.push({}); - } - } - - /** - * Sends a request packet and returns only the response type expected - * @param {number} type - * @param {boolean} sendChallenge - * @param {?string|Buffer} payload - * @param {number} expect - * @param {boolean=} allowTimeout - * @returns Buffer|null - **/ - async sendPacket( - type, - payload, - expect, - allowTimeout - ) { - for (let keyRetry = 0; keyRetry < 3; keyRetry++) { - let receivedNewChallengeKey = false; - const response = await this.sendPacketRaw( - type, payload, - (payload) => { - const reader = this.reader(payload); - const type = reader.uint(1); - this.logger.debug(() => "Received 0x" + type.toString(16) + " expected 0x" + expect.toString(16)); - if (type === 0x41) { - const key = reader.uint(4); - if (this._challenge !== key) { - this.logger.debug('Received new challenge key: 0x' + key.toString(16)); - this._challenge = key; - receivedNewChallengeKey = true; - } - } - if (type === expect) { - return reader.rest(); - } else if (receivedNewChallengeKey) { - return null; - } - }, - () => { - if (allowTimeout) return null; - } - ); - if (!receivedNewChallengeKey) { - return response; - } - } - throw new Error('Received too many challenge key responses'); - } - - /** - * Sends a request packet and assembles partial responses - * @param {number} type - * @param {boolean} sendChallenge - * @param {?string|Buffer} payload - * @param {function(Buffer)} onResponse - * @param {function()} onTimeout - **/ - async sendPacketRaw( - type, - payload, - onResponse, - onTimeout - ) { - const challengeAtBeginning = type === 0x55 || type === 0x56; - const challengeAtEnd = type === 0x54 && !!this._challenge; - - if (typeof payload === 'string') payload = Buffer.from(payload, 'binary'); - - const b = Buffer.alloc(4 - + (type !== undefined ? 1 : 0) - + (challengeAtBeginning ? 4 : 0) - + (challengeAtEnd ? 4 : 0) - + (payload ? payload.length : 0) - ); - let offset = 0; - - let challenge = this._challenge; - if (!challenge) challenge = 0xffffffff; - - b.writeInt32LE(-1, offset); - offset += 4; - - if (type !== undefined) { - b.writeUInt8(type, offset); - offset += 1; - } - - if (challengeAtBeginning) { - if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset); - else b.writeUInt32BE(challenge, offset); - offset += 4; - } - - if (payload) { - payload.copy(b, offset); - offset += payload.length; - } - - if (challengeAtEnd) { - if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset); - else b.writeUInt32BE(challenge, offset); - offset += 4; - } - - const packetStorage = {}; - return await this.udpSend( - b, - (buffer) => { - const reader = this.reader(buffer); - const header = reader.int(4); - if(header === -1) { - // full package - this.logger.debug("Received full packet"); - return onResponse(reader.rest()); - } - if(header === -2) { - // partial package - const uid = reader.uint(4); - if(!(uid in packetStorage)) packetStorage[uid] = {}; - const packets = packetStorage[uid]; - - let bzip = false; - if(!this.goldsrcSplits && uid & 0x80000000) bzip = true; - - let packetNum,payload,numPackets; - if(this.goldsrcSplits) { - packetNum = reader.uint(1); - numPackets = packetNum & 0x0f; - packetNum = (packetNum & 0xf0) >> 4; - payload = reader.rest(); - } else { - numPackets = reader.uint(1); - packetNum = reader.uint(1); - if(!this._skipSizeInSplitHeader) reader.skip(2); - if(packetNum === 0 && bzip) reader.skip(8); - payload = reader.rest(); - } - - packets[packetNum] = payload; - - this.logger.debug(() => "Received partial packet uid: 0x"+uid.toString(16)+" num: "+packetNum); - this.logger.debug(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID"); - - if(Object.keys(packets).length !== numPackets) return; - - // assemble the parts - const list = []; - for(let i = 0; i < numPackets; i++) { - if(!(i in packets)) { - throw new Error('Missing packet #'+i); - } - list.push(packets[i]); - } - - let assembled = Buffer.concat(list); - if(bzip) { - this.logger.debug("BZIP DETECTED - Extracing packet..."); - try { - assembled = Bzip2.decode(assembled); - } catch(e) { - throw new Error('Invalid bzip packet'); - } - } - const assembledReader = this.reader(assembled); - assembledReader.skip(4); // header - return onResponse(assembledReader.rest()); - } - }, - onTimeout - ); - } -} +import Bzip2 from 'seek-bzip' +import Core from './core.js' + +const AppId = { + Squad: 393380, + Bat1944: 489940, + Ship: 2400, + DayZ: 221100, + Rust: 252490, + CSGO: 730, + CS_Source: 240, + EternalSilence: 17550, + Insurgency_MIC: 17700, + Source_SDK_Base_2006: 215 +} + +export default class valve extends Core { + constructor () { + super() + + // legacy goldsrc info response -- basically not used by ANYTHING now, + // as most (all?) goldsrc servers respond with the source info reponse + // delete in a few years if nothing ends up using it anymore + this.goldsrcInfo = false + + // unfortunately, the split format from goldsrc is still around, but we + // can detect that during the query + this.goldsrcSplits = false + + // some mods require a challenge, but don't provide them in the new format + // at all, use the old dedicated challenge query if needed + this.legacyChallenge = false + + // 2006 engines don't pass packet switching size in split packet header + // while all others do, this need is detected automatically + this._skipSizeInSplitHeader = false + + this._challenge = '' + } + + async run (state) { + if (!this.options.port) this.options.port = 27015 + await this.queryInfo(state) + await this.queryChallenge() + await this.queryPlayers(state) + await this.queryRules(state) + await this.cleanup(state) + } + + async queryInfo (/** Results */ state) { + this.logger.debug('Requesting info ...') + const b = await this.sendPacket( + this.goldsrcInfo ? undefined : 0x54, + this.goldsrcInfo ? 'details' : 'Source Engine Query\0', + this.goldsrcInfo ? 0x6D : 0x49, + false + ) + + const reader = this.reader(b) + + if (this.goldsrcInfo) state.raw.address = reader.string() + else state.raw.protocol = reader.uint(1) + + state.name = reader.string() + state.map = reader.string() + state.raw.folder = reader.string() + state.raw.game = reader.string() + if (!this.goldsrcInfo) state.raw.appId = reader.uint(2) + state.raw.numplayers = reader.uint(1) + state.maxplayers = reader.uint(1) + + if (this.goldsrcInfo) state.raw.protocol = reader.uint(1) + else state.raw.numbots = reader.uint(1) + + state.raw.listentype = String.fromCharCode(reader.uint(1)) + state.raw.environment = String.fromCharCode(reader.uint(1)) + + state.password = !!reader.uint(1) + if (this.goldsrcInfo) { + state.raw.ismod = reader.uint(1) + if (state.raw.ismod) { + state.raw.modlink = reader.string() + state.raw.moddownload = reader.string() + reader.skip(1) + state.raw.modversion = reader.uint(4) + state.raw.modsize = reader.uint(4) + state.raw.modtype = reader.uint(1) + state.raw.moddll = reader.uint(1) + } + } else { + state.raw.secure = reader.uint(1) + if (state.raw.appId === AppId.Ship) { + state.raw.shipmode = reader.uint(1) + state.raw.shipwitnesses = reader.uint(1) + state.raw.shipduration = reader.uint(1) + } + state.raw.version = reader.string() + const extraFlag = reader.uint(1) + if (extraFlag & 0x80) state.gamePort = reader.uint(2) + if (extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString() + if (extraFlag & 0x40) { + state.raw.sourcetvport = reader.uint(2) + state.raw.sourcetvname = reader.string() + } + if (extraFlag & 0x20) state.raw.tags = reader.string().split(',') + if (extraFlag & 0x01) { + const gameId = reader.uint(8) + const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff + if (betterAppId) { + state.raw.appId = betterAppId + } + } + } + + const appId = state.raw.appId + + // from https://developer.valvesoftware.com/wiki/Server_queries + if ( + state.raw.protocol === 7 && ( + state.raw.appId === AppId.Source_SDK_Base_2006 || + state.raw.appId === AppId.EternalSilence || + state.raw.appId === AppId.Insurgency_MIC || + state.raw.appId === AppId.CS_Source + ) + ) { + this._skipSizeInSplitHeader = true + } + this.logger.debug('INFO: ', state.raw) + if (state.raw.protocol === 48) { + this.logger.debug('GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT') + this.goldsrcSplits = true + } + + // DayZ embeds some of the server information inside the tags attribute + if (appId === AppId.DayZ) { + if (state.raw.tags) { + state.raw.dlcEnabled = false + state.raw.firstPerson = false + for (const tag of state.raw.tags) { + if (tag.startsWith('lqs')) { + const value = parseInt(tag.replace('lqs', '')) + if (!isNaN(value)) { + state.raw.queue = value + } + } + if (tag.includes('no3rd')) { + state.raw.firstPerson = true + } + if (tag.includes('isDLC')) { + state.raw.dlcEnabled = true + } + if (tag.includes(':')) { + state.raw.time = tag + } + if (tag.startsWith('etm')) { + const value = parseInt(tag.replace('etm', '')) + if (!isNaN(value)) { + state.raw.dayAcceleration = value + } + } + if (tag.startsWith('entm')) { + const value = parseInt(tag.replace('entm', '')) + if (!isNaN(value)) { + state.raw.nightAcceleration = value + } + } + } + } + } + + if (appId === AppId.Rust) { + if (state.raw.tags) { + for (const tag of state.raw.tags) { + if (tag.startsWith('mp')) { + const value = parseInt(tag.replace('mp', '')) + if (!isNaN(value)) { + state.maxplayers = value + } + } + } + } + } + } + + async queryChallenge () { + if (this.legacyChallenge) { + // sendPacket will catch the response packet and + // save the challenge for us + this.logger.debug('Requesting legacy challenge key ...') + await this.sendPacket( + 0x57, + null, + 0x41, + false + ) + } + } + + async queryPlayers (/** Results */ state) { + state.raw.players = [] + + this.logger.debug('Requesting player list ...') + const b = await this.sendPacket( + this.goldsrcInfo ? undefined : 0x55, + this.goldsrcInfo ? 'players' : null, + 0x44, + true + ) + + if (b === null) { + // Player query timed out + // CSGO doesn't respond to player query if host_players_show is not 2 + // Conan Exiles never responds to player query + // Just skip it, and we'll fill with dummy objects in cleanup() + return + } + + const reader = this.reader(b) + const num = reader.uint(1) + for (let i = 0; i < num; i++) { + reader.skip(1) + const name = reader.string() + const score = reader.int(4) + const time = reader.float() + + this.logger.debug('Found player: ' + name + ' ' + score + ' ' + time) + + // connecting players don't count as players. + if (!name) continue + + // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2 + if (state.raw.appId === AppId.CSGO && name === 'Max Players') continue + + state.raw.players.push({ + name, score, time + }) + } + } + + async queryRules (/** Results */ state) { + const appId = state.raw.appId + if (appId === AppId.Squad || + appId === AppId.Bat1944 || + this.options.requestRules) { + // let's get 'em + } else { + return + } + + const rules = {} + state.raw.rules = rules + const dayZPayload = [] + + this.logger.debug('Requesting rules ...') + + if (this.goldsrcInfo) { + const b = await this.udpSend('\xff\xff\xff\xffrules', b => b, () => null) + if (b === null) return // timed out - the server probably has rules disabled + const reader = this.reader(b) + while (!reader.done()) { + const key = reader.string() + const value = reader.string() + rules[key] = value + } + } else { + const b = await this.sendPacket(0x56, null, 0x45, true) + if (b === null) return // timed out - the server probably has rules disabled + + let dayZPayloadEnded = false + + const reader = this.reader(b) + const num = reader.uint(2) + for (let i = 0; i < num; i++) { + if (appId === AppId.DayZ && !dayZPayloadEnded) { + const one = reader.uint(1) + const two = reader.uint(1) + const three = reader.uint(1) + if (one !== 0 && two !== 0 && three === 0) { + while (true) { + const byte = reader.uint(1) + if (byte === 0) break + dayZPayload.push(byte) + } + continue + } else { + reader.skip(-3) + dayZPayloadEnded = true + } + } + + const key = reader.string() + const value = reader.string() + rules[key] = value + } + } + + // Battalion 1944 puts its info into rules fields for some reason + if (appId === AppId.Bat1944) { + if ('bat_name_s' in rules) { + state.name = rules.bat_name_s + delete rules.bat_name_s + if ('bat_player_count_s' in rules) { + state.raw.numplayers = parseInt(rules.bat_player_count_s) + delete rules.bat_player_count_s + } + if ('bat_max_players_i' in rules) { + state.maxplayers = parseInt(rules.bat_max_players_i) + delete rules.bat_max_players_i + } + if ('bat_has_password_s' in rules) { + state.password = rules.bat_has_password_s === 'Y' + delete rules.bat_has_password_s + } + // apparently map is already right, and this var is often wrong + delete rules.bat_map_s + } + } + + // Squad keeps its password in a separate field + if (appId === AppId.Squad) { + if (rules.Password_b === 'true') { + state.password = true + } + } + + if (appId === AppId.DayZ) { + state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload)) + } + } + + readDayzMods (/** Buffer */ buffer) { + if (!buffer.length) { + return {} + } + + this.logger.debug('DAYZ BUFFER') + this.logger.debug(buffer) + + const reader = this.reader(buffer) + const version = this.readDayzByte(reader) + const overflow = this.readDayzByte(reader) + const dlc1 = this.readDayzByte(reader) + const dlc2 = this.readDayzByte(reader) + this.logger.debug('version ' + version) + this.logger.debug('overflow ' + overflow) + this.logger.debug('dlc1 ' + dlc1) + this.logger.debug('dlc2 ' + dlc2) + if (dlc1) { + const unknown = this.readDayzUint(reader, 4) // ? + this.logger.debug('unknown ' + unknown) + } + if (dlc2) { + const unknown = this.readDayzUint(reader, 4) // ? + this.logger.debug('unknown ' + unknown) + } + const mods = [] + mods.push(...this.readDayzModsSection(reader, true)) + mods.push(...this.readDayzModsSection(reader, false)) + this.logger.debug('dayz buffer rest:', reader.rest()) + return mods + } + + readDayzModsSection (/** Reader */ reader, withHeader) { + const out = [] + const count = this.readDayzByte(reader) + this.logger.debug('dayz mod section withHeader:' + withHeader + ' count:' + count) + for (let i = 0; i < count; i++) { + if (reader.done()) break + const mod = {} + if (withHeader) { + mod.unknown = this.readDayzUint(reader, 4) // ? + + // For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes? + const offset = reader.offset() + const flag = this.readDayzByte(reader) + if (flag !== 4) reader.setOffset(offset) + + mod.workshopId = this.readDayzUint(reader, 4) + } + mod.title = this.readDayzString(reader) + this.logger.debug(mod) + out.push(mod) + } + return out + } + + readDayzUint (reader, bytes) { + const out = [] + for (let i = 0; i < bytes; i++) { + out.push(this.readDayzByte(reader)) + } + const buf = Buffer.from(out) + const r2 = this.reader(buf) + return r2.uint(bytes) + } + + readDayzByte (reader) { + const byte = reader.uint(1) + if (byte === 1) { + const byte2 = reader.uint(1) + if (byte2 === 1) return 1 + if (byte2 === 2) return 0 + if (byte2 === 3) return 0xff + return 0 // ? + } + return byte + } + + readDayzString (reader) { + const length = this.readDayzByte(reader) + const out = [] + for (let i = 0; i < length; i++) { + out.push(this.readDayzByte(reader)) + } + return Buffer.from(out).toString('utf8') + } + + async cleanup (/** Results */ state) { + // Organize players / hidden players into player / bot arrays + const botProbability = (p) => { + if (p.time === -1) return Number.MAX_VALUE + return p.time + } + const sortedPlayers = state.raw.players.sort((a, b) => { + return botProbability(a) - botProbability(b) + }) + delete state.raw.players + const numBots = state.raw.numbots || 0 + const numPlayers = state.raw.numplayers - numBots + while (state.bots.length < numBots) { + if (sortedPlayers.length) state.bots.push(sortedPlayers.pop()) + else state.bots.push({}) + } + while (state.players.length < numPlayers || sortedPlayers.length) { + if (sortedPlayers.length) state.players.push(sortedPlayers.pop()) + else state.players.push({}) + } + } + + /** + * Sends a request packet and returns only the response type expected + * @param {number} type + * @param {boolean} sendChallenge + * @param {?string|Buffer} payload + * @param {number} expect + * @param {boolean=} allowTimeout + * @returns Buffer|null + **/ + async sendPacket ( + type, + payload, + expect, + allowTimeout + ) { + for (let keyRetry = 0; keyRetry < 3; keyRetry++) { + let receivedNewChallengeKey = false + const response = await this.sendPacketRaw( + type, payload, + (payload) => { + const reader = this.reader(payload) + const type = reader.uint(1) + this.logger.debug(() => 'Received 0x' + type.toString(16) + ' expected 0x' + expect.toString(16)) + if (type === 0x41) { + const key = reader.uint(4) + if (this._challenge !== key) { + this.logger.debug('Received new challenge key: 0x' + key.toString(16)) + this._challenge = key + receivedNewChallengeKey = true + } + } + if (type === expect) { + return reader.rest() + } else if (receivedNewChallengeKey) { + return null + } + }, + () => { + if (allowTimeout) return null + } + ) + if (!receivedNewChallengeKey) { + return response + } + } + throw new Error('Received too many challenge key responses') + } + + /** + * Sends a request packet and assembles partial responses + * @param {number} type + * @param {boolean} sendChallenge + * @param {?string|Buffer} payload + * @param {function(Buffer)} onResponse + * @param {function()} onTimeout + **/ + async sendPacketRaw ( + type, + payload, + onResponse, + onTimeout + ) { + const challengeAtBeginning = type === 0x55 || type === 0x56 + const challengeAtEnd = type === 0x54 && !!this._challenge + + if (typeof payload === 'string') payload = Buffer.from(payload, 'binary') + + const b = Buffer.alloc(4 + + (type !== undefined ? 1 : 0) + + (challengeAtBeginning ? 4 : 0) + + (challengeAtEnd ? 4 : 0) + + (payload ? payload.length : 0) + ) + let offset = 0 + + let challenge = this._challenge + if (!challenge) challenge = 0xffffffff + + b.writeInt32LE(-1, offset) + offset += 4 + + if (type !== undefined) { + b.writeUInt8(type, offset) + offset += 1 + } + + if (challengeAtBeginning) { + if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset) + else b.writeUInt32BE(challenge, offset) + offset += 4 + } + + if (payload) { + payload.copy(b, offset) + offset += payload.length + } + + if (challengeAtEnd) { + if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset) + else b.writeUInt32BE(challenge, offset) + offset += 4 + } + + const packetStorage = {} + return await this.udpSend( + b, + (buffer) => { + const reader = this.reader(buffer) + const header = reader.int(4) + if (header === -1) { + // full package + this.logger.debug('Received full packet') + return onResponse(reader.rest()) + } + if (header === -2) { + // partial package + const uid = reader.uint(4) + if (!(uid in packetStorage)) packetStorage[uid] = {} + const packets = packetStorage[uid] + + let bzip = false + if (!this.goldsrcSplits && uid & 0x80000000) bzip = true + + let packetNum, payload, numPackets + if (this.goldsrcSplits) { + packetNum = reader.uint(1) + numPackets = packetNum & 0x0f + packetNum = (packetNum & 0xf0) >> 4 + payload = reader.rest() + } else { + numPackets = reader.uint(1) + packetNum = reader.uint(1) + if (!this._skipSizeInSplitHeader) reader.skip(2) + if (packetNum === 0 && bzip) reader.skip(8) + payload = reader.rest() + } + + packets[packetNum] = payload + + this.logger.debug(() => 'Received partial packet uid: 0x' + uid.toString(16) + ' num: ' + packetNum) + this.logger.debug(() => 'Received ' + Object.keys(packets).length + '/' + numPackets + ' packets for this UID') + + if (Object.keys(packets).length !== numPackets) return + + // assemble the parts + const list = [] + for (let i = 0; i < numPackets; i++) { + if (!(i in packets)) { + throw new Error('Missing packet #' + i) + } + list.push(packets[i]) + } + + let assembled = Buffer.concat(list) + if (bzip) { + this.logger.debug('BZIP DETECTED - Extracing packet...') + try { + assembled = Bzip2.decode(assembled) + } catch (e) { + throw new Error('Invalid bzip packet') + } + } + const assembledReader = this.reader(assembled) + assembledReader.skip(4) // header + return onResponse(assembledReader.rest()) + } + }, + onTimeout + ) + } +} diff --git a/protocols/vcmp.js b/protocols/vcmp.js index 1ccc810..2171d3e 100644 --- a/protocols/vcmp.js +++ b/protocols/vcmp.js @@ -1,10 +1,10 @@ -import samp from './samp.js'; - -export default class vcmp extends samp { - constructor() { - super(); - this.magicHeader = 'VCMP'; - this.responseMagicHeader = 'MP04'; - this.isVcmp = true; - } -} +import samp from './samp.js' + +export default class vcmp extends samp { + constructor () { + super() + this.magicHeader = 'VCMP' + this.responseMagicHeader = 'MP04' + this.isVcmp = true + } +} diff --git a/protocols/ventrilo.js b/protocols/ventrilo.js index fc1f8b7..dbad272 100644 --- a/protocols/ventrilo.js +++ b/protocols/ventrilo.js @@ -1,235 +1,236 @@ -import Core from './core.js'; - -export default class ventrilo extends Core { - constructor() { - super(); - this.byteorder = 'be'; - } - - async run(state) { - const data = await this.sendCommand(2,''); - state.raw = splitFields(data.toString()); - for (const client of state.raw.CLIENTS) { - client.name = client.NAME; - delete client.NAME; - client.ping = parseInt(client.PING); - delete client.PING; - state.players.push(client); - } - delete state.raw.CLIENTS; - - if('NAME' in state.raw) state.name = state.raw.NAME; - if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; - if(this.trueTest(state.raw.AUTH)) state.password = true; - } - async sendCommand(cmd,password) { - const body = Buffer.alloc(16); - body.write(password,0,15,'utf8'); - const encrypted = encrypt(cmd,body); - - const packets = {}; - return await this.udpSend(encrypted, (buffer) => { - if(buffer.length < 20) return; - const data = decrypt(buffer); - - if(data.zero !== 0) return; - packets[data.packetNum] = data.body; - if(Object.keys(packets).length !== data.packetTotal) return; - - const out = []; - for(let i = 0; i < data.packetTotal; i++) { - if(!(i in packets)) throw new Error('Missing packet #'+i); - out.push(packets[i]); - } - return Buffer.concat(out); - }); - } -} - -function splitFields(str,subMode) { - let splitter,delim; - if(subMode) { - splitter = '='; - delim = ','; - } else { - splitter = ': '; - delim = '\n'; - } - - const split = str.split(delim); - const out = {}; - if(!subMode) { - out.CHANNELS = []; - out.CLIENTS = []; - } - for (const one of split) { - const equal = one.indexOf(splitter); - const key = equal === -1 ? one : one.substring(0,equal); - if(!key || key === '\0') continue; - const value = equal === -1 ? '' : one.substring(equal+splitter.length); - if(!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value,true)); - else if(!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value,true)); - else out[key] = value; - } - return out; -} - -function randInt(min,max) { - return Math.floor(Math.random()*(max-min+1)+min) -} - -function crc(body) { - let crc = 0; - for(let i = 0; i < body.length; i++) { - crc = crc_table[crc>>8] ^ body.readUInt8(i) ^ (crc<<8); - crc &= 0xffff; - } - return crc; -} - -function encrypt(cmd,body) { - const headerKeyStart = randInt(0,0xff); - const headerKeyAdd = randInt(1,0xff); - const bodyKeyStart = randInt(0,0xff); - const bodyKeyAdd = randInt(1,0xff); - - const header = Buffer.alloc(20); - header.writeUInt8(headerKeyStart,0); - header.writeUInt8(headerKeyAdd,1); - header.writeUInt16BE(cmd,4); - header.writeUInt16BE(body.length,8); - header.writeUInt16BE(body.length,10); - header.writeUInt16BE(1,12); - header.writeUInt16BE(0,14); - header.writeUInt8(bodyKeyStart,16); - header.writeUInt8(bodyKeyAdd,17); - header.writeUInt16BE(crc(body),18); - - let offset = headerKeyStart; - for(let i = 2; i < header.length; i++) { - let val = header.readUInt8(i); - val += code_head.charCodeAt(offset) + ((i-2) % 5); - val = val & 0xff; - header.writeUInt8(val,i); - offset = (offset+headerKeyAdd) & 0xff; - } - - offset = bodyKeyStart; - for(let i = 0; i < body.length; i++) { - let val = body.readUInt8(i); - val += code_body.charCodeAt(offset) + (i % 72); - val = val & 0xff; - body.writeUInt8(val,i); - offset = (offset+bodyKeyAdd) & 0xff; - } - - return Buffer.concat([header,body]); -} -function decrypt(data) { - const header = data.slice(0,20); - const body = data.slice(20); - const headerKeyStart = header.readUInt8(0); - const headerKeyAdd = header.readUInt8(1); - - let offset = headerKeyStart; - for(let i = 2; i < header.length; i++) { - let val = header.readUInt8(i); - val -= code_head.charCodeAt(offset) + ((i-2) % 5); - val = val & 0xff; - header.writeUInt8(val,i); - offset = (offset+headerKeyAdd) & 0xff; - } - - const bodyKeyStart = header.readUInt8(16); - const bodyKeyAdd = header.readUInt8(17); - offset = bodyKeyStart; - for(let i = 0; i < body.length; i++) { - let val = body.readUInt8(i); - val -= code_body.charCodeAt(offset) + (i % 72); - val = val & 0xff; - body.writeUInt8(val,i); - offset = (offset+bodyKeyAdd) & 0xff; - } - - // header format: - // key, zero, cmd, echo, totallength, thislength - // totalpacket, packetnum, body key, crc - return { - zero: header.readUInt16BE(2), - cmd: header.readUInt16BE(4), - packetTotal: header.readUInt16BE(12), - packetNum: header.readUInt16BE(14), - body: body - }; -} - -const code_head = - '\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e'+ - '\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea'+ - '\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24'+ - '\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb'+ - '\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c'+ - '\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53'+ - '\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae'+ - '\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09'+ - '\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62'+ - '\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5'+ - '\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f'+ - '\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff'+ - '\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f'+ - '\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f'+ - '\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a'+ - '\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'; - -const code_body = - '\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23'+ - '\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48'+ - '\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e'+ - '\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe'+ - '\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42'+ - '\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81'+ - '\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36'+ - '\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59'+ - '\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24'+ - '\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e'+ - '\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2'+ - '\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67'+ - '\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08'+ - '\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e'+ - '\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90'+ - '\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'; - -const crc_table = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 -]; +import Core from './core.js' + +export default class ventrilo extends Core { + constructor () { + super() + this.byteorder = 'be' + } + + async run (state) { + const data = await this.sendCommand(2, '') + state.raw = splitFields(data.toString()) + for (const client of state.raw.CLIENTS) { + client.name = client.NAME + delete client.NAME + client.ping = parseInt(client.PING) + delete client.PING + state.players.push(client) + } + delete state.raw.CLIENTS + + if ('NAME' in state.raw) state.name = state.raw.NAME + if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS + if (this.trueTest(state.raw.AUTH)) state.password = true + } + + async sendCommand (cmd, password) { + const body = Buffer.alloc(16) + body.write(password, 0, 15, 'utf8') + const encrypted = encrypt(cmd, body) + + const packets = {} + return await this.udpSend(encrypted, (buffer) => { + if (buffer.length < 20) return + const data = decrypt(buffer) + + if (data.zero !== 0) return + packets[data.packetNum] = data.body + if (Object.keys(packets).length !== data.packetTotal) return + + const out = [] + for (let i = 0; i < data.packetTotal; i++) { + if (!(i in packets)) throw new Error('Missing packet #' + i) + out.push(packets[i]) + } + return Buffer.concat(out) + }) + } +} + +function splitFields (str, subMode) { + let splitter, delim + if (subMode) { + splitter = '=' + delim = ',' + } else { + splitter = ': ' + delim = '\n' + } + + const split = str.split(delim) + const out = {} + if (!subMode) { + out.CHANNELS = [] + out.CLIENTS = [] + } + for (const one of split) { + const equal = one.indexOf(splitter) + const key = equal === -1 ? one : one.substring(0, equal) + if (!key || key === '\0') continue + const value = equal === -1 ? '' : one.substring(equal + splitter.length) + if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true)) + else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true)) + else out[key] = value + } + return out +} + +function randInt (min, max) { + return Math.floor(Math.random() * (max - min + 1) + min) +} + +function crc (body) { + let crc = 0 + for (let i = 0; i < body.length; i++) { + crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8) + crc &= 0xffff + } + return crc +} + +function encrypt (cmd, body) { + const headerKeyStart = randInt(0, 0xff) + const headerKeyAdd = randInt(1, 0xff) + const bodyKeyStart = randInt(0, 0xff) + const bodyKeyAdd = randInt(1, 0xff) + + const header = Buffer.alloc(20) + header.writeUInt8(headerKeyStart, 0) + header.writeUInt8(headerKeyAdd, 1) + header.writeUInt16BE(cmd, 4) + header.writeUInt16BE(body.length, 8) + header.writeUInt16BE(body.length, 10) + header.writeUInt16BE(1, 12) + header.writeUInt16BE(0, 14) + header.writeUInt8(bodyKeyStart, 16) + header.writeUInt8(bodyKeyAdd, 17) + header.writeUInt16BE(crc(body), 18) + + let offset = headerKeyStart + for (let i = 2; i < header.length; i++) { + let val = header.readUInt8(i) + val += codeHead.charCodeAt(offset) + ((i - 2) % 5) + val = val & 0xff + header.writeUInt8(val, i) + offset = (offset + headerKeyAdd) & 0xff + } + + offset = bodyKeyStart + for (let i = 0; i < body.length; i++) { + let val = body.readUInt8(i) + val += codeBody.charCodeAt(offset) + (i % 72) + val = val & 0xff + body.writeUInt8(val, i) + offset = (offset + bodyKeyAdd) & 0xff + } + + return Buffer.concat([header, body]) +} +function decrypt (data) { + const header = data.slice(0, 20) + const body = data.slice(20) + const headerKeyStart = header.readUInt8(0) + const headerKeyAdd = header.readUInt8(1) + + let offset = headerKeyStart + for (let i = 2; i < header.length; i++) { + let val = header.readUInt8(i) + val -= codeHead.charCodeAt(offset) + ((i - 2) % 5) + val = val & 0xff + header.writeUInt8(val, i) + offset = (offset + headerKeyAdd) & 0xff + } + + const bodyKeyStart = header.readUInt8(16) + const bodyKeyAdd = header.readUInt8(17) + offset = bodyKeyStart + for (let i = 0; i < body.length; i++) { + let val = body.readUInt8(i) + val -= codeBody.charCodeAt(offset) + (i % 72) + val = val & 0xff + body.writeUInt8(val, i) + offset = (offset + bodyKeyAdd) & 0xff + } + + // header format: + // key, zero, cmd, echo, totallength, thislength + // totalpacket, packetnum, body key, crc + return { + zero: header.readUInt16BE(2), + cmd: header.readUInt16BE(4), + packetTotal: header.readUInt16BE(12), + packetNum: header.readUInt16BE(14), + body + } +} + +const codeHead = + '\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' + + '\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' + + '\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' + + '\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' + + '\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' + + '\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' + + '\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' + + '\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' + + '\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' + + '\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' + + '\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' + + '\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' + + '\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' + + '\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' + + '\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' + + '\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d' + +const codeBody = + '\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' + + '\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' + + '\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' + + '\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' + + '\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' + + '\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' + + '\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' + + '\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' + + '\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' + + '\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' + + '\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' + + '\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' + + '\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' + + '\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' + + '\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' + + '\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29' + +const crcTable = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 +] diff --git a/protocols/warsow.js b/protocols/warsow.js index eee0902..2eeb57f 100644 --- a/protocols/warsow.js +++ b/protocols/warsow.js @@ -1,13 +1,13 @@ -import quake3 from './quake3.js'; - -export default class warsow extends quake3 { - async run(state) { - await super.run(state); - if(state.players) { - for(const player of state.players) { - player.team = player.address; - delete player.address; - } - } - } -} +import quake3 from './quake3.js' + +export default class warsow extends quake3 { + async run (state) { + await super.run(state) + if (state.players) { + for (const player of state.players) { + player.team = player.address + delete player.address + } + } + } +}