From 77b2cc1c7fa4123104de578e230f3afe7337d576 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Mon, 7 Jan 2019 00:52:29 -0600 Subject: [PATCH 01/11] Initial es6 async conversion work --- bin/gamedig.js | 6 +- lib/index.js | 44 +--- lib/typeresolver.js | 7 +- protocols/americasarmy.js | 4 +- protocols/armagetron.js | 6 +- protocols/ase.js | 70 +++-- protocols/battlefield.js | 246 +++++++++--------- protocols/core.js | 460 +++++++++++++++++--------------- protocols/samp.js | 143 +++++----- protocols/valve.js | 535 +++++++++++++++++++------------------- 10 files changed, 773 insertions(+), 748 deletions(-) diff --git a/bin/gamedig.js b/bin/gamedig.js index 919246d..a874ee9 100644 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -5,7 +5,7 @@ const argv = require('minimist')(process.argv.slice(2)), const debug = argv.debug; delete argv.debug; -const outputFormat = argv.output; +const pretty = !!argv.pretty; delete argv.output; const options = {}; @@ -25,7 +25,7 @@ Gamedig.isCommandLine = 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 +42,7 @@ Gamedig.query(options) if (error instanceof Error) { error = error.message; } - if (outputFormat === 'pretty') { + if (pretty) { console.log(JSON.stringify({error: error}, null, ' ')); } else { console.log(JSON.stringify({error: error})); diff --git a/lib/index.js b/lib/index.js index c0466f0..cda6184 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,7 @@ const dgram = require('dgram'), TypeResolver = require('./typeresolver'), HexUtil = require('./HexUtil'); -const activeQueries = []; +const activeQueries = new Set(); const udpSocket = dgram.createSocket('udp4'); udpSocket.unref(); @@ -13,12 +13,9 @@ udpSocket.on('message', (buffer, rinfo) => { 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.address !== rinfo.address) continue; if(query.options.port_query !== rinfo.port) continue; - query._udpResponse(buffer); + query._udpIncoming(buffer); break; } }); @@ -29,27 +26,14 @@ udpSocket.on('error', (e) => { class Gamedig { static query(options,callback) { - const promise = new Promise((resolve,reject) => { + const promise = (async () => { 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; - } + let query = TypeResolver.lookup(options.type); query.debug = Gamedig.debug; query.udpSocket = udpSocket; query.type = options.type; @@ -73,17 +57,13 @@ class Gamedig { 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(); - }); - }); + activeQueries.add(query); + try { + return await query.runAll(); + } finally { + activeQueries.delete(query); + } + })(); if (callback && callback instanceof Function) { if(callback.length === 2) { diff --git a/lib/typeresolver.js b/lib/typeresolver.js index 78f27f0..dd33536 100644 --- a/lib/typeresolver.js +++ b/lib/typeresolver.js @@ -1,5 +1,6 @@ const Path = require('path'), - fs = require('fs'); + fs = require('fs'), + Core = require('../protocols/core'); const protocolDir = Path.normalize(__dirname+'/../protocols'); const gamesFile = Path.normalize(__dirname+'/../games.txt'); @@ -55,6 +56,10 @@ function createProtocolInstance(type) { } class TypeResolver { + /** + * @param {string} type + * @returns Core + */ static lookup(type) { if(!type) throw Error('No game specified'); diff --git a/protocols/americasarmy.js b/protocols/americasarmy.js index cf92afc..34b6bc2 100644 --- a/protocols/americasarmy.js +++ b/protocols/americasarmy.js @@ -1,8 +1,8 @@ const Gamespy2 = require('./gamespy2'); class AmericasArmy extends Gamespy2 { - finalizeState(state) { - super.finalizeState(state); + 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)) { diff --git a/protocols/armagetron.js b/protocols/armagetron.js index e156023..1b3d628 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -7,10 +7,10 @@ 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) => { + await this.udpSend(b,(buffer) => { const reader = this.reader(buffer); reader.skip(6); @@ -37,7 +37,7 @@ class Armagetron extends Core { state.raw.uri = this.readString(reader); state.raw.globalids = this.readString(reader); this.finish(state); - return true; + return null; }); } diff --git a/protocols/ase.js b/protocols/ase.js index 038a7c7..ae0557f 100644 --- a/protocols/ase.js +++ b/protocols/ase.js @@ -1,44 +1,42 @@ const Core = require('./core'); class Ase extends Core { - run(state) { - this.udpSend('s',(buffer) => { + async run(state) { + const buffer = await this.udpSend('s',(buffer) => { const reader = this.reader(buffer); - - const header = reader.string({length:4}); - if(header !== 'EYE1') return; - - state.raw.gamename = this.readString(reader); - state.raw.port = parseInt(this.readString(reader)); - state.name = this.readString(reader); - state.raw.gametype = this.readString(reader); - state.map = this.readString(reader); - state.raw.version = this.readString(reader); - state.password = this.readString(reader) === '1'; - state.raw.numplayers = parseInt(this.readString(reader)); - state.maxplayers = parseInt(this.readString(reader)); - - while(!reader.done()) { - const key = this.readString(reader); - if(!key) break; - const value = this.readString(reader); - state.raw[key] = value; - } - - while(!reader.done()) { - const flags = reader.uint(1); - const player = {}; - if(flags & 1) player.name = this.readString(reader); - if(flags & 2) player.team = this.readString(reader); - if(flags & 4) player.skin = this.readString(reader); - if(flags & 8) player.score = parseInt(this.readString(reader)); - if(flags & 16) player.ping = parseInt(this.readString(reader)); - if(flags & 32) player.time = parseInt(this.readString(reader)); - state.players.push(player); - } - - this.finish(state); + const header = reader.string({length: 4}); + if (header === 'EYE1') return buffer; }); + + const reader = this.reader(buffer); + 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); + } } readString(reader) { diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 9f3463f..45968c5 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -1,5 +1,4 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Battlefield extends Core { constructor() { @@ -8,114 +7,128 @@ class Battlefield extends Core { 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.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()); - 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.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(); - 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.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); - }); + 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'); + } + } + + { + 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) { + if(this.debug) console.log(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); @@ -134,29 +147,4 @@ class Battlefield extends Core { } } -function buildPacket(params) { - const paramBuffers = []; - for (const param of params) { - paramBuffers.push(Buffer.from(param,'utf8')); - } - - let totalLength = 12; - for (const paramBuffer of paramBuffers) { - totalLength += paramBuffer.length+1+4; - } - - const b = Buffer.alloc(totalLength); - b.writeUInt32LE(0,0); - b.writeUInt32LE(totalLength,4); - b.writeUInt32LE(params.length,8); - let offset = 12; - for (const paramBuffer of paramBuffers) { - b.writeUInt32LE(paramBuffer.length, offset); offset += 4; - paramBuffer.copy(b, offset); offset += paramBuffer.length; - b.writeUInt8(0, offset); offset += 1; - } - - return b; -} - module.exports = Battlefield; \ No newline at end of file diff --git a/protocols/core.js b/protocols/core.js index 1fadb09..97598fc 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -1,9 +1,11 @@ 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); class Core extends EventEmitter { constructor() { @@ -13,23 +15,15 @@ class Core extends EventEmitter { 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; - } - fatal(err,noretry) { - if(!noretry && this.attempt < this.options.maxAttempts) { - this.attempt++; - this.start(); - return; - } - - this.done({error: err.toString()}); + this.attemptAbortables = new Set(); + this.udpCallback = null; + this.udpLocked = false; + this.lastAbortableId = 0; } initState() { @@ -46,128 +40,138 @@ class Core extends EventEmitter { }; } - finalizeState(state) {} + // Run all attempts + async runAll() { + 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; + } + } - finish(state) { - this.finalizeState(state); - this.done(state); + if (result === null) { + throw lastError; + } + return result; } - done(state) { - if(this.finished) return; + // 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.attemptAbortables.size) { + let out = []; + for (const abortable of this.attemptAbortables) { + out.push(abortable.id + " " + abortable.stack); + } + throw new Error('Query succeeded, but abortables were not empty (async leak?):\n' + out.join('\n---\n')); + } + return result; + } finally { + // Clean up any lingering long-running functions + for (const abortable of this.attemptAbortables) { + try { + abortable.abort(); + } catch(e) {} + } + this.attemptAbortables.clear(); + } + } - if(this.options.notes) + timedPromise(promise, timeoutMs, timeoutMsg) { + return new Promise((resolve, reject) => { + const cancelTimeout = this.setTimeout( + () => reject(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")), + timeoutMs + ); + promise.finally(cancelTimeout).then(resolve,reject); + }); + } + + async runOnce() { + const startMillis = Date.now(); + const options = this.options; + 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(); + 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; + 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; + if ('pretty' in this) state.query.pretty = this.pretty; + state.query.duration = Date.now() - startMillis; - this.reset(); - this.finished = true; - this.emit('finished',state); - if(this.options.callback) this.options.callback(state); + return state; } - reset() { - clearTimeout(this.attemptTimeoutTimer); - if(this.timers) { - for (const timer of this.timers) { - clearTimeout(timer); - } - } - this.timers = []; + async run(state) {} - if(this.tcpSocket) { - this.tcpSocket.destroy(); - delete this.tcpSocket; - } - - this.udpTimeoutTimer = false; - this.udpCallback = false; - } - - start() { - const options = this.options; - this.reset(); - - 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); - } - }, - (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()); - } - - ]); - } - - run() {} - - parseDns(host,c) { - const resolveStandard = (host,c) => { + /** + * @param {string} host + * @returns {Promise} + */ + async parseDns(host) { + const isIp = (host) => { + return !!host.match(/\d+\.\d+\.\d+\.\d+/); + }; + const resolveStandard = async (host) => { + if(isIp(host)) return host; 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(); - }); + const {address,family} = await dnsLookupAsync(host); + if(this.debug) console.log(address); + return address; }; - - const resolveSrv = (srv,host,c) => { + const resolveSrv = async (srv,host) => { + if(isIp(host)) return host; 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(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) { - this.options.address = srvhost; - c(); - } else { - // resolve yet again - resolveStandard(srvhost,c); - } - return; + let records; + try { + records = await dnsResolveAsync(srv + '.' + host, 'SRV'); + if(this.debug) console.log(records); + if(records.length >= 1) { + const record = records[0]; + this.options.port = record.port; + const srvhost = record.name; + return await resolveStandard(srvhost); } - return resolveStandard(host,c); - }); + } catch(e) { + if (this.debug) console.log(e.toString()); + } + return await resolveStandard(host); }; - if(this.srvRecord) resolveSrv(this.srvRecord,host,c); - else resolveStandard(host,c); + if(this.srvRecord) return await resolveSrv(this.srvRecord, host); + else return await resolveStandard(host); + } + + addAbortable(fn) { + const id = ++this.lastAbortableId; + const stack = new Error().stack; + const entry = { id: id, abort: fn, stack: stack }; + if (this.debug) console.log("Adding abortable: " + id); + this.attemptAbortables.add(entry); + return () => { + if (this.debug) console.log("Removing abortable: " + id); + this.attemptAbortables.delete(entry); + } } // utils @@ -184,125 +188,169 @@ 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([]); + /** + * @param {function(Socket):Promise} fn + * @returns {Promise} + */ + async withTcp(fn) { 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); - }); + const socket = net.connect(port,address); socket.setNoDelay(true); - if(this.debug) console.log(address+':'+port+" TCPCONNECT"); + const cancelAbortable = this.addAbortable(() => socket.destroy()); - const writeHook = socket.write; - socket.write = (...args) => { - if(this.debug) { + if(this.debug) { + console.log(address+':'+port+" TCP Connecting"); + const writeHook = socket.write; + socket.write = (...args) => { 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([]); - } - }); - } - 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) => { - socket.write(buffer); + writeHook.apply(socket,args); + }; + socket.on('error', e => console.log('TCP Error: ' + e)); + socket.on('close', () => console.log('TCP Closed')); + socket.on('data', (data) => { + if(this.debug) { + console.log(address+':'+port+" <--TCP"); + console.log(HexUtil.debugDump(data)); + } }); - if(!ondata) return; + socket.on('ready', () => console.log(address+':'+port+" TCP Connected")); + } - this.tcpTimeoutTimer = this.setTimeout(() => { - this.tcpCallback = false; - this.fatal('TCP Watchdog Timeout'); - },this.options.socketTimeout); - this.tcpCallback = ondata; - }); + 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' + ); + return await fn(socket); + } finally { + cancelAbortable(); + socket.destroy(); + } } - 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; - }); + setTimeout(callback, time) { + let cancelAbortable; + const onTimeout = () => { + cancelAbortable(); + callback(); + }; + const timeout = setTimeout(onTimeout, time); + cancelAbortable = this.addAbortable(() => clearTimeout(timeout)); + return () => { + cancelAbortable(); + clearTimeout(timeout); + } } - _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 {Socket} socket + * @param {Buffer} buffer + * @param {function(Buffer):boolean} ondata + * @returns {Promise} + */ + async tcpSend(socket,buffer,ondata) { + return await this.timedPromise( + 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); + }), + 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(); + } finally { + this.udpLocked = false; + this.udpCallback = null; + } + } + + /** + * @param {Buffer|string} buffer + * @param {function(Buffer):T} onPacket + * @param {(function():T)=} onTimeout + * @returns Promise + * @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'); if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); - if(this.debug) { console.log(this.options.address+':'+this.options.port_query+" UDP-->"); console.log(HexUtil.debugDump(buffer)); } - this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); + + return await this.withUdpLock(async() => { + this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); + + return await new Promise((resolve,reject) => { + const cancelTimeout = this.setTimeout(() => { + if (this.debug) console.log("UDP timeout detected"); + let success = false; + if (onTimeout) { + const result = onTimeout(); + if (result !== undefined) { + if (this.debug) console.log("UDP timeout resolved by callback"); + resolve(result); + success = true; + } + } + if (!success) { + reject(new Error('UDP Watchdog Timeout')); + } + },this.options.socketTimeout); + + this.udpCallback = (buffer) => { + const result = onPacket(buffer); + if(result !== undefined) { + if (this.debug) console.log("UDP send finished by callback"); + cancelTimeout(); + resolve(result); + } + }; + }); + }); } - _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; - } - } else { - this.udpResponse(buffer); - } + + _udpIncoming(buffer) { + this.udpCallback && this.udpCallback(buffer); } - udpResponse() {} } module.exports = Core; diff --git a/protocols/samp.js b/protocols/samp.js index 43934a4..57cff96 100644 --- a/protocols/samp.js +++ b/protocols/samp.js @@ -1,5 +1,4 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'); class Samp extends Core { constructor() { @@ -7,87 +6,83 @@ class Samp extends Core { this.encoding = 'win1252'; } - run(state) { - async.series([ - (c) => { - this.sendPacket('i',(reader) => { - state.password = !!reader.uint(1); - state.raw.numplayers = reader.uint(2); - state.maxplayers = reader.uint(2); - state.name = this.readString(reader,4); - state.raw.gamemode = this.readString(reader,4); - this.map = this.readString(reader,4); - c(); - }); - }, - (c) => { - this.sendPacket('r',(reader) => { - const ruleCount = reader.uint(2); - state.raw.rules = {}; - for(let i = 0; i < ruleCount; i++) { - const key = this.readString(reader,1); - const value = this.readString(reader,1); - state.raw.rules[key] = value; - } - if('mapname' in state.raw.rules) - state.map = state.raw.rules.mapname; - c(); - }); - }, - (c) => { - this.sendPacket('d',(reader) => { - const playerCount = reader.uint(2); - for(let i = 0; i < playerCount; i++) { - const player = {}; - player.id = reader.uint(1); - player.name = this.readString(reader,1); - player.score = reader.int(4); - player.ping = reader.uint(4); - state.players.push(player); - } - c(); - },() => { - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - c(); - }); - }, - (c) => { - this.finish(state); + async run(state) { + // read info + { + const reader = await this.sendPacket('i'); + state.password = !!reader.uint(1); + state.raw.numplayers = reader.uint(2); + state.maxplayers = reader.uint(2); + state.name = this.readString(reader,4); + state.raw.gamemode = this.readString(reader,4); + this.map = this.readString(reader,4); + } + + // read rules + { + const reader = await this.sendPacket('r'); + const ruleCount = reader.uint(2); + state.raw.rules = {}; + for(let i = 0; i < ruleCount; i++) { + const key = this.readString(reader,1); + const value = this.readString(reader,1); + state.raw.rules[key] = value; } - ]); + if('mapname' in state.raw.rules) + state.map = state.raw.rules.mapname; + } + + // read players + { + const reader = await this.sendPacket('d', true); + if (reader !== null) { + const playerCount = reader.uint(2); + for(let i = 0; i < playerCount; i++) { + const player = {}; + player.id = reader.uint(1); + player.name = this.readString(reader,1); + player.score = reader.int(4); + player.ping = reader.uint(4); + state.players.push(player); + } + } else { + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + } + } } readString(reader,lenBytes) { const length = reader.uint(lenBytes); if(!length) return ''; - const string = reader.string({length:length}); - return string; + return reader.string({length:length}); } - sendPacket(type,onresponse,ontimeout) { - const outbuffer = Buffer.alloc(11); - outbuffer.writeUInt32BE(0x53414D50,0); + async sendPacket(type,allowTimeout) { + const outBuffer = Buffer.alloc(11); + outBuffer.writeUInt32BE(0x53414D50,0); const ipSplit = this.options.address.split('.'); - outbuffer.writeUInt8(parseInt(ipSplit[0]),4); - outbuffer.writeUInt8(parseInt(ipSplit[1]),5); - outbuffer.writeUInt8(parseInt(ipSplit[2]),6); - outbuffer.writeUInt8(parseInt(ipSplit[3]),7); - outbuffer.writeUInt16LE(this.options.port,8); - outbuffer.writeUInt8(type.charCodeAt(0),10); + outBuffer.writeUInt8(parseInt(ipSplit[0]),4); + outBuffer.writeUInt8(parseInt(ipSplit[1]),5); + outBuffer.writeUInt8(parseInt(ipSplit[2]),6); + outBuffer.writeUInt8(parseInt(ipSplit[3]),7); + outBuffer.writeUInt16LE(this.options.port,8); + outBuffer.writeUInt8(type.charCodeAt(0),10); - this.udpSend(outbuffer,(buffer) => { - const reader = this.reader(buffer); - for(let i = 0; i < outbuffer.length; i++) { - if(outbuffer.readUInt8(i) !== reader.uint(1)) return; + return await this.udpSend( + outBuffer, + (buffer) => { + const reader = this.reader(buffer); + for(let i = 0; i < outBuffer.length; i++) { + if(outBuffer.readUInt8(i) !== reader.uint(1)) return; + } + return reader; + }, + () => { + if(allowTimeout) { + return null; + } } - onresponse(reader); - return true; - },() => { - if(ontimeout) { - ontimeout(); - return true; - } - }); + ); } } diff --git a/protocols/valve.js b/protocols/valve.js index a925a57..9a77511 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -1,5 +1,4 @@ -const async = require('async'), - Bzip2 = require('compressjs').Bzip2, +const Bzip2 = require('compressjs').Bzip2, Core = require('./core'); class Valve extends Core { @@ -28,173 +27,169 @@ 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) { + 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) { + 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.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; } } - queryPlayers(state,c) { + async queryChallenge() { + if(this.legacyChallenge) { + // sendPacket will catch the response packet and + // save the challenge for us + 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; + 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 - }); - } + if(this.debug) console.log("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; - }); + 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 +229,158 @@ 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 retryQuery = false; + const response = await this.sendPacketRaw( + type, sendChallenge, payload, + (payload) => { + const reader = this.reader(payload); + const type = reader.uint(1); + if (type === 0x41) { + const key = reader.uint(4); + if (this._challenge !== key) { + if (this.debug) console.log('Received new challenge key: ' + key); + this._challenge = key; + retryQuery = true; + if (keyRetry === 0 && sendChallenge) { + if (this.debug) console.log('Restarting query'); + return null; + } + } + } + if (this.debug) console.log("Received " + type.toString(16) + " expected " + expect.toString(16)); + if (type === expect) { + return reader.rest(); + } + }, + () => { + if (allowTimeout) return null; + } + ); + if (!retryQuery) 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 + if(this.debug) console.log("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; - }; - - const receivedOne = (buffer) => { - const reader = this.reader(buffer); - - 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; + if(this.debug) { + console.log("Received partial packet uid:"+uid+" num:"+packetNum); + console.log("Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID"); } - 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; + 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; + } + 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; + } + } + 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 + ); } } From 9b8423b20a92e314c9bc51ec2e5d50ccac3cbc81 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Wed, 9 Jan 2019 05:35:11 -0600 Subject: [PATCH 02/11] More async conversions --- games.txt | 4 +- package-lock.json | 526 ++++++++++++++++++++++++------------- package.json | 5 +- protocols/buildandshoot.js | 96 ++++--- protocols/core.js | 63 +++-- protocols/doom3.js | 140 +++++----- protocols/ffow.js | 51 ++-- protocols/fivem.js | 58 ++-- protocols/gamespy1.js | 92 +++---- protocols/gamespy2.js | 99 ++++--- protocols/gamespy3.js | 262 +++++++++--------- protocols/jc2mp.js | 5 +- protocols/quake2.js | 137 +++++----- protocols/ut3.js | 4 +- protocols/valve.js | 21 +- 15 files changed, 859 insertions(+), 704 deletions(-) diff --git a/games.txt b/games.txt index 001684e..c9b9245 100644 --- a/games.txt +++ b/games.txt @@ -63,7 +63,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 @@ -148,7 +148,7 @@ il2|IL-2 Sturmovik|gamespy1|port_query=21000 insurgency|Insurgency|valve 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 diff --git a/package-lock.json b/package-lock.json index 5526b12..1cf28bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,23 @@ { "name": "gamedig", - "version": "1.0.41", + "version": "1.0.49", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", + "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "amdefine": { @@ -21,9 +26,12 @@ "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", @@ -46,51 +54,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 +115,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 +123,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 +132,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 +161,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 +224,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 +239,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 +253,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 +263,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 +277,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 +338,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", @@ -278,11 +358,15 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, "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 +374,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 +394,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 +427,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 +599,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 +642,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "xmlbuilder": { diff --git a/package.json b/package.json index 66048a9..9a1cf4a 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,16 @@ }, "dependencies": { "async": "^0.9.2", + "cheerio": "^1.0.0-rc.2", "compressjs": "^1.0.2", "gbxremote": "^0.1.4", "iconv-lite": "^0.4.18", + "jquery": "^3.3.1", "long": "^2.4.0", "minimist": "^1.2.0", "moment": "^2.21.0", - "request": "^2.85.0", + "request": "^2.88.0", + "request-promise": "^4.2.2", "varint": "^4.0.1" }, "bin": { diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index 6620c40..db8ebb9 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -1,59 +1,51 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'), + cheerio = require('cheerio'); class BuildAndShoot extends Core { - run(state) { - request({ + async run(state) { + const body = await this.request({ uri: 'http://'+this.options.address+':'+this.options.port_query+'/', - timeout: 3000, - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - - let m; - - m = body.match(/status server for (.*?)\r|\n/); - if(m) state.name = m[1]; - - m = body.match(/Current uptime: (\d+)/); - if(m) state.raw.uptime = m[1]; - - m = body.match(/currently running (.*?) by /); - if(m) state.map = m[1]; - - m = body.match(/Current players: (\d+)\/(\d+)/); - if(m) { - state.raw.numplayers = m[1]; - state.maxplayers = m[2]; - } - - m = body.match(/class="playerlist"([^]+?)\/table/); - if(m) { - const table = m[1]; - const pre = /[^]*([^]*)<\/td>[^]*([^]*)<\/td>[^]*([^]*)<\/td>[^]*([^]*)<\/td>/g; - let pm; - while(pm = pre.exec(table)) { - if(pm[2] === 'Ping') continue; - state.players.push({ - name: pm[1], - ping: pm[2], - team: pm[3], - score: pm[4] - }); - } - } - /* - var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); - if(m) { - var o1 = parseInt(m[1]); - var o2 = parseInt(m[2]); - var o3 = parseInt(m[3]); - var o4 = parseInt(m[4]); - var addr = o1+(o2<<8)+(o3<<16)+(o4<<24); - state.raw.url = 'aos://'+addr; - } - */ - this.finish(state); }); + + 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]; + } + + const $ = cheerio.load(body); + $('#playerlist tbody tr').each((i,tr) => { + if (!$(tr).find('td').first().attr('colspan')) { + state.players.push({ + name: $(tr).find('td').eq(2).text(), + ping: $(tr).find('td').eq(3).text().trim(), + team: $(tr).find('td').eq(4).text().toLowerCase(), + score: parseInt($(tr).find('td').eq(5).text()) + }); + } + }); + /* + var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); + if(m) { + var o1 = parseInt(m[1]); + var o2 = parseInt(m[2]); + var o3 = parseInt(m[3]); + var o4 = parseInt(m[4]); + var addr = o1+(o2<<8)+(o3<<16)+(o4<<24); + state.raw.url = 'aos://'+addr; + } + */ } } diff --git a/protocols/core.js b/protocols/core.js index 97598fc..960d1a0 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -5,7 +5,8 @@ const EventEmitter = require('events').EventEmitter, HexUtil = require('../lib/HexUtil'), util = require('util'), dnsLookupAsync = util.promisify(dns.lookup), - dnsResolveAsync = util.promisify(dns.resolve); + dnsResolveAsync = util.promisify(dns.resolve), + requestAsync = require('request-promise'); class Core extends EventEmitter { constructor() { @@ -20,10 +21,10 @@ class Core extends EventEmitter { this.delimiter = '\0'; this.srvRecord = null; - this.attemptAbortables = new Set(); + this.asyncLeaks = new Set(); this.udpCallback = null; this.udpLocked = false; - this.lastAbortableId = 0; + this.lastAsyncLeakId = 0; } initState() { @@ -64,22 +65,24 @@ class Core extends EventEmitter { async runOnceSafe() { try { const result = await this.timedPromise(this.runOnce(), this.options.attemptTimeout, "Attempt"); - if (this.attemptAbortables.size) { + if (this.asyncLeaks.size) { let out = []; - for (const abortable of this.attemptAbortables) { - out.push(abortable.id + " " + abortable.stack); + for (const leak of this.asyncLeaks) { + out.push(leak.id + " " + leak.stack); } - throw new Error('Query succeeded, but abortables were not empty (async leak?):\n' + out.join('\n---\n')); + 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 abortable of this.attemptAbortables) { + for (const leak of this.asyncLeaks) { try { - abortable.abort(); - } catch(e) {} + leak.cleanup(); + } catch(e) { + if (this.debug) console.log("Error during async cleanup: " + e.stack); + } } - this.attemptAbortables.clear(); + this.asyncLeaks.clear(); } } @@ -162,15 +165,15 @@ class Core extends EventEmitter { else return await resolveStandard(host); } - addAbortable(fn) { - const id = ++this.lastAbortableId; + addAsyncLeak(fn) { + const id = ++this.lastAsyncLeakId; const stack = new Error().stack; - const entry = { id: id, abort: fn, stack: stack }; - if (this.debug) console.log("Adding abortable: " + id); - this.attemptAbortables.add(entry); + const entry = { id: id, cleanup: fn, stack: stack }; + if (this.debug) console.log("Registering async leak: " + id); + this.asyncLeaks.add(entry); return () => { - if (this.debug) console.log("Removing abortable: " + id); - this.attemptAbortables.delete(entry); + if (this.debug) console.log("Removing async leak: " + id); + this.asyncLeaks.delete(entry); } } @@ -210,7 +213,7 @@ class Core extends EventEmitter { const socket = net.connect(port,address); socket.setNoDelay(true); - const cancelAbortable = this.addAbortable(() => socket.destroy()); + const cancelAsyncLeak = this.addAsyncLeak(() => socket.destroy()); if(this.debug) { console.log(address+':'+port+" TCP Connecting"); @@ -242,21 +245,21 @@ class Core extends EventEmitter { ); return await fn(socket); } finally { - cancelAbortable(); + cancelAsyncLeak(); socket.destroy(); } } setTimeout(callback, time) { - let cancelAbortable; + let cancelAsyncLeak; const onTimeout = () => { - cancelAbortable(); + cancelAsyncLeak(); callback(); }; const timeout = setTimeout(onTimeout, time); - cancelAbortable = this.addAbortable(() => clearTimeout(timeout)); + cancelAsyncLeak = this.addAsyncLeak(() => clearTimeout(timeout)); return () => { - cancelAbortable(); + cancelAsyncLeak(); clearTimeout(timeout); } } @@ -351,6 +354,18 @@ class Core extends EventEmitter { _udpIncoming(buffer) { this.udpCallback && this.udpCallback(buffer); } + + request(params) { + const promise = requestAsync({ + ...params, + timeout: this.options.socketTimeout + }); + const cancelAsyncLeak = this.addAsyncLeak(() => { + promise.cancel(); + }); + promise.finally(cancelAsyncLeak); + return promise; + } } module.exports = Core; diff --git a/protocols/doom3.js b/protocols/doom3.js index dc55713..ecca307 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -10,82 +10,80 @@ class Doom3 extends Core { 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; + return reader.rest(); }); + + const reader = this.reader(body); + 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; } stripColors(str) { diff --git a/protocols/ffow.js b/protocols/ffow.js index 6df907b..8f228d3 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -6,29 +6,34 @@ class Ffow extends Valve { this.byteorder = 'be'; this.legacyChallenge = true; } - queryInfo(state,c) { - this.sendPacket(0x46,false,'LSQ',0x49, (b) => { - const reader = this.reader(b); - state.raw.protocol = reader.uint(1); - state.name = reader.string(); - state.map = reader.string(); - state.raw.mod = reader.string(); - state.raw.gamemode = reader.string(); - state.raw.description = reader.string(); - state.raw.version = reader.string(); - state.raw.port = reader.uint(2); - state.raw.numplayers = reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.listentype = String.fromCharCode(reader.uint(1)); - state.raw.environment = String.fromCharCode(reader.uint(1)); - state.password = !!reader.uint(1); - state.raw.secure = reader.uint(1); - state.raw.averagefps = reader.uint(1); - state.raw.round = reader.uint(1); - state.raw.maxrounds = reader.uint(1); - state.raw.timeleft = reader.uint(2); - c(); - }); + async queryInfo(state) { + if(this.debug) console.log("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.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); } } diff --git a/protocols/fivem.js b/protocols/fivem.js index 49ed7a7..35d1ea0 100644 --- a/protocols/fivem.js +++ b/protocols/fivem.js @@ -1,5 +1,4 @@ -const request = require('request'), - Quake2 = require('./quake2'); +const Quake2 = require('./quake2'); class FiveM extends Quake2 { constructor() { @@ -9,43 +8,28 @@ class FiveM extends Quake2 { this.encoding = 'utf8'; } - finish(state) { - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/info.json', - timeout: this.options.socketTimeout - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid JSON'); - } + async run(state) { + await super.run(state); - state.raw.info = json; - - request({ - uri: 'http://'+this.options.address+':'+this.options.port_query+'/players.json', - timeout: this.options.socketTimeout - }, (e,r,body) => { - if(e) return this.fatal('HTTP error'); - let json; - try { - json = JSON.parse(body); - } catch(e) { - return this.fatal('Invalid JSON'); - } - - state.raw.players = json; - - state.players = []; - for (const player of json) { - state.players.push({name:player.name, ping:player.ping}); - } - - super.finish(state); + { + const raw = await this.request({ + uri: 'http://' + this.options.address + ':' + this.options.port_query + '/info.json' }); - }); + const json = JSON.parse(raw); + state.raw.info = json; + } + + { + const raw = await this.request({ + uri: 'http://' + this.options.address + ':' + this.options.port_query + '/players.json' + }); + const json = JSON.parse(raw); + state.raw.players = json; + state.players = []; + for (const player of json) { + state.players.push({name: player.name, ping: player.ping}); + } + } } } diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index 76a078d..9b9729c 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -1,68 +1,57 @@ -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); + } + { + 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 +68,7 @@ class Gamespy1 extends Core { if('final' in output) { delete output.final; delete output.queryid; - callback(output); - return true; + return output; } }); } diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index d6c7f25..941ece2 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -3,53 +3,71 @@ 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); + } + + // 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); + } + } + + 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 + + // 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); + if (this.debug) console.log("Detected missing count byte, rewinding by 1"); + } else { + if (this.debug) console.log("Detected row count: " + count); + } if(this.debug) console.log("Reading fields, starting at: "+reader.rest()); @@ -57,11 +75,12 @@ class Gamespy2 extends Core { 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); } + if (!fields.length) return []; + const units = []; outer: while(!reader.done()) { const unit = {}; diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 48ace96..020418b 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -1,5 +1,5 @@ -const async = require('async'), - Core = require('./core'); +const Core = require('./core'), + HexUtil = require('../lib/HexUtil'); class Gamespy3 extends Core { constructor() { @@ -12,143 +12,129 @@ class Gamespy3 extends Core { this.isJc2mp = false; } - run(state) { - let challenge; + 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()); + } + 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 = {}; - 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 + for(let iPacket = 0; iPacket < packets.length; iPacket++) { + const packet = packets[iPacket]; + const reader = this.reader(packet); - 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); + if(this.debug) { + console.log("Parsing packet #" + iPacket); + console.log(HexUtil.debugDump(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; + if (this.debug) console.log(key + " = " + value); + } + } + + // Parse player, team, item array state + + if(this.isJc2mp) { + state.raw.numPlayers2 = reader.uint(2); + while(!reader.done()) { + const player = {}; + player.name = reader.string(); + player.steamid = reader.string(); + player.ping = reader.uint(2); + state.players.push(player); + } + } else { + let firstMode = true; + while(!reader.done()) { + if (reader.uint(1) <= 2) continue; + reader.skip(-1); + let fieldId = reader.string(); + if(!fieldId) continue; + const fieldIdSplit = fieldId.split('_'); + const fieldName = fieldIdSplit[0]; + const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'; + + if(!(itemType in state.raw.playerTeamInfo)) { + state.raw.playerTeamInfo[itemType] = []; + } + const items = state.raw.playerTeamInfo[itemType]; + + let offset = reader.uint(1); + firstMode = false; + + if (this.debug) { + console.log("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; + if (this.debug) console.log("* " + 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('' 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 +147,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 +155,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' @@ -199,13 +183,11 @@ class Gamespy3 extends Core { const list = []; for(let i = 0; i < numPackets; i++) { if(!(i in packets)) { - this.fatal('Missing packet #'+i); - return true; + throw new Error('Missing packet #'+i); } list.push(packets[i]); } - c(list); - return true; + return list; }); } } diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 2c704e1..9268c20 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -6,9 +6,10 @@ class Jc2mp extends Gamespy3 { constructor() { super(); this.useOnlySingleSplit = true; + this.isJc2mp = true; } - finalizeState(state) { - super.finalizeState(state); + async run(state) { + super.run(state); if(!state.players.length && parseInt(state.raw.numplayers)) { for(let i = 0; i < parseInt(state.raw.numplayers); i++) { state.players.push({}); diff --git a/protocols/quake2.js b/protocols/quake2.js index b9f8706..2f0a958 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -10,79 +10,76 @@ 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() || ''; + 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; } } diff --git a/protocols/ut3.js b/protocols/ut3.js index 20fd9b2..9b96b41 100644 --- a/protocols/ut3.js +++ b/protocols/ut3.js @@ -1,8 +1,8 @@ const Gamespy3 = require('./gamespy3'); class Ut3 extends Gamespy3 { - finalizeState(state) { - super.finalizeState(state); + async run(state) { + await super.run(state); this.translate(state.raw,{ 'mapname': false, diff --git a/protocols/valve.js b/protocols/valve.js index 9a77511..7dfc082 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -36,6 +36,7 @@ class Valve extends Core { } async queryInfo(state) { + if(this.debug) console.log("Requesting info ..."); const b = await this.sendPacket( 0x54, false, @@ -127,6 +128,7 @@ class Valve extends Core { if(this.legacyChallenge) { // sendPacket will catch the response packet and // save the challenge for us + if(this.debug) console.log("Requesting legacy challenge key ..."); await this.sendPacket( 0x57, false, @@ -144,6 +146,7 @@ class Valve extends Core { // Ignore timeouts in only this case const allowTimeout = state.raw.steamappid === 730; + if(this.debug) console.log("Requesting player list ..."); const b = await this.sendPacket( 0x55, true, @@ -177,6 +180,7 @@ class Valve extends Core { async queryRules(state) { state.raw.rules = {}; + if(this.debug) console.log("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 @@ -248,34 +252,37 @@ class Valve extends Core { allowTimeout ) { for (let keyRetry = 0; keyRetry < 3; keyRetry++) { - let retryQuery = false; + let requestKeyChanged = false; const response = await this.sendPacketRaw( type, sendChallenge, payload, (payload) => { const reader = this.reader(payload); const type = reader.uint(1); + if (this.debug) console.log("Received " + type.toString(16) + " expected " + expect.toString(16)); if (type === 0x41) { const key = reader.uint(4); if (this._challenge !== key) { if (this.debug) console.log('Received new challenge key: ' + key); this._challenge = key; - retryQuery = true; - if (keyRetry === 0 && sendChallenge) { - if (this.debug) console.log('Restarting query'); - return null; + if (sendChallenge) { + if (this.debug) console.log('Challenge key changed -- allowing query retry if needed'); + requestKeyChanged = true; } } } - if (this.debug) console.log("Received " + type.toString(16) + " expected " + expect.toString(16)); if (type === expect) { return reader.rest(); + } else if (requestKeyChanged) { + return null; } }, () => { if (allowTimeout) return null; } ); - if (!retryQuery) return response; + if (!requestKeyChanged) { + return response; + } } throw new Error('Received too many challenge key responses'); } From 484e99b29c6c0cd91b42ec59e8e6d59b2c3752aa Mon Sep 17 00:00:00 2001 From: mmorrison Date: Wed, 9 Jan 2019 05:50:30 -0600 Subject: [PATCH 03/11] Improve logging --- protocols/battlefield.js | 2 +- protocols/core.js | 70 ++++++++++++++++++++++++---------------- protocols/ffow.js | 2 +- protocols/gamespy2.js | 10 +++--- protocols/gamespy3.js | 17 ++++------ protocols/minecraft.js | 8 ++--- protocols/unreal2.js | 24 +++++++------- protocols/valve.js | 40 ++++++++++------------- 8 files changed, 88 insertions(+), 85 deletions(-) diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 45968c5..8790b32 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -98,7 +98,7 @@ class Battlefield extends Core { return await this.tcpSend(socket, outPacket, (data) => { const decoded = this.decodePacket(data); if(decoded) { - if(this.debug) console.log(decoded); + this.debugLog(decoded); if(decoded.shift() !== 'OK') throw new Error('Missing OK'); return decoded; } diff --git a/protocols/core.js b/protocols/core.js index 960d1a0..5e27799 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -79,7 +79,7 @@ class Core extends EventEmitter { try { leak.cleanup(); } catch(e) { - if (this.debug) console.log("Error during async cleanup: " + e.stack); + this.debugLog("Error during async cleanup: " + e.stack); } } this.asyncLeaks.clear(); @@ -137,18 +137,18 @@ class Core extends EventEmitter { }; const resolveStandard = async (host) => { if(isIp(host)) return host; - if(this.debug) console.log("Standard DNS Lookup: " + host); + this.debugLog("Standard DNS Lookup: " + host); const {address,family} = await dnsLookupAsync(host); - if(this.debug) console.log(address); + this.debugLog(address); return address; }; const resolveSrv = async (srv,host) => { if(isIp(host)) return host; - if(this.debug) console.log("SRV DNS Lookup: " + srv+'.'+host); + this.debugLog("SRV DNS Lookup: " + srv+'.'+host); let records; try { records = await dnsResolveAsync(srv + '.' + host, 'SRV'); - if(this.debug) console.log(records); + this.debugLog(records); if(records.length >= 1) { const record = records[0]; this.options.port = record.port; @@ -156,7 +156,7 @@ class Core extends EventEmitter { return await resolveStandard(srvhost); } } catch(e) { - if (this.debug) console.log(e.toString()); + this.debugLog(e.toString()); } return await resolveStandard(host); }; @@ -169,10 +169,10 @@ class Core extends EventEmitter { const id = ++this.lastAsyncLeakId; const stack = new Error().stack; const entry = { id: id, cleanup: fn, stack: stack }; - if (this.debug) console.log("Registering async leak: " + id); + this.debugLog("Registering async leak: " + id); this.asyncLeaks.add(entry); return () => { - if (this.debug) console.log("Removing async leak: " + id); + this.debugLog("Removing async leak: " + id); this.asyncLeaks.delete(entry); } } @@ -215,24 +215,22 @@ class Core extends EventEmitter { socket.setNoDelay(true); const cancelAsyncLeak = this.addAsyncLeak(() => socket.destroy()); - if(this.debug) { - console.log(address+':'+port+" TCP Connecting"); + this.debugLog(log => { + this.debugLog(address+':'+port+" TCP Connecting"); const writeHook = socket.write; socket.write = (...args) => { - console.log(address+':'+port+" TCP-->"); - console.log(HexUtil.debugDump(args[0])); + log(address+':'+port+" TCP-->"); + log(HexUtil.debugDump(args[0])); writeHook.apply(socket,args); }; - socket.on('error', e => console.log('TCP Error: ' + e)); - socket.on('close', () => console.log('TCP Closed')); + socket.on('error', e => log('TCP Error: ' + e)); + socket.on('close', () => log('TCP Closed')); socket.on('data', (data) => { - if(this.debug) { - console.log(address+':'+port+" <--TCP"); - console.log(HexUtil.debugDump(data)); - } + log(address+':'+port+" <--TCP"); + log(data); }); - socket.on('ready', () => console.log(address+':'+port+" TCP Connected")); - } + socket.on('ready', () => log(address+':'+port+" TCP Connected")); + }); try { await this.timedPromise( @@ -314,22 +312,22 @@ class Core extends EventEmitter { 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'); if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); - if(this.debug) { - console.log(this.options.address+':'+this.options.port_query+" UDP-->"); - console.log(HexUtil.debugDump(buffer)); - } + this.debugLog(log => { + log(this.options.address+':'+this.options.port_query+" UDP-->"); + log(HexUtil.debugDump(buffer)); + }); return await this.withUdpLock(async() => { this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); return await new Promise((resolve,reject) => { const cancelTimeout = this.setTimeout(() => { - if (this.debug) console.log("UDP timeout detected"); + this.debugLog("UDP timeout detected"); let success = false; if (onTimeout) { const result = onTimeout(); if (result !== undefined) { - if (this.debug) console.log("UDP timeout resolved by callback"); + this.debugLog("UDP timeout resolved by callback"); resolve(result); success = true; } @@ -342,7 +340,7 @@ class Core extends EventEmitter { this.udpCallback = (buffer) => { const result = onPacket(buffer); if(result !== undefined) { - if (this.debug) console.log("UDP send finished by callback"); + this.debugLog("UDP send finished by callback"); cancelTimeout(); resolve(result); } @@ -366,6 +364,24 @@ class Core extends EventEmitter { promise.finally(cancelAsyncLeak); return promise; } + + debugLog(...args) { + if (!this.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); + } + } catch(e) { + console.log("Error while debug logging: " + e); + } + } } module.exports = Core; diff --git a/protocols/ffow.js b/protocols/ffow.js index 8f228d3..92245d6 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -7,7 +7,7 @@ class Ffow extends Valve { this.legacyChallenge = true; } async queryInfo(state) { - if(this.debug) console.log("Requesting ffow info ..."); + this.debugLog("Requesting ffow info ..."); const b = await this.sendPacket( 0x46, false, diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index 941ece2..7076e80 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -64,19 +64,19 @@ class Gamespy2 extends Core { // so we can detect this. if (count > 64) { reader.skip(-1); - if (this.debug) console.log("Detected missing count byte, rewinding by 1"); + this.debugLog("Detected missing count byte, rewinding by 1"); } else { - if (this.debug) console.log("Detected row count: " + count); + this.debugLog("Detected row count: " + count); } - if(this.debug) console.log("Reading fields, starting at: "+reader.rest()); + this.debugLog(() => "Reading fields, starting at: "+reader.rest()); const fields = []; while(!reader.done()) { let field = reader.string(); if(!field) break; fields.push(field); - if(this.debug) console.log("field:"+field); + this.debugLog("field:"+field); } if (!fields.length) return []; @@ -88,7 +88,7 @@ class Gamespy2 extends Core { let key = fields[iField]; let value = reader.string(); if(!value && iField === 0) break outer; - if(this.debug) console.log("value:"+value); + this.debugLog("value:"+value); if(key === 'player_') key = 'name'; else if(key === 'score_') key = 'score'; else if(key === 'deaths_') key = 'deaths'; diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 020418b..6a39475 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -39,10 +39,8 @@ class Gamespy3 extends Core { const packet = packets[iPacket]; const reader = this.reader(packet); - if(this.debug) { - console.log("Parsing packet #" + iPacket); - console.log(HexUtil.debugDump(packet)); - } + this.debugLog("Parsing packet #" + iPacket); + this.debugLog(packet); // Parse raw server key/values @@ -58,7 +56,7 @@ class Gamespy3 extends Core { } state.raw[key] = value; - if (this.debug) console.log(key + " = " + value); + this.debugLog(key + " = " + value); } } @@ -92,9 +90,7 @@ class Gamespy3 extends Core { let offset = reader.uint(1); firstMode = false; - if (this.debug) { - console.log("Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset); - } + this.debugLog(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset); while(!reader.done()) { const item = reader.string(); @@ -102,7 +98,7 @@ class Gamespy3 extends Core { while(items.length <= offset) { items.push({}); } items[offset][fieldName] = item; - if (this.debug) console.log("* " + item); + this.debugLog("* " + item); offset++; } } @@ -173,8 +169,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; diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 61c0730..fcf221b 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -57,17 +57,15 @@ class Minecraft extends Core { let data = receivedData; const packetId = varint.decode(data); - if(this.debug) console.log("Packet ID: "+packetId); + this.debugLog("Packet ID: "+packetId); data = data.slice(varint.decode.bytes); const strLen = varint.decode(data); - if(this.debug) console.log("String Length: "+strLen); + this.debugLog("String Length: "+strLen); data = data.slice(varint.decode.bytes); const str = data.toString('utf8'); - if(this.debug) { - console.log(str); - } + this.debugLog(str); let json; try { diff --git a/protocols/unreal2.js b/protocols/unreal2.js index a34021a..d89147c 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -87,14 +87,14 @@ class Unreal2 extends Core { ]); } 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)); - } + 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); @@ -105,10 +105,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}); } diff --git a/protocols/valve.js b/protocols/valve.js index 7dfc082..52c033f 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -36,7 +36,7 @@ class Valve extends Core { } async queryInfo(state) { - if(this.debug) console.log("Requesting info ..."); + this.debugLog("Requesting info ..."); const b = await this.sendPacket( 0x54, false, @@ -114,12 +114,10 @@ class Valve extends Core { ) { this._skipSizeInSplitHeader = true; } - if(this.debug) { - console.log("STEAM APPID: "+state.raw.steamappid); - console.log("PROTOCOL: "+state.raw.protocol); - } + this.debugLog("STEAM APPID: "+state.raw.steamappid); + this.debugLog("PROTOCOL: "+state.raw.protocol); if(state.raw.protocol === 48) { - if(this.debug) console.log("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); + this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); this.goldsrcSplits = true; } } @@ -128,7 +126,7 @@ class Valve extends Core { if(this.legacyChallenge) { // sendPacket will catch the response packet and // save the challenge for us - if(this.debug) console.log("Requesting legacy challenge key ..."); + this.debugLog("Requesting legacy challenge key ..."); await this.sendPacket( 0x57, false, @@ -146,7 +144,7 @@ class Valve extends Core { // Ignore timeouts in only this case const allowTimeout = state.raw.steamappid === 730; - if(this.debug) console.log("Requesting player list ..."); + this.debugLog("Requesting player list ..."); const b = await this.sendPacket( 0x55, true, @@ -164,7 +162,7 @@ class Valve extends Core { const score = reader.int(4); const time = reader.float(); - if(this.debug) console.log("Found player: "+name+" "+score+" "+time); + this.debugLog("Found player: "+name+" "+score+" "+time); // connecting players don't count as players. if(!name) continue; @@ -180,7 +178,7 @@ class Valve extends Core { async queryRules(state) { state.raw.rules = {}; - if(this.debug) console.log("Requesting rules ..."); + 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 @@ -258,14 +256,14 @@ class Valve extends Core { (payload) => { const reader = this.reader(payload); const type = reader.uint(1); - if (this.debug) console.log("Received " + type.toString(16) + " expected " + expect.toString(16)); + this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16)); if (type === 0x41) { const key = reader.uint(4); if (this._challenge !== key) { - if (this.debug) console.log('Received new challenge key: ' + key); + this.debugLog('Received new challenge key: ' + key); this._challenge = key; if (sendChallenge) { - if (this.debug) console.log('Challenge key changed -- allowing query retry if needed'); + this.debugLog('Challenge key changed -- allowing query retry if needed'); requestKeyChanged = true; } } @@ -326,7 +324,7 @@ class Valve extends Core { const header = reader.int(4); if(header === -1) { // full package - if(this.debug) console.log("Received full packet"); + this.debugLog("Received full packet"); return onResponse(reader.rest()); } if(header === -2) { @@ -354,10 +352,8 @@ class Valve extends Core { 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"); - } + this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum); + this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID"); if(Object.keys(packets).length !== numPackets) return; @@ -365,20 +361,18 @@ class Valve 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]); } let assembled = Buffer.concat(list); if(bzip) { - if(this.debug) console.log("BZIP DETECTED - Extracing packet..."); + this.debugLog("BZIP DETECTED - Extracing packet..."); try { assembled = Buffer.from(Bzip2.decompressFile(assembled)); } catch(e) { - this.fatal('Invalid bzip packet'); - return true; + throw new Error('Invalid bzip packet'); } } const assembledReader = this.reader(assembled); From efe12a00aa4c5fe4085eacf8e4f29044e4af60e2 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Thu, 10 Jan 2019 06:03:07 -0600 Subject: [PATCH 04/11] More async conversion --- lib/reader.js | 9 +- package-lock.json | 10 -- package.json | 2 - protocols/armagetron.js | 47 ++++----- protocols/core.js | 31 ++++-- protocols/geneshift.js | 90 ++++++++--------- protocols/jc2mp.js | 5 +- protocols/kspdmp.js | 53 ++++------ protocols/m2mp.js | 42 ++++---- protocols/minecraft.js | 145 ++++++++++++--------------- protocols/mumble.js | 54 +++++----- protocols/mumbleping.js | 33 +++--- protocols/nadeo.js | 131 +++++++++++++----------- protocols/openttd.js | 199 +++++++++++++++++-------------------- protocols/quake2.js | 2 + protocols/quake3.js | 3 +- protocols/starmade.js | 99 +++++++++--------- protocols/teamspeak2.js | 127 +++++++++++------------ protocols/teamspeak3.js | 117 ++++++++++------------ protocols/terraria.js | 39 +++----- protocols/tribes1.js | 140 +++++++++++++------------- protocols/tribes1master.js | 65 ++++++------ protocols/unreal2.js | 145 +++++++++++++-------------- protocols/ventrilo.js | 40 ++++---- protocols/warsow.js | 4 +- 25 files changed, 774 insertions(+), 858 deletions(-) diff --git a/lib/reader.js b/lib/reader.js index 662c9a2..b5aafa9 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -1,7 +1,8 @@ const Iconv = require('iconv-lite'), Long = require('long'), Core = require('../protocols/core'), - Buffer = require('buffer'); + Buffer = require('buffer'), + Varint = require('varint'); function readUInt64BE(buffer,offset) { const high = buffer.readUInt32BE(offset); @@ -126,6 +127,12 @@ class Reader { return r; } + varint() { + const out = Varint.decode(this.buffer, this.i); + this.i += Varint.decode.bytes; + return out; + } + /** @returns Buffer */ part(bytes) { let r; diff --git a/package-lock.json b/package-lock.json index 1cf28bb..ad5c650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,11 +38,6 @@ "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", @@ -358,11 +353,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jquery": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", diff --git a/package.json b/package.json index 9a1cf4a..27b9e5d 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,10 @@ "node": ">=6.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", - "jquery": "^3.3.1", "long": "^2.4.0", "minimist": "^1.2.0", "moment": "^2.21.0", diff --git a/protocols/armagetron.js b/protocols/armagetron.js index 1b3d628..2d73e39 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -10,35 +10,32 @@ class Armagetron extends Core { async run(state) { const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); - await 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.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); - 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 null; - }); + state.raw.options = this.stripColorCodes(this.readString(reader)); + state.raw.uri = this.readString(reader); + state.raw.globalids = this.readString(reader); } readUInt(reader) { diff --git a/protocols/core.js b/protocols/core.js index 5e27799..39d9125 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -87,12 +87,13 @@ class Core extends EventEmitter { } timedPromise(promise, timeoutMs, timeoutMsg) { - return new Promise((resolve, reject) => { + return new Promise((resolve,reject) => { const cancelTimeout = this.setTimeout( () => reject(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")), timeoutMs ); - promise.finally(cancelTimeout).then(resolve,reject); + promise = promise.finally(cancelTimeout); + promise.then(resolve,reject); }); } @@ -204,8 +205,9 @@ class Core extends EventEmitter { } /** - * @param {function(Socket):Promise} fn - * @returns {Promise} + * @template T + * @param {function(Socket):Promise} fn + * @returns {Promise} */ async withTcp(fn) { const address = this.options.address; @@ -263,10 +265,11 @@ class Core extends EventEmitter { } /** + * @template T * @param {Socket} socket - * @param {Buffer} buffer - * @param {function(Buffer):boolean} ondata - * @returns {Promise} + * @param {Buffer|string} buffer + * @param {function(Buffer):T} ondata + * @returns Promise */ async tcpSend(socket,buffer,ondata) { return await this.timedPromise( @@ -354,14 +357,22 @@ class Core extends EventEmitter { } request(params) { - const promise = requestAsync({ + let promise = requestAsync({ ...params, - timeout: this.options.socketTimeout + timeout: this.options.socketTimeout, + resolveWithFullResponse: true }); const cancelAsyncLeak = this.addAsyncLeak(() => { promise.cancel(); }); - promise.finally(cancelAsyncLeak); + 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; } diff --git a/protocols/geneshift.js b/protocols/geneshift.js index bdfff25..b9559c6 100644 --- a/protocols/geneshift.js +++ b/protocols/geneshift.js @@ -1,53 +1,49 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'); class GeneShift extends Core { - run(state) { - request({ - uri: 'http://geneshift.net/game/receiveLobby.php', - timeout: 3000, - }, (e,r,body) => { - if(e) return this.fatal('Lobby request error'); - - const split = body.split('
'); - let found = false; - for(const line of split) { - const fields = line.split('::'); - const ip = fields[2]; - const port = fields[3]; - if(ip === this.options.address && parseInt(port) === this.options.port) { - found = fields; - break; - } - } - - if(!found) return this.fatal('Server not found in list'); - - state.raw.countrycode = found[0]; - state.raw.country = found[1]; - state.name = found[4]; - state.map = found[5]; - state.raw.numplayers = parseInt(found[6]); - state.maxplayers = parseInt(found[7]); - // fields[8] is unknown? - state.raw.rules = found[9]; - state.raw.gamemode = parseInt(found[10]); - state.raw.gangsters = parseInt(found[11]); - state.raw.cashrate = parseInt(found[12]); - state.raw.missions = !!parseInt(found[13]); - state.raw.vehicles = !!parseInt(found[14]); - state.raw.customweapons = !!parseInt(found[15]); - state.raw.friendlyfire = !!parseInt(found[16]); - state.raw.mercs = !!parseInt(found[17]); - // fields[18] is unknown? listen server? - state.raw.version = found[19]; - - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - - this.finish(state); + async run(state) { + const body = await this.request({ + uri: 'http://geneshift.net/game/receiveLobby.php' }); + + const split = body.split('
'); + let found = null; + for(const line of split) { + const fields = line.split('::'); + const ip = fields[2]; + const port = fields[3]; + if(ip === this.options.address && parseInt(port) === this.options.port) { + found = fields; + break; + } + } + + if(found === null) { + throw new Error('Server not found in list'); + } + + state.raw.countrycode = found[0]; + state.raw.country = found[1]; + state.name = found[4]; + state.map = found[5]; + state.raw.numplayers = parseInt(found[6]); + state.maxplayers = parseInt(found[7]); + // fields[8] is unknown? + state.raw.rules = found[9]; + state.raw.gamemode = parseInt(found[10]); + state.raw.gangsters = parseInt(found[11]); + state.raw.cashrate = parseInt(found[12]); + state.raw.missions = !!parseInt(found[13]); + state.raw.vehicles = !!parseInt(found[14]); + state.raw.customweapons = !!parseInt(found[15]); + state.raw.friendlyfire = !!parseInt(found[16]); + state.raw.mercs = !!parseInt(found[17]); + // fields[18] is unknown? listen server? + state.raw.version = found[19]; + + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } } } diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 9268c20..77d2bf6 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -1,15 +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'; } async run(state) { - super.run(state); + await super.run(state); if(!state.players.length && parseInt(state.raw.numplayers)) { for(let i = 0; i < parseInt(state.raw.numplayers); i++) { state.players.push({}); diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 9008c15..3de3532 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -1,38 +1,27 @@ -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_query }); + + 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; + if (json.players) { + const split = json.players.split(', '); + for (const name of split) { + state.players.push({name:name}); + } + } } } diff --git a/protocols/m2mp.js b/protocols/m2mp.js index 262b728..47b9a56 100644 --- a/protocols/m2mp.js +++ b/protocols/m2mp.js @@ -6,30 +6,28 @@ 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); + + while(!reader.done()) { + const name = this.readString(reader); + if(!name) break; + state.players.push({ + name:name + }); + } } readString(reader) { diff --git a/protocols/minecraft.js b/protocols/minecraft.js index fcf221b..65e223c 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -1,96 +1,75 @@ -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; + async run(state) { + const portBuf = Buffer.alloc(2); + portBuf.writeUInt16BE(this.options.port_query,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); - this.debugLog("Packet ID: "+packetId); - data = data.slice(varint.decode.bytes); - - const strLen = varint.decode(data); - this.debugLog("String Length: "+strLen); - data = data.slice(varint.decode.bytes); - - const str = data.toString('utf8'); - this.debugLog(str); - - let json; - try { - json = JSON.parse(str); - delete json.favicon; - } catch(e) { - return this.fatal('Invalid JSON'); - } - - state.raw = json; - state.maxplayers = json.players.max; - if(json.players.sample) { - for(const player of json.players.sample) { - state.players.push({ - id: player.id, - name: player.name - }); - } - } - while(state.players.length < json.players.online) { - state.players.push({}); - } - - this.finish(state); } + } + while(state.players.length < json.players.online) { + state.players.push({}); + } + } + + varIntBuffer(num) { + return Buffer.from(Varint.encode(num)); + } + buildPacket(id,data) { + if(!data) data = Buffer.from([]); + const idBuffer = this.varIntBuffer(id); + return Buffer.concat([ + this.varIntBuffer(data.length+idBuffer.length), + idBuffer, + data ]); } } diff --git a/protocols/mumble.js b/protocols/mumble.js index cf3f060..4222956 100644 --- a/protocols/mumble.js +++ b/protocols/mumble.js @@ -6,35 +6,35 @@ class Mumble extends Core { 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; + + let channelStack = [state.raw.root]; + while(channelStack.length) { + const channel = channelStack.shift(); + channel.description = this.cleanComment(channel.description); + channelStack = channelStack.concat(channel.channels); + for(const user of channel.users) { + user.comment = this.cleanComment(user.comment); + state.players.push(user); + } + } } cleanComment(str) { diff --git a/protocols/mumbleping.js b/protocols/mumbleping.js index 30a4adf..7737dee 100644 --- a/protocols/mumbleping.js +++ b/protocols/mumbleping.js @@ -6,24 +6,23 @@ class MumblePing extends Core { this.byteorder = 'be'; } - run(state) { - this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { - if(buffer.length < 24) return; - const reader = this.reader(buffer); - reader.skip(1); - state.raw.versionMajor = reader.uint(1); - state.raw.versionMinor = reader.uint(1); - state.raw.versionPatch = reader.uint(1); - reader.skip(8); - state.raw.numplayers = reader.uint(4); - state.maxplayers = reader.uint(4); - state.raw.allowedbandwidth = reader.uint(4); - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - this.finish(state); - return true; + async run(state) { + const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { + if (buffer.length >= 24) return buffer; }); + + const reader = this.reader(data); + reader.skip(1); + state.raw.versionMajor = reader.uint(1); + state.raw.versionMinor = reader.uint(1); + state.raw.versionPatch = reader.uint(1); + reader.skip(8); + state.raw.numplayers = reader.uint(4); + state.maxplayers = reader.uint(4); + state.raw.allowedbandwidth = reader.uint(4); + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } } } diff --git a/protocols/nadeo.js b/protocols/nadeo.js index fe851c0..72db975 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -1,5 +1,4 @@ const gbxremote = require('gbxremote'), - async = require('async'), Core = require('./core'); class Nadeo extends Core { @@ -7,79 +6,91 @@ class Nadeo extends Core { super(); this.options.port = 2350; this.options.port_query = 5000; - this.gbxclient = false; } - reset() { - super.reset(); - if(this.gbxclient) { - this.gbxclient.terminate(); - this.gbxclient = false; - } - } + async run(state) { + await this.withClient(async client => { + await this.methodCall(client, 'Authenticate', this.options.login, this.options.password); + //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; + } + + 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_query, this.options.host); + const cancelAsyncLeak = this.addAsyncLeak(() => socket.terminate()); + try { + await this.timedPromise( + new Promise((resolve,reject) => { + socket.on('connect', resolve); + socket.on('error', e => reject(new Error('GBX Remote Connection Error: ' + e))); + socket.on('close', () => reject(new Error('GBX Remote Connection Refused'))); + }), + this.options.socketTimeout, + 'GBX Remote Opening' + ); + return await fn(socket); + } finally { + cancelAsyncLeak(); + socket.terminate(); + } + } + + async methodCall(client, ...cmdset) { + const cmd = cmdset[0]; + const params = cmdset.slice(1); + return await this.timedPromise( + new Promise(async (resolve,reject) => { + client.methodCall(cmd, params, (err, value) => { + if (err) reject('XMLRPC error ' + JSON.stringify(err)); + resolve(value); + }); + }), + this.options.socketTimeout, + 'GBX Method Call' + ); + } + stripColors(str) { return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,''); } diff --git a/protocols/openttd.js b/protocols/openttd.js index 39558ad..09c0959 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -1,131 +1,116 @@ -const async = require('async'), - moment = require('moment'), +const moment = require('moment'), Core = require('./core'); class OpenTtd extends Core { - run(state) { - async.series([ - (c) => { - this.query(0,1,1,4,(reader, version) => { - if(version >= 4) { - const numGrf = reader.uint(1); - state.raw.grfs = []; - for(let i = 0; i < numGrf; i++) { - const grf = {}; - grf.id = reader.part(4).toString('hex'); - grf.md5 = reader.part(16).toString('hex'); - state.raw.grfs.push(grf); - } - } - if(version >= 3) { - state.raw.date_current = this.readDate(reader); - state.raw.date_start = this.readDate(reader); - } - if(version >= 2) { - state.raw.maxcompanies = reader.uint(1); - state.raw.numcompanies = reader.uint(1); - state.raw.maxspectators = reader.uint(1); - } - - state.name = reader.string(); - state.raw.version = reader.string(); - - state.raw.language = this.decode( - reader.uint(1), - ['any','en','de','fr'] - ); - - state.password = !!reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.numplayers = reader.uint(1); - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - state.raw.numspectators = reader.uint(1); - state.map = reader.string(); - state.raw.map_width = reader.uint(2); - state.raw.map_height = reader.uint(2); - - state.raw.landscape = this.decode( - reader.uint(1), - ['temperate','arctic','desert','toyland'] - ); - - state.raw.dedicated = !!reader.uint(1); - - c(); - }); - }, - - (c) => { - const vehicle_types = ['train','truck','bus','aircraft','ship']; - const station_types = ['station','truckbay','busstation','airport','dock']; - - this.query(2,3,-1,-1, (reader,version) => { - // we don't know how to deal with companies outside version 6 - if(version !== 6) return c(); - - state.raw.companies = []; - const numCompanies = reader.uint(1); - for(let iCompany = 0; iCompany < numCompanies; iCompany++) { - const company = {}; - company.id = reader.uint(1); - company.name = reader.string(); - company.year_start = reader.uint(4); - company.value = reader.uint(8); - company.money = reader.uint(8); - company.income = reader.uint(8); - company.performance = reader.uint(2); - company.password = !!reader.uint(1); - - company.vehicles = {}; - for(const type of vehicle_types) { - company.vehicles[type] = reader.uint(2); - } - company.stations = {}; - for(const type of station_types) { - company.stations[type] = reader.uint(2); - } - - company.clients = reader.string(); - state.raw.companies.push(company); - } - - c(); - }); - }, - - (c) => { - this.finish(state); + async run(state) { + { + const [reader, version] = await this.query(0, 1, 1, 4); + if (version >= 4) { + const numGrf = reader.uint(1); + state.raw.grfs = []; + for (let i = 0; i < numGrf; i++) { + const grf = {}; + grf.id = reader.part(4).toString('hex'); + grf.md5 = reader.part(16).toString('hex'); + state.raw.grfs.push(grf); + } } - ]); + if (version >= 3) { + state.raw.date_current = this.readDate(reader); + state.raw.date_start = this.readDate(reader); + } + if (version >= 2) { + state.raw.maxcompanies = reader.uint(1); + state.raw.numcompanies = reader.uint(1); + state.raw.maxspectators = reader.uint(1); + } + + state.name = reader.string(); + state.raw.version = reader.string(); + + state.raw.language = this.decode( + reader.uint(1), + ['any', 'en', 'de', 'fr'] + ); + + state.password = !!reader.uint(1); + state.maxplayers = reader.uint(1); + state.raw.numplayers = reader.uint(1); + for (let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + state.raw.numspectators = reader.uint(1); + state.map = reader.string(); + state.raw.map_width = reader.uint(2); + state.raw.map_height = reader.uint(2); + + state.raw.landscape = this.decode( + reader.uint(1), + ['temperate', 'arctic', 'desert', 'toyland'] + ); + + state.raw.dedicated = !!reader.uint(1); + } + + { + const [reader,version] = await this.query(2,3,-1,-1); + // we don't know how to deal with companies outside version 6 + if(version === 6) { + state.raw.companies = []; + const numCompanies = reader.uint(1); + for (let iCompany = 0; iCompany < numCompanies; iCompany++) { + const company = {}; + company.id = reader.uint(1); + company.name = reader.string(); + company.year_start = reader.uint(4); + company.value = reader.uint(8); + company.money = reader.uint(8); + company.income = reader.uint(8); + company.performance = reader.uint(2); + company.password = !!reader.uint(1); + + const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship']; + const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock']; + + company.vehicles = {}; + for (const type of vehicle_types) { + company.vehicles[type] = reader.uint(2); + } + company.stations = {}; + for (const type of station_types) { + company.stations[type] = reader.uint(2); + } + + company.clients = reader.string(); + state.raw.companies.push(company); + } + } + } } - query(type,expected,minver,maxver,done) { + async query(type,expected,minver,maxver) { const b = Buffer.from([0x03,0x00,type]); - this.udpSend(b,(buffer) => { + return await this.udpSend(b,(buffer) => { const reader = this.reader(buffer); const packetLen = reader.uint(2); if(packetLen !== buffer.length) { - this.fatal('Invalid reported packet length: '+packetLen+' '+buffer.length); - return true; + this.debugLog('Invalid reported packet length: '+packetLen+' '+buffer.length); + return; } const packetType = reader.uint(1); if(packetType !== expected) { - this.fatal('Unexpected response packet type: '+packetType); - return true; + this.debugLog('Unexpected response packet type: '+packetType); + return; } const protocolVersion = reader.uint(1); if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { - this.fatal('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); - return true; + throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); } - done(reader,protocolVersion); - return true; + return [reader,protocolVersion]; }); } diff --git a/protocols/quake2.js b/protocols/quake2.js index 2f0a958..78215be 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -68,7 +68,9 @@ class Quake2 extends Core { 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); diff --git a/protocols/quake3.js b/protocols/quake3.js index 77a1be1..70a7b2c 100644 --- a/protocols/quake3.js +++ b/protocols/quake3.js @@ -6,7 +6,8 @@ class Quake3 extends Quake2 { this.sendHeader = 'getstatus'; this.responseHeader = 'statusResponse'; } - finalizeState(state) { + async run(state) { + await super.run(state); state.name = this.stripColors(state.name); for(const key of Object.keys(state.raw)) { state.raw[key] = this.stripColors(state.raw[key]); diff --git a/protocols/starmade.js b/protocols/starmade.js index a1ee3ce..18d063b 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -6,58 +6,59 @@ class Starmade extends Core { this.encoding = 'latin1'; this.byteorder = 'be'; } - run(state) { + + async run(state) { const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]); - this.tcpSend(b,(buffer) => { - const reader = this.reader(buffer); - - if(buffer.length < 4) return false; - const packetLength = reader.uint(4); - if(buffer.length < packetLength+12) return false; - - const data = []; - state.raw.data = data; - - reader.skip(2); - while(!reader.done()) { - const mark = reader.uint(1); - if(mark === 1) { - // signed int - data.push(reader.int(4)); - } else if(mark === 3) { - // float - data.push(reader.float()); - } else if(mark === 4) { - // string - const length = reader.uint(2); - data.push(reader.string(length)); - } else if(mark === 6) { - // byte - data.push(reader.uint(1)); - } - } - - if(data.length < 9) { - this.fatal("Not enough units in data packet"); - return true; - } - - if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, ''); - if(typeof data[4] === 'string') state.name = data[4]; - if(typeof data[5] === 'string') state.raw.description = data[5]; - if(typeof data[7] === 'number') state.raw.numplayers = data[7]; - if(typeof data[8] === 'number') state.maxplayers = data[8]; - - if('numplayers' in state.raw) { - for(let i = 0; i < state.raw.numplayers; i++) { - state.players.push({}); - } - } - - this.finish(state); - return true; + const payload = await this.withTcp(async socket => { + return await this.tcpSend(socket, b, buffer => { + if (buffer.length < 4) return; + const reader = this.reader(buffer); + const packetLength = reader.uint(4); + if (buffer.length < packetLength + 12) return; + return reader.rest(); + }); }); + + const reader = this.reader(payload); + + const data = []; + state.raw.data = data; + + reader.skip(2); + while(!reader.done()) { + const mark = reader.uint(1); + if(mark === 1) { + // signed int + data.push(reader.int(4)); + } else if(mark === 3) { + // float + data.push(reader.float()); + } else if(mark === 4) { + // string + const length = reader.uint(2); + data.push(reader.string(length)); + } else if(mark === 6) { + // byte + data.push(reader.uint(1)); + } + } + + if(data.length < 9) { + throw new Error("Not enough units in data packet"); + } + + if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, ''); + if(typeof data[4] === 'string') state.name = data[4]; + if(typeof data[5] === 'string') state.raw.description = data[5]; + if(typeof data[7] === 'number') state.raw.numplayers = data[7]; + if(typeof data[8] === 'number') state.maxplayers = data[8]; + + if('numplayers' in state.raw) { + for(let i = 0; i < state.raw.numplayers; i++) { + state.players.push({}); + } + } } } diff --git a/protocols/teamspeak2.js b/protocols/teamspeak2.js index d2e665e..9b637f0 100644 --- a/protocols/teamspeak2.js +++ b/protocols/teamspeak2.js @@ -1,77 +1,68 @@ -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) { + 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); + } + } + }); } - sendCommand(cmd,c) { - this.tcpSend(cmd+'\x0A', (buffer) => { + + async sendCommand(socket,cmd) { + return await this.tcpSend(socket, cmd+'\x0A', buffer => { if(buffer.length < 6) return; if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return; - c(buffer.slice(0,-6).toString()); - return true; + return buffer.slice(0,-6).toString(); }); } } diff --git a/protocols/teamspeak3.js b/protocols/teamspeak3.js index 3d4e59a..c28c63a 100644 --- a/protocols/teamspeak3.js +++ b/protocols/teamspeak3.js @@ -1,79 +1,66 @@ -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) { + 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); - - return true; + { + const data = await this.sendCommand(socket, 'channellist -topic'); + state.raw.channels = data; + } }); } + + 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; + } + } } module.exports = Teamspeak3; diff --git a/protocols/terraria.js b/protocols/terraria.js index e343f00..ee0fdc5 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -1,36 +1,25 @@ -const request = require('request'), - Core = require('./core'); +const Core = require('./core'); class Terraria extends Core { - run(state) { - request({ + async run(state) { + const body = await this.request({ uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status', - timeout: this.options.socketTimeout, 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.raw.port = json.port; + state.raw.numplayers = json.playercount; } } diff --git a/protocols/tribes1.js b/protocols/tribes1.js index ece5efe..e503b8f 100644 --- a/protocols/tribes1.js +++ b/protocols/tribes1.js @@ -5,81 +5,81 @@ class Tribes1 extends Core { super(); this.encoding = 'latin1'; } - run(state) { + + async run(state) { const queryBuffer = Buffer.from('b++'); - this.udpSend(queryBuffer,(buffer) => { + const reader = await this.udpSend(queryBuffer,(buffer) => { const reader = this.reader(buffer); - const header = reader.string({length:4}); + const header = reader.string({length: 4}); if (header !== 'c++b') { - this.fatal('Header response does not match: ' + header); - return true; + this.debugLog('Header response does not match: ' + header); + return; } - state.raw.gametype = this.readString(reader); - state.raw.version = this.readString(reader); - state.name = this.readString(reader); - state.raw.dedicated = !!reader.uint(1); - state.password = !!reader.uint(1); - state.raw.playerCount = reader.uint(1); - state.maxplayers = reader.uint(1); - state.raw.cpu = reader.uint(2); - state.raw.mod = this.readString(reader); - state.raw.type = this.readString(reader); - state.map = this.readString(reader); - state.raw.motd = this.readString(reader); - state.raw.teamCount = reader.uint(1); - - const teamFields = this.readFieldList(reader); - const playerFields = this.readFieldList(reader); - - state.raw.teams = []; - for(let i = 0; i < state.raw.teamCount; i++) { - const teamName = this.readString(reader); - const teamValues = this.readValues(reader); - - const teamInfo = {}; - for (let i = 0; i < teamValues.length && i < teamFields.length; i++) { - let key = teamFields[i]; - let value = teamValues[i]; - if (key === 'ultra_base') key = 'name'; - if (value === '%t') value = teamName; - if (['score','players'].includes(key)) value = parseInt(value); - teamInfo[key] = value; - } - state.raw.teams.push(teamInfo); - } - - for(let i = 0; i < state.raw.playerCount; i++) { - const ping = reader.uint(1) * 4; - const packetLoss = reader.uint(1); - const teamNum = reader.uint(1); - const name = this.readString(reader); - const playerValues = this.readValues(reader); - - const playerInfo = {}; - for (let i = 0; i < playerValues.length && i < playerFields.length; i++) { - let key = playerFields[i]; - let value = playerValues[i]; - if (value === '%p') value = ping; - if (value === '%l') value = packetLoss; - if (value === '%t') value = teamNum; - if (value === '%n') value = name; - if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value); - if (key === 'team') { - const teamId = parseInt(value); - if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) { - value = state.raw.teams[teamId].name; - } else { - continue; - } - } - playerInfo[key] = value; - } - state.players.push(playerInfo); - } - - this.finish(state); - return true; + return reader; }); + + state.raw.gametype = this.readString(reader); + state.raw.version = this.readString(reader); + state.name = this.readString(reader); + state.raw.dedicated = !!reader.uint(1); + state.password = !!reader.uint(1); + state.raw.playerCount = reader.uint(1); + state.maxplayers = reader.uint(1); + state.raw.cpu = reader.uint(2); + state.raw.mod = this.readString(reader); + state.raw.type = this.readString(reader); + state.map = this.readString(reader); + state.raw.motd = this.readString(reader); + state.raw.teamCount = reader.uint(1); + + const teamFields = this.readFieldList(reader); + const playerFields = this.readFieldList(reader); + + state.raw.teams = []; + for(let i = 0; i < state.raw.teamCount; i++) { + const teamName = this.readString(reader); + const teamValues = this.readValues(reader); + + const teamInfo = {}; + for (let i = 0; i < teamValues.length && i < teamFields.length; i++) { + let key = teamFields[i]; + let value = teamValues[i]; + if (key === 'ultra_base') key = 'name'; + if (value === '%t') value = teamName; + if (['score','players'].includes(key)) value = parseInt(value); + teamInfo[key] = value; + } + state.raw.teams.push(teamInfo); + } + + for(let i = 0; i < state.raw.playerCount; i++) { + const ping = reader.uint(1) * 4; + const packetLoss = reader.uint(1); + const teamNum = reader.uint(1); + const name = this.readString(reader); + const playerValues = this.readValues(reader); + + const playerInfo = {}; + for (let i = 0; i < playerValues.length && i < playerFields.length; i++) { + let key = playerFields[i]; + let value = playerValues[i]; + if (value === '%p') value = ping; + if (value === '%l') value = packetLoss; + if (value === '%t') value = teamNum; + if (value === '%n') value = name; + if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value); + if (key === 'team') { + const teamId = parseInt(value); + if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) { + value = state.raw.teams[teamId].name; + } else { + continue; + } + } + playerInfo[key] = value; + } + state.players.push(playerInfo); + } } readFieldList(reader) { const str = this.readString(reader); diff --git a/protocols/tribes1master.js b/protocols/tribes1master.js index b06cf83..c9bfb47 100644 --- a/protocols/tribes1master.js +++ b/protocols/tribes1master.js @@ -7,7 +7,8 @@ class Tribes1Master extends Core { super(); this.encoding = 'latin1'; } - run(state) { + + async run(state) { const queryBuffer = Buffer.from([ 0x10, // standard header 0x03, // dump servers @@ -18,28 +19,27 @@ class Tribes1Master extends Core { let parts = new Map(); let total = 0; - this.udpSend(queryBuffer,(buffer) => { + const full = await this.udpSend(queryBuffer,(buffer) => { const reader = this.reader(buffer); const header = reader.uint(2); if (header !== 0x0610) { - this.fatal('Header response does not match: ' + header.toString(16)); - return true; + this.debugLog('Header response does not match: ' + header.toString(16)); + return; } const num = reader.uint(1); const t = reader.uint(1); if (t <= 0 || (total > 0 && t !== total)) { - this.fatal('Conflicting total: ' + t); - return true; + throw new Error('Conflicting packet total: ' + t); } total = t; if (num < 1 || num > total) { - this.fatal('Invalid packet number: ' + num + ' ' + total); - return true; + this.debugLog('Invalid packet number: ' + num + ' ' + total); + return; } if (parts.has(num)) { - this.fatal('Duplicate part: ' + num); - return true; + this.debugLog('Duplicate part: ' + num); + return; } reader.skip(2); // challenge (0x0201) @@ -49,32 +49,29 @@ class Tribes1Master extends Core { if (parts.size === total) { const ordered = []; for (let i = 1; i <= total; i++) ordered.push(parts.get(i)); - const full = Buffer.concat(ordered); - const fullReader = this.reader(full); - - state.raw.name = this.readString(fullReader); - state.raw.motd = this.readString(fullReader); - - state.raw.servers = []; - while (!fullReader.done()) { - fullReader.skip(1); // junk ? - const count = fullReader.uint(1); - for (let i = 0; i < count; i++) { - const six = fullReader.uint(1); - if (six !== 6) { - this.fatal('Expecting 6'); - return true; - } - const ip = fullReader.uint(4); - const port = fullReader.uint(2); - const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24); - state.raw.servers.push(ipStr+":"+port); - } - } - this.finish(state); - return true; + return Buffer.concat(ordered); } }); + + const fullReader = this.reader(full); + state.raw.name = this.readString(fullReader); + state.raw.motd = this.readString(fullReader); + + state.raw.servers = []; + while (!fullReader.done()) { + fullReader.skip(1); // junk ? + const count = fullReader.uint(1); + for (let i = 0; i < count; i++) { + const six = fullReader.uint(1); + if (six !== 6) { + throw new Error('Expecting 6'); + } + const ip = fullReader.uint(4); + const port = fullReader.uint(2); + const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24); + state.raw.servers.push(ipStr+":"+port); + } + } } readString(reader) { const length = reader.uint(1); diff --git a/protocols/unreal2.js b/protocols/unreal2.js index d89147c..8b0634f 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -1,91 +1,79 @@ -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.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); + } - 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) { this.debugLog(log => { log("UNREAL2 EXTRA INFO:"); @@ -96,6 +84,7 @@ class Unreal2 extends Core { log(reader.buffer.slice(reader.i)); }); } + readUnrealString(reader, stripColor) { let length = reader.uint(1); let out; @@ -120,11 +109,12 @@ class Unreal2 extends Core { return out; } - sendPacket(type,required,callback) { + + async sendPacket(type,required) { const outbuffer = Buffer.from([0x79,0,0,0,type]); const packets = []; - this.udpSend(outbuffer,(buffer) => { + return await this.udpSend(outbuffer,(buffer) => { const reader = this.reader(buffer); const header = reader.uint(4); const iType = reader.uint(1); @@ -132,8 +122,7 @@ class Unreal2 extends Core { packets.push(reader.rest()); }, () => { if(!packets.length && required) return; - callback(Buffer.concat(packets)); - return true; + return Buffer.concat(packets); }); } } diff --git a/protocols/ventrilo.js b/protocols/ventrilo.js index 95acdc1..5e7a998 100644 --- a/protocols/ventrilo.js +++ b/protocols/ventrilo.js @@ -5,31 +5,30 @@ class Ventrilo extends Core { super(); this.byteorder = 'be'; } - run(state) { - this.sendCommand(2,'',(data) => { - state.raw = splitFields(data.toString()); - for (const client of state.raw.CLIENTS) { - client.name = client.NAME; - delete client.NAME; - client.ping = parseInt(client.PING); - delete client.PING; - state.players.push(client); - } - delete state.raw.CLIENTS; - if('NAME' in state.raw) state.name = state.raw.NAME; - if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; - if(this.trueTest(state.raw.AUTH)) state.password = true; - this.finish(state); - }); + async run(state) { + const data = await this.sendCommand(2,''); + state.raw = splitFields(data.toString()); + for (const client of state.raw.CLIENTS) { + client.name = client.NAME; + delete client.NAME; + client.ping = parseInt(client.PING); + delete client.PING; + state.players.push(client); + } + delete state.raw.CLIENTS; + + if('NAME' in state.raw) state.name = state.raw.NAME; + if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; + if(this.trueTest(state.raw.AUTH)) state.password = true; } - sendCommand(cmd,password,c) { + async sendCommand(cmd,password) { const body = Buffer.alloc(16); body.write(password,0,15,'utf8'); const encrypted = encrypt(cmd,body); const packets = {}; - this.udpSend(encrypted, (buffer) => { + return await this.udpSend(encrypted, (buffer) => { if(buffer.length < 20) return; const data = decrypt(buffer); @@ -39,11 +38,10 @@ class Ventrilo extends Core { const out = []; for(let i = 0; i < data.packetTotal; i++) { - if(!(i in packets)) return this.fatal('Missing packet #'+i); + if(!(i in packets)) throw new Error('Missing packet #'+i); out.push(packets[i]); } - c(Buffer.concat(out)); - return true; + return Buffer.concat(out); }); } } diff --git a/protocols/warsow.js b/protocols/warsow.js index b5f82e0..c970be5 100644 --- a/protocols/warsow.js +++ b/protocols/warsow.js @@ -1,8 +1,8 @@ const Quake3 = require('./quake3'); class Warsow extends Quake3 { - finalizeState(state) { - super.finalizeState(state); + async run(state) { + await super.run(state); if(state.players) { for(const player of state.players) { player.team = player.address; From 29ce0b82d0357a437773fdb8e2288b2d634693b8 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 04:43:36 -0600 Subject: [PATCH 05/11] Additional async rewrite --- bin/gamedig.js | 15 +- bin/genreadme.js | 2 +- games.txt | 28 +-- lib/GameResolver.js | 84 +++++++++ lib/GlobalUdpSocket.js | 42 +++++ lib/Promises.js | 20 +++ lib/ProtocolResolver.js | 22 +++ lib/QueryRunner.js | 103 +++++++++++ lib/index.js | 97 +++------- lib/typeresolver.js | 102 ----------- protocols/americasarmy.js | 25 --- protocols/armagetron.js | 2 +- protocols/ase.js | 4 +- protocols/battlefield.js | 52 ++++-- protocols/buildandshoot.js | 2 +- protocols/core.js | 356 +++++++++++++++++-------------------- protocols/doom3.js | 106 ++++++++--- protocols/fivem.js | 4 +- protocols/gamespy2.js | 24 +++ protocols/gamespy3.js | 14 +- protocols/kspdmp.js | 2 +- protocols/minecraft.js | 6 +- protocols/nadeo.js | 10 +- protocols/terraria.js | 2 +- 24 files changed, 654 insertions(+), 470 deletions(-) create mode 100644 lib/GameResolver.js create mode 100644 lib/GlobalUdpSocket.js create mode 100644 lib/Promises.js create mode 100644 lib/ProtocolResolver.js create mode 100644 lib/QueryRunner.js delete mode 100644 lib/typeresolver.js delete mode 100644 protocols/americasarmy.js 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 From e937c725bbf3e6230322c9cdb6bc4f5fb62b1a33 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 05:45:09 -0600 Subject: [PATCH 06/11] More async --- README.md | 2 +- games.txt | 93 +++++++++++++++++++------------------- lib/QueryRunner.js | 3 +- protocols/buildandshoot.js | 7 ++- protocols/core.js | 11 ++--- protocols/doom3.js | 12 +++-- protocols/ffow.js | 2 +- protocols/gamespy1.js | 1 + protocols/gamespy2.js | 13 ++---- protocols/gamespy3.js | 9 ++-- protocols/hexen2.js | 4 ++ protocols/kspdmp.js | 1 + protocols/m2mp.js | 1 + protocols/mumble.js | 6 +-- protocols/nadeo.js | 4 ++ protocols/teamspeak2.js | 4 +- protocols/teamspeak3.js | 4 +- protocols/terraria.js | 2 +- protocols/unreal2.js | 2 +- protocols/valve.js | 5 +- 20 files changed, 100 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index f698a98..fff824d 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,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 --- diff --git a/games.txt b/games.txt index 185eedf..16e0b68 100644 --- a/games.txt +++ b/games.txt @@ -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,15 +27,15 @@ 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|gamespy2|port=1716,port_query_offset=1 americasarmy2|America's Army 2|gamespy2|port=1716,port_query_offset=1 @@ -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,14 +103,14 @@ 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 fear|F.E.A.R.|gamespy2|port_query=27888 @@ -119,33 +118,33 @@ 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 @@ -154,11 +153,11 @@ 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 @@ -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,7 +191,7 @@ 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 @@ -206,7 +205,7 @@ quake2|Quake 2|quake2|port=27910 quake3|Quake 3: Arena|quake3|port=27960 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|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,22 +249,22 @@ 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 +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 @@ -292,5 +291,5 @@ wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1 wolfenstein2009|Wolfenstein 2009|doom3|port=27666 wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960 xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123 -zombiemaster|Zombie Master|valve -zps|Zombie Panic: Source|valve +zombiemaster|Zombie Master|valve|port=27015 +zps|Zombie Panic: Source|valve|port=27015 diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js index ab9db13..1cb16b8 100644 --- a/lib/QueryRunner.js +++ b/lib/QueryRunner.js @@ -62,7 +62,8 @@ class QueryRunner { attempts.push({ ...defaultOptions, ...gameOptions, - ...userOptions + ...userOptions, + port: gameOptions.port + (gameQueryPortOffset || 0) }); } else { throw new Error("Could not determine port to query. Did you provide a port or gameid?"); diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index f8048ff..f52ed2e 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -9,7 +9,7 @@ class BuildAndShoot extends Core { let m; - m = body.match(/status server for (.*?)\r|\n/); + m = body.match(/status server for (.*?)\.?(\r|\n)/); if(m) state.name = m[1]; m = body.match(/Current uptime: (\d+)/); @@ -24,6 +24,11 @@ class BuildAndShoot extends Core { 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')) { diff --git a/protocols/core.js b/protocols/core.js index 0e921a8..39edf46 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -92,7 +92,7 @@ class Core extends EventEmitter { state.notes = this.options.notes; // because lots of servers prefix with spaces to try to appear first - state.name = state.name.trim(); + state.name = (state.name || '').trim(); state.duration = Date.now() - startMillis; if (!('connect' in state)) { @@ -185,9 +185,9 @@ class Core extends EventEmitter { * @param {function(Socket):Promise} fn * @returns {Promise} */ - async withTcp(fn) { + async withTcp(fn, port) { const address = this.options.address; - const port = this.options.port; + if (!port) port = this.options.port; this.assertValidPort(port); let socket, connectionTimeout; @@ -342,10 +342,9 @@ class Core extends EventEmitter { log(() => params.uri + " HTTP-->"); requestPromise .then((response) => log(params.uri + " <--HTTP " + response.statusCode)) - .catch(() => { - }); + .catch(() => {}); }); - const wrappedPromise = promise.then(response => response.body); + const wrappedPromise = requestPromise.then(response => response.body); return await Promise.race([wrappedPromise, this.abortedPromise]); } finally { requestPromise && requestPromise.cancel(); diff --git a/protocols/doom3.js b/protocols/doom3.js index 52fed01..401aa1a 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -71,7 +71,7 @@ class Doom3 extends Core { } state.raw.osmask = reader.uint(4); - if(isEtqw) { + if (isEtqw) { state.raw.ranked = reader.uint(1); state.raw.timeleft = reader.uint(4); state.raw.gamestate = reader.uint(1); @@ -85,10 +85,12 @@ class Doom3 extends Core { } } - 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; + 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 } diff --git a/protocols/ffow.js b/protocols/ffow.js index 92245d6..ca82d55 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -23,7 +23,7 @@ class Ffow extends Valve { state.raw.gamemode = reader.string(); state.raw.description = reader.string(); state.raw.version = reader.string(); - state.raw.port = reader.uint(2); + state.gamePort = reader.uint(2); state.raw.numplayers = reader.uint(1); state.maxplayers = reader.uint(1); state.raw.listentype = String.fromCharCode(reader.uint(1)); diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index 9b9729c..5a1ed46 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -15,6 +15,7 @@ class Gamespy1 extends Core { 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'); diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index c2674d4..2936762 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -18,10 +18,11 @@ class Gamespy2 extends Core { 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 ('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 @@ -57,10 +58,6 @@ class Gamespy2 extends Core { 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 f8dea75..c3408ab 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -109,11 +109,12 @@ class Gamespy3 extends Core { // Turn all that raw state into something useful - if('hostname' in state.raw) state.name = state.raw.hostname; + 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 ('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['']) { diff --git a/protocols/hexen2.js b/protocols/hexen2.js index 067b23c..4773b0a 100644 --- a/protocols/hexen2.js +++ b/protocols/hexen2.js @@ -6,6 +6,10 @@ class Hexen2 extends Quake1 { this.sendHeader = '\xFFstatus\x0a'; this.responseHeader = '\xffn'; } + async run(state) { + await super.run(state); + state.gamePort = this.options.port - 50; + } } module.exports = Hexen2; diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 1aaa1f9..29e402b 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -16,6 +16,7 @@ class Kspdmp extends Core { } 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) { diff --git a/protocols/m2mp.js b/protocols/m2mp.js index 47b9a56..3d21fbc 100644 --- a/protocols/m2mp.js +++ b/protocols/m2mp.js @@ -20,6 +20,7 @@ class M2mp extends Core { 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); diff --git a/protocols/mumble.js b/protocols/mumble.js index 4222956..f60f3c4 100644 --- a/protocols/mumble.js +++ b/protocols/mumble.js @@ -1,11 +1,6 @@ const Core = require('./core'); class Mumble extends Core { - constructor() { - super(); - this.options.socketTimeout = 5000; - } - async run(state) { const json = await this.withTcp(async socket => { return await this.tcpSend(socket, 'json', (buffer) => { @@ -24,6 +19,7 @@ class Mumble extends Core { state.raw = json; state.name = json.name; + state.gamePort = json.x_gtmurmur_connectport || 64738; let channelStack = [state.raw.root]; while(channelStack.length) { diff --git a/protocols/nadeo.js b/protocols/nadeo.js index 62711b8..f214360 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -41,6 +41,10 @@ class Nadeo extends Core { 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({ diff --git a/protocols/teamspeak2.js b/protocols/teamspeak2.js index 9b637f0..fc449c0 100644 --- a/protocols/teamspeak2.js +++ b/protocols/teamspeak2.js @@ -2,6 +2,8 @@ const Core = require('./core'); class Teamspeak2 extends Core { async run(state) { + const queryPort = this.options.teamspeakQueryPort || 51234; + await this.withTcp(async socket => { { const data = await this.sendCommand(socket, 'sel '+this.options.port); @@ -55,7 +57,7 @@ class Teamspeak2 extends Core { state.raw.channels.push(channel); } } - }); + }, queryPort); } async sendCommand(socket,cmd) { diff --git a/protocols/teamspeak3.js b/protocols/teamspeak3.js index c28c63a..7fdf6bd 100644 --- a/protocols/teamspeak3.js +++ b/protocols/teamspeak3.js @@ -2,6 +2,8 @@ const Core = require('./core'); class Teamspeak3 extends Core { async run(state) { + const queryPort = this.options.teamspeakQueryPort || 10011; + await this.withTcp(async socket => { { const data = await this.sendCommand(socket, 'use port='+this.options.port, true); @@ -31,7 +33,7 @@ class Teamspeak3 extends Core { const data = await this.sendCommand(socket, 'channellist -topic'); state.raw.channels = data; } - }); + }, queryPort); } async sendCommand(socket,cmd,raw) { diff --git a/protocols/terraria.js b/protocols/terraria.js index 555a966..688194d 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -18,7 +18,7 @@ class Terraria extends Core { } state.name = json.name; - state.raw.port = json.port; + state.gamePort = json.port; state.raw.numplayers = json.playercount; } } diff --git a/protocols/unreal2.js b/protocols/unreal2.js index 8b0634f..e2c8d08 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -11,7 +11,7 @@ class Unreal2 extends Core { const reader = this.reader(b); state.raw.serverid = reader.uint(4); state.raw.ip = this.readUnrealString(reader); - state.raw.port = reader.uint(4); + state.gamePort = reader.uint(4); state.raw.queryport = reader.uint(4); state.name = this.readUnrealString(reader, true); state.map = this.readUnrealString(reader, true); diff --git a/protocols/valve.js b/protocols/valve.js index 52c033f..d78ebfc 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -5,8 +5,6 @@ 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,6 +26,7 @@ class Valve extends Core { } async run(state) { + if (!this.options.port) this.options.port = 27015; await this.queryInfo(state); await this.queryChallenge(); await this.queryPlayers(state); @@ -93,7 +92,7 @@ class Valve extends Core { } state.raw.version = reader.string(); const extraFlag = reader.uint(1); - if(extraFlag & 0x80) state.raw.port = reader.uint(2); + 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); From 64966606339eaf7fc4923cf2a69df9f1a18695fb Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 21:32:24 -0600 Subject: [PATCH 07/11] Add ping field, start improving README for 2.0 --- README.md | 32 +++++++++++++++++++++++++++++++- protocols/core.js | 43 +++++++++++++++++++++++++++++++++++++++---- protocols/nadeo.js | 3 +++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fff824d..6d125c4 100644 --- a/README.md +++ b/README.md @@ -443,9 +443,39 @@ gamedig --type minecraft --host mc.example.com --port 11234 The output of the command will be in JSON format. -Major Version Changes +Changelog --- +### 2.0 +##### Breaking 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. +##### Minor Changes +* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified +by using async/await across the codebase, eliminating callback chains and the 'async' dependency. +* Replaced `--output pretty` cli parameter with `--pretty`. +* You can now query from CLI using shorthand syntax: `gamedig --type [:]` +* UDP socket is only opened if needed by a query. +* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a +game port or query port by querying twice: once to the port provided, and once to the port including the game's query +port offset (if available). +* Simplified detection of BC2 when using battlefield protocol. +* Fixed buildandshoot not reading player list +* 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. Since 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, tcp socket connection, or http requests made +during the query. + + + + ### 1.0 * First official release * Node.js 6.0 is now required diff --git a/protocols/core.js b/protocols/core.js index 39edf46..7db0a67 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -21,6 +21,8 @@ class Core extends EventEmitter { // Sent to us by QueryRunner this.options = null; this.udpSocket = null; + this.shortestRTT = 0; + this.usedTcp = false; } async runAllAttempts() { @@ -68,7 +70,6 @@ class Core extends EventEmitter { } async runOnce() { - const startMillis = Date.now(); const options = this.options; if (('host' in options) && !('address' in options)) { options.address = await this.parseDns(options.host); @@ -94,13 +95,13 @@ class Core extends EventEmitter { // 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) } + state.ping = this.shortestRTT; delete state.gameHost; delete state.gamePort; @@ -148,6 +149,23 @@ class Core extends EventEmitter { else return await resolveStandard(host); } + /** 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 /** @returns {Reader} */ reader(buffer) { @@ -186,6 +204,7 @@ class Core extends EventEmitter { * @returns {Promise} */ async withTcp(fn, port) { + this.usedTcp = true; const address = this.options.address; if (!port) port = this.options.port; this.assertValidPort(port); @@ -216,6 +235,7 @@ class Core extends EventEmitter { 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, @@ -284,10 +304,17 @@ class Core extends EventEmitter { 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); + } this.debugLog(log => { log(fromAddress + ':' + fromPort + " <--UDP"); log(HexUtil.debugDump(buffer)); @@ -307,7 +334,6 @@ class Core extends EventEmitter { const wrappedTimeout = new Promise((resolve, reject) => { timeout.catch((e) => { this.debugLog("UDP timeout detected"); - let success = false; if (onTimeout) { try { const result = onTimeout(); @@ -331,6 +357,12 @@ class Core extends EventEmitter { } 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({ @@ -344,7 +376,10 @@ class Core extends EventEmitter { .then((response) => log(params.uri + " <--HTTP " + response.statusCode)) .catch(() => {}); }); - const wrappedPromise = requestPromise.then(response => response.body); + const wrappedPromise = requestPromise.then(response => { + if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode); + return response.body; + }); return await Promise.race([wrappedPromise, this.abortedPromise]); } finally { requestPromise && requestPromise.cancel(); diff --git a/protocols/nadeo.js b/protocols/nadeo.js index f214360..2280bd7 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -4,7 +4,10 @@ const gbxremote = require('gbxremote'), class Nadeo extends Core { 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); + //const data = this.methodCall(client, 'GetStatus'); { From fdc08b5c09358d6b3daa969d79f66d4ec7809e7b Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 22:22:26 -0600 Subject: [PATCH 08/11] Finalized 2.0 release notes --- README.md | 111 ++++++++++++++++++++-------------------------- protocols/core.js | 3 -- 2 files changed, 48 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 6d125c4..37781cb 100644 --- a/README.md +++ b/README.md @@ -14,75 +14,53 @@ Usage from Node.js npm install gamedig ``` -Promise: ```javascript const Gamedig = require('gamedig'); Gamedig.query({ - type: 'minecraft', - host: 'mc.example.com' + type: 'minecraft', + host: 'mc.example.com' }).then((state) => { - console.log(state); + console.log(state); }).catch((error) => { - console.log("Server is offline"); + console.log("Server is offline"); }); ``` -or Node.JS Callback: -```javascript -const Gamedig = require('gamedig'); -Gamedig.query({ - type: 'minecraft', - host: 'mc.example.com' -}, -function(e,state) { - if(e) console.log("Server is offline"); - else console.log(state); -}); -``` - -> Is NPM out of date? If you're feeling lucky, you can install the latest code with -> ```shell -> npm install sonicsnes/node-gamedig -> ``` - ### Query Options **Typical** -* **type**: One of the game IDs listed in the game list below -* **host**: Hostname or IP of the game server -* **port**: (optional) Uses the protocol default if not set +* **type**: string - One of the game IDs listed in the game list below +* **host**: string - Hostname or IP of the game server +* **port**: number (optional) - Connection port or query port for the game server. Some +games utilize a separate "query" port. If specifying the game port does not seem to work as expected, passing in +this query port may work instead. (defaults to protocol default port) **Advanced** -* **notes**: (optional) An object passed through in the return value. -* **maxAttempts**: (optional) Number of attempts to query server in case of failure. (default 1) -* **socketTimeout**: (optional) Milliseconds to wait for a single packet. Beware that increasing this +* **maxAttempts**: number - Number of attempts to query server in case of failure. (default 1) +* **socketTimeout**: number - Milliseconds to wait for a single packet. Beware that increasing this will cause many queries to take longer even if the server is online. (default 2000) -* **attemptTimeout**: (optional) Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, +* **attemptTimeout**: number - Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, as the socketTimeout typically fires first. (default 10000) +* **debug**: boolean - Enables massive amounts of debug logging to stdout. (default false) ### Return Value The returned state object will contain the following keys: -**Stable, always present:** - -* **name** -* **map** -* **password**: Boolean -* **maxplayers** -* **players**: (array of objects) Each object **may** contain name, ping, score, team, address -* **bots**: Same schema as players -* **notes**: Passed through from the input - -**Unstable, not guaranteed:** - -* **raw**: Contains all information received from the server -* **query**: Details about the query performed - -It can usually be assumed that the number of players online is equal to the length of the players array. -Some servers may return an additional player count number, which may be present in the unstable raw object. +* **name**: string - Server name +* **map**: string - Current server game map +* **password**: boolean - If a password is required +* **maxplayers**: number +* **players**: array of objects + * Each object **may or may not** contain name, ping, score, team, address. + * The number of players online can be determined by `players.length`. + * For servers which do not provide player names, this may be an array +of empty objects (ex. `[{},{},{}]`), one for each player without a name. +* **bots**: array of objects - Same schema as players +* **raw**: freeform object - Contains all information received from the server in a disorganized format. The content of this +field is unstable, and may change on a per-protocol basis between GameDig patch releases (although not typical). Games List --- @@ -365,8 +343,6 @@ Games List > __Know how to code?__ Protocols for most of the games above are documented > in the /reference folder, ready for you to develop into GameDig! - - > Don't see your game listed here? > > First, let us know so we can fix it. Then, you can try using some common query @@ -392,7 +368,7 @@ have set the cvar: host_players_show 2 ### DayZ DayZ uses a query port that is separate from its main game port. The query port is usually -the game port PLUS 24714 or 24715. You may need to pass this port in as the 'port_query' request option. +the game port PLUS 24714 or 24715. You may need to pass this query port into GameDig instead. ### Mumble For full query results from Mumble, you must be running the @@ -435,26 +411,29 @@ 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. Changelog --- ### 2.0 -##### Breaking changes -* Node 8 is now required + +##### 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. @@ -464,18 +443,24 @@ by using async/await across the codebase, eliminating callback chains and the 'a * 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). -* Simplified detection of BC2 when using battlefield protocol. -* Fixed buildandshoot not reading player list * 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. Since 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, tcp socket connection, or http requests made +* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support +for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket during the query. +* Improved debug logging across all parts of GameDig +* Removed global `Gamedig.debug`. `debug` is now an option on each query. - - +##### Protocol Changes +* Added support for games using older versions of battlefield protocol. +* Simplified detection of BC2 when using battlefield protocol. +* Fixed buildandshoot not reading player list +* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically. +* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically. +* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically. +* Improved valve protocol challenge key retry process ### 1.0 * First official release -* Node.js 6.0 is now required +* Node.js 6 is now required diff --git a/protocols/core.js b/protocols/core.js index 7db0a67..246c660 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -89,9 +89,6 @@ class Core extends EventEmitter { await this.run(state); - if (this.options.notes) - state.notes = this.options.notes; - // because lots of servers prefix with spaces to try to appear first state.name = (state.name || '').trim(); From dfa5c95efc29fd2b1199ad3484e85cbb952ea4f6 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 22:38:49 -0600 Subject: [PATCH 09/11] Remove callback support and global Gamedig.debug option --- bin/gamedig.js | 5 +++-- lib/GlobalUdpSocket.js | 18 ++++++++++++++---- lib/QueryRunner.js | 10 +++++----- lib/index.js | 35 +++++------------------------------ protocols/core.js | 6 +----- 5 files changed, 28 insertions(+), 46 deletions(-) diff --git a/bin/gamedig.js b/bin/gamedig.js index 64e2411..d12a80b 100644 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -27,8 +27,9 @@ if (argv._.length >= 1) { options.port = split[1]; } } - -if(debug) Gamedig.debug = true; +if (debug) { + options.debug = true; +} Gamedig.query(options) .then((state) => { diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js index a59fff7..af45ead 100644 --- a/lib/GlobalUdpSocket.js +++ b/lib/GlobalUdpSocket.js @@ -5,7 +5,7 @@ class GlobalUdpSocket { constructor() { this.socket = null; this.callbacks = new Set(); - this.debug = false; + this.debuggingCallbacks = new Set(); } _getSocket() { @@ -14,12 +14,18 @@ class GlobalUdpSocket { 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(rinfo.address, rinfo.port, buffer); + cb(fromAddress, fromPort, buffer); } }); udpSocket.on('error', (e) => { - if (this.debug) { + if (this.debuggingCallbacks.size) { console.log("UDP ERROR: " + e); } }); @@ -31,11 +37,15 @@ class GlobalUdpSocket { this._getSocket().send(buffer,0,buffer.length,port,address); } - addCallback(callback) { + addCallback(callback, debug) { this.callbacks.add(callback); + if (debug) { + this.debuggingCallbacks.add(callback); + } } removeCallback(callback) { this.callbacks.delete(callback); + this.debuggingCallbacks.delete(callback); } } diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js index 1cb16b8..fe4fb19 100644 --- a/lib/QueryRunner.js +++ b/lib/QueryRunner.js @@ -1,5 +1,6 @@ const GameResolver = require('./GameResolver'), - ProtocolResolver = require('./ProtocolResolver'); + ProtocolResolver = require('./ProtocolResolver'), + GlobalUdpSocket = require('./GlobalUdpSocket'); const defaultOptions = { socketTimeout: 2000, @@ -8,9 +9,8 @@ const defaultOptions = { }; class QueryRunner { - constructor(udpSocket, debug) { - this.debug = debug; - this.udpSocket = udpSocket; + constructor() { + this.udpSocket = new GlobalUdpSocket(); this.gameResolver = new GameResolver(); this.protocolResolver = new ProtocolResolver(); } @@ -90,7 +90,7 @@ class QueryRunner { } async _attempt(options) { - if (this.debug) { + if (options.debug) { console.log("Running attempt with options:"); console.log(options); } diff --git a/lib/index.js b/lib/index.js index a5b6ddd..4ded359 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,48 +1,23 @@ -const QueryRunner = require('./QueryRunner'), - GlobalUdpSocket = require('./GlobalUdpSocket'); +const QueryRunner = require('./QueryRunner'); let singleton = null; class Gamedig { constructor() { - this.udpSocket = new GlobalUdpSocket(); - this.queryRunner = new QueryRunner(this.udpSocket); - this._debug = false; - } - - setDebug(on) { - this.udpSocket.debug = on; - this._debug = on; - this.queryRunner.debug = on; + this.queryRunner = new QueryRunner(); } async query(userOptions) { - userOptions.debug |= this._debug; return await this.queryRunner.run(userOptions); } static getInstance() { - if (!singleton) { - singleton = new Gamedig(); - } + 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) { - 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; + static async query(...args) { + return await Gamedig.getInstance().query(...args); } } -Object.defineProperty(Gamedig, "debug", { set: on => Gamedig.getInstance().setDebug(on) }); module.exports = Gamedig; diff --git a/protocols/core.js b/protocols/core.js index 246c660..38f7722 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -312,10 +312,6 @@ class Core extends EventEmitter { const rtt = end-start; this.registerRtt(rtt); } - 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"); @@ -325,7 +321,7 @@ class Core extends EventEmitter { reject(e); } }; - socket.addCallback(socketCallback); + socket.addCallback(socketCallback, this.options.debug); }); timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP'); const wrappedTimeout = new Promise((resolve, reject) => { From 31e6051a04c6b98f1d384154aecc8b7289a190f8 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 22:42:52 -0600 Subject: [PATCH 10/11] Release 2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27b9e5d..c7314e3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "main": "lib/index.js", "author": "Michael Morrison", - "version": "1.0.49", + "version": "2.0", "repository": { "type": "git", "url": "https://github.com/sonicsnes/node-gamedig.git" From b974b3e6ee4f383eda0d41adea9bb45cf6399ec2 Mon Sep 17 00:00:00 2001 From: mmorrison Date: Sat, 12 Jan 2019 22:43:42 -0600 Subject: [PATCH 11/11] Bump node minimum to 8.0.0 for 2.0 gamedig release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7314e3..f9f9287 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=8.0.0" }, "dependencies": { "cheerio": "^1.0.0-rc.2",