More async conversions

This commit is contained in:
mmorrison 2019-01-09 05:35:11 -06:00
parent 77b2cc1c7f
commit 9b8423b20a
15 changed files with 859 additions and 704 deletions

View file

@ -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 = /<tr>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>/g;
let pm;
while(pm = pre.exec(table)) {
if(pm[2] === 'Ping') continue;
state.players.push({
name: pm[1],
ping: pm[2],
team: pm[3],
score: pm[4]
});
}
}
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
this.finish(state);
});
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;
}
*/
}
}

View file

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

View file

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

View file

@ -6,29 +6,34 @@ class Ffow extends Valve {
this.byteorder = 'be';
this.legacyChallenge = true;
}
queryInfo(state,c) {
this.sendPacket(0x46,false,'LSQ',0x49, (b) => {
const reader = this.reader(b);
state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
state.raw.mod = reader.string();
state.raw.gamemode = reader.string();
state.raw.description = reader.string();
state.raw.version = reader.string();
state.raw.port = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.listentype = String.fromCharCode(reader.uint(1));
state.raw.environment = String.fromCharCode(reader.uint(1));
state.password = !!reader.uint(1);
state.raw.secure = reader.uint(1);
state.raw.averagefps = reader.uint(1);
state.raw.round = reader.uint(1);
state.raw.maxrounds = reader.uint(1);
state.raw.timeleft = reader.uint(2);
c();
});
async queryInfo(state) {
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);
}
}

View file

@ -1,5 +1,4 @@
const request = require('request'),
Quake2 = require('./quake2');
const Quake2 = require('./quake2');
class FiveM extends Quake2 {
constructor() {
@ -9,43 +8,28 @@ class FiveM extends Quake2 {
this.encoding = 'utf8';
}
finish(state) {
request({
uri: 'http://'+this.options.address+':'+this.options.port_query+'/info.json',
timeout: this.options.socketTimeout
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
}
async run(state) {
await super.run(state);
state.raw.info = json;
request({
uri: 'http://'+this.options.address+':'+this.options.port_query+'/players.json',
timeout: this.options.socketTimeout
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
}
state.raw.players = json;
state.players = [];
for (const player of json) {
state.players.push({name:player.name, ping:player.ping});
}
super.finish(state);
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port_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});
}
}
}
}

View file

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

View file

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

View file

@ -1,5 +1,5 @@
const async = require('async'),
Core = require('./core');
const Core = require('./core'),
HexUtil = require('../lib/HexUtil');
class Gamespy3 extends Core {
constructor() {
@ -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;
});
}
}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
const Gamespy3 = require('./gamespy3');
class Ut3 extends Gamespy3 {
finalizeState(state) {
super.finalizeState(state);
async run(state) {
await super.run(state);
this.translate(state.raw,{
'mapname': false,

View file

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