diff --git a/bin/gamedig.js b/bin/gamedig.js index a874ee9..64e2411 100644 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -5,8 +5,8 @@ const argv = require('minimist')(process.argv.slice(2)), const debug = argv.debug; delete argv.debug; -const pretty = !!argv.pretty; -delete argv.output; +const pretty = !!argv.pretty || debug; +delete argv.pretty; const options = {}; for(const key of Object.keys(argv)) { @@ -14,14 +14,21 @@ for(const key of Object.keys(argv)) { if( key === '_' || key.charAt(0) === '$' - || (typeof value !== 'string' && typeof value !== 'number') ) continue; options[key] = value; } +if (argv._.length >= 1) { + const target = argv._[0]; + const split = target.split(':'); + options.host = split[0]; + if (split.length >= 2) { + options.port = split[1]; + } +} + if(debug) Gamedig.debug = true; -Gamedig.isCommandLine = true; Gamedig.query(options) .then((state) => { diff --git a/bin/genreadme.js b/bin/genreadme.js index 1a69010..6089b36 100644 --- a/bin/genreadme.js +++ b/bin/genreadme.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const fs = require('fs'), - TypeResolver = require('../lib/typeresolver'); + TypeResolver = require('../lib/GameResolver'); const generated = TypeResolver.printReadme(); diff --git a/games.txt b/games.txt index c9b9245..185eedf 100644 --- a/games.txt +++ b/games.txt @@ -1,4 +1,4 @@ -# id | pretty | protocol | options | parameters +# id | pretty name for readme | protocol | options | extra #### TODO: # cube1|Cube 1|cube|port=28786,port_query_offset=1 @@ -38,8 +38,8 @@ avp2|Aliens vs Predator 2|gamespy1|port=27888 # both port and port_query should be specified when used avp2010|Aliens vs Predator 2010|valve -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 +53,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 @@ -113,7 +113,7 @@ doi|Day of Infamy|valve doom3|Doom 3|doom3|port=27666 dota2|DOTA 2|valve 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 @@ -168,9 +168,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 @@ -198,13 +198,13 @@ 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 @@ -230,7 +230,7 @@ ss2|Serious Sam 2|gamespy2|port=25600 shatteredhorizon|Shattered Horizon|valve ship|The Ship|valve 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 soldat|Soldat|ase|port=13073,port_query_offset=123 @@ -266,8 +266,8 @@ 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 +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,7 +289,7 @@ 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 diff --git a/lib/GameResolver.js b/lib/GameResolver.js new file mode 100644 index 0000000..7cf03f9 --- /dev/null +++ b/lib/GameResolver.js @@ -0,0 +1,84 @@ +const Path = require('path'), + fs = require('fs'); + +class GameResolver { + constructor() { + this.games = this._readGames(); + } + + lookup(type) { + if(!type) throw Error('No game specified'); + + if(type.substr(0,9) === 'protocol-') { + return { + protocol: type.substr(9) + }; + } + + const game = this.games.get(type); + if(!game) throw Error('Invalid game: '+type); + return game.options; + } + + printReadme() { + let out = ''; + for(const key of Object.keys(games)) { + const game = games[key]; + if (!game.pretty) { + continue; + } + out += "* "+game.pretty+" ("+key+")"; + if(game.options.port_query_offset || game.options.port_query) + out += " [[Separate Query Port](#separate-query-port)]"; + if(game.extra.doc_notes) + out += " [[Additional Notes](#"+game.extra.doc_notes+")]"; + out += "\n"; + } + return out; + } + + _readGames() { + const gamesFile = Path.normalize(__dirname+'/../games.txt'); + const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); + const games = new Map(); + + for (let line of lines) { + // strip comments + const comment = line.indexOf('#'); + if(comment !== -1) line = line.substr(0,comment); + line = line.trim(); + if(!line) continue; + + const split = line.split('|'); + const gameId = split[0].trim(); + const options = this._parseList(split[3]); + options.protocol = split[2].trim(); + + games.set(gameId, { + pretty: split[1].trim(), + options: options, + extra: this._parseList(split[4]) + }); + } + return games; + } + + _parseList(str) { + if(!str) return {}; + const out = {}; + for (const one of str.split(',')) { + const equals = one.indexOf('='); + const key = equals === -1 ? one : one.substr(0,equals); + let value = equals === -1 ? '' : one.substr(equals+1); + + if(value === 'true' || value === '') value = true; + else if(value === 'false') value = false; + else if(!isNaN(parseInt(value))) value = parseInt(value); + + out[key] = value; + } + return out; + } +} + +module.exports = GameResolver; diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js new file mode 100644 index 0000000..a59fff7 --- /dev/null +++ b/lib/GlobalUdpSocket.js @@ -0,0 +1,42 @@ +const dgram = require('dgram'), + HexUtil = require('./HexUtil'); + +class GlobalUdpSocket { + constructor() { + this.socket = null; + this.callbacks = new Set(); + this.debug = false; + } + + _getSocket() { + if (!this.socket) { + const udpSocket = this.socket = dgram.createSocket('udp4'); + udpSocket.unref(); + udpSocket.bind(); + udpSocket.on('message', (buffer, rinfo) => { + for (const cb of this.callbacks) { + cb(rinfo.address, rinfo.port, buffer); + } + }); + udpSocket.on('error', (e) => { + if (this.debug) { + console.log("UDP ERROR: " + e); + } + }); + } + return this.socket; + } + + send(buffer, address, port) { + this._getSocket().send(buffer,0,buffer.length,port,address); + } + + addCallback(callback) { + this.callbacks.add(callback); + } + removeCallback(callback) { + this.callbacks.delete(callback); + } +} + +module.exports = GlobalUdpSocket; diff --git a/lib/Promises.js b/lib/Promises.js new file mode 100644 index 0000000..b807672 --- /dev/null +++ b/lib/Promises.js @@ -0,0 +1,20 @@ +class Promises { + static createTimeout(timeoutMs, timeoutMsg) { + let cancel = null; + const wrapped = new Promise((res, rej) => { + const timeout = setTimeout( + () => { + rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")); + }, + timeoutMs + ); + cancel = () => { + clearTimeout(timeout); + }; + }); + wrapped.cancel = cancel; + return wrapped; + } +} + +module.exports = Promises; diff --git a/lib/ProtocolResolver.js b/lib/ProtocolResolver.js new file mode 100644 index 0000000..ef99e9e --- /dev/null +++ b/lib/ProtocolResolver.js @@ -0,0 +1,22 @@ +const Path = require('path'), + fs = require('fs'), + Core = require('../protocols/core'); + +class ProtocolResolver { + constructor() { + this.protocolDir = Path.normalize(__dirname+'/../protocols'); + } + + /** + * @returns Core + */ + create(protocolId) { + protocolId = Path.basename(protocolId); + const path = this.protocolDir+'/'+protocolId; + if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type); + const protocol = require(path); + return new protocol(); + } +} + +module.exports = ProtocolResolver; diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js new file mode 100644 index 0000000..ab9db13 --- /dev/null +++ b/lib/QueryRunner.js @@ -0,0 +1,103 @@ +const GameResolver = require('./GameResolver'), + ProtocolResolver = require('./ProtocolResolver'); + +const defaultOptions = { + socketTimeout: 2000, + attemptTimeout: 10000, + maxAttempts: 1 +}; + +class QueryRunner { + constructor(udpSocket, debug) { + this.debug = debug; + this.udpSocket = udpSocket; + 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 + }); + } 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 (this.debug) { + console.log("Running attempt with options:"); + console.log(options); + } + const core = this.protocolResolver.create(options.protocol); + core.options = options; + core.udpSocket = this.udpSocket; + return await core.runAllAttempts(); + } +} + +module.exports = QueryRunner; diff --git a/lib/index.js b/lib/index.js index cda6184..a5b6ddd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,87 +1,48 @@ -const dgram = require('dgram'), - TypeResolver = require('./typeresolver'), - HexUtil = require('./HexUtil'); +const QueryRunner = require('./QueryRunner'), + GlobalUdpSocket = require('./GlobalUdpSocket'); -const activeQueries = new Set(); - -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) continue; - if(query.options.port_query !== rinfo.port) continue; - query._udpIncoming(buffer); - break; - } -}); -udpSocket.on('error', (e) => { - if(Gamedig.debug) console.log("UDP ERROR: "+e); -}); +let singleton = null; class Gamedig { + constructor() { + this.udpSocket = new GlobalUdpSocket(); + this.queryRunner = new QueryRunner(this.udpSocket); + this._debug = false; + } - static query(options,callback) { - const promise = (async () => { - for (const key of Object.keys(options)) { - if (['port_query', 'port'].includes(key)) { - options[key] = parseInt(options[key]); - } - } + setDebug(on) { + this.udpSocket.debug = on; + this._debug = on; + this.queryRunner.debug = on; + } - let query = TypeResolver.lookup(options.type); - 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.add(query); - try { - return await query.runAll(); - } finally { - activeQueries.delete(query); - } - })(); + async query(userOptions) { + userOptions.debug |= this._debug; + return await this.queryRunner.run(userOptions); + } + static getInstance() { + if (!singleton) { + singleton = new Gamedig(); + } + return singleton; + } + static query(userOptions, callback) { + const promise = Gamedig.getInstance().query(userOptions); if (callback && callback instanceof Function) { - if(callback.length === 2) { + if (callback.length === 2) { promise - .then((state) => callback(null,state)) + .then((state) => callback(null, state)) .catch((error) => callback(error)); } else if (callback.length === 1) { promise .then((state) => callback(state)) - .catch((error) => callback({error:error})); + .catch((error) => callback({error: error})); } } - return promise; } - } -Gamedig.debug = false; -Gamedig.isCommandLine = false; +Object.defineProperty(Gamedig, "debug", { set: on => Gamedig.getInstance().setDebug(on) }); module.exports = Gamedig; diff --git a/lib/typeresolver.js b/lib/typeresolver.js deleted file mode 100644 index dd33536..0000000 --- a/lib/typeresolver.js +++ /dev/null @@ -1,102 +0,0 @@ -const Path = require('path'), - fs = require('fs'), - Core = require('../protocols/core'); - -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 { - /** - * @param {string} type - * @returns Core - */ - static lookup(type) { - if(!type) throw Error('No game specified'); - - if(type.substr(0,9) === 'protocol-') { - return createProtocolInstance(type.substr(9)); - } - - const game = games[type]; - if(!game) throw Error('Invalid game: '+type); - - const query = createProtocolInstance(game.protocol); - query.pretty = game.pretty; - for(const key of Object.keys(game.options)) { - query.options[key] = game.options[key]; - } - for(const key of Object.keys(game.params)) { - query[key] = game.params[key]; - } - - return query; - } - static printReadme() { - let out = ''; - for(const key of Object.keys(games)) { - const game = games[key]; - if (!game.pretty) { - continue; - } - out += "* "+game.pretty+" ("+key+")"; - if(game.options.port_query_offset || game.options.port_query) - out += " [[Separate Query Port](#separate-query-port)]"; - if(game.params.doc_notes) - out += " [[Additional Notes](#"+game.params.doc_notes+")]"; - out += "\n"; - } - return out; - } -} - -module.exports = TypeResolver; diff --git a/protocols/americasarmy.js b/protocols/americasarmy.js deleted file mode 100644 index 34b6bc2..0000000 --- a/protocols/americasarmy.js +++ /dev/null @@ -1,25 +0,0 @@ -const Gamespy2 = require('./gamespy2'); - -class AmericasArmy extends Gamespy2 { - async run(state) { - await super.run(state); - state.name = this.stripColor(state.name); - state.map = this.stripColor(state.map); - for(const key of Object.keys(state.raw)) { - if(typeof state.raw[key] === 'string') { - state.raw[key] = this.stripColor(state.raw[key]); - } - } - for(const player of state.players) { - if(!('name' in player)) continue; - player.name = this.stripColor(player.name); - } - } - - stripColor(str) { - // uses unreal 2 color codes - return str.replace(/\x1b...|[\x00-\x1a]/g,''); - } -} - -module.exports = AmericasArmy; diff --git a/protocols/armagetron.js b/protocols/armagetron.js index 2d73e39..1e52e8e 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -15,7 +15,7 @@ class Armagetron extends Core { reader.skip(6); - state.raw.port = 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); diff --git a/protocols/ase.js b/protocols/ase.js index ae0557f..c6332b1 100644 --- a/protocols/ase.js +++ b/protocols/ase.js @@ -5,12 +5,12 @@ class Ase extends Core { const buffer = await this.udpSend('s',(buffer) => { const reader = this.reader(buffer); const header = reader.string({length: 4}); - if (header === 'EYE1') return buffer; + if (header === 'EYE1') return reader.rest(); }); const reader = this.reader(buffer); state.raw.gamename = this.readString(reader); - state.raw.port = parseInt(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); diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 8790b32..609da3f 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -4,14 +4,13 @@ class Battlefield extends Core { constructor() { super(); this.encoding = 'latin1'; - this.isBadCompany2 = false; } async run(state) { await this.withTcp(async socket => { { const data = await this.query(socket, ['serverInfo']); - state.raw.name = data.shift(); + state.name = data.shift(); state.raw.numplayers = parseInt(data.shift()); state.maxplayers = parseInt(data.shift()); state.raw.gametype = data.shift(); @@ -29,25 +28,39 @@ class Battlefield extends Core { } 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(); + state.raw.status = data.shift(); + + // Seems like the fields end at random places beyond this point + // depending on the server version + + if (data.length) state.raw.ranked = (data.shift() === 'true'); + if (data.length) state.raw.punkbuster = (data.shift() === 'true'); + if (data.length) state.password = (data.shift() === 'true'); + if (data.length) state.raw.uptime = parseInt(data.shift()); + if (data.length) state.raw.roundtime = parseInt(data.shift()); + + const isBadCompany2 = data[0] === 'BC2'; + if (isBadCompany2) { + if (data.length) data.shift(); + if (data.length) data.shift(); } - 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'); + if (data.length) { + state.raw.ip = data.shift(); + const split = state.raw.ip.split(':'); + state.gameHost = split[0]; + state.gamePort = split[1]; + } else { + // best guess if the server doesn't tell us what the server port is + // these are just the default game ports for different default query ports + if (this.options.port === 48888) state.gamePort = 7673; + if (this.options.port === 22000) state.gamePort = 25200; } + if (data.length) state.raw.punkbusterversion = data.shift(); + if (data.length) state.raw.joinqueue = (data.shift() === 'true'); + if (data.length) state.raw.region = data.shift(); + if (data.length) state.raw.pingsite = data.shift(); + if (data.length) state.raw.country = data.shift(); + if (data.length) state.raw.quickmatch = (data.shift() === 'true'); } { @@ -135,6 +148,7 @@ class Battlefield extends Core { 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 = []; diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index db8ebb9..f8048ff 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -4,7 +4,7 @@ const Core = require('./core'), class BuildAndShoot extends Core { async run(state) { const body = await this.request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/', + uri: 'http://'+this.options.address+':'+this.options.port+'/', }); let m; diff --git a/protocols/core.js b/protocols/core.js index 39d9125..0e921a8 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -6,43 +6,24 @@ const EventEmitter = require('events').EventEmitter, util = require('util'), dnsLookupAsync = util.promisify(dns.lookup), dnsResolveAsync = util.promisify(dns.resolve), - requestAsync = require('request-promise'); + requestAsync = require('request-promise'), + Promises = require('../lib/Promises'); class Core extends EventEmitter { constructor() { super(); - this.options = { - socketTimeout: 2000, - attemptTimeout: 10000, - maxAttempts: 1 - }; this.encoding = 'utf8'; this.byteorder = 'le'; this.delimiter = '\0'; this.srvRecord = null; + this.abortedPromise = null; - this.asyncLeaks = new Set(); - this.udpCallback = null; - this.udpLocked = false; - this.lastAsyncLeakId = 0; + // Sent to us by QueryRunner + this.options = null; + this.udpSocket = null; } - initState() { - return { - name: '', - map: '', - password: false, - - raw: {}, - - maxplayers: 0, - players: [], - bots: [] - }; - } - - // Run all attempts - async runAll() { + async runAllAttempts() { let result = null; let lastError = null; for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) { @@ -63,38 +44,27 @@ class Core extends EventEmitter { // Runs a single attempt with a timeout and cleans up afterward async runOnceSafe() { - try { - const result = await this.timedPromise(this.runOnce(), this.options.attemptTimeout, "Attempt"); - if (this.asyncLeaks.size) { - let out = []; - for (const leak of this.asyncLeaks) { - out.push(leak.id + " " + leak.stack); - } - throw new Error('Query succeeded, but async leak was detected:\n' + out.join('\n---\n')); - } - return result; - } finally { - // Clean up any lingering long-running functions - for (const leak of this.asyncLeaks) { - try { - leak.cleanup(); - } catch(e) { - this.debugLog("Error during async cleanup: " + e.stack); - } - } - this.asyncLeaks.clear(); - } - } - - timedPromise(promise, timeoutMs, timeoutMsg) { - return new Promise((resolve,reject) => { - const cancelTimeout = this.setTimeout( - () => reject(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")), - timeoutMs - ); - promise = promise.finally(cancelTimeout); - promise.then(resolve,reject); + 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() { @@ -103,25 +73,37 @@ class Core extends EventEmitter { if (('host' in options) && !('address' in options)) { options.address = await this.parseDns(options.host); } - if(!('port_query' in options) && 'port' in options) { - const offset = options.port_query_offset || 0; - options.port_query = options.port + offset; - } - const state = this.initState(); + const state = { + name: '', + map: '', + password: false, + + raw: {}, + + maxplayers: 0, + players: [], + bots: [] + }; + await this.run(state); 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() - startMillis; + // because lots of servers prefix with spaces to try to appear first + state.name = state.name.trim(); + + state.duration = Date.now() - startMillis; + if (!('connect' in state)) { + state.connect = '' + + (state.gameHost || this.options.host || this.options.address) + + ':' + + (state.gamePort || this.options.port) + } + delete state.gameHost; + delete state.gamePort; + return state; } @@ -166,18 +148,6 @@ class Core extends EventEmitter { else return await resolveStandard(host); } - addAsyncLeak(fn) { - const id = ++this.lastAsyncLeakId; - const stack = new Error().stack; - const entry = { id: id, cleanup: fn, stack: stack }; - this.debugLog("Registering async leak: " + id); - this.asyncLeaks.add(entry); - return () => { - this.debugLog("Removing async leak: " + id); - this.asyncLeaks.delete(entry); - } - } - // utils /** @returns {Reader} */ reader(buffer) { @@ -204,6 +174,12 @@ class Core extends EventEmitter { return false; } + assertValidPort(port) { + if (!port || port < 1 || port > 65535) { + throw new Error("Invalid tcp/ip port: " + port); + } + } + /** * @template T * @param {function(Socket):Promise} fn @@ -211,56 +187,45 @@ class Core extends EventEmitter { */ async withTcp(fn) { const address = this.options.address; - const port = this.options.port_query; + const port = this.options.port; + this.assertValidPort(port); - const socket = net.connect(port,address); - socket.setNoDelay(true); - const cancelAsyncLeak = this.addAsyncLeak(() => socket.destroy()); + 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) => { + 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")); }); - socket.on('ready', () => log(address+':'+port+" TCP Connected")); - }); - try { - await this.timedPromise( - new Promise((resolve,reject) => { - socket.on('ready', resolve); - socket.on('close', () => reject(new Error('TCP Connection Refused'))); - }), - this.options.socketTimeout, - 'TCP Opening' - ); + const connectionPromise = new Promise((resolve,reject) => { + socket.on('ready', resolve); + socket.on('close', () => reject(new Error('TCP Connection Refused'))); + }); + connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening'); + await Promise.race([ + connectionPromise, + connectionTimeout, + this.abortedPromise + ]); return await fn(socket); } finally { - cancelAsyncLeak(); - socket.destroy(); - } - } - - setTimeout(callback, time) { - let cancelAsyncLeak; - const onTimeout = () => { - cancelAsyncLeak(); - callback(); - }; - const timeout = setTimeout(onTimeout, time); - cancelAsyncLeak = this.addAsyncLeak(() => clearTimeout(timeout)); - return () => { - cancelAsyncLeak(); - clearTimeout(timeout); + socket && socket.destroy(); + connectionTimeout && connectionTimeout.cancel(); } } @@ -272,8 +237,9 @@ class Core extends EventEmitter { * @returns Promise */ async tcpSend(socket,buffer,ondata) { - return await this.timedPromise( - new Promise(async (resolve,reject) => { + let timeout; + try { + const promise = new Promise(async (resolve, reject) => { let received = Buffer.from([]); const onData = (data) => { received = Buffer.concat([received, data]); @@ -285,22 +251,11 @@ class Core extends EventEmitter { }; socket.on('data', onData); socket.write(buffer); - }), - this.options.socketTimeout, - 'TCP' - ); - } - - async withUdpLock(fn) { - if (this.udpLocked) { - throw new Error('Attempted to lock UDP when already locked'); - } - this.udpLocked = true; - try { - return await fn(); + }); + timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP'); + return await Promise.race([promise, timeout, this.abortedPromise]); } finally { - this.udpLocked = false; - this.udpCallback = null; + timeout && timeout.cancel(); } } @@ -312,72 +267,93 @@ class Core extends EventEmitter { * @template T */ async udpSend(buffer,onPacket,onTimeout) { - if(!('port_query' in this.options)) throw new Error('Attempted to send without setting a port'); - if(!('address' in this.options)) throw new Error('Attempted to send without setting an address'); + 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(this.options.address+':'+this.options.port_query+" UDP-->"); + log(address+':'+port+" UDP-->"); log(HexUtil.debugDump(buffer)); }); - return await this.withUdpLock(async() => { - this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); + const socket = this.udpSocket; + socket.send(buffer, address, port); - return await new Promise((resolve,reject) => { - const cancelTimeout = this.setTimeout(() => { + let socketCallback; + let timeout; + try { + const promise = new Promise((resolve, reject) => { + socketCallback = (fromAddress, fromPort, buffer) => { + try { + if (fromAddress !== address) return; + if (fromPort !== port) return; + this.debugLog(log => { + log(fromAddress + ':' + fromPort + " <--UDP"); + log(HexUtil.debugDump(buffer)); + }); + const result = onPacket(buffer); + if (result !== undefined) { + this.debugLog("UDP send finished by callback"); + resolve(result); + } + } catch(e) { + reject(e); + } + }; + socket.addCallback(socketCallback); + }); + timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP'); + const wrappedTimeout = new Promise((resolve, reject) => { + timeout.catch((e) => { this.debugLog("UDP timeout detected"); let success = false; if (onTimeout) { - const result = onTimeout(); - if (result !== undefined) { - this.debugLog("UDP timeout resolved by callback"); - resolve(result); - success = true; + try { + const result = onTimeout(); + if (result !== undefined) { + this.debugLog("UDP timeout resolved by callback"); + resolve(result); + return; + } + } catch(e) { + reject(e); } } - if (!success) { - reject(new Error('UDP Watchdog Timeout')); - } - },this.options.socketTimeout); - - this.udpCallback = (buffer) => { - const result = onPacket(buffer); - if(result !== undefined) { - this.debugLog("UDP send finished by callback"); - cancelTimeout(); - resolve(result); - } - }; + reject(e); + }); }); - }); + return await Promise.race([promise, wrappedTimeout, this.abortedPromise]); + } finally { + timeout && timeout.cancel(); + socketCallback && socket.removeCallback(socketCallback); + } } - _udpIncoming(buffer) { - this.udpCallback && this.udpCallback(buffer); - } - - request(params) { - let promise = requestAsync({ - ...params, - timeout: this.options.socketTimeout, - resolveWithFullResponse: true - }); - const cancelAsyncLeak = this.addAsyncLeak(() => { - promise.cancel(); - }); - this.debugLog(log => { - log(() => params.uri+" HTTP-->"); - promise - .then((response) => log(params.uri+" <--HTTP " + response.statusCode)) - .catch(()=>{}); - }); - promise = promise.finally(cancelAsyncLeak); - promise = promise.then(response => response.body); - return promise; + async request(params) { + 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 = promise.then(response => response.body); + return await Promise.race([wrappedPromise, this.abortedPromise]); + } finally { + requestPromise && requestPromise.cancel(); + } } debugLog(...args) { - if (!this.debug) return; + if (!this.options.debug) return; try { if(args[0] instanceof Buffer) { this.debugLog(HexUtil.debugDump(args[0])); diff --git a/protocols/doom3.js b/protocols/doom3.js index ecca307..52fed01 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -3,7 +3,6 @@ const Core = require('./core'); class Doom3 extends Core { constructor() { super(); - this.pretty = 'Doom 3'; this.encoding = 'latin1'; this.isEtqw = false; this.hasSpaceBeforeClanTag = false; @@ -11,26 +10,33 @@ class Doom3 extends Core { this.hasTypeFlag = false; } async run(state) { - const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00', packet => { + const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => { const reader = this.reader(packet); const header = reader.uint(2); if(header !== 0xffff) return; const header2 = reader.string(); if(header2 !== 'infoResponse') return; + const challengePart1 = reader.string({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(); }); - const reader = this.reader(body); - if(this.isEtqw) { - const taskId = reader.uint(4); - } - - const challenge = reader.uint(4); + let reader = this.reader(body); const protoVersion = reader.uint(4); state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff); - if(this.isEtqw) { + // 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()) { @@ -42,23 +48,22 @@ class Doom3 extends Core { } if(!key) break; state.raw[key] = value; + this.debugLog(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); + 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 @@ -66,7 +71,7 @@ class Doom3 extends Core { } state.raw.osmask = reader.uint(4); - if(this.isEtqw) { + if(isEtqw) { state.raw.ranked = reader.uint(1); state.raw.timeleft = reader.uint(4); state.raw.gamestate = reader.uint(1); @@ -84,6 +89,59 @@ class Doom3 extends Core { 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; + if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port + } + + attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) { + this.debugLog("starting player parse attempt:"); + this.debugLog("isEtqw: " + isEtqw); + this.debugLog("hasClanTag: " + hasClanTag); + this.debugLog("hasClanTagPos: " + hasClanTagPos); + this.debugLog("hasTypeFlag: " + hasTypeFlag); + const reader = this.reader(rest); + let lastId = -1; + const players = []; + while(true) { + this.debugLog("---"); + if (reader.done()) { + this.debugLog("* aborting attempt, overran buffer *"); + return null; + } + const player = {}; + player.id = reader.uint(1); + this.debugLog("id: " + player.id); + if (player.id <= lastId || player.id > 0x20) { + this.debugLog("* aborting attempt, invalid player id *"); + return null; + } + lastId = player.id; + if(player.id === 0x20) { + this.debugLog("* player parse successful *"); + break; + } + player.ping = reader.uint(2); + this.debugLog("ping: " + player.ping); + if(!isEtqw) { + player.rate = reader.uint(4); + this.debugLog("rate: " + player.rate); + } + player.name = this.stripColors(reader.string()); + this.debugLog("name: " + player.name); + if(hasClanTag) { + if(hasClanTagPos) { + const clanTagPos = reader.uint(1); + this.debugLog("clanTagPos: " + clanTagPos); + } + player.clantag = this.stripColors(reader.string()); + this.debugLog("clan tag: " + player.clantag); + } + if(hasTypeFlag) { + player.typeflag = reader.uint(1); + this.debugLog("type flag: " + player.typeflag); + } + players.push(player); + } + return [players,reader]; } stripColors(str) { diff --git a/protocols/fivem.js b/protocols/fivem.js index 35d1ea0..b091ccc 100644 --- a/protocols/fivem.js +++ b/protocols/fivem.js @@ -13,7 +13,7 @@ class FiveM extends Quake2 { { const raw = await this.request({ - uri: 'http://' + this.options.address + ':' + this.options.port_query + '/info.json' + uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json' }); const json = JSON.parse(raw); state.raw.info = json; @@ -21,7 +21,7 @@ class FiveM extends Quake2 { { const raw = await this.request({ - uri: 'http://' + this.options.address + ':' + this.options.port_query + '/players.json' + uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json' }); const json = JSON.parse(raw); state.raw.players = json; diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index 7076e80..c2674d4 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -37,6 +37,30 @@ class Gamespy2 extends Core { 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); + } + } + + if (state.raw.hostport) { + state.gamePort = parseInt(state.raw.hostport); + } } async sendPacket(type) { diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 6a39475..f8dea75 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -7,17 +7,19 @@ class Gamespy3 extends Core { this.sessionId = 1; this.encoding = 'latin1'; this.byteorder = 'be'; - this.noChallenge = false; this.useOnlySingleSplit = false; this.isJc2mp = false; } async run(state) { - let challenge = null; - if (!this.noChallenge) { - const buffer = await this.sendPacket(9, false, false, false); - const reader = this.reader(buffer); - challenge = parseInt(reader.string()); + 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; diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 3de3532..1aaa1f9 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -3,7 +3,7 @@ const Core = require('./core'); class Kspdmp extends Core { async run(state) { const body = await this.request({ - uri: 'http://'+this.options.address+':'+this.options.port_query + uri: 'http://'+this.options.address+':'+this.options.port }); const json = JSON.parse(body); diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 65e223c..260751a 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -2,9 +2,13 @@ const Core = require('./core'), Varint = require('varint'); class Minecraft extends Core { + constructor() { + super(); + this.srvRecord = "_minecraft._tcp"; + } async run(state) { const portBuf = Buffer.alloc(2); - portBuf.writeUInt16BE(this.options.port_query,0); + portBuf.writeUInt16BE(this.options.port,0); const addressBuf = Buffer.from(this.options.host,'utf8'); diff --git a/protocols/nadeo.js b/protocols/nadeo.js index 72db975..62711b8 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -2,12 +2,6 @@ const gbxremote = require('gbxremote'), Core = require('./core'); class Nadeo extends Core { - constructor() { - super(); - this.options.port = 2350; - this.options.port_query = 5000; - } - async run(state) { await this.withClient(async client => { await this.methodCall(client, 'Authenticate', this.options.login, this.options.password); @@ -57,8 +51,8 @@ class Nadeo extends Core { } async withClient(fn) { - const socket = gbxremote.createClient(this.options.port_query, this.options.host); - const cancelAsyncLeak = this.addAsyncLeak(() => socket.terminate()); + const socket = gbxremote.createClient(this.options.port, this.options.host); + const cancelAsyncLeak = this.addCleanup(() => socket.terminate()); try { await this.timedPromise( new Promise((resolve,reject) => { diff --git a/protocols/terraria.js b/protocols/terraria.js index ee0fdc5..555a966 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -3,7 +3,7 @@ const Core = require('./core'); class Terraria extends Core { async run(state) { const body = await this.request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status', + uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status', qs: { players: 'true', token: this.options.token