node-gamedig/protocols/core.js

388 lines
12 KiB
JavaScript
Raw Normal View History

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
class Core extends EventEmitter {
2017-08-09 12:32:09 +02:00
constructor() {
super();
this.options = {
socketTimeout: 2000,
2018-01-31 11:03:13 +01:00
attemptTimeout: 10000,
maxAttempts: 1
2017-08-09 12:32:09 +02:00
};
this.encoding = 'utf8';
this.byteorder = 'le';
this.delimiter = '\0';
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) {
2019-01-09 12:50:30 +01:00
this.debugLog("Error during async cleanup: " + e.stack);
2019-01-09 12:35:11 +01:00
}
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-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;
2019-01-09 12:50:30 +01:00
this.debugLog("Standard DNS Lookup: " + host);
2019-01-07 07:52:29 +01:00
const {address,family} = await dnsLookupAsync(host);
2019-01-09 12:50:30 +01:00
this.debugLog(address);
2019-01-07 07:52:29 +01:00
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;
2019-01-09 12:50:30 +01:00
this.debugLog("SRV DNS Lookup: " + srv+'.'+host);
2019-01-07 07:52:29 +01:00
let records;
try {
records = await dnsResolveAsync(srv + '.' + host, 'SRV');
2019-01-09 12:50:30 +01:00
this.debugLog(records);
2019-01-07 07:52:29 +01:00
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) {
2019-01-09 12:50:30 +01:00
this.debugLog(e.toString());
2019-01-07 07:52:29 +01:00
}
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 };
2019-01-09 12:50:30 +01:00
this.debugLog("Registering async leak: " + id);
2019-01-09 12:35:11 +01:00
this.asyncLeaks.add(entry);
2019-01-07 07:52:29 +01:00
return () => {
2019-01-09 12:50:30 +01:00
this.debugLog("Removing async leak: " + id);
2019-01-09 12:35:11 +01:00
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)) {
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;
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-09 12:50:30 +01:00
this.debugLog(log => {
this.debugLog(address+':'+port+" TCP Connecting");
2019-01-07 07:52:29 +01:00
const writeHook = socket.write;
socket.write = (...args) => {
2019-01-09 12:50:30 +01:00
log(address+':'+port+" TCP-->");
log(HexUtil.debugDump(args[0]));
2019-01-07 07:52:29 +01:00
writeHook.apply(socket,args);
};
2019-01-09 12:50:30 +01:00
socket.on('error', e => log('TCP Error: ' + e));
socket.on('close', () => log('TCP Closed'));
2019-01-07 07:52:29 +01:00
socket.on('data', (data) => {
2019-01-09 12:50:30 +01:00
log(address+':'+port+" <--TCP");
log(data);
2019-01-07 07:52:29 +01:00
});
2019-01-09 12:50:30 +01:00
socket.on('ready', () => 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');
2019-01-09 12:50:30 +01:00
this.debugLog(log => {
log(this.options.address+':'+this.options.port_query+" UDP-->");
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(() => {
2019-01-09 12:50:30 +01:00
this.debugLog("UDP timeout detected");
2019-01-07 07:52:29 +01:00
let success = false;
if (onTimeout) {
const result = onTimeout();
if (result !== undefined) {
2019-01-09 12:50:30 +01:00
this.debugLog("UDP timeout resolved by callback");
2019-01-07 07:52:29 +01:00
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) {
2019-01-09 12:50:30 +01:00
this.debugLog("UDP send finished by callback");
2019-01-07 07:52:29 +01:00
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;
}
2019-01-09 12:50:30 +01:00
debugLog(...args) {
if (!this.debug) return;
try {
if(args[0] instanceof Buffer) {
this.debugLog(HexUtil.debugDump(args[0]));
} else if (typeof args[0] == 'function') {
const result = args[0].call(undefined, this.debugLog.bind(this));
if (result !== undefined) {
this.debugLog(result);
}
} else {
console.log(...args);
}
} catch(e) {
console.log("Error while debug logging: " + e);
}
}
}
module.exports = Core;