diff --git a/README.md b/README.md index f698a98..37781cb 100644 --- a/README.md +++ b/README.md @@ -14,75 +14,53 @@ Usage from Node.js npm install gamedig ``` -Promise: ```javascript const Gamedig = require('gamedig'); Gamedig.query({ - type: 'minecraft', - host: 'mc.example.com' + type: 'minecraft', + host: 'mc.example.com' }).then((state) => { - console.log(state); + console.log(state); }).catch((error) => { - console.log("Server is offline"); + console.log("Server is offline"); }); ``` -or Node.JS Callback: -```javascript -const Gamedig = require('gamedig'); -Gamedig.query({ - type: 'minecraft', - host: 'mc.example.com' -}, -function(e,state) { - if(e) console.log("Server is offline"); - else console.log(state); -}); -``` - -> Is NPM out of date? If you're feeling lucky, you can install the latest code with -> ```shell -> npm install sonicsnes/node-gamedig -> ``` - ### Query Options **Typical** -* **type**: One of the game IDs listed in the game list below -* **host**: Hostname or IP of the game server -* **port**: (optional) Uses the protocol default if not set +* **type**: string - One of the game IDs listed in the game list below +* **host**: string - Hostname or IP of the game server +* **port**: number (optional) - Connection port or query port for the game server. Some +games utilize a separate "query" port. If specifying the game port does not seem to work as expected, passing in +this query port may work instead. (defaults to protocol default port) **Advanced** -* **notes**: (optional) An object passed through in the return value. -* **maxAttempts**: (optional) Number of attempts to query server in case of failure. (default 1) -* **socketTimeout**: (optional) Milliseconds to wait for a single packet. Beware that increasing this +* **maxAttempts**: number - Number of attempts to query server in case of failure. (default 1) +* **socketTimeout**: number - Milliseconds to wait for a single packet. Beware that increasing this will cause many queries to take longer even if the server is online. (default 2000) -* **attemptTimeout**: (optional) Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, +* **attemptTimeout**: number - Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, as the socketTimeout typically fires first. (default 10000) +* **debug**: boolean - Enables massive amounts of debug logging to stdout. (default false) ### Return Value The returned state object will contain the following keys: -**Stable, always present:** - -* **name** -* **map** -* **password**: Boolean -* **maxplayers** -* **players**: (array of objects) Each object **may** contain name, ping, score, team, address -* **bots**: Same schema as players -* **notes**: Passed through from the input - -**Unstable, not guaranteed:** - -* **raw**: Contains all information received from the server -* **query**: Details about the query performed - -It can usually be assumed that the number of players online is equal to the length of the players array. -Some servers may return an additional player count number, which may be present in the unstable raw object. +* **name**: string - Server name +* **map**: string - Current server game map +* **password**: boolean - If a password is required +* **maxplayers**: number +* **players**: array of objects + * Each object **may or may not** contain name, ping, score, team, address. + * The number of players online can be determined by `players.length`. + * For servers which do not provide player names, this may be an array +of empty objects (ex. `[{},{},{}]`), one for each player without a name. +* **bots**: array of objects - Same schema as players +* **raw**: freeform object - Contains all information received from the server in a disorganized format. The content of this +field is unstable, and may change on a per-protocol basis between GameDig patch releases (although not typical). Games List --- @@ -365,8 +343,6 @@ Games List > __Know how to code?__ Protocols for most of the games above are documented > in the /reference folder, ready for you to develop into GameDig! - - > Don't see your game listed here? > > First, let us know so we can fix it. Then, you can try using some common query @@ -392,7 +368,7 @@ have set the cvar: host_players_show 2 ### DayZ DayZ uses a query port that is separate from its main game port. The query port is usually -the game port PLUS 24714 or 24715. You may need to pass this port in as the 'port_query' request option. +the game port PLUS 24714 or 24715. You may need to pass this query port into GameDig instead. ### Mumble For full query results from Mumble, you must be running the @@ -424,7 +400,7 @@ additional option: token Games with this note use a query port which is usually not the same as the game's connection port. Usually, no action will be required from you. The 'port' option you pass GameDig should be the game's connection port. GameDig will attempt to calculate the query port automatically. If the query still fails, -you may need to pass the 'port_query' option to GameDig as well, indicating the separate query port. +you may need to find your server's query port, and pass that to GameDig instead. Usage from Command Line --- @@ -435,17 +411,56 @@ You'll still need npm to install gamedig: npm install gamedig -g ``` -After installing gamedig globally, you can call gamedig via the command line -using the same parameters mentioned in the API above: +After installing gamedig globally, you can call gamedig via the command line: ```shell -gamedig --type minecraft --host mc.example.com --port 11234 +gamedig --type minecraft mc.example.com:11234 ``` -The output of the command will be in JSON format. +The output of the command will be in JSON format. Additional advanced parameters can be passed in +as well: `--debug`, `--pretty`, `--socketTimeout 5000`, etc. -Major Version Changes +Changelog --- +### 2.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 * First official release -* Node.js 6.0 is now required +* Node.js 6 is now required diff --git a/bin/gamedig.js b/bin/gamedig.js index 919246d..d12a80b 100644 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -5,8 +5,8 @@ const argv = require('minimist')(process.argv.slice(2)), const debug = argv.debug; delete argv.debug; -const outputFormat = argv.output; -delete argv.output; +const pretty = !!argv.pretty || debug; +delete argv.pretty; const options = {}; for(const key of Object.keys(argv)) { @@ -14,18 +14,26 @@ for(const key of Object.keys(argv)) { if( key === '_' || key.charAt(0) === '$' - || (typeof value !== 'string' && typeof value !== 'number') ) continue; options[key] = value; } -if(debug) Gamedig.debug = true; -Gamedig.isCommandLine = true; +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; +} Gamedig.query(options) .then((state) => { - if(outputFormat === 'pretty') { + if(pretty) { console.log(JSON.stringify(state,null,' ')); } else { console.log(JSON.stringify(state)); @@ -42,7 +50,7 @@ Gamedig.query(options) if (error instanceof Error) { error = error.message; } - if (outputFormat === 'pretty') { + if (pretty) { console.log(JSON.stringify({error: error}, null, ' ')); } else { console.log(JSON.stringify({error: error})); diff --git a/bin/genreadme.js b/bin/genreadme.js index 1a69010..6089b36 100644 --- a/bin/genreadme.js +++ b/bin/genreadme.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const fs = require('fs'), - TypeResolver = require('../lib/typeresolver'); + TypeResolver = require('../lib/GameResolver'); const generated = TypeResolver.printReadme(); diff --git a/games.txt b/games.txt index 001684e..16e0b68 100644 --- a/games.txt +++ b/games.txt @@ -1,4 +1,4 @@ -# id | pretty | protocol | options | parameters +# id | pretty name for readme | protocol | options | extra #### TODO: # cube1|Cube 1|cube|port=28786,port_query_offset=1 @@ -15,7 +15,6 @@ # gr|Ghost Recon|ghostrecon|port=2346,port_query_offset=2 # gtr2|GTR2|gtr2|port=34297,port_query_offset=1 # haze|Haze|haze -# openttd|OpenTTD|openttd|port=3979 # plainsight|Plain Sight|plainsight # redfaction|Red Faction|redfaction|port_query=7755 # savage|Savage|savage|port_query=11235 @@ -28,18 +27,18 @@ 7d2d|7 Days to Die|valve|port=26900,port_query_offset=1 -ageofchivalry|Age of Chivalry|valve +ageofchivalry|Age of Chivalry|valve|port=27015 aoe2|Age of Empires 2|ase|port_query=27224 alienarena|Alien Arena|quake2|port_query=27910 -alienswarm|Alien Swarm|valve +alienswarm|Alien Swarm|valve|port=27015 arkse|ARK: Survival Evolved|valve|port=7777,port_query=27015 avp2|Aliens vs Predator 2|gamespy1|port=27888 # avp2010 doesn't really... have a default port or query port # both port and port_query should be specified when used -avp2010|Aliens vs Predator 2010|valve +avp2010|Aliens vs Predator 2010|valve|port=27015 -americasarmy|America's Army|americasarmy|port=1716,port_query_offset=1 -americasarmy2|America's Army 2|americasarmy|port=1716,port_query_offset=1 +americasarmy|America's Army|gamespy2|port=1716,port_query_offset=1 +americasarmy2|America's Army 2|gamespy2|port=1716,port_query_offset=1 americasarmy3|America's Army 3|valve|port=8777,port_query=27020 americasarmypg|America's Army: Proving Grounds|valve|port=8777,port_query=27020 @@ -53,9 +52,9 @@ bat1944|Battalion 1944|valve|port=7777,port_query_offset=3 bf1942|Battlefield 1942|gamespy1|port=14567,port_query=23000 bfv|Battlefield Vietnam|gamespy2|port=15567,port_query=23000 -bf2|Battlefield 2|gamespy3|port=16567,port_query=29900|noChallenge +bf2|Battlefield 2|gamespy3|port=16567,port_query=29900 bf2142|Battlefield 2142|gamespy3|port=16567,port_query=29900 -bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888|isBadCompany2 +bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888 bf3|Battlefield 3|battlefield|port=25200,port_query_offset=22000 bf4|Battlefield 4|battlefield|port=25200,port_query_offset=22000 bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000 @@ -63,7 +62,7 @@ bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000 breach|Breach|valve|port=27016 breed|Breed|gamespy2|port=7649 brink|Brink|valve|port_query_offset=1 -buildandshoot|Build and Shoot|buildandshoot|port=32887,port_query=32886 +buildandshoot|Build and Shoot|buildandshoot|port=32887,port_query_offset=-1 cod|Call of Duty|quake3|port=28960 coduo|Call of Duty: United Offensive|quake3|port=28960 @@ -83,10 +82,10 @@ cacrenegade|Command and Conquer: Renegade|gamespy1|port=4848,port_query=25300 conanexiles|Conan Exiles|valve|port=7777,port_query=27015 contactjack|Contact J.A.C.K.|gamespy1|port_query=27888 -cs16|Counter-Strike 1.6|valve -cscz|Counter-Strike: Condition Zero|valve -css|Counter-Strike: Source|valve -csgo|Counter-Strike: Global Offensive|valve||doc_notes=csgo +cs16|Counter-Strike 1.6|valve|port=27015 +cscz|Counter-Strike: Condition Zero|valve|port=27015 +css|Counter-Strike: Source|valve|port=27015 +csgo|Counter-Strike: Global Offensive|valve||port=27015|doc_notes=csgo crossracing|Cross Racing Championship|ase|port=12321,port_query_offset=123 @@ -95,7 +94,7 @@ crysiswars|Crysis Wars|gamespy3|port=64100 crysis2|Crysis 2|gamespy3|port=64000 daikatana|Daikatana|quake2|port=27982,port_query_offset=10 -dmomam|Dark Messiah of Might and Magic|valve +dmomam|Dark Messiah of Might and Magic|valve|port=27015 darkesthour|Darkest Hour|unreal2|port=7757,port_query_offset=1 dayz|DayZ|valve|port=2302,port_query_offset=24714|doc_notes=dayz dayzmod|DayZ Mod|valve|port=2302,port_query_offset=1 @@ -104,61 +103,61 @@ dh2005|Deer Hunter 2005|gamespy2|port=23459,port_query=34567 descent3|Descent 3|gamespy1|port=2092,port_query=20142 deusex|Deus Ex|gamespy2|port=7791,port_query_offset=1 devastation|Devastation|unreal2|port=7777,port_query_offset=1 -dinodday|Dino D-Day|valve +dinodday|Dino D-Day|valve|port=27015 dirttrackracing2|Dirt Track Racing 2|gamespy1|port=32240,port_query_offset=-100 dnl|Dark and Light|valve|port=7777,port_query=27015 -dod|Day of Defeat|valve -dods|Day of Defeat: Source|valve -doi|Day of Infamy|valve +dod|Day of Defeat|valve|port=27015 +dods|Day of Defeat: Source|valve|port=27015 +doi|Day of Infamy|valve|port=27015 doom3|Doom 3|doom3|port=27666 -dota2|DOTA 2|valve +dota2|DOTA 2|valve|port=27015 drakan|Drakan|gamespy1|port=27045,port_query_offset=1 -etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733|isEtqw,hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag +etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733 fear|F.E.A.R.|gamespy2|port_query=27888 f12002|F1 2002|gamespy1|port_query=3297 f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397 farcry|Far Cry|ase|port=49001,port_query_offset=123 farcry2|Far Cry|ase|port_query=14001 -fortressforever|Fortress Forever|valve +fortressforever|Fortress Forever|valve|port=27015 flashpoint|Flashpoint|gamespy1|port=2302,port_query_offset=1 ffow|Frontlines: Fuel of War|ffow|port=5476,port_query_offset=2 fivem|FiveM|fivem|port=30120 -garrysmod|Garry's Mod|valve +garrysmod|Garry's Mod|valve|port=27015 graw|Ghost Recon: Advanced Warfighter|gamespy2|port_query=15250 graw2|Ghost Recon: Advanced Warfighter 2|gamespy2|port_query=16250 giantscitizenkabuto|Giants: Citizen Kabuto|gamespy1|port_query=8911 globaloperations|Global Operations|gamespy1|port_query=28672 geneshift|Geneshift|geneshift|port=11235 -ges|GoldenEye: Source|valve +ges|GoldenEye: Source|valve|port=27015 gore|Gore|gamespy1|port=27777,port_query_offset=1 -gunmanchronicles|Gunman Chronicles|valve -hldm|Half-Life 1 Deathmatch|valve -hl2dm|Half-Life 2 Deathmatch|valve +gunmanchronicles|Gunman Chronicles|valve|port=27015 +hldm|Half-Life 1 Deathmatch|valve|port=27015 +hl2dm|Half-Life 2 Deathmatch|valve|port=27015 halo|Halo|gamespy2|port=2302 halo2|Halo 2|gamespy2|port=2302 heretic2|Heretic 2|gamespy1|port=27900,port_query_offset=1 hexen2|Hexen 2|hexen2|port=26900,port_query_offset=50 -hidden|The Hidden: Source|valve +hidden|The Hidden: Source|valve|port=27015 had2|Hidden and Dangerous 2|gamespy1|port=11001,port_query_offset=3 -homefront|Homefront|valve +homefront|Homefront|valve|port=27015 homeworld2|Homeworld 2|gamespy1|port_query=6500 hurtworld|Hurtworld|valve|port=12871,port_query=12881 igi2|IGI-2: Covert Strike|gamespy1|port_query=26001 il2|IL-2 Sturmovik|gamespy1|port_query=21000 -insurgency|Insurgency|valve +insurgency|Insurgency|valve|port=27015 ironstorm|Iron Storm|gamespy1|port_query=3505 jamesbondnightfire|James Bond: Nightfire|gamespy1|port_query=6550 -jc2mp|Just Cause 2 Multiplayer|jc2mp|port=7777|isJc2mp +jc2mp|Just Cause 2 Multiplayer|jc2mp|port=7777 killingfloor|Killing Floor|killingfloor|port=7707,port_query_offset=1 killingfloor2|Killing Floor 2|valve|port=7777,port_query=27015 kingpin|Kingpin: Life of Crime|gamespy1|port=31510,port_query_offset=-10 kisspc|KISS Psycho Circus|gamespy1|port=7777,port_query_offset=1 kspdmp|DMP - KSP Multiplayer|kspdmp|port=6702,port_query_offset=1 -kzmod|KzMod|valve -left4dead|Left 4 Dead|valve -left4dead2|Left 4 Dead 2|valve +kzmod|KzMod|valve|port=27015 +left4dead|Left 4 Dead|valve|port=27015 +left4dead2|Left 4 Dead 2|valve|port=27015 m2mp|Mafia 2 Multiplayer|m2mp|port=27016,port_query_offset=1 -medievalengineers|Medieval Engineers|valve +medievalengineers|Medieval Engineers|valve|port=27015 mohaa|Medal of Honor: Allied Assault|gamespy1|port=12203,port_query_offset=97 mohpa|Medal of Honor: Pacific Assault|gamespy1|port=13203,port_query_offset=97 @@ -168,9 +167,9 @@ mohbt|Medal of Honor: Breakthrough|gamespy1|port=12203,port_query_offset=97 moh2010|Medal of Honor 2010|battlefield|port=7673,port_query=48888 mohwf|Medal of Honor: Warfighter|battlefield|port=25200,port_query_offset=22000 -minecraft|Minecraft|minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft +minecraft|Minecraft|minecraft|port=25565|doc_notes=minecraft # Legacy name -minecraftping||minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft +minecraftping||minecraft|port=25565|doc_notes=minecraft minecraftpe|Minecraft: Pocket Edition|gamespy3|port=19132,maxAttempts=2 mnc|Monday Night Combat|valve|port=7777,port_query=27016 @@ -180,9 +179,9 @@ mumble|Mumble|mumble|port=64738,port_query=27800|doc_notes=mumble mumbleping|Mumble|mumbleping|port=64738|doc_notes=mumble mutantfactions|Mutant Factions|geneshift|port=11235 nascarthunder2004|Nascar Thunder 2004|gamespy2|port_query=13333 -netpanzer|netPanzer|gamespy1|3030 -nmrih|No More Room in Hell|valve -ns|Natural Selection|valve +netpanzer|netPanzer|gamespy1|port=3030 +nmrih|No More Room in Hell|valve|port=27015 +ns|Natural Selection|valve|port=27015 ns2|Natural Selection 2|valve|port_query_offset=1 nfshp2|Need for Speed: Hot Pursuit 2|gamespy1|port_query=61220 nab|Nerf Arena Blast|gamespy1|port=4444,port_query_offset=1 @@ -192,21 +191,21 @@ nexuiz|Nexuiz|quake3|port_query=26000 nitrofamily|Nitro Family|gamespy1|port_query=25601 nolf|No One Lives Forever|gamespy1|port_query=27888 nolf2|No One Lives Forever 2|gamespy1|port_query=27890 -nucleardawn|Nuclear Dawn|valve +nucleardawn|Nuclear Dawn|valve|port=27015 openarena|OpenArena|quake3|port_query=27960 openttd|OpenTTD|openttd|port=3979 operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1 painkiller|Painkiller|ase|port=3455,port_query_offset=123 postal2|Postal 2|gamespy1|port=7777,port_query_offset=1 -prey|Prey|doom3|port_query=27719 +prey|Prey|doom3|port=27719 primalcarnage|Primal Carnage: Extinction|valve|port=7777,port_query=27015 quake1|Quake 1: QuakeWorld|quake1|port=27500 quake2|Quake 2|quake2|port=27910 quake3|Quake 3: Arena|quake3|port=27960 -quake4|Quake 4|doom3|port=28004|hasClanTag +quake4|Quake 4|doom3|port=28004 -ragdollkungfu|Rag Doll Kung Fu|valve +ragdollkungfu|Rag Doll Kung Fu|valve|port=27015 r6|Rainbow Six|gamespy1|port_query=2348 r6roguespear|Rainbow Six 2: Rogue Spear|gamespy1|port_query=2346 @@ -219,20 +218,20 @@ redorchestraost|Red Orchestra: Ostfront 41-45|gamespy1|port=7757,port_query_offs redorchestra2|Red Orchestra 2|valve|port=7777,port_query=27015 redline|Redline|gamespy1|port_query=25252 rtcw|Return to Castle Wolfenstein|quake3|port_query=27960 -ricochet|Ricochet|valve +ricochet|Ricochet|valve|port=27015 riseofnations|Rise of Nations|gamespy1|port_query=6501 rune|Rune|gamespy1|port=7777,port_query_offset=1 rust|Rust|valve|port=28015 samp|San Andreas Multiplayer|samp|port=7777 -spaceengineers|Space Engineers|valve +spaceengineers|Space Engineers|valve|port=27015 ss|Serious Sam|gamespy1|port=25600,port_query_offset=1 ss2|Serious Sam 2|gamespy2|port=25600 -shatteredhorizon|Shattered Horizon|valve -ship|The Ship|valve +shatteredhorizon|Shattered Horizon|valve|port=27015 +ship|The Ship|valve|port=27015 shogo|Shogo|gamespy1|port_query=27888 -shootmania|Shootmania|nadeo||doc_notes=nadeo-shootmania--trackmania--etc +shootmania|Shootmania|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc sin|SiN|gamespy1|port_query=22450 -sinep|SiN Episodes|valve +sinep|SiN Episodes|valve|port=27015 soldat|Soldat|ase|port=13073,port_query_offset=123 sof|Soldier of Fortune|quake1|port_query=28910 sof2|Soldier of Fortune 2|quake3|port_query=20100 @@ -250,24 +249,24 @@ swrc|Star Wars: Republic Commando|gamespy2|port=7777,port_query=11138 starbound|Starbound|valve|port=21025 starmade|StarMade|starmade|port=4242 -suicidesurvival|Suicide Survival|valve +suicidesurvival|Suicide Survival|valve|port=27015 swat4|SWAT 4|gamespy2|port=10480,port_query_offset=2 -svencoop|Sven Coop|valve -synergy|Synergy|valve +svencoop|Sven Coop|valve|port=27015 +synergy|Synergy|valve|port=27015 tacticalops|Tactical Ops|gamespy1|port=7777,port_query_offset=1 teamfactor|Team Factor|gamespy1|port_query=57778 -tfc|Team Fortress Classic|valve -tf2|Team Fortress 2|valve -teamspeak2|Teamspeak 2|teamspeak2|port=8767,port_query=51234 -teamspeak3|Teamspeak 3|teamspeak3|port=9987,port_query=10011|doc_notes=teamspeak3 +tfc|Team Fortress Classic|valve|port=27015 +tf2|Team Fortress 2|valve|port=27015 +teamspeak2|Teamspeak 2|teamspeak2|port=8767 +teamspeak3|Teamspeak 3|teamspeak3|port=9987|doc_notes=teamspeak3 terminus|Terminus|gamespy1|port_query=12286 terraria|Terraria|terraria|port=7777,port_query_offset=101|doc_notes=terraria thps3|Tony Hawk's Pro Skater 3|gamespy1|port_query=6500 thps4|Tony Hawk's Pro Skater 4|gamespy1|port_query=6500 thu2|Tony Hawk's Underground 2|gamespy1|port_query=5153 -towerunite|Tower Unite|valve -trackmania2|Trackmania 2|nadeo||doc_notes=nadeo-shootmania--trackmania--etc -trackmaniaforever|Trackmania Forever|nadeo||doc_notes=nadeo-shootmania--trackmania--etc +towerunite|Tower Unite|valve|port=27015 +trackmania2|Trackmania 2|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc +trackmaniaforever|Trackmania Forever|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc tremulous|Tremulous|quake3|port_query=30720 tribes1|Tribes 1: Starsiege|tribes1|port=28001 tribesvengeance|Tribes: Vengeance|gamespy2|port=7777,port_query_offset=1 @@ -289,8 +288,8 @@ vietcong|Vietcong|gamespy1|port=5425,port_query=15425 vietcong2|Vietcong 2|gamespy2|port=5001,port_query=19967 warsow|Warsow|warsow|port=44400 wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1 -wolfenstein2009|Wolfenstein 2009|doom3|port_query=27666|hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag +wolfenstein2009|Wolfenstein 2009|doom3|port=27666 wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960 xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123 -zombiemaster|Zombie Master|valve -zps|Zombie Panic: Source|valve +zombiemaster|Zombie Master|valve|port=27015 +zps|Zombie Panic: Source|valve|port=27015 diff --git a/lib/GameResolver.js b/lib/GameResolver.js new file mode 100644 index 0000000..7cf03f9 --- /dev/null +++ b/lib/GameResolver.js @@ -0,0 +1,84 @@ +const Path = require('path'), + fs = require('fs'); + +class GameResolver { + constructor() { + this.games = this._readGames(); + } + + lookup(type) { + if(!type) throw Error('No game specified'); + + if(type.substr(0,9) === 'protocol-') { + return { + protocol: type.substr(9) + }; + } + + const game = this.games.get(type); + if(!game) throw Error('Invalid game: '+type); + return game.options; + } + + printReadme() { + let out = ''; + for(const key of Object.keys(games)) { + const game = games[key]; + if (!game.pretty) { + continue; + } + out += "* "+game.pretty+" ("+key+")"; + if(game.options.port_query_offset || game.options.port_query) + out += " [[Separate Query Port](#separate-query-port)]"; + if(game.extra.doc_notes) + out += " [[Additional Notes](#"+game.extra.doc_notes+")]"; + out += "\n"; + } + return out; + } + + _readGames() { + const gamesFile = Path.normalize(__dirname+'/../games.txt'); + const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); + const games = new Map(); + + for (let line of lines) { + // strip comments + const comment = line.indexOf('#'); + if(comment !== -1) line = line.substr(0,comment); + line = line.trim(); + if(!line) continue; + + const split = line.split('|'); + const gameId = split[0].trim(); + const options = this._parseList(split[3]); + options.protocol = split[2].trim(); + + games.set(gameId, { + pretty: split[1].trim(), + options: options, + extra: this._parseList(split[4]) + }); + } + return games; + } + + _parseList(str) { + if(!str) return {}; + const out = {}; + for (const one of str.split(',')) { + const equals = one.indexOf('='); + const key = equals === -1 ? one : one.substr(0,equals); + let value = equals === -1 ? '' : one.substr(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; + } +} + +module.exports = GameResolver; diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js new file mode 100644 index 0000000..af45ead --- /dev/null +++ b/lib/GlobalUdpSocket.js @@ -0,0 +1,52 @@ +const dgram = require('dgram'), + HexUtil = require('./HexUtil'); + +class GlobalUdpSocket { + constructor() { + this.socket = null; + this.callbacks = new Set(); + this.debuggingCallbacks = new Set(); + } + + _getSocket() { + if (!this.socket) { + const udpSocket = this.socket = dgram.createSocket('udp4'); + udpSocket.unref(); + udpSocket.bind(); + udpSocket.on('message', (buffer, rinfo) => { + const fromAddress = rinfo.address; + const fromPort = rinfo.port; + if (this.debuggingCallbacks.size) { + console.log(fromAddress + ':' + fromPort + " <--UDP"); + console.log(HexUtil.debugDump(buffer)); + } + for (const cb of this.callbacks) { + cb(fromAddress, fromPort, buffer); + } + }); + udpSocket.on('error', (e) => { + if (this.debuggingCallbacks.size) { + console.log("UDP ERROR: " + e); + } + }); + } + return this.socket; + } + + send(buffer, address, port) { + this._getSocket().send(buffer,0,buffer.length,port,address); + } + + addCallback(callback, debug) { + this.callbacks.add(callback); + if (debug) { + this.debuggingCallbacks.add(callback); + } + } + removeCallback(callback) { + this.callbacks.delete(callback); + this.debuggingCallbacks.delete(callback); + } +} + +module.exports = GlobalUdpSocket; diff --git a/lib/Promises.js b/lib/Promises.js new file mode 100644 index 0000000..b807672 --- /dev/null +++ b/lib/Promises.js @@ -0,0 +1,20 @@ +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; + } +} + +module.exports = Promises; diff --git a/lib/ProtocolResolver.js b/lib/ProtocolResolver.js new file mode 100644 index 0000000..ef99e9e --- /dev/null +++ b/lib/ProtocolResolver.js @@ -0,0 +1,22 @@ +const Path = require('path'), + fs = require('fs'), + Core = require('../protocols/core'); + +class ProtocolResolver { + constructor() { + this.protocolDir = Path.normalize(__dirname+'/../protocols'); + } + + /** + * @returns Core + */ + create(protocolId) { + protocolId = Path.basename(protocolId); + const path = this.protocolDir+'/'+protocolId; + if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type); + const protocol = require(path); + return new protocol(); + } +} + +module.exports = ProtocolResolver; diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js new file mode 100644 index 0000000..fe4fb19 --- /dev/null +++ b/lib/QueryRunner.js @@ -0,0 +1,104 @@ +const GameResolver = require('./GameResolver'), + ProtocolResolver = require('./ProtocolResolver'), + GlobalUdpSocket = require('./GlobalUdpSocket'); + +const defaultOptions = { + socketTimeout: 2000, + attemptTimeout: 10000, + maxAttempts: 1 +}; + +class QueryRunner { + constructor() { + this.udpSocket = new GlobalUdpSocket(); + this.gameResolver = new GameResolver(); + this.protocolResolver = new ProtocolResolver(); + } + async run(userOptions) { + for (const key of Object.keys(userOptions)) { + const value = userOptions[key]; + if (['port'].includes(key)) { + userOptions[key] = parseInt(value); + } + } + + const { + port_query: gameQueryPort, + port_query_offset: gameQueryPortOffset, + ...gameOptions + } = this.gameResolver.lookup(userOptions.type); + const attempts = []; + + if (userOptions.port) { + if (gameQueryPortOffset) { + attempts.push({ + ...defaultOptions, + ...gameOptions, + ...userOptions, + port: userOptions.port + gameQueryPortOffset + }); + } + if (userOptions.port === gameOptions.port && gameQueryPort) { + attempts.push({ + ...defaultOptions, + ...gameOptions, + ...userOptions, + port: gameQueryPort + }); + } + attempts.push({ + ...defaultOptions, + ...gameOptions, + ...userOptions + }); + } else if (gameQueryPort) { + attempts.push({ + ...defaultOptions, + ...gameOptions, + ...userOptions, + port: gameQueryPort + }); + } else if (gameOptions.port) { + attempts.push({ + ...defaultOptions, + ...gameOptions, + ...userOptions, + port: gameOptions.port + (gameQueryPortOffset || 0) + }); + } else { + throw new Error("Could not determine port to query. Did you provide a port or gameid?"); + } + + if (attempts.length === 1) { + return await this._attempt(attempts[0]); + } else { + const errors = []; + for (const attempt of attempts) { + try { + return await this._attempt(attempt); + } catch(e) { + const e2 = new Error('Failed to query port ' + attempt.port); + e2.stack += "\nCaused by:\n" + e.stack; + errors.push(e2); + } + } + + const err = new Error('Failed all port attempts'); + err.stack = errors.map(e => e.stack).join('\n'); + throw err; + } + } + + async _attempt(options) { + if (options.debug) { + console.log("Running attempt with options:"); + console.log(options); + } + const core = this.protocolResolver.create(options.protocol); + core.options = options; + core.udpSocket = this.udpSocket; + return await core.runAllAttempts(); + } +} + +module.exports = QueryRunner; diff --git a/lib/index.js b/lib/index.js index c0466f0..4ded359 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,107 +1,23 @@ -const dgram = require('dgram'), - TypeResolver = require('./typeresolver'), - HexUtil = require('./HexUtil'); +const QueryRunner = require('./QueryRunner'); -const activeQueries = []; - -const udpSocket = dgram.createSocket('udp4'); -udpSocket.unref(); -udpSocket.bind(); -udpSocket.on('message', (buffer, rinfo) => { - if(Gamedig.debug) { - console.log(rinfo.address+':'+rinfo.port+" <--UDP"); - console.log(HexUtil.debugDump(buffer)); - } - for(const query of activeQueries) { - if( - query.options.address !== rinfo.address - && query.options.altaddress !== rinfo.address - ) continue; - if(query.options.port_query !== rinfo.port) continue; - query._udpResponse(buffer); - break; - } -}); -udpSocket.on('error', (e) => { - if(Gamedig.debug) console.log("UDP ERROR: "+e); -}); +let singleton = null; class Gamedig { - - static query(options,callback) { - const promise = new Promise((resolve,reject) => { - for (const key of Object.keys(options)) { - if (['port_query', 'port'].includes(key)) { - options[key] = parseInt(options[key]); - } - } - - options.callback = (state) => { - if (state.error) reject(state.error); - else resolve(state); - }; - - let query; - try { - query = TypeResolver.lookup(options.type); - } catch(e) { - process.nextTick(() => { - options.callback({error:e}); - }); - return; - } - query.debug = Gamedig.debug; - query.udpSocket = udpSocket; - query.type = options.type; - - if(!('port' in query.options) && ('port_query' in query.options)) { - if(Gamedig.isCommandLine) { - process.stderr.write( - "Warning! This game is so old, that we don't know" - +" what the server's connection port is. We've guessed that" - +" the query port for "+query.type+" is "+query.options.port_query+"." - +" If you know the connection port for this type of server, please let" - +" us know on the GameDig issue tracker, thanks!\n" - ); - } - query.options.port = query.options.port_query; - delete query.options.port_query; - } - - // copy over options - for(const key of Object.keys(options)) { - query.options[key] = options[key]; - } - - activeQueries.push(query); - - query.on('finished',() => { - const i = activeQueries.indexOf(query); - if(i >= 0) activeQueries.splice(i, 1); - }); - - process.nextTick(() => { - query.start(); - }); - }); - - if (callback && callback instanceof Function) { - if(callback.length === 2) { - promise - .then((state) => callback(null,state)) - .catch((error) => callback(error)); - } else if (callback.length === 1) { - promise - .then((state) => callback(state)) - .catch((error) => callback({error:error})); - } - } - - return promise; + constructor() { + this.queryRunner = new QueryRunner(); } + 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); + } } -Gamedig.debug = false; -Gamedig.isCommandLine = false; module.exports = Gamedig; diff --git a/lib/reader.js b/lib/reader.js index 662c9a2..b5aafa9 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -1,7 +1,8 @@ const Iconv = require('iconv-lite'), Long = require('long'), Core = require('../protocols/core'), - Buffer = require('buffer'); + Buffer = require('buffer'), + Varint = require('varint'); function readUInt64BE(buffer,offset) { const high = buffer.readUInt32BE(offset); @@ -126,6 +127,12 @@ class Reader { return r; } + varint() { + const out = Varint.decode(this.buffer, this.i); + this.i += Varint.decode.bytes; + return out; + } + /** @returns Buffer */ part(bytes) { let r; diff --git a/lib/typeresolver.js b/lib/typeresolver.js deleted file mode 100644 index 78f27f0..0000000 --- a/lib/typeresolver.js +++ /dev/null @@ -1,97 +0,0 @@ -const Path = require('path'), - fs = require('fs'); - -const protocolDir = Path.normalize(__dirname+'/../protocols'); -const gamesFile = Path.normalize(__dirname+'/../games.txt'); - -function parseList(str) { - if(!str) return {}; - const out = {}; - for (const one of str.split(',')) { - const equals = one.indexOf('='); - const key = equals === -1 ? one : one.substr(0,equals); - let value = equals === -1 ? '' : one.substr(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; -} -function readGames() { - const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); - const games = {}; - - for (let line of lines) { - // strip comments - const comment = line.indexOf('#'); - if(comment !== -1) line = line.substr(0,comment); - line = line.trim(); - if(!line) continue; - - const split = line.split('|'); - - games[split[0].trim()] = { - pretty: split[1].trim(), - protocol: split[2].trim(), - options: parseList(split[3]), - params: parseList(split[4]) - }; - } - return games; -} -const games = readGames(); - -function createProtocolInstance(type) { - type = Path.basename(type); - - const path = protocolDir+'/'+type; - if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type); - const protocol = require(path); - - return new protocol(); -} - -class TypeResolver { - static lookup(type) { - if(!type) throw Error('No game specified'); - - if(type.substr(0,9) === 'protocol-') { - return createProtocolInstance(type.substr(9)); - } - - const game = games[type]; - if(!game) throw Error('Invalid game: '+type); - - const query = createProtocolInstance(game.protocol); - query.pretty = game.pretty; - for(const key of Object.keys(game.options)) { - query.options[key] = game.options[key]; - } - for(const key of Object.keys(game.params)) { - query[key] = game.params[key]; - } - - return query; - } - static printReadme() { - let out = ''; - for(const key of Object.keys(games)) { - const game = games[key]; - if (!game.pretty) { - continue; - } - out += "* "+game.pretty+" ("+key+")"; - if(game.options.port_query_offset || game.options.port_query) - out += " [[Separate Query Port](#separate-query-port)]"; - if(game.params.doc_notes) - out += " [[Additional Notes](#"+game.params.doc_notes+")]"; - out += "\n"; - } - return out; - } -} - -module.exports = TypeResolver; diff --git a/package-lock.json b/package-lock.json index 5526b12..ad5c650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,23 @@ { "name": "gamedig", - "version": "1.0.41", + "version": "1.0.49", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", + "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "amdefine": { @@ -21,20 +26,18 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -46,51 +49,60 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "barse": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz", "integrity": "sha1-KJhk15XQECu7sYHmbs0IxUobwMs=", "requires": { - "readable-stream": "1.0.34" + "readable-stream": "~1.0.2" } }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.1" - } + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "cheerio": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", + "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + } }, "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -98,7 +110,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "requires": { - "graceful-readlink": "1.0.1" + "graceful-readlink": ">= 1.0.0" } }, "compressjs": { @@ -106,8 +118,8 @@ "resolved": "https://registry.npmjs.org/compressjs/-/compressjs-1.0.3.tgz", "integrity": "sha1-ldt03VuQOM+AvKMhqw7eJxtJWbY=", "requires": { - "amdefine": "1.0.1", - "commander": "2.8.1" + "amdefine": "~1.0.0", + "commander": "~2.8.1" } }, "core-util-is": { @@ -115,30 +127,28 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.1" - } - } + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" } }, + "css-what": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz", + "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "delayed-stream": { @@ -146,19 +156,62 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "requires": { - "jsbn": "0.1.1" + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } } }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extsprintf": { "version": "1.3.0", @@ -166,9 +219,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -181,13 +234,13 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, "gbxremote": { @@ -195,8 +248,8 @@ "resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.1.4.tgz", "integrity": "sha1-x+0iWC5WBRtOF2AbPdWjAE7u/UM=", "requires": { - "barse": "0.4.3", - "sax": "0.4.3", + "barse": "~0.4.2", + "sax": "0.4.x", "xmlbuilder": "0.3.1" } }, @@ -205,7 +258,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "graceful-readlink": { @@ -219,38 +272,55 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "htmlparser2": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", + "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -263,6 +333,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ip-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-3.0.0.tgz", + "integrity": "sha512-T8wDtjy+Qf2TAPDQmBp0eGKJ8GavlWlUnamr3wRn6vvdZlKVuJXXMlSncYFRYgVHOM3If5NR1H4+OvVQU9Idvg==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -281,8 +356,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-schema": { "version": "0.2.3", @@ -290,9 +364,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "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==" }, "json-stringify-safe": { "version": "5.0.1", @@ -310,22 +384,27 @@ "verror": "1.10.0" } }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, "long": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.37.0" } }, "minimist": { @@ -338,115 +417,171 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "requires": { + "@types/node": "*" + } }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", + "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-0.4.3.tgz", "integrity": "sha1-cA46NOsueSzjgHkccSgPNzGWXdw=" }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "sshpk": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", + "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", "requires": { - "hoek": "4.2.1" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.0.tgz", + "integrity": "sha512-LHMvg+RBP/mAVNqVbOX8t+iJ+tqhBA/t49DuI7+IDAWHrASnesqSu1vWbKB7UrE2yk+HMFUBMadRGMkB4VCfog==", "requires": { - "punycode": "1.4.1" + "ip-regex": "^3.0.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "tunnel-agent": { @@ -454,19 +589,38 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "varint": { "version": "4.0.1", @@ -478,9 +632,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "xmlbuilder": { diff --git a/package.json b/package.json index 66048a9..f9f9287 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "main": "lib/index.js", "author": "Michael Morrison", - "version": "1.0.49", + "version": "2.0", "repository": { "type": "git", "url": "https://github.com/sonicsnes/node-gamedig.git" @@ -21,17 +21,18 @@ }, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=8.0.0" }, "dependencies": { - "async": "^0.9.2", + "cheerio": "^1.0.0-rc.2", "compressjs": "^1.0.2", "gbxremote": "^0.1.4", "iconv-lite": "^0.4.18", "long": "^2.4.0", "minimist": "^1.2.0", "moment": "^2.21.0", - "request": "^2.85.0", + "request": "^2.88.0", + "request-promise": "^4.2.2", "varint": "^4.0.1" }, "bin": { diff --git a/protocols/americasarmy.js b/protocols/americasarmy.js deleted file mode 100644 index cf92afc..0000000 --- a/protocols/americasarmy.js +++ /dev/null @@ -1,25 +0,0 @@ -const Gamespy2 = require('./gamespy2'); - -class AmericasArmy extends Gamespy2 { - finalizeState(state) { - super.finalizeState(state); - state.name = this.stripColor(state.name); - state.map = this.stripColor(state.map); - for(const key of Object.keys(state.raw)) { - if(typeof state.raw[key] === 'string') { - state.raw[key] = this.stripColor(state.raw[key]); - } - } - for(const player of state.players) { - if(!('name' in player)) continue; - player.name = this.stripColor(player.name); - } - } - - stripColor(str) { - // uses unreal 2 color codes - return str.replace(/\x1b...|[\x00-\x1a]/g,''); - } -} - -module.exports = AmericasArmy; diff --git a/protocols/armagetron.js b/protocols/armagetron.js index e156023..1e52e8e 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -7,38 +7,35 @@ class Armagetron extends Core { this.byteorder = 'be'; } - run(state) { + async run(state) { const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); - this.udpSend(b,(buffer) => { - const reader = this.reader(buffer); + const buffer = await this.udpSend(b,b => b); + const reader = this.reader(buffer); - reader.skip(6); + reader.skip(6); - state.raw.port = 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); + 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) - }); - } + 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); - this.finish(state); - return true; - }); + state.raw.options = this.stripColorCodes(this.readString(reader)); + state.raw.uri = this.readString(reader); + state.raw.globalids = this.readString(reader); } readUInt(reader) { diff --git a/protocols/ase.js b/protocols/ase.js index 038a7c7..c6332b1 100644 --- a/protocols/ase.js +++ b/protocols/ase.js @@ -1,44 +1,42 @@ const Core = require('./core'); class Ase extends Core { - run(state) { - this.udpSend('s',(buffer) => { + async run(state) { + const buffer = await this.udpSend('s',(buffer) => { const reader = this.reader(buffer); - - const header = reader.string({length:4}); - if(header !== 'EYE1') return; - - state.raw.gamename = this.readString(reader); - state.raw.port = 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); - } - - this.finish(state); + const header = reader.string({length: 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) { diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 9f3463f..609da3f 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -1,127 +1,154 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Battlefield extends Core { constructor() { super(); this.encoding = 'latin1'; - this.isBadCompany2 = false; } - run(state) { - async.series([ - (c) => { - this.query(['serverInfo'], (data) => { - if(this.debug) console.log(data); - if(data.shift() !== 'OK') return this.fatal('Missing OK'); + 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()); - state.raw.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 + }); + } - 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(); - state.raw.targetscore = parseInt(data.shift()); - data.shift(); - state.raw.ranked = (data.shift() === 'true'); - state.raw.punkbuster = (data.shift() === 'true'); - state.password = (data.shift() === 'true'); - state.raw.uptime = parseInt(data.shift()); - state.raw.roundtime = parseInt(data.shift()); - if(this.isBadCompany2) { - data.shift(); - 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(); - state.raw.punkbusterversion = data.shift(); - state.raw.joinqueue = (data.shift() === 'true'); - state.raw.region = data.shift(); - if(!this.isBadCompany2) { - state.raw.pingsite = data.shift(); - state.raw.country = data.shift(); - state.raw.quickmatch = (data.shift() === 'true'); - } - - c(); - }); - }, - (c) => { - this.query(['version'], (data) => { - if(this.debug) console.log(data); - if(data[0] !== 'OK') return this.fatal('Missing OK'); - - state.raw.version = data[2]; - - c(); - }); - }, - (c) => { - this.query(['listPlayers','all'], (data) => { - if(this.debug) console.log(data); - if(data.shift() !== 'OK') return this.fatal('Missing OK'); - - 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); - } - - this.finish(state); - }); + 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); + } } - ]); - } - query(params,c) { - this.tcpSend(buildPacket(params), (data) => { - const decoded = this.decodePacket(data); - if(!decoded) return false; - c(decoded); - return true; }); } + + async query(socket, params) { + const outPacket = this.buildPacket(params); + return await this.tcpSend(socket, outPacket, (data) => { + const decoded = this.decodePacket(data); + if(decoded) { + this.debugLog(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.debugLog("Expected " + totalLength + " bytes, have " + buffer.length); const paramCount = reader.uint(4); const params = []; @@ -134,29 +161,4 @@ class Battlefield extends Core { } } -function 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; -} - module.exports = Battlefield; \ No newline at end of file diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index 6620c40..f52ed2e 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -1,59 +1,56 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'), + cheerio = require('cheerio'); class BuildAndShoot extends Core { - run(state) { - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/', - timeout: 3000, - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - - 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(/class="playerlist"([^]+?)\/table/); - if(m) { - const table = m[1]; - const pre = /[^]*([^]*)<\/td>[^]*([^]*)<\/td>[^]*([^]*)<\/td>[^]*([^]*)<\/td>/g; - let pm; - while(pm = pre.exec(table)) { - if(pm[2] === 'Ping') continue; - state.players.push({ - name: pm[1], - ping: pm[2], - team: pm[3], - score: pm[4] - }); - } - } - /* - 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; - } - */ - this.finish(state); + async run(state) { + const body = await this.request({ + uri: '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 1fadb09..38f7722 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -1,39 +1,81 @@ const EventEmitter = require('events').EventEmitter, dns = require('dns'), net = require('net'), - async = require('async'), Reader = require('../lib/reader'), - HexUtil = require('../lib/HexUtil'); + HexUtil = require('../lib/HexUtil'), + util = require('util'), + dnsLookupAsync = util.promisify(dns.lookup), + dnsResolveAsync = util.promisify(dns.resolve), + requestAsync = require('request-promise'), + Promises = require('../lib/Promises'); class Core extends EventEmitter { constructor() { super(); - this.options = { - socketTimeout: 2000, - attemptTimeout: 10000, - maxAttempts: 1 - }; - this.attempt = 1; - this.finished = false; this.encoding = 'utf8'; this.byteorder = 'le'; this.delimiter = '\0'; this.srvRecord = null; - this.attemptTimeoutTimer = null; + this.abortedPromise = null; + + // Sent to us by QueryRunner + this.options = null; + this.udpSocket = null; + this.shortestRTT = 0; + this.usedTcp = false; } - fatal(err,noretry) { - if(!noretry && this.attempt < this.options.maxAttempts) { - this.attempt++; - this.start(); - return; + async runAllAttempts() { + let result = null; + let lastError = null; + for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) { + try { + result = await this.runOnceSafe(); + result.query.attempts = attempt; + break; + } catch (e) { + lastError = e; + } } - this.done({error: err.toString()}); + if (result === null) { + throw lastError; + } + return result; } - initState() { - return { + // Runs a single attempt with a timeout and cleans up afterward + async runOnceSafe() { + let abortCall = null; + this.abortedPromise = new Promise((resolve,reject) => { + abortCall = () => reject("Query is finished -- cancelling outstanding promises"); + }); + + // Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection + this.abortedPromise.catch(() => {}); + + let timeout; + try { + const promise = this.runOnce(); + timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt"); + return await Promise.race([promise,timeout]); + } finally { + timeout && timeout.cancel(); + try { + abortCall(); + } catch(e) { + this.debugLog("Error during abort cleanup: " + e.stack); + } + } + } + + async runOnce() { + const options = this.options; + if (('host' in options) && !('address' in options)) { + options.address = await this.parseDns(options.host); + } + + const state = { name: '', map: '', password: false, @@ -44,130 +86,81 @@ class Core extends EventEmitter { players: [], bots: [] }; - } - finalizeState(state) {} + await this.run(state); - finish(state) { - this.finalizeState(state); - this.done(state); - } + // because lots of servers prefix with spaces to try to appear first + state.name = (state.name || '').trim(); - done(state) { - if(this.finished) return; - - if(this.options.notes) - state.notes = this.options.notes; - - state.query = {}; - if('host' in this.options) state.query.host = this.options.host; - if('address' in this.options) state.query.address = this.options.address; - if('port' in this.options) state.query.port = this.options.port; - if('port_query' in this.options) state.query.port_query = this.options.port_query; - state.query.type = this.type; - if('pretty' in this) state.query.pretty = this.pretty; - state.query.duration = Date.now() - this.startMillis; - state.query.attempts = this.attempt; - - this.reset(); - this.finished = true; - this.emit('finished',state); - if(this.options.callback) this.options.callback(state); - } - - reset() { - clearTimeout(this.attemptTimeoutTimer); - if(this.timers) { - for (const timer of this.timers) { - clearTimeout(timer); - } + if (!('connect' in state)) { + state.connect = '' + + (state.gameHost || this.options.host || this.options.address) + + ':' + + (state.gamePort || this.options.port) } - this.timers = []; + state.ping = this.shortestRTT; + delete state.gameHost; + delete state.gamePort; - if(this.tcpSocket) { - this.tcpSocket.destroy(); - delete this.tcpSocket; - } - this.udpTimeoutTimer = false; - this.udpCallback = false; + return state; } - start() { - const options = this.options; - this.reset(); + async run(state) {} - this.startMillis = Date.now(); - - this.attemptTimeoutTimer = setTimeout(() => { - this.fatal('timeout'); - },this.options.attemptTimeout); - - async.series([ - (c) => { - // resolve host names - if(!('host' in options)) return c(); - if(options.host.match(/\d+\.\d+\.\d+\.\d+/)) { - options.address = options.host; - c(); - } else { - this.parseDns(options.host,c); + /** + * @param {string} host + * @returns {Promise} + */ + async parseDns(host) { + const isIp = (host) => { + return !!host.match(/\d+\.\d+\.\d+\.\d+/); + }; + const resolveStandard = async (host) => { + if(isIp(host)) return host; + this.debugLog("Standard DNS Lookup: " + host); + const {address,family} = await dnsLookupAsync(host); + this.debugLog(address); + return address; + }; + const resolveSrv = async (srv,host) => { + if(isIp(host)) return host; + this.debugLog("SRV DNS Lookup: " + srv+'.'+host); + let records; + try { + records = await dnsResolveAsync(srv + '.' + host, 'SRV'); + this.debugLog(records); + if(records.length >= 1) { + const record = records[0]; + this.options.port = record.port; + const srvhost = record.name; + return await resolveStandard(srvhost); } - }, - (c) => { - // calculate query port if needed - if(!('port_query' in options) && 'port' in options) { - const offset = options.port_query_offset || 0; - options.port_query = options.port + offset; - } - c(); - }, - (c) => { - // run - this.run(this.initState()); + } catch(e) { + this.debugLog(e.toString()); } - - ]); - } - - run() {} - - parseDns(host,c) { - const resolveStandard = (host,c) => { - if(this.debug) console.log("Standard DNS Lookup: " + host); - dns.lookup(host, (err,address,family) => { - if(err) return this.fatal(err); - if(this.debug) console.log(address); - this.options.address = address; - c(); - }); + return await resolveStandard(host); }; - const resolveSrv = (srv,host,c) => { - if(this.debug) console.log("SRV DNS Lookup: " + srv+'.'+host); - dns.resolve(srv+'.'+host, 'SRV', (err,addresses) => { - if(this.debug) console.log(err, addresses); - if(err) return resolveStandard(host,c); - if(addresses.length >= 1) { - const line = addresses[0]; - this.options.port = line.port; - const srvhost = line.name; + if(this.srvRecord) return await resolveSrv(this.srvRecord, host); + else return await resolveStandard(host); + } - if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) { - this.options.address = srvhost; - c(); - } else { - // resolve yet again - resolveStandard(srvhost,c); - } - return; - } - return resolveStandard(host,c); - }); - }; - - if(this.srvRecord) resolveSrv(this.srvRecord,host,c); - else resolveStandard(host,c); + /** 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.debugLog("Registered RTT: " + param + "ms"); + if (this.shortestRTT === 0 || param < this.shortestRTT) { + this.shortestRTT = param; + } + } } // utils @@ -184,125 +177,225 @@ class Core extends EventEmitter { } } } - setTimeout(c,t) { - if(this.finished) return 0; - const id = setTimeout(c,t); - this.timers.push(id); - return id; - } 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 === 'yes') return true; + if(str.toLowerCase() === 'yes') return true; if(str === '1') return true; } return false; } - _tcpConnect(c) { - if(this.tcpSocket) return c(this.tcpSocket); - - let connected = false; - let received = Buffer.from([]); - const address = this.options.address; - const port = this.options.port_query; - - const socket = this.tcpSocket = net.connect(port,address,() => { - if(this.debug) console.log(address+':'+port+" TCPCONNECTED"); - connected = true; - c(socket); - }); - socket.setNoDelay(true); - if(this.debug) console.log(address+':'+port+" TCPCONNECT"); - - const writeHook = socket.write; - socket.write = (...args) => { - if(this.debug) { - console.log(address+':'+port+" TCP-->"); - console.log(HexUtil.debugDump(args[0])); - } - writeHook.apply(socket,args); - }; - - socket.on('error', () => {}); - socket.on('close', () => { - if(!this.tcpCallback) return; - if(connected) return this.fatal('Socket closed while waiting on TCP'); - else return this.fatal('TCP Connection Refused'); - }); - socket.on('data', (data) => { - if(!this.tcpCallback) return; - if(this.debug) { - console.log(address+':'+port+" <--TCP"); - console.log(HexUtil.debugDump(data)); - } - received = Buffer.concat([received,data]); - if(this.tcpCallback(received)) { - clearTimeout(this.tcpTimeoutTimer); - this.tcpCallback = false; - received = Buffer.from([]); - } - }); + assertValidPort(port) { + if (!port || port < 1 || port > 65535) { + throw new Error("Invalid tcp/ip port: " + port); + } } - tcpSend(buffer,ondata) { - process.nextTick(() => { - if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response'); - this._tcpConnect((socket) => { + + /** + * @template T + * @param {function(Socket):Promise} fn + * @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); + + this.debugLog(log => { + this.debugLog(address+':'+port+" TCP Connecting"); + const writeHook = socket.write; + socket.write = (...args) => { + log(address+':'+port+" TCP-->"); + log(HexUtil.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 {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.off('data', onData); + resolve(result); + } + }; + socket.on('data', onData); socket.write(buffer); }); - if(!ondata) return; - - this.tcpTimeoutTimer = this.setTimeout(() => { - this.tcpCallback = false; - this.fatal('TCP Watchdog Timeout'); - },this.options.socketTimeout); - this.tcpCallback = ondata; - }); + timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP'); + return await Promise.race([promise, timeout, this.abortedPromise]); + } finally { + timeout && timeout.cancel(); + } } - udpSend(buffer,onpacket,ontimeout) { - process.nextTick(() => { - if(this.udpCallback) return this.fatal('Attempted to send UDP packet while still waiting on a managed response'); - this._udpSendNow(buffer); - if(!onpacket) return; - - this.udpTimeoutTimer = this.setTimeout(() => { - this.udpCallback = false; - let timeout = false; - if(!ontimeout || ontimeout() !== true) timeout = true; - if(timeout) this.fatal('UDP Watchdog Timeout'); - },this.options.socketTimeout); - this.udpCallback = onpacket; - }); - } - _udpSendNow(buffer) { - if(!('port_query' in this.options)) return this.fatal('Attempted to send without setting a port'); - if(!('address' in this.options)) return this.fatal('Attempted to send without setting an address'); + /** + * @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'); + this.debugLog(log => { + log(address+':'+port+" UDP-->"); + log(HexUtil.debugDump(buffer)); + }); - if(this.debug) { - console.log(this.options.address+':'+this.options.port_query+" UDP-->"); - console.log(HexUtil.debugDump(buffer)); + const socket = this.udpSocket; + socket.send(buffer, address, port); + + 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.debugLog("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.debugLog("UDP timeout detected"); + if (onTimeout) { + try { + const result = onTimeout(); + if (result !== undefined) { + this.debugLog("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); } - this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); } - _udpResponse(buffer) { - if(this.udpCallback) { - const result = this.udpCallback(buffer); - if(result === true) { - // we're done with this udp session - clearTimeout(this.udpTimeoutTimer); - this.udpCallback = false; + + async request(params) { + // If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it. + // This will give us a much more accurate RTT than using the rtt of the http request. + if (!this.usedTcp) { + await this.withTcp(() => {}); + } + + let requestPromise; + try { + requestPromise = requestAsync({ + ...params, + timeout: this.options.socketTimeout, + resolveWithFullResponse: true + }); + this.debugLog(log => { + log(() => params.uri + " HTTP-->"); + requestPromise + .then((response) => log(params.uri + " <--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(); + } + } + + debugLog(...args) { + if (!this.options.debug) return; + try { + if(args[0] instanceof Buffer) { + this.debugLog(HexUtil.debugDump(args[0])); + } else if (typeof args[0] == 'function') { + const result = args[0].call(undefined, this.debugLog.bind(this)); + if (result !== undefined) { + this.debugLog(result); + } + } else { + console.log(...args); } - } else { - this.udpResponse(buffer); + } catch(e) { + console.log("Error while debug logging: " + e); } } - udpResponse() {} } module.exports = Core; diff --git a/protocols/doom3.js b/protocols/doom3.js index dc55713..401aa1a 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -3,89 +3,147 @@ const Core = require('./core'); class Doom3 extends Core { constructor() { super(); - this.pretty = 'Doom 3'; this.encoding = 'latin1'; this.isEtqw = false; this.hasSpaceBeforeClanTag = false; this.hasClanTag = false; this.hasTypeFlag = false; } - run(state) { - this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00', (buffer) => { - const reader = this.reader(buffer); - + 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; - - if(this.isEtqw) { - const taskId = reader.uint(4); - } - - const challenge = reader.uint(4); - const protoVersion = reader.uint(4); - state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff); - - if(this.isEtqw) { - const size = reader.uint(4); - } - - 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; - } - - let i = 0; - while(!reader.done()) { - i++; - const player = {}; - player.id = reader.uint(1); - if(player.id === 32) break; - player.ping = reader.uint(2); - if(!this.isEtqw) player.rate = reader.uint(4); - player.name = this.stripColors(reader.string()); - if(this.hasClanTag) { - if(this.hasSpaceBeforeClanTag) reader.uint(1); - player.clantag = this.stripColors(reader.string()); - } - if(this.hasTypeFlag) player.typeflag = reader.uint(1); - - if(!player.ping || player.typeflag) - state.bots.push(player); - else - state.players.push(player); - } - - state.raw.osmask = reader.uint(4); - if(this.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_usepass === '1') state.password = true; - - this.finish(state); - return true; + const challengePart1 = reader.string({length:4}); + if (challengePart1 !== "PiNG") return; + // some doom3 implementations only return the first 4 bytes of the challenge + const challengePart2 = reader.string({length: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.debugLog("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.debugLog(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.debugLog("starting player parse attempt:"); + this.debugLog("isEtqw: " + isEtqw); + this.debugLog("hasClanTag: " + hasClanTag); + this.debugLog("hasClanTagPos: " + hasClanTagPos); + this.debugLog("hasTypeFlag: " + hasTypeFlag); + const reader = this.reader(rest); + let lastId = -1; + const players = []; + while(true) { + this.debugLog("---"); + if (reader.done()) { + this.debugLog("* aborting attempt, overran buffer *"); + return null; + } + const player = {}; + player.id = reader.uint(1); + this.debugLog("id: " + player.id); + if (player.id <= lastId || player.id > 0x20) { + this.debugLog("* aborting attempt, invalid player id *"); + return null; + } + lastId = player.id; + if(player.id === 0x20) { + this.debugLog("* player parse successful *"); + break; + } + player.ping = reader.uint(2); + this.debugLog("ping: " + player.ping); + if(!isEtqw) { + player.rate = reader.uint(4); + this.debugLog("rate: " + player.rate); + } + player.name = this.stripColors(reader.string()); + this.debugLog("name: " + player.name); + if(hasClanTag) { + if(hasClanTagPos) { + const clanTagPos = reader.uint(1); + this.debugLog("clanTagPos: " + clanTagPos); + } + player.clantag = this.stripColors(reader.string()); + this.debugLog("clan tag: " + player.clantag); + } + if(hasTypeFlag) { + player.typeflag = reader.uint(1); + this.debugLog("type flag: " + player.typeflag); + } + players.push(player); + } + return [players,reader]; } stripColors(str) { diff --git a/protocols/ffow.js b/protocols/ffow.js index 6df907b..ca82d55 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -6,29 +6,34 @@ class Ffow extends Valve { this.byteorder = 'be'; this.legacyChallenge = true; } - queryInfo(state,c) { - this.sendPacket(0x46,false,'LSQ',0x49, (b) => { - 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.raw.port = 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); - c(); - }); + async queryInfo(state) { + this.debugLog("Requesting ffow info ..."); + const b = await this.sendPacket( + 0x46, + false, + '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 49ed7a7..b091ccc 100644 --- a/protocols/fivem.js +++ b/protocols/fivem.js @@ -1,5 +1,4 @@ -const request = require('request'), - Quake2 = require('./quake2'); +const Quake2 = require('./quake2'); class FiveM extends Quake2 { constructor() { @@ -9,43 +8,28 @@ class FiveM extends Quake2 { this.encoding = 'utf8'; } - finish(state) { - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/info.json', - timeout: this.options.socketTimeout - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid JSON'); - } + async run(state) { + await super.run(state); - state.raw.info = json; - - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/players.json', - timeout: this.options.socketTimeout - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid JSON'); - } - - state.raw.players = json; - - state.players = []; - for (const player of json) { - state.players.push({name:player.name, ping:player.ping}); - } - - super.finish(state); + { + const raw = await this.request({ + uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json' }); - }); + const json = JSON.parse(raw); + state.raw.info = json; + } + + { + const raw = await this.request({ + uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json' + }); + const json = JSON.parse(raw); + state.raw.players = json; + state.players = []; + 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 76a078d..5a1ed46 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -1,68 +1,58 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Gamespy1 extends Core { constructor() { super(); - this.sessionId = 1; this.encoding = 'latin1'; this.byteorder = 'be'; } - run(state) { - async.series([ - (c) => { - this.sendPacket('info', (data) => { - state.raw = data; - 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); - c(); - }); - }, - (c) => { - this.sendPacket('rules', (data) => { - state.raw.rules = data; - c(); - }); - }, - (c) => { - this.sendPacket('players', (data) => { - const players = {}; - const teams = {}; - for(const ident of Object.keys(data)) { - const split = ident.split('_'); - let key = split[0]; - const id = split[1]; - let value = data[ident]; + async run(state) { + { + const data = await this.sendPacket('info'); + state.raw = data; + 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); + } + { + const data = await this.sendPacket('rules'); + state.raw.rules = data; + } + { + const data = await this.sendPacket('players'); + const players = {}; + const teams = {}; + for (const ident of Object.keys(data)) { + const split = ident.split('_'); + let key = split[0]; + const id = split[1]; + let value = data[ident]; - if(key === 'teamname') { - teams[id] = value; - } else { - if(!(id in players)) players[id] = {}; - if(key === 'playername') key = 'name'; - else if(key === 'team') value = parseInt(value); - else if(key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value); - players[id][key] = value; - } - } - - state.raw.teams = teams; - for(const id of Object.keys(players)) { - state.players.push(players[id]); - } - this.finish(state); - }); + if (key === 'teamname') { + teams[id] = value; + } else { + if (!(id in players)) players[id] = {}; + if (key === 'playername') key = 'name'; + else if (key === 'team') value = parseInt(value); + else if (key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value); + players[id][key] = value; + } } - ]); + state.raw.teams = teams; + for (const id of Object.keys(players)) { + state.players.push(players[id]); + } + } } - sendPacket(type,callback) { + async sendPacket(type) { const queryId = ''; const output = {}; - this.udpSend('\\'+type+'\\', (buffer) => { + return await this.udpSend('\\'+type+'\\', buffer => { const reader = this.reader(buffer); const str = reader.string({length:buffer.length}); const split = str.split('\\'); @@ -79,8 +69,7 @@ class Gamespy1 extends Core { if('final' in output) { delete output.final; delete output.queryid; - callback(output); - return true; + return output; } }); } diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index d6c7f25..2936762 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -3,65 +3,105 @@ const Core = require('./core'); class Gamespy2 extends Core { constructor() { super(); - this.sessionId = 1; this.encoding = 'latin1'; this.byteorder = 'be'; } - run(state) { - const request = Buffer.from([0xfe,0xfd,0x00,0x00,0x00,0x00,0x01,0xff,0xff,0xff]); - const packets = []; - this.udpSend(request, - (buffer) => { - if(packets.length && buffer.readUInt8(0) === 0) - buffer = buffer.slice(1); - packets.push(buffer); - }, - () => { - const buffer = Buffer.concat(packets); - const reader = this.reader(buffer); - const header = reader.uint(1); - if(header !== 0) return; - const pingId = reader.uint(4); - if(pingId !== 1) return; - - 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); - - state.players = this.readFieldData(reader); - state.raw.teams = this.readFieldData(reader); - - this.finish(state); - return true; + 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); + state.players = this.readFieldData(reader); + } + + // 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 count = reader.uint(1); - // count is unreliable (often it's wrong), so we don't use it. - // read until we hit an empty first field string + const zero = reader.uint(1); // always 0 + const count = reader.uint(1); // number of rows in this data - if(this.debug) console.log("Reading fields, starting at: "+reader.rest()); + // 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.debugLog("Detected missing count byte, rewinding by 1"); + } else { + this.debugLog("Detected row count: " + count); + } + + this.debugLog(() => "Reading fields, starting at: "+reader.rest()); const fields = []; while(!reader.done()) { let field = reader.string(); if(!field) break; - if(field.charCodeAt(0) <= 2) field = field.substring(1); fields.push(field); - if(this.debug) console.log("field:"+field); + this.debugLog("field:"+field); } + if (!fields.length) return []; + const units = []; outer: while(!reader.done()) { const unit = {}; @@ -69,7 +109,7 @@ class Gamespy2 extends Core { let key = fields[iField]; let value = reader.string(); if(!value && iField === 0) break outer; - if(this.debug) console.log("value:"+value); + this.debugLog("value:"+value); if(key === 'player_') key = 'name'; else if(key === 'score_') key = 'score'; else if(key === 'deaths_') key = 'deaths'; diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 48ace96..c3408ab 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -1,5 +1,5 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'), + HexUtil = require('../lib/HexUtil'); class Gamespy3 extends Core { constructor() { @@ -7,148 +7,133 @@ class Gamespy3 extends Core { this.sessionId = 1; this.encoding = 'latin1'; this.byteorder = 'be'; - this.noChallenge = false; this.useOnlySingleSplit = false; this.isJc2mp = false; } - run(state) { - let challenge; + async run(state) { + const buffer = await this.sendPacket(9, false, false, false); + const reader = this.reader(buffer); + let challenge = parseInt(reader.string()); + this.debugLog("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[] */ - let packets; + const packets = await this.sendPacket(0,challenge,requestPayload,true); - async.series([ - (c) => { - if(this.noChallenge) return c(); - this.sendPacket(9,false,false,false,(buffer) => { - const reader = this.reader(buffer); - challenge = parseInt(reader.string()); - c(); - }); - }, - (c) => { - 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]); + // 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.debugLog("Parsing packet #" + iPacket); + this.debugLog(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.debugLog(key + " = " + value); } - - this.sendPacket(0,challenge,requestPayload,true,(b) => { - packets = b; - c(); - }); - }, - (c) => { - // 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); - - if(this.debug) { - console.log("+++"+packet.toString('hex')); - console.log(":::"+packet.toString('ascii')); - } - - // Parse raw server key/values - - if(iPacket === 0) { - while(!reader.done()) { - const key = reader.string(); - if(!key) break; - let value = reader.string(); - - // reread the next line if we hit the weird ut3 bug - if(value === 'p1073741829') value = reader.string(); - - state.raw[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()) { - let mode = reader.string(); - if(mode.charCodeAt(0) <= 2) mode = mode.substring(1); - if(!mode) continue; - let offset = 0; - if(iPacket !== 0 && firstMode) offset = reader.uint(1); - reader.skip(1); - firstMode = false; - - const modeSplit = mode.split('_'); - const modeName = modeSplit[0]; - const modeType = modeSplit.length > 1 ? modeSplit[1] : 'no_'; - - if(!(modeType in state.raw.playerTeamInfo)) { - state.raw.playerTeamInfo[modeType] = []; - } - const store = state.raw.playerTeamInfo[modeType]; - - while(!reader.done()) { - const item = reader.string(); - if(!item) break; - - while(store.length <= offset) { store.push({}); } - store[offset][modeName] = item; - offset++; - } - } - } - } - - c(); - }, - - (c) => { - // 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('' 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); - } - } - - this.finish(state); } - ]); + + // 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.debugLog(() => "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.debugLog("* " + 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); + } + } } - sendPacket(type,challenge,payload,assemble,c) { - const challengeLength = (this.noChallenge || challenge === false) ? 0 : 4; + 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); @@ -161,7 +146,7 @@ class Gamespy3 extends Core { let numPackets = 0; const packets = {}; - this.udpSend(b,(buffer) => { + return this.udpSend(b,(buffer) => { const reader = this.reader(buffer); const iType = reader.uint(1); if(iType !== type) return; @@ -169,14 +154,12 @@ class Gamespy3 extends Core { if(iSessionId !== this.sessionId) return; if(!assemble) { - c(reader.rest()); - return true; + return reader.rest(); } if(this.useOnlySingleSplit) { // has split headers, but they are worthless and only one packet is used reader.skip(11); - c([reader.rest()]); - return true; + return [reader.rest()]; } reader.skip(9); // filler data -- usually set to 'splitnum\0' @@ -189,8 +172,7 @@ class Gamespy3 extends Core { packets[id] = reader.rest(); if(this.debug) { - console.log("Received packet #"+id); - if(last) console.log("(last)"); + this.debugLog("Received packet #"+id + (last ? " (last)" : "")); } if(!numPackets || Object.keys(packets).length !== numPackets) return; @@ -199,13 +181,11 @@ class Gamespy3 extends Core { const list = []; for(let i = 0; i < numPackets; i++) { if(!(i in packets)) { - this.fatal('Missing packet #'+i); - return true; + throw new Error('Missing packet #'+i); } list.push(packets[i]); } - c(list); - return true; + return list; }); } } diff --git a/protocols/geneshift.js b/protocols/geneshift.js index bdfff25..b9559c6 100644 --- a/protocols/geneshift.js +++ b/protocols/geneshift.js @@ -1,53 +1,49 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'); class GeneShift extends Core { - run(state) { - request({ - uri: 'http://geneshift.net/game/receiveLobby.php', - timeout: 3000, - }, (e,r,body) => { - if(e) return this.fatal('Lobby request error'); - - const split = body.split('
'); - let found = false; - 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) return this.fatal('Server not found in list'); - - state.raw.countrycode = found[0]; - state.raw.country = found[1]; - state.name = found[4]; - state.map = found[5]; - state.raw.numplayers = 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]; - - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - - this.finish(state); + async run(state) { + const body = await this.request({ + uri: '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.raw.numplayers = 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]; + + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } } } diff --git a/protocols/hexen2.js b/protocols/hexen2.js index 067b23c..4773b0a 100644 --- a/protocols/hexen2.js +++ b/protocols/hexen2.js @@ -6,6 +6,10 @@ class Hexen2 extends Quake1 { this.sendHeader = '\xFFstatus\x0a'; this.responseHeader = '\xffn'; } + async run(state) { + await super.run(state); + state.gamePort = this.options.port - 50; + } } module.exports = Hexen2; diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 2c704e1..77d2bf6 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -1,14 +1,16 @@ const Gamespy3 = require('./gamespy3'); // supposedly, gamespy3 is the "official" query protocol for jcmp, -// but it's broken (requires useOnlySingleSplit), and doesn't include player names +// but it's broken (requires useOnlySingleSplit), and may not include some player names class Jc2mp extends Gamespy3 { constructor() { super(); this.useOnlySingleSplit = true; + this.isJc2mp = true; + this.encoding = 'utf8'; } - finalizeState(state) { - super.finalizeState(state); + async run(state) { + await super.run(state); if(!state.players.length && parseInt(state.raw.numplayers)) { for(let i = 0; i < parseInt(state.raw.numplayers); i++) { state.players.push({}); diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 9008c15..29e402b 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -1,38 +1,28 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'); class Kspdmp extends Core { - run(state) { - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query, - timeout: this.options.socketTimeout - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid 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; - if (json.players) { - const split = json.players.split(', '); - for (const name of split) { - state.players.push({name:name}); - } - } - - this.finish(state); + async run(state) { + const body = await this.request({ + uri: 'http://'+this.options.address+':'+this.options.port }); + + const json = JSON.parse(body); + 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}); + } + } } } diff --git a/protocols/m2mp.js b/protocols/m2mp.js index 262b728..3d21fbc 100644 --- a/protocols/m2mp.js +++ b/protocols/m2mp.js @@ -6,30 +6,29 @@ class M2mp extends Core { this.encoding = 'latin1'; } - run(state) { - this.udpSend('M2MP',(buffer) => { + async run(state) { + const body = await this.udpSend('M2MP',(buffer) => { const reader = this.reader(buffer); - - const header = reader.string({length:4}); - if(header !== 'M2MP') return; - - state.name = this.readString(reader); - state.raw.numplayers = this.readString(reader); - state.maxplayers = this.readString(reader); - state.raw.gamemode = this.readString(reader); - state.password = !!reader.uint(1); - - while(!reader.done()) { - const name = this.readString(reader); - if(!name) break; - state.players.push({ - name:name - }); - } - - this.finish(state); - return true; + const header = reader.string({length: 4}); + if (header !== 'M2MP') return; + return reader.rest(); }); + + const reader = this.reader(body); + state.name = this.readString(reader); + state.raw.numplayers = this.readString(reader); + state.maxplayers = this.readString(reader); + state.raw.gamemode = this.readString(reader); + state.password = !!reader.uint(1); + state.gamePort = this.options.port - 1; + + while(!reader.done()) { + const name = this.readString(reader); + if(!name) break; + state.players.push({ + name:name + }); + } } readString(reader) { diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 61c0730..260751a 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -1,98 +1,79 @@ -const varint = require('varint'), - async = require('async'), - Core = require('./core'); - -function varIntBuffer(num) { - return Buffer.from(varint.encode(num)); -} -function buildPacket(id,data) { - if(!data) data = Buffer.from([]); - const idBuffer = varIntBuffer(id); - return Buffer.concat([ - varIntBuffer(data.length+idBuffer.length), - idBuffer, - data - ]); -} +const Core = require('./core'), + Varint = require('varint'); class Minecraft extends Core { - run(state) { - /** @type Buffer */ - let receivedData; + constructor() { + super(); + this.srvRecord = "_minecraft._tcp"; + } + async run(state) { + const portBuf = Buffer.alloc(2); + portBuf.writeUInt16BE(this.options.port,0); - async.series([ - (c) => { - // build and send handshake and status TCP packet + const addressBuf = Buffer.from(this.options.host,'utf8'); - const portBuf = Buffer.alloc(2); - portBuf.writeUInt16BE(this.options.port_query,0); + const bufs = [ + this.varIntBuffer(4), + this.varIntBuffer(addressBuf.length), + addressBuf, + portBuf, + this.varIntBuffer(1) + ]; - const addressBuf = Buffer.from(this.options.address,'utf8'); + const outBuffer = Buffer.concat([ + this.buildPacket(0,Buffer.concat(bufs)), + this.buildPacket(0) + ]); - const bufs = [ - varIntBuffer(4), - varIntBuffer(addressBuf.length), - addressBuf, - portBuf, - varIntBuffer(1) - ]; + 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 outBuffer = Buffer.concat([ - buildPacket(0,Buffer.concat(bufs)), - buildPacket(0) - ]); + const reader = this.reader(data); - this.tcpSend(outBuffer, (data) => { - if(data.length < 10) return false; - const expected = varint.decode(data); - data = data.slice(varint.decode.bytes); - if(data.length < expected) return false; - receivedData = data; - c(); - return true; + const packetId = reader.varint(); + this.debugLog("Packet ID: "+packetId); + + const strLen = reader.varint(); + this.debugLog("String Length: "+strLen); + + const str = reader.rest().toString('utf8'); + this.debugLog(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 }); - }, - (c) => { - // parse response - - let data = receivedData; - const packetId = varint.decode(data); - if(this.debug) console.log("Packet ID: "+packetId); - data = data.slice(varint.decode.bytes); - - const strLen = varint.decode(data); - if(this.debug) console.log("String Length: "+strLen); - data = data.slice(varint.decode.bytes); - - const str = data.toString('utf8'); - if(this.debug) { - console.log(str); - } - - let json; - try { - json = JSON.parse(str); - delete json.favicon; - } catch(e) { - return this.fatal('Invalid JSON'); - } - - 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 - }); - } - } - while(state.players.length < json.players.online) { - state.players.push({}); - } - - this.finish(state); } + } + while(state.players.length < json.players.online) { + 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 cf3f060..f60f3c4 100644 --- a/protocols/mumble.js +++ b/protocols/mumble.js @@ -1,40 +1,36 @@ const Core = require('./core'); class Mumble extends Core { - constructor() { - super(); - this.options.socketTimeout = 5000; - } - - run(state) { - this.tcpSend('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; - } - - state.raw = json; - state.name = json.name; - - 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); + 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; } - } - - this.finish(state); - return true; + 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) { diff --git a/protocols/mumbleping.js b/protocols/mumbleping.js index 30a4adf..7737dee 100644 --- a/protocols/mumbleping.js +++ b/protocols/mumbleping.js @@ -6,24 +6,23 @@ class MumblePing extends Core { this.byteorder = 'be'; } - run(state) { - this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { - if(buffer.length < 24) return; - const reader = this.reader(buffer); - 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.raw.numplayers = reader.uint(4); - state.maxplayers = reader.uint(4); - state.raw.allowedbandwidth = reader.uint(4); - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - this.finish(state); - return true; + 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.raw.numplayers = reader.uint(4); + state.maxplayers = reader.uint(4); + state.raw.allowedbandwidth = reader.uint(4); + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } } } diff --git a/protocols/nadeo.js b/protocols/nadeo.js index fe851c0..2280bd7 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -1,85 +1,97 @@ const gbxremote = require('gbxremote'), - async = require('async'), Core = require('./core'); class Nadeo extends Core { - constructor() { - super(); - this.options.port = 2350; - this.options.port_query = 5000; - this.gbxclient = false; - } + async run(state) { + await this.withClient(async client => { + const start = Date.now(); + await this.methodCall(client, 'Authenticate', this.options.login, this.options.password); + this.registerRtt(Date.now()-start); - reset() { - super.reset(); - if(this.gbxclient) { - this.gbxclient.terminate(); - this.gbxclient = false; - } - } + //const data = this.methodCall(client, 'GetStatus'); - run(state) { - const cmds = [ - ['Connect'], - ['Authenticate', this.options.login,this.options.password], - ['GetStatus'], // 1 - ['GetPlayerList',10000,0], // 2 - ['GetServerOptions'], // 3 - ['GetCurrentMapInfo'], // 4 - ['GetCurrentGameInfo'], // 5 - ['GetNextMapInfo'] // 6 - ]; - const results = []; - - async.eachSeries(cmds, (cmdset,c) => { - const cmd = cmdset[0]; - const params = cmdset.slice(1); - - if(cmd === 'Connect') { - const client = this.gbxclient = gbxremote.createClient(this.options.port_query,this.options.host, (err) => { - if(err) return this.fatal('GBX error '+JSON.stringify(err)); - c(); - }); - client.on('error',() => {}); - } else { - this.gbxclient.methodCall(cmd, params, (err, value) => { - if(err) return this.fatal('XMLRPC error '+JSON.stringify(err)); - results.push(value); - c(); - }); + { + const results = await this.methodCall(client, 'GetServerOptions'); + state.name = this.stripColors(results.Name); + state.password = (results.Password !== 'No password'); + state.maxplayers = results.CurrentMaxPlayers; + state.raw.maxspectators = results.CurrentMaxSpectators; } - }, () => { - let gamemode = ''; - const igm = results[5].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.name = this.stripColors(results[3].Name); - state.password = (results[3].Password !== 'No password'); - state.maxplayers = results[3].CurrentMaxPlayers; - state.raw.maxspectators = results[3].CurrentMaxSpectators; - state.map = this.stripColors(results[4].Name); - state.raw.mapUid = results[4].UId; - state.raw.gametype = gamemode; - state.raw.players = results[2]; - state.raw.mapcount = results[5].NbChallenge; - state.raw.nextmapName = this.stripColors(results[6].Name); - state.raw.nextmapUid = results[6].UId; + { + const results = await this.methodCall(client, 'GetCurrentMapInfo'); + state.map = this.stripColors(results.Name); + state.raw.mapUid = results.UId; + } + { + const results = await this.methodCall(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.methodCall(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.methodCall(client, 'GetPlayerList', 10000, 0); for (const player of state.raw.players) { state.players.push({ name:this.stripColors(player.Name || player.NickName) }); } - - this.finish(state); }); } + async withClient(fn) { + const socket = gbxremote.createClient(this.options.port, this.options.host); + const cancelAsyncLeak = this.addCleanup(() => socket.terminate()); + try { + await this.timedPromise( + new Promise((resolve,reject) => { + socket.on('connect', resolve); + socket.on('error', e => reject(new Error('GBX Remote Connection Error: ' + e))); + socket.on('close', () => reject(new Error('GBX Remote Connection Refused'))); + }), + this.options.socketTimeout, + 'GBX Remote Opening' + ); + return await fn(socket); + } finally { + cancelAsyncLeak(); + socket.terminate(); + } + } + + async methodCall(client, ...cmdset) { + const cmd = cmdset[0]; + const params = cmdset.slice(1); + return await this.timedPromise( + new Promise(async (resolve,reject) => { + client.methodCall(cmd, params, (err, value) => { + if (err) reject('XMLRPC error ' + JSON.stringify(err)); + resolve(value); + }); + }), + this.options.socketTimeout, + 'GBX Method Call' + ); + } + stripColors(str) { return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,''); } diff --git a/protocols/openttd.js b/protocols/openttd.js index 39558ad..09c0959 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -1,131 +1,116 @@ -const async = require('async'), - moment = require('moment'), +const moment = require('moment'), Core = require('./core'); class OpenTtd extends Core { - run(state) { - async.series([ - (c) => { - this.query(0,1,1,4,(reader, version) => { - 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.raw.numplayers = reader.uint(1); - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - 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); - - c(); - }); - }, - - (c) => { - const vehicle_types = ['train','truck','bus','aircraft','ship']; - const station_types = ['station','truckbay','busstation','airport','dock']; - - this.query(2,3,-1,-1, (reader,version) => { - // we don't know how to deal with companies outside version 6 - if(version !== 6) return c(); - - 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); - company.money = reader.uint(8); - company.income = reader.uint(8); - company.performance = reader.uint(2); - company.password = !!reader.uint(1); - - 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); - } - - c(); - }); - }, - - (c) => { - this.finish(state); + 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.raw.numplayers = reader.uint(1); + for (let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + 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); + company.money = reader.uint(8); + company.income = reader.uint(8); + 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); + } + } + } } - query(type,expected,minver,maxver,done) { + async query(type,expected,minver,maxver) { const b = Buffer.from([0x03,0x00,type]); - this.udpSend(b,(buffer) => { + return await this.udpSend(b,(buffer) => { const reader = this.reader(buffer); const packetLen = reader.uint(2); if(packetLen !== buffer.length) { - this.fatal('Invalid reported packet length: '+packetLen+' '+buffer.length); - return true; + this.debugLog('Invalid reported packet length: '+packetLen+' '+buffer.length); + return; } const packetType = reader.uint(1); if(packetType !== expected) { - this.fatal('Unexpected response packet type: '+packetType); - return true; + this.debugLog('Unexpected response packet type: '+packetType); + return; } const protocolVersion = reader.uint(1); if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { - this.fatal('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); - return true; + throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); } - done(reader,protocolVersion); - return true; + return [reader,protocolVersion]; }); } diff --git a/protocols/quake2.js b/protocols/quake2.js index b9f8706..78215be 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -10,79 +10,78 @@ class Quake2 extends Core { this.isQuake1 = false; } - run(state) { - this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', (buffer) => { - const reader = this.reader(buffer); - - const header = reader.string({length:4,encoding:'latin1'}); - if(header !== '\xff\xff\xff\xff') return; - - let response; - if(this.isQuake1) { - response = reader.string({length:this.responseHeader.length}); + 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({length: this.responseHeader.length}); } else { - response = reader.string({encoding:'latin1'}); + type = reader.string({encoding: 'latin1'}); } - if(response !== this.responseHeader) return; - - 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() || ''; - player.address = args.shift() || ''; - } - - (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; - - this.finish(state); - return true; + 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 77a1be1..70a7b2c 100644 --- a/protocols/quake3.js +++ b/protocols/quake3.js @@ -6,7 +6,8 @@ class Quake3 extends Quake2 { this.sendHeader = 'getstatus'; this.responseHeader = 'statusResponse'; } - finalizeState(state) { + 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]); diff --git a/protocols/samp.js b/protocols/samp.js index 43934a4..57cff96 100644 --- a/protocols/samp.js +++ b/protocols/samp.js @@ -1,5 +1,4 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Samp extends Core { constructor() { @@ -7,87 +6,83 @@ class Samp extends Core { this.encoding = 'win1252'; } - run(state) { - async.series([ - (c) => { - this.sendPacket('i',(reader) => { - state.password = !!reader.uint(1); - state.raw.numplayers = reader.uint(2); - state.maxplayers = reader.uint(2); - state.name = this.readString(reader,4); - state.raw.gamemode = this.readString(reader,4); - this.map = this.readString(reader,4); - c(); - }); - }, - (c) => { - this.sendPacket('r',(reader) => { - const ruleCount = reader.uint(2); - state.raw.rules = {}; - for(let i = 0; i < ruleCount; i++) { - const key = this.readString(reader,1); - const value = this.readString(reader,1); - state.raw.rules[key] = value; - } - if('mapname' in state.raw.rules) - state.map = state.raw.rules.mapname; - c(); - }); - }, - (c) => { - this.sendPacket('d',(reader) => { - const playerCount = reader.uint(2); - for(let i = 0; i < playerCount; i++) { - const player = {}; - player.id = reader.uint(1); - player.name = this.readString(reader,1); - player.score = reader.int(4); - player.ping = reader.uint(4); - state.players.push(player); - } - c(); - },() => { - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - c(); - }); - }, - (c) => { - this.finish(state); + async run(state) { + // read info + { + const reader = await this.sendPacket('i'); + state.password = !!reader.uint(1); + state.raw.numplayers = reader.uint(2); + state.maxplayers = reader.uint(2); + state.name = this.readString(reader,4); + state.raw.gamemode = this.readString(reader,4); + this.map = this.readString(reader,4); + } + + // read rules + { + const reader = await this.sendPacket('r'); + const ruleCount = reader.uint(2); + state.raw.rules = {}; + for(let i = 0; i < ruleCount; i++) { + const key = this.readString(reader,1); + const value = this.readString(reader,1); + state.raw.rules[key] = value; } - ]); + if('mapname' in state.raw.rules) + state.map = state.raw.rules.mapname; + } + + // read players + { + const reader = await this.sendPacket('d', true); + if (reader !== null) { + const playerCount = reader.uint(2); + for(let i = 0; i < playerCount; i++) { + const player = {}; + player.id = reader.uint(1); + player.name = this.readString(reader,1); + player.score = reader.int(4); + player.ping = reader.uint(4); + state.players.push(player); + } + } else { + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + } + } } readString(reader,lenBytes) { const length = reader.uint(lenBytes); if(!length) return ''; - const string = reader.string({length:length}); - return string; + return reader.string({length:length}); } - sendPacket(type,onresponse,ontimeout) { - const outbuffer = Buffer.alloc(11); - outbuffer.writeUInt32BE(0x53414D50,0); + async sendPacket(type,allowTimeout) { + const outBuffer = Buffer.alloc(11); + outBuffer.writeUInt32BE(0x53414D50,0); 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); + 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); - this.udpSend(outbuffer,(buffer) => { - const reader = this.reader(buffer); - for(let i = 0; i < outbuffer.length; i++) { - if(outbuffer.readUInt8(i) !== reader.uint(1)) return; + return await this.udpSend( + outBuffer, + (buffer) => { + const reader = this.reader(buffer); + for(let i = 0; i < outBuffer.length; i++) { + if(outBuffer.readUInt8(i) !== reader.uint(1)) return; + } + return reader; + }, + () => { + if(allowTimeout) { + return null; + } } - onresponse(reader); - return true; - },() => { - if(ontimeout) { - ontimeout(); - return true; - } - }); + ); } } diff --git a/protocols/starmade.js b/protocols/starmade.js index a1ee3ce..18d063b 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -6,58 +6,59 @@ class Starmade extends Core { this.encoding = 'latin1'; this.byteorder = 'be'; } - run(state) { + + async run(state) { const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]); - this.tcpSend(b,(buffer) => { - const reader = this.reader(buffer); - - if(buffer.length < 4) return false; - const packetLength = reader.uint(4); - if(buffer.length < packetLength+12) return false; - - const data = []; - state.raw.data = data; - - reader.skip(2); - 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 - const length = reader.uint(2); - data.push(reader.string(length)); - } else if(mark === 6) { - // byte - data.push(reader.uint(1)); - } - } - - if(data.length < 9) { - this.fatal("Not enough units in data packet"); - return true; - } - - if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, ''); - if(typeof data[4] === 'string') state.name = data[4]; - if(typeof data[5] === 'string') state.raw.description = data[5]; - if(typeof data[7] === 'number') state.raw.numplayers = data[7]; - if(typeof data[8] === 'number') state.maxplayers = data[8]; - - if('numplayers' in state.raw) { - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - } - - this.finish(state); - return true; + const payload = await this.withTcp(async socket => { + return await this.tcpSend(socket, b, buffer => { + if (buffer.length < 4) return; + const reader = this.reader(buffer); + const packetLength = reader.uint(4); + if (buffer.length < packetLength + 12) return; + return reader.rest(); + }); }); + + const reader = this.reader(payload); + + const data = []; + state.raw.data = data; + + reader.skip(2); + 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 + const length = reader.uint(2); + data.push(reader.string(length)); + } else if(mark === 6) { + // byte + data.push(reader.uint(1)); + } + } + + if(data.length < 9) { + throw new Error("Not enough units in data packet"); + } + + if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, ''); + if(typeof data[4] === 'string') state.name = data[4]; + if(typeof data[5] === 'string') state.raw.description = data[5]; + if(typeof data[7] === 'number') state.raw.numplayers = data[7]; + if(typeof data[8] === 'number') state.maxplayers = data[8]; + + if('numplayers' in state.raw) { + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + } } } diff --git a/protocols/teamspeak2.js b/protocols/teamspeak2.js index d2e665e..fc449c0 100644 --- a/protocols/teamspeak2.js +++ b/protocols/teamspeak2.js @@ -1,77 +1,70 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Teamspeak2 extends Core { - run(state) { - async.series([ - (c) => { - this.sendCommand('sel '+this.options.port, (data) => { - if(data !== '[TS]') this.fatal('Invalid header'); - c(); - }); - }, - (c) => { - this.sendCommand('si', (data) => { - for (const line of data.split('\r\n')) { - const equals = line.indexOf('='); - const key = equals === -1 ? line : line.substr(0,equals); - const value = equals === -1 ? '' : line.substr(equals+1); - state.raw[key] = value; - } - c(); - }); - }, - (c) => { - this.sendCommand('pl', (data) => { - 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); - } - c(); - }); - }, - (c) => { - this.sendCommand('cl', (data) => { - 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); - } - c(); - }); - }, - (c) => { - this.finish(state); + 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.substr(0,equals); + const value = equals === -1 ? '' : line.substr(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); } - sendCommand(cmd,c) { - this.tcpSend(cmd+'\x0A', (buffer) => { + + 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; - c(buffer.slice(0,-6).toString()); - return true; + return buffer.slice(0,-6).toString(); }); } } diff --git a/protocols/teamspeak3.js b/protocols/teamspeak3.js index 3d4e59a..7fdf6bd 100644 --- a/protocols/teamspeak3.js +++ b/protocols/teamspeak3.js @@ -1,78 +1,67 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Teamspeak3 extends Core { - run(state) { - async.series([ - (c) => { - this.sendCommand('use port='+this.options.port, (data) => { - const split = data.split('\n\r'); - if(split[0] !== 'TS3') this.fatal('Invalid header'); - c(); - }, true); - }, - (c) => { - this.sendCommand('serverinfo', (data) => { - 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; - c(); - }); - }, - (c) => { - this.sendCommand('clientlist', (list) => { - for (const client of list) { - client.name = client.client_nickname; - delete client.client_nickname; - if(client.client_type === '0') { - state.players.push(client); - } - } - c(); - }); - }, - (c) => { - this.sendCommand('channellist -topic', (data) => { - state.raw.channels = data; - c(); - }); - }, - (c) => { - this.finish(state); + 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'); } - ]); - } - sendCommand(cmd,c,raw) { - this.tcpSend(cmd+'\x0A', (buffer) => { - if(buffer.length < 21) return; - if(buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return; - const body = buffer.slice(0,-21).toString(); - let out; + { + 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; + } - if(raw) { - out = body; - } else { - const segments = body.split('|'); - 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.substr(0,equals); - const value = equals === -1 ? '' : field.substr(equals+1) - .replace(/\\s/g,' ').replace(/\\\//g,'/'); - unit[key] = value; + { + 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); } - out.push(unit); } } - c(out); + { + const data = await this.sendCommand(socket, 'channellist -topic'); + state.raw.channels = data; + } + }, queryPort); + } - return true; + 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.substr(0,equals); + const value = equals === -1 ? '' : field.substr(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 e343f00..688194d 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -1,36 +1,25 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'); class Terraria extends Core { - run(state) { - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status', - timeout: this.options.socketTimeout, + async run(state) { + const body = await this.request({ + uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status', qs: { players: 'true', token: this.options.token } - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid JSON'); - } - - if(json.status !== 200) return this.fatal('Invalid status'); - - for (const one of json.players) { - state.players.push({name:one.nickname,team:one.team}); - } - - state.name = json.name; - state.raw.port = json.port; - state.raw.numplayers = json.playercount; - - this.finish(state); }); + + const json = JSON.parse(body); + 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 ece5efe..e503b8f 100644 --- a/protocols/tribes1.js +++ b/protocols/tribes1.js @@ -5,81 +5,81 @@ class Tribes1 extends Core { super(); this.encoding = 'latin1'; } - run(state) { + + async run(state) { const queryBuffer = Buffer.from('b++'); - this.udpSend(queryBuffer,(buffer) => { + const reader = await this.udpSend(queryBuffer,(buffer) => { const reader = this.reader(buffer); - const header = reader.string({length:4}); + const header = reader.string({length: 4}); if (header !== 'c++b') { - this.fatal('Header response does not match: ' + header); - return true; + this.debugLog('Header response does not match: ' + header); + return; } - state.raw.gametype = this.readString(reader); - state.raw.version = this.readString(reader); - state.name = this.readString(reader); - state.raw.dedicated = !!reader.uint(1); - state.password = !!reader.uint(1); - state.raw.playerCount = reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.cpu = 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); - } - - this.finish(state); - return true; + return reader; }); + + state.raw.gametype = this.readString(reader); + state.raw.version = this.readString(reader); + state.name = this.readString(reader); + state.raw.dedicated = !!reader.uint(1); + state.password = !!reader.uint(1); + state.raw.playerCount = reader.uint(1); + state.maxplayers = reader.uint(1); + state.raw.cpu = 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); diff --git a/protocols/tribes1master.js b/protocols/tribes1master.js index b06cf83..c9bfb47 100644 --- a/protocols/tribes1master.js +++ b/protocols/tribes1master.js @@ -7,7 +7,8 @@ class Tribes1Master extends Core { super(); this.encoding = 'latin1'; } - run(state) { + + async run(state) { const queryBuffer = Buffer.from([ 0x10, // standard header 0x03, // dump servers @@ -18,28 +19,27 @@ class Tribes1Master extends Core { let parts = new Map(); let total = 0; - this.udpSend(queryBuffer,(buffer) => { + const full = await this.udpSend(queryBuffer,(buffer) => { const reader = this.reader(buffer); const header = reader.uint(2); if (header !== 0x0610) { - this.fatal('Header response does not match: ' + header.toString(16)); - return true; + this.debugLog('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)) { - this.fatal('Conflicting total: ' + t); - return true; + throw new Error('Conflicting packet total: ' + t); } total = t; if (num < 1 || num > total) { - this.fatal('Invalid packet number: ' + num + ' ' + total); - return true; + this.debugLog('Invalid packet number: ' + num + ' ' + total); + return; } if (parts.has(num)) { - this.fatal('Duplicate part: ' + num); - return true; + this.debugLog('Duplicate part: ' + num); + return; } reader.skip(2); // challenge (0x0201) @@ -49,32 +49,29 @@ class Tribes1Master extends Core { if (parts.size === total) { const ordered = []; for (let i = 1; i <= total; i++) ordered.push(parts.get(i)); - const full = 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) { - this.fatal('Expecting 6'); - return true; - } - 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); - } - } - this.finish(state); - return true; + 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) { const length = reader.uint(1); diff --git a/protocols/unreal2.js b/protocols/unreal2.js index a34021a..e2c8d08 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -1,101 +1,90 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Unreal2 extends Core { constructor() { super(); this.encoding = 'latin1'; } - run(state) { - async.series([ - (c) => { - this.sendPacket(0,true,(b) => { - const reader = this.reader(b); - state.raw.serverid = reader.uint(4); - state.raw.ip = this.readUnrealString(reader); - state.raw.port = 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.readExtraInfo(reader,state); + async run(state) { + { + 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.readExtraInfo(reader, state); + } - c(); - }); - }, - (c) => { - this.sendPacket(1,true,(b) => { - const reader = this.reader(b); - state.raw.mutators = []; - state.raw.rules = {}; - while(!reader.done()) { + { + 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); + if(key === 'Mutator') state.raw.mutators.push(value); + else state.raw.rules[key] = value; + } + if('GamePassword' in state.raw.rules) + state.password = state.raw.rules.GamePassword !== 'True'; + } + + { + const b = await this.sendPacket(2,false); + const reader = this.reader(b); + + while(!reader.done()) { + const player = {}; + player.id = reader.uint(4); + if(!player.id) break; + if(player.id === 0) { + // Unreal2XMP Player (ID is always 0) + reader.skip(4); + } + player.name = this.readUnrealString(reader,true); + player.ping = reader.uint(4); + player.score = reader.int(4); + reader.skip(4); // stats ID + + // Extra data for Unreal2XMP players + if(player.id === 0) { + const count = reader.uint(1); + for(let iField = 0; iField < count; iField++) { const key = this.readUnrealString(reader,true); const value = this.readUnrealString(reader,true); - if(key === 'Mutator') state.raw.mutators.push(value); - else state.raw.rules[key] = value; + player[key] = value; } + } - if('GamePassword' in state.raw.rules) - state.password = state.raw.rules.GamePassword !== 'True'; + if(player.id === 0 && player.name === 'Player') { + // these show up in ut2004 queries, but aren't real + // not even really sure why they're there + continue; + } - c(); - }); - }, - (c) => { - this.sendPacket(2,false,(b) => { - const reader = this.reader(b); - - while(!reader.done()) { - const player = {}; - player.id = reader.uint(4); - if(!player.id) break; - if(player.id === 0) { - // Unreal2XMP Player (ID is always 0) - reader.skip(4); - } - player.name = this.readUnrealString(reader,true); - player.ping = reader.uint(4); - player.score = reader.int(4); - reader.skip(4); // stats ID - - // Extra data for Unreal2XMP players - if(player.id === 0) { - const count = reader.uint(1); - for(let iField = 0; iField < count; iField++) { - const key = this.readUnrealString(reader,true); - const value = this.readUnrealString(reader,true); - player[key] = value; - } - } - - if(player.id === 0 && player.name === 'Player') { - // these show up in ut2004 queries, but aren't real - // not even really sure why they're there - continue; - } - - (player.ping ? state.players : state.bots).push(player); - } - c(); - }); - }, - (c) => { - this.finish(state); + (player.ping ? state.players : state.bots).push(player); } - ]); - } - readExtraInfo(reader,state) { - if(this.debug) { - console.log("UNREAL2 EXTRA INFO:"); - console.log(reader.uint(4)); - console.log(reader.uint(4)); - console.log(reader.uint(4)); - console.log(reader.uint(4)); - console.log(reader.buffer.slice(reader.i)); } } + + readExtraInfo(reader,state) { + this.debugLog(log => { + log("UNREAL2 EXTRA INFO:"); + log(reader.uint(4)); + log(reader.uint(4)); + log(reader.uint(4)); + log(reader.uint(4)); + log(reader.buffer.slice(reader.i)); + }); + } + readUnrealString(reader, stripColor) { let length = reader.uint(1); let out; @@ -105,10 +94,10 @@ class Unreal2 extends Core { if(length > 0) out = reader.string(); } else { length = (length&0x7f)*2; - if(this.debug) { - console.log("UCS2 STRING"); - console.log(length,reader.buffer.slice(reader.i,reader.i+length)); - } + this.debugLog(log => { + log("UCS2 STRING"); + log(length,reader.buffer.slice(reader.i,reader.i+length)); + }); out = reader.string({encoding:'ucs2',length:length}); } @@ -120,11 +109,12 @@ class Unreal2 extends Core { return out; } - sendPacket(type,required,callback) { + + async sendPacket(type,required) { const outbuffer = Buffer.from([0x79,0,0,0,type]); const packets = []; - this.udpSend(outbuffer,(buffer) => { + return await this.udpSend(outbuffer,(buffer) => { const reader = this.reader(buffer); const header = reader.uint(4); const iType = reader.uint(1); @@ -132,8 +122,7 @@ class Unreal2 extends Core { packets.push(reader.rest()); }, () => { if(!packets.length && required) return; - callback(Buffer.concat(packets)); - return true; + return Buffer.concat(packets); }); } } diff --git a/protocols/ut3.js b/protocols/ut3.js index 20fd9b2..9b96b41 100644 --- a/protocols/ut3.js +++ b/protocols/ut3.js @@ -1,8 +1,8 @@ const Gamespy3 = require('./gamespy3'); class Ut3 extends Gamespy3 { - finalizeState(state) { - super.finalizeState(state); + async run(state) { + await super.run(state); this.translate(state.raw,{ 'mapname': false, diff --git a/protocols/valve.js b/protocols/valve.js index a925a57..d78ebfc 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -1,13 +1,10 @@ -const async = require('async'), - Bzip2 = require('compressjs').Bzip2, +const Bzip2 = require('compressjs').Bzip2, Core = require('./core'); class Valve extends Core { constructor() { super(); - this.options.port = 27015; - // 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 @@ -28,173 +25,172 @@ class Valve extends Core { this._challenge = ''; } - run(state) { - async.series([ - (c) => { this.queryInfo(state,c); }, - (c) => { this.queryChallenge(state,c); }, - (c) => { this.queryPlayers(state,c); }, - (c) => { this.queryRules(state,c); }, - (c) => { this.cleanup(state,c); }, - (c) => { this.finish(state); } - ]); + 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); } - queryInfo(state,c) { - this.sendPacket( - 0x54,false,'Source Engine Query\0', + async queryInfo(state) { + this.debugLog("Requesting info ..."); + const b = await this.sendPacket( + 0x54, + false, + 'Source Engine Query\0', this.goldsrcInfo ? 0x6D : 0x49, - (b) => { - 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(); - state.raw.steamappid = 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 = reader.uint(1); - state.raw.environment = reader.uint(1); - if(!this.goldsrcInfo) { - state.raw.listentype = String.fromCharCode(state.raw.listentype); - state.raw.environment = String.fromCharCode(state.raw.environment); - } - - 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); - } - } - state.raw.secure = reader.uint(1); - - if(this.goldsrcInfo) { - state.raw.numbots = reader.uint(1); - } else { - if(state.raw.folder === '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.raw.port = reader.uint(2); - if(extraFlag & 0x10) state.raw.steamid = reader.uint(8); - if(extraFlag & 0x40) { - state.raw.sourcetvport = reader.uint(2); - state.raw.sourcetvname = reader.string(); - } - if(extraFlag & 0x20) state.raw.tags = reader.string(); - if(extraFlag & 0x01) state.raw.gameid = reader.uint(8); - } - - // from https://developer.valvesoftware.com/wiki/Server_queries - if( - state.raw.protocol === 7 && ( - state.raw.steamappid === 215 - || state.raw.steamappid === 17550 - || state.raw.steamappid === 17700 - || state.raw.steamappid === 240 - ) - ) { - this._skipSizeInSplitHeader = true; - } - if(this.debug) { - console.log("STEAM APPID: "+state.raw.steamappid); - console.log("PROTOCOL: "+state.raw.protocol); - } - if(state.raw.protocol === 48) { - if(this.debug) console.log("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); - this.goldsrcSplits = true; - } - - c(); - } + false ); - } - queryChallenge(state,c) { - if(this.legacyChallenge) { - this.sendPacket(0x57,false,null,0x41,(b) => { - // sendPacket will catch the response packet and - // save the challenge for us - c(); - }); + 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(); + state.raw.steamappid = 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 = reader.uint(1); + state.raw.environment = reader.uint(1); + if(!this.goldsrcInfo) { + state.raw.listentype = String.fromCharCode(state.raw.listentype); + state.raw.environment = String.fromCharCode(state.raw.environment); + } + + 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); + } + } + state.raw.secure = reader.uint(1); + + if(this.goldsrcInfo) { + state.raw.numbots = reader.uint(1); } else { - c(); + if(state.raw.folder === '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); + if(extraFlag & 0x40) { + state.raw.sourcetvport = reader.uint(2); + state.raw.sourcetvname = reader.string(); + } + if(extraFlag & 0x20) state.raw.tags = reader.string(); + if(extraFlag & 0x01) state.raw.gameid = reader.uint(8); + } + + // from https://developer.valvesoftware.com/wiki/Server_queries + if( + state.raw.protocol === 7 && ( + state.raw.steamappid === 215 + || state.raw.steamappid === 17550 + || state.raw.steamappid === 17700 + || state.raw.steamappid === 240 + ) + ) { + this._skipSizeInSplitHeader = true; + } + this.debugLog("STEAM APPID: "+state.raw.steamappid); + this.debugLog("PROTOCOL: "+state.raw.protocol); + if(state.raw.protocol === 48) { + this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); + this.goldsrcSplits = true; } } - queryPlayers(state,c) { + async queryChallenge() { + if(this.legacyChallenge) { + // sendPacket will catch the response packet and + // save the challenge for us + this.debugLog("Requesting legacy challenge key ..."); + await this.sendPacket( + 0x57, + false, + null, + 0x41, + false + ); + } + } + + async queryPlayers(state) { state.raw.players = []; - this.sendPacket(0x55,true,null,0x44,(b) => { - 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(); - if(this.debug) console.log("Found player: "+name+" "+score+" "+time); + // CSGO doesn't even respond sometimes if host_players_show is not 2 + // Ignore timeouts in only this case + const allowTimeout = state.raw.steamappid === 730; - // connecting players don't count as players. - if(!name) continue; + this.debugLog("Requesting player list ..."); + const b = await this.sendPacket( + 0x55, + true, + null, + 0x44, + allowTimeout + ); + if (b === null) return; // timed out - // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2 - if (state.raw.steamappid === 730 && name === 'Max Players') continue; + 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(); - state.raw.players.push({ - name:name, score:score, time:time - }); - } + this.debugLog("Found player: "+name+" "+score+" "+time); - c(); - }, () => { - // CSGO doesn't even respond sometimes if host_players_show is not 2 - // Ignore timeouts in only this case - if (state.raw.steamappid === 730) { - c(); - return true; - } - }); + // 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.steamappid === 730 && name === 'Max Players') continue; + + state.raw.players.push({ + name:name, score:score, time:time + }); + } } - queryRules(state,c) { + async queryRules(state) { state.raw.rules = {}; - this.sendPacket(0x56,true,null,0x45,(b) => { - const reader = this.reader(b); - const num = reader.uint(2); - for(let i = 0; i < num; i++) { - const key = reader.string(); - const value = reader.string(); - state.raw.rules[key] = value; - } - c(); - }, () => { - // no rules were returned after timeout -- - // the server probably has them disabled - // ignore the timeout - c(); - return true; - }); + this.debugLog("Requesting rules ..."); + const b = await this.sendPacket(0x56,true,null,0x45,true); + if (b === null) return; // timed out - the server probably just has rules disabled + + const reader = this.reader(b); + const num = reader.uint(2); + for(let i = 0; i < num; i++) { + const key = reader.string(); + const value = reader.string(); + state.raw.rules[key] = value; + } } - cleanup(state,c) { + async cleanup(state) { // Battalion 1944 puts its info into rules fields for some reason if ('bat_name_s' in state.raw.rules) { state.name = state.raw.rules.bat_name_s; @@ -234,142 +230,157 @@ class Valve extends Core { if (sortedPlayers.length) state.players.push(sortedPlayers.pop()); else state.players.push({}); } - - c(); } /** + * 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 {function(Buffer)} callback - * @param {(function():boolean)=} ontimeout + * @param {boolean=} allowTimeout + * @returns Buffer|null **/ - sendPacket( + async sendPacket( type, sendChallenge, payload, expect, - callback, - ontimeout + allowTimeout ) { + for (let keyRetry = 0; keyRetry < 3; keyRetry++) { + let requestKeyChanged = false; + const response = await this.sendPacketRaw( + type, sendChallenge, payload, + (payload) => { + const reader = this.reader(payload); + const type = reader.uint(1); + this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16)); + if (type === 0x41) { + const key = reader.uint(4); + if (this._challenge !== key) { + this.debugLog('Received new challenge key: ' + key); + this._challenge = key; + if (sendChallenge) { + this.debugLog('Challenge key changed -- allowing query retry if needed'); + requestKeyChanged = true; + } + } + } + if (type === expect) { + return reader.rest(); + } else if (requestKeyChanged) { + return null; + } + }, + () => { + if (allowTimeout) return null; + } + ); + if (!requestKeyChanged) { + 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, + sendChallenge, + payload, + onResponse, + onTimeout + ) { + if (typeof payload === 'string') payload = Buffer.from(payload, 'binary'); + const challengeLength = sendChallenge ? 4 : 0; + const payloadLength = payload ? payload.length : 0; + + const b = Buffer.alloc(5 + challengeLength + payloadLength); + b.writeInt32LE(-1, 0); + b.writeUInt8(type, 4); + + if (sendChallenge) { + let challenge = this._challenge; + if (!challenge) challenge = 0xffffffff; + if (this.byteorder === 'le') b.writeUInt32LE(challenge, 5); + else b.writeUInt32BE(challenge, 5); + } + if (payloadLength) payload.copy(b, 5 + challengeLength); + const packetStorage = {}; + return await this.udpSend( + b, + (buffer) => { + const reader = this.reader(buffer); + const header = reader.int(4); + if(header === -1) { + // full package + this.debugLog("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]; - const receivedFull = (reader) => { - const type = reader.uint(1); + let bzip = false; + if(!this.goldsrcSplits && uid & 0x80000000) bzip = true; - if(type === 0x41) { - const key = reader.uint(4); - - if(this.debug) console.log('Received challenge key: ' + key); - - if(this._challenge !== key) { - this._challenge = key; - if(sendChallenge) { - if (this.debug) console.log('Restarting query'); - send(); - return 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(); } - } - return; - } + packets[packetNum] = payload; - if(this.debug) console.log("Received "+type.toString(16)+" expected "+expect.toString(16)); - if(type !== expect) return; - callback(reader.rest()); - return true; - }; + this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum); + this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID"); - const receivedOne = (buffer) => { - const reader = this.reader(buffer); + if(Object.keys(packets).length !== numPackets) return; - const header = reader.int(4); - if(header === -1) { - // full package - if(this.debug) console.log("Received full packet"); - return receivedFull(reader); - } - 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; - - if(this.debug) { - console.log("Received partial packet uid:"+uid+" num:"+packetNum); - console.log("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)) { - this.fatal('Missing packet #'+i); - return true; + // 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]); } - list.push(packets[i]); - } - let assembled = Buffer.concat(list); - if(bzip) { - if(this.debug) console.log("BZIP DETECTED - Extracing packet..."); - try { - assembled = Buffer.from(Bzip2.decompressFile(assembled)); - } catch(e) { - this.fatal('Invalid bzip packet'); - return true; + let assembled = Buffer.concat(list); + if(bzip) { + this.debugLog("BZIP DETECTED - Extracing packet..."); + try { + assembled = Buffer.from(Bzip2.decompressFile(assembled)); + } catch(e) { + throw new Error('Invalid bzip packet'); + } } + const assembledReader = this.reader(assembled); + assembledReader.skip(4); // header + return onResponse(assembledReader.rest()); } - const assembledReader = this.reader(assembled); - assembledReader.skip(4); // header - return receivedFull(assembledReader); - } - }; - - const send = (c) => { - if(typeof payload === 'string') payload = Buffer.from(payload,'binary'); - const challengeLength = sendChallenge ? 4 : 0; - const payloadLength = payload ? payload.length : 0; - - const b = Buffer.alloc(5 + challengeLength + payloadLength); - b.writeInt32LE(-1, 0); - b.writeUInt8(type, 4); - - if(sendChallenge) { - let challenge = this._challenge; - if(!challenge) challenge = 0xffffffff; - if(this.byteorder === 'le') b.writeUInt32LE(challenge, 5); - else b.writeUInt32BE(challenge, 5); - } - if(payloadLength) payload.copy(b, 5+challengeLength); - - this.udpSend(b,receivedOne,ontimeout); - }; - - send(); + }, + onTimeout + ); } } diff --git a/protocols/ventrilo.js b/protocols/ventrilo.js index 95acdc1..5e7a998 100644 --- a/protocols/ventrilo.js +++ b/protocols/ventrilo.js @@ -5,31 +5,30 @@ class Ventrilo extends Core { super(); this.byteorder = 'be'; } - run(state) { - this.sendCommand(2,'',(data) => { - 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; - this.finish(state); - }); + 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; } - sendCommand(cmd,password,c) { + async sendCommand(cmd,password) { const body = Buffer.alloc(16); body.write(password,0,15,'utf8'); const encrypted = encrypt(cmd,body); const packets = {}; - this.udpSend(encrypted, (buffer) => { + return await this.udpSend(encrypted, (buffer) => { if(buffer.length < 20) return; const data = decrypt(buffer); @@ -39,11 +38,10 @@ class Ventrilo extends Core { const out = []; for(let i = 0; i < data.packetTotal; i++) { - if(!(i in packets)) return this.fatal('Missing packet #'+i); + if(!(i in packets)) throw new Error('Missing packet #'+i); out.push(packets[i]); } - c(Buffer.concat(out)); - return true; + return Buffer.concat(out); }); } } diff --git a/protocols/warsow.js b/protocols/warsow.js index b5f82e0..c970be5 100644 --- a/protocols/warsow.js +++ b/protocols/warsow.js @@ -1,8 +1,8 @@ const Quake3 = require('./quake3'); class Warsow extends Quake3 { - finalizeState(state) { - super.finalizeState(state); + async run(state) { + await super.run(state); if(state.players) { for(const player of state.players) { player.team = player.address;