mirror of
https://github.com/tribufu/node-gamedig
synced 2026-05-18 09:35:50 +00:00
Additional async rewrite
This commit is contained in:
parent
efe12a00aa
commit
29ce0b82d0
24 changed files with 654 additions and 470 deletions
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<T>} 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<T>
|
||||
*/
|
||||
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]));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue