2017-08-09 11:05:55 +02:00
|
|
|
const EventEmitter = require('events').EventEmitter,
|
2017-08-09 12:32:09 +02:00
|
|
|
dns = require('dns'),
|
|
|
|
net = require('net'),
|
2017-08-10 13:49:42 +02:00
|
|
|
Reader = require('../lib/reader'),
|
2019-01-07 07:52:29 +01:00
|
|
|
HexUtil = require('../lib/HexUtil'),
|
|
|
|
util = require('util'),
|
|
|
|
dnsLookupAsync = util.promisify(dns.lookup),
|
2019-01-09 12:35:11 +01:00
|
|
|
dnsResolveAsync = util.promisify(dns.resolve),
|
|
|
|
requestAsync = require('request-promise');
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2017-08-09 11:05:55 +02:00
|
|
|
class Core extends EventEmitter {
|
2017-08-09 12:32:09 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.options = {
|
2018-11-11 13:13:31 +01:00
|
|
|
socketTimeout: 2000,
|
2018-01-31 11:03:13 +01:00
|
|
|
attemptTimeout: 10000,
|
2018-01-31 07:41:57 +01:00
|
|
|
maxAttempts: 1
|
2017-08-09 12:32:09 +02:00
|
|
|
};
|
|
|
|
this.encoding = 'utf8';
|
|
|
|
this.byteorder = 'le';
|
|
|
|
this.delimiter = '\0';
|
2017-08-09 12:41:30 +02:00
|
|
|
this.srvRecord = null;
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-09 12:35:11 +01:00
|
|
|
this.asyncLeaks = new Set();
|
2019-01-07 07:52:29 +01:00
|
|
|
this.udpCallback = null;
|
|
|
|
this.udpLocked = false;
|
2019-01-09 12:35:11 +01:00
|
|
|
this.lastAsyncLeakId = 0;
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
initState() {
|
|
|
|
return {
|
|
|
|
name: '',
|
|
|
|
map: '',
|
|
|
|
password: false,
|
|
|
|
|
|
|
|
raw: {},
|
|
|
|
|
|
|
|
maxplayers: 0,
|
|
|
|
players: [],
|
|
|
|
bots: []
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
// Run all attempts
|
|
|
|
async runAll() {
|
|
|
|
let result = null;
|
|
|
|
let lastError = null;
|
|
|
|
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
|
|
|
|
try {
|
|
|
|
result = await this.runOnceSafe();
|
|
|
|
result.query.attempts = attempt;
|
|
|
|
break;
|
|
|
|
} catch (e) {
|
|
|
|
lastError = e;
|
|
|
|
}
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
if (result === null) {
|
|
|
|
throw lastError;
|
|
|
|
}
|
|
|
|
return result;
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
// 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");
|
2019-01-09 12:35:11 +01:00
|
|
|
if (this.asyncLeaks.size) {
|
2019-01-07 07:52:29 +01:00
|
|
|
let out = [];
|
2019-01-09 12:35:11 +01:00
|
|
|
for (const leak of this.asyncLeaks) {
|
|
|
|
out.push(leak.id + " " + leak.stack);
|
2019-01-07 07:52:29 +01:00
|
|
|
}
|
2019-01-09 12:35:11 +01:00
|
|
|
throw new Error('Query succeeded, but async leak was detected:\n' + out.join('\n---\n'));
|
2019-01-07 07:52:29 +01:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
} finally {
|
|
|
|
// Clean up any lingering long-running functions
|
2019-01-09 12:35:11 +01:00
|
|
|
for (const leak of this.asyncLeaks) {
|
2019-01-07 07:52:29 +01:00
|
|
|
try {
|
2019-01-09 12:35:11 +01:00
|
|
|
leak.cleanup();
|
|
|
|
} catch(e) {
|
|
|
|
if (this.debug) console.log("Error during async cleanup: " + e.stack);
|
|
|
|
}
|
2019-01-07 07:52:29 +01:00
|
|
|
}
|
2019-01-09 12:35:11 +01:00
|
|
|
this.asyncLeaks.clear();
|
2019-01-07 07:52:29 +01:00
|
|
|
}
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
timedPromise(promise, timeoutMs, timeoutMsg) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const cancelTimeout = this.setTimeout(
|
|
|
|
() => reject(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")),
|
|
|
|
timeoutMs
|
|
|
|
);
|
|
|
|
promise.finally(cancelTimeout).then(resolve,reject);
|
|
|
|
});
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
async runOnce() {
|
|
|
|
const startMillis = Date.now();
|
|
|
|
const options = this.options;
|
|
|
|
if (('host' in options) && !('address' in options)) {
|
|
|
|
options.address = await this.parseDns(options.host);
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2019-01-07 07:52:29 +01:00
|
|
|
if(!('port_query' in options) && 'port' in options) {
|
|
|
|
const offset = options.port_query_offset || 0;
|
|
|
|
options.port_query = options.port + offset;
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
const state = this.initState();
|
|
|
|
await this.run(state);
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
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;
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
return state;
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
async run(state) {}
|
2019-01-06 21:10:46 +01:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
/**
|
|
|
|
* @param {string} host
|
|
|
|
* @returns {Promise<string>}
|
|
|
|
*/
|
|
|
|
async parseDns(host) {
|
|
|
|
const isIp = (host) => {
|
|
|
|
return !!host.match(/\d+\.\d+\.\d+\.\d+/);
|
|
|
|
};
|
|
|
|
const resolveStandard = async (host) => {
|
|
|
|
if(isIp(host)) return host;
|
2017-08-09 12:04:32 +02:00
|
|
|
if(this.debug) console.log("Standard DNS Lookup: " + host);
|
2019-01-07 07:52:29 +01:00
|
|
|
const {address,family} = await dnsLookupAsync(host);
|
|
|
|
if(this.debug) console.log(address);
|
|
|
|
return address;
|
2017-08-09 12:32:09 +02:00
|
|
|
};
|
2019-01-07 07:52:29 +01:00
|
|
|
const resolveSrv = async (srv,host) => {
|
|
|
|
if(isIp(host)) return host;
|
2017-08-09 12:04:32 +02:00
|
|
|
if(this.debug) console.log("SRV DNS Lookup: " + srv+'.'+host);
|
2019-01-07 07:52:29 +01:00
|
|
|
let records;
|
|
|
|
try {
|
|
|
|
records = await dnsResolveAsync(srv + '.' + host, 'SRV');
|
|
|
|
if(this.debug) console.log(records);
|
|
|
|
if(records.length >= 1) {
|
|
|
|
const record = records[0];
|
|
|
|
this.options.port = record.port;
|
|
|
|
const srvhost = record.name;
|
|
|
|
return await resolveStandard(srvhost);
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2019-01-07 07:52:29 +01:00
|
|
|
} catch(e) {
|
|
|
|
if (this.debug) console.log(e.toString());
|
|
|
|
}
|
|
|
|
return await resolveStandard(host);
|
2017-08-09 12:32:09 +02:00
|
|
|
};
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
if(this.srvRecord) return await resolveSrv(this.srvRecord, host);
|
|
|
|
else return await resolveStandard(host);
|
|
|
|
}
|
|
|
|
|
2019-01-09 12:35:11 +01:00
|
|
|
addAsyncLeak(fn) {
|
|
|
|
const id = ++this.lastAsyncLeakId;
|
2019-01-07 07:52:29 +01:00
|
|
|
const stack = new Error().stack;
|
2019-01-09 12:35:11 +01:00
|
|
|
const entry = { id: id, cleanup: fn, stack: stack };
|
|
|
|
if (this.debug) console.log("Registering async leak: " + id);
|
|
|
|
this.asyncLeaks.add(entry);
|
2019-01-07 07:52:29 +01:00
|
|
|
return () => {
|
2019-01-09 12:35:11 +01:00
|
|
|
if (this.debug) console.log("Removing async leak: " + id);
|
|
|
|
this.asyncLeaks.delete(entry);
|
2019-01-07 07:52:29 +01:00
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// utils
|
|
|
|
/** @returns {Reader} */
|
|
|
|
reader(buffer) {
|
|
|
|
return new Reader(this,buffer);
|
|
|
|
}
|
|
|
|
translate(obj,trans) {
|
|
|
|
for(const from of Object.keys(trans)) {
|
2017-08-09 11:05:55 +02:00
|
|
|
const to = trans[from];
|
2017-08-09 12:32:09 +02:00
|
|
|
if(from in obj) {
|
|
|
|
if(to) obj[to] = obj[from];
|
|
|
|
delete obj[from];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trueTest(str) {
|
|
|
|
if(typeof str === 'boolean') return str;
|
|
|
|
if(typeof str === 'number') return str !== 0;
|
|
|
|
if(typeof str === 'string') {
|
|
|
|
if(str.toLowerCase() === 'true') return true;
|
2019-01-07 07:52:29 +01:00
|
|
|
if(str.toLowerCase() === 'yes') return true;
|
2017-08-09 12:32:09 +02:00
|
|
|
if(str === '1') return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
/**
|
|
|
|
* @param {function(Socket):Promise} fn
|
|
|
|
* @returns {Promise<Socket>}
|
|
|
|
*/
|
|
|
|
async withTcp(fn) {
|
2017-08-09 12:32:09 +02:00
|
|
|
const address = this.options.address;
|
2017-08-09 11:05:55 +02:00
|
|
|
const port = this.options.port_query;
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
const socket = net.connect(port,address);
|
2017-08-09 12:32:09 +02:00
|
|
|
socket.setNoDelay(true);
|
2019-01-09 12:35:11 +01:00
|
|
|
const cancelAsyncLeak = this.addAsyncLeak(() => socket.destroy());
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
if(this.debug) {
|
|
|
|
console.log(address+':'+port+" TCP Connecting");
|
|
|
|
const writeHook = socket.write;
|
|
|
|
socket.write = (...args) => {
|
2017-08-10 13:49:42 +02:00
|
|
|
console.log(address+':'+port+" TCP-->");
|
|
|
|
console.log(HexUtil.debugDump(args[0]));
|
2019-01-07 07:52:29 +01:00
|
|
|
writeHook.apply(socket,args);
|
|
|
|
};
|
|
|
|
socket.on('error', e => console.log('TCP Error: ' + e));
|
|
|
|
socket.on('close', () => console.log('TCP Closed'));
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
if(this.debug) {
|
|
|
|
console.log(address+':'+port+" <--TCP");
|
|
|
|
console.log(HexUtil.debugDump(data));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
socket.on('ready', () => console.log(address+':'+port+" TCP Connected"));
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
try {
|
|
|
|
await this.timedPromise(
|
|
|
|
new Promise((resolve,reject) => {
|
|
|
|
socket.on('ready', resolve);
|
|
|
|
socket.on('close', () => reject(new Error('TCP Connection Refused')));
|
|
|
|
}),
|
|
|
|
this.options.socketTimeout,
|
|
|
|
'TCP Opening'
|
|
|
|
);
|
|
|
|
return await fn(socket);
|
|
|
|
} finally {
|
2019-01-09 12:35:11 +01:00
|
|
|
cancelAsyncLeak();
|
2019-01-07 07:52:29 +01:00
|
|
|
socket.destroy();
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
setTimeout(callback, time) {
|
2019-01-09 12:35:11 +01:00
|
|
|
let cancelAsyncLeak;
|
2019-01-07 07:52:29 +01:00
|
|
|
const onTimeout = () => {
|
2019-01-09 12:35:11 +01:00
|
|
|
cancelAsyncLeak();
|
2019-01-07 07:52:29 +01:00
|
|
|
callback();
|
|
|
|
};
|
|
|
|
const timeout = setTimeout(onTimeout, time);
|
2019-01-09 12:35:11 +01:00
|
|
|
cancelAsyncLeak = this.addAsyncLeak(() => clearTimeout(timeout));
|
2019-01-07 07:52:29 +01:00
|
|
|
return () => {
|
2019-01-09 12:35:11 +01:00
|
|
|
cancelAsyncLeak();
|
2019-01-07 07:52:29 +01:00
|
|
|
clearTimeout(timeout);
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
/**
|
|
|
|
* @param {Socket} socket
|
|
|
|
* @param {Buffer} buffer
|
|
|
|
* @param {function(Buffer):boolean} ondata
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
|
|
|
async tcpSend(socket,buffer,ondata) {
|
|
|
|
return await this.timedPromise(
|
|
|
|
new Promise(async (resolve,reject) => {
|
|
|
|
let received = Buffer.from([]);
|
|
|
|
const onData = (data) => {
|
|
|
|
received = Buffer.concat([received, data]);
|
|
|
|
const result = ondata(received);
|
|
|
|
if (result !== undefined) {
|
|
|
|
socket.off('data', onData);
|
|
|
|
resolve(result);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
socket.on('data', onData);
|
|
|
|
socket.write(buffer);
|
|
|
|
}),
|
|
|
|
this.options.socketTimeout,
|
|
|
|
'TCP'
|
|
|
|
);
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
async withUdpLock(fn) {
|
|
|
|
if (this.udpLocked) {
|
|
|
|
throw new Error('Attempted to lock UDP when already locked');
|
|
|
|
}
|
|
|
|
this.udpLocked = true;
|
|
|
|
try {
|
|
|
|
return await fn();
|
|
|
|
} finally {
|
|
|
|
this.udpLocked = false;
|
|
|
|
this.udpCallback = null;
|
|
|
|
}
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-07 07:52:29 +01:00
|
|
|
/**
|
|
|
|
* @param {Buffer|string} buffer
|
|
|
|
* @param {function(Buffer):T} onPacket
|
|
|
|
* @param {(function():T)=} onTimeout
|
|
|
|
* @returns Promise<T>
|
|
|
|
* @template T
|
|
|
|
*/
|
|
|
|
async udpSend(buffer,onPacket,onTimeout) {
|
|
|
|
if(!('port_query' in this.options)) throw new Error('Attempted to send without setting a port');
|
|
|
|
if(!('address' in this.options)) throw new Error('Attempted to send without setting an address');
|
|
|
|
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
|
2017-08-10 13:49:42 +02:00
|
|
|
if(this.debug) {
|
|
|
|
console.log(this.options.address+':'+this.options.port_query+" UDP-->");
|
|
|
|
console.log(HexUtil.debugDump(buffer));
|
|
|
|
}
|
2019-01-07 07:52:29 +01:00
|
|
|
|
|
|
|
return await this.withUdpLock(async() => {
|
|
|
|
this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address);
|
|
|
|
|
|
|
|
return await new Promise((resolve,reject) => {
|
|
|
|
const cancelTimeout = this.setTimeout(() => {
|
|
|
|
if (this.debug) console.log("UDP timeout detected");
|
|
|
|
let success = false;
|
|
|
|
if (onTimeout) {
|
|
|
|
const result = onTimeout();
|
|
|
|
if (result !== undefined) {
|
|
|
|
if (this.debug) console.log("UDP timeout resolved by callback");
|
|
|
|
resolve(result);
|
|
|
|
success = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!success) {
|
|
|
|
reject(new Error('UDP Watchdog Timeout'));
|
|
|
|
}
|
|
|
|
},this.options.socketTimeout);
|
|
|
|
|
|
|
|
this.udpCallback = (buffer) => {
|
|
|
|
const result = onPacket(buffer);
|
|
|
|
if(result !== undefined) {
|
|
|
|
if (this.debug) console.log("UDP send finished by callback");
|
|
|
|
cancelTimeout();
|
|
|
|
resolve(result);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2019-01-07 07:52:29 +01:00
|
|
|
|
|
|
|
_udpIncoming(buffer) {
|
|
|
|
this.udpCallback && this.udpCallback(buffer);
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2019-01-09 12:35:11 +01:00
|
|
|
|
|
|
|
request(params) {
|
|
|
|
const promise = requestAsync({
|
|
|
|
...params,
|
|
|
|
timeout: this.options.socketTimeout
|
|
|
|
});
|
|
|
|
const cancelAsyncLeak = this.addAsyncLeak(() => {
|
|
|
|
promise.cancel();
|
|
|
|
});
|
|
|
|
promise.finally(cancelAsyncLeak);
|
|
|
|
return promise;
|
|
|
|
}
|
2017-08-09 11:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Core;
|