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;