Merge remote-tracking branch 'origin/async'

This commit is contained in:
mmorrison 2019-01-12 22:43:54 -06:00
commit 075106c190
50 changed files with 2951 additions and 2622 deletions

129
README.md
View File

@ -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 <gameid> <ip>[:<port>]`
* 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

View File

@ -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}));

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
const fs = require('fs'),
TypeResolver = require('../lib/typeresolver');
TypeResolver = require('../lib/GameResolver');
const generated = TypeResolver.printReadme();

125
games.txt
View File

@ -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

84
lib/GameResolver.js Normal file
View File

@ -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;

52
lib/GlobalUdpSocket.js Normal file
View File

@ -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;

20
lib/Promises.js Normal file
View File

@ -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;

22
lib/ProtocolResolver.js Normal file
View File

@ -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;

104
lib/QueryRunner.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

526
package-lock.json generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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 = /<tr>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<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;
}
*/
}
}

View File

@ -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<string>}
*/
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<T>} fn
* @returns {Promise<T>}
*/
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<T>
*/
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<T>
* @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;

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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});
}
}
}
}

View File

@ -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;
}
});
}

View File

@ -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';

View File

@ -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;
});
}
}

View File

@ -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('<br/>');
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('<br/>');
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({});
}
}
}

View File

@ -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;

View File

@ -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({});

View File

@ -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});
}
}
}
}

View File

@ -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) {

View File

@ -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
]);
}
}

View File

@ -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) {

View File

@ -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({});
}
}
}

View File

@ -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,'');
}

View File

@ -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];
});
}

View File

@ -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;
}
}

View File

@ -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]);

View File

@ -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;
}
});
);
}
}

View File

@ -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({});
}
}
}
}

View File

@ -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();
});
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
});
}
}

View File

@ -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,

View File

@ -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
);
}
}

View File

@ -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);
});
}
}

View File

@ -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;