node-gamedig/protocols/core.js

311 lines
7.5 KiB
JavaScript

const EventEmitter = require('events').EventEmitter,
dns = require('dns'),
net = require('net'),
async = require('async'),
Reader = require('../lib/reader');
class Core extends EventEmitter {
constructor() {
super();
this.options = {
tcpTimeout: 1000,
udpTimeout: 1000
};
this.maxAttempts = 1;
this.attempt = 1;
this.finished = false;
this.encoding = 'utf8';
this.byteorder = 'le';
this.delimiter = '\0';
this.globalTimeoutTimer = setTimeout(() => {
this.fatal('timeout');
},10000);
}
fatal(err,noretry) {
if(!noretry && this.attempt < this.maxAttempts) {
this.attempt++;
this.start();
return;
}
this.done({error: err.toString()});
}
initState() {
return {
name: '',
map: '',
password: false,
raw: {},
maxplayers: 0,
players: [],
bots: []
};
}
finalizeState(state) {}
finish(state) {
this.finalizeState(state);
this.done(state);
}
done(state) {
if(this.finished) return;
clearTimeout(this.globalTimeoutTimer);
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;
this.reset();
this.finished = true;
this.emit('finished',state);
if(this.options.callback) this.options.callback(state);
}
reset() {
if(this.timers) {
for (const timer of this.timers) {
clearTimeout(timer);
}
}
this.timers = [];
if(this.tcpSocket) {
this.tcpSocket.destroy();
delete this.tcpSocket;
}
this.udpTimeoutTimer = false;
this.udpCallback = false;
}
start() {
const options = this.options;
this.reset();
async.series([
(c) => {
// resolve host names
if(!('host' in options)) return c();
if(options.host.match(/\d+\.\d+\.\d+\.\d+/)) {
options.address = options.host;
c();
} else {
this.parseDns(options.host,c);
}
},
(c) => {
// calculate query port if needed
if(!('port_query' in options) && 'port' in options) {
const offset = options.port_query_offset || 0;
options.port_query = options.port + offset;
}
c();
},
(c) => {
// run
this.run(this.initState());
}
]);
}
parseDns(host,c) {
const resolveStandard = (host,c) => {
dns.lookup(host, (err,address,family) => {
if(err) return this.fatal(err);
this.options.address = address;
c();
});
};
const resolveSrv = (srv,host,c) => {
dns.resolve(srv+'.'+host, 'SRV', (err,addresses) => {
if(err) return resolveStandard(host,c);
if(addresses.length >= 1) {
const line = addresses[0];
this.options.port = line.port;
const srvhost = line.name;
if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) {
this.options.address = srvhost;
c();
} else {
// resolve yet again
resolveStandard(srvhost,c);
}
return;
}
return resolveStandard(host,c);
});
};
if(this.srvRecord) resolveSrv(this.srvRecord,host,c);
else resolveStandard(host,c);
}
// utils
/** @returns {Reader} */
reader(buffer) {
return new Reader(this,buffer);
}
translate(obj,trans) {
for(const from of Object.keys(trans)) {
const to = trans[from];
if(from in obj) {
if(to) obj[to] = obj[from];
delete obj[from];
}
}
}
setTimeout(c,t) {
if(this.finished) return 0;
const id = setTimeout(c,t);
this.timers.push(id);
return id;
}
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;
if(str === 'yes') return true;
if(str === '1') return true;
}
return false;
}
debugBuffer(buffer) {
let out = '';
let out2 = '';
for(let i = 0; i < buffer.length; i++) {
const sliced = buffer.slice(i,i+1);
out += sliced.toString('hex')+' ';
let chr = sliced.toString();
if(chr < ' ' || chr > '~') chr = ' ';
out2 += chr+' ';
if(out.length > 60) {
console.log(out);
console.log(out2);
out = out2 = '';
}
}
console.log(out);
console.log(out2);
}
_tcpConnect(c) {
if(this.tcpSocket) return c(this.tcpSocket);
let connected = false;
let received = Buffer.from([]);
const address = this.options.address;
const port = this.options.port_query;
const socket = this.tcpSocket = net.connect(port,address,() => {
if(this.debug) console.log(address+':'+port+" TCPCONNECTED");
connected = true;
c(socket);
});
socket.setTimeout(10000);
socket.setNoDelay(true);
if(this.debug) console.log(address+':'+port+" TCPCONNECT");
const writeHook = socket.write;
socket.write = (...args) => {
if(this.debug) console.log(address+':'+port+" TCP--> "+args[0].toString('hex'));
writeHook.apply(socket,args);
};
socket.on('error', () => {});
socket.on('close', () => {
if(!this.tcpCallback) return;
if(connected) return this.fatal('Socket closed while waiting on TCP');
else return this.fatal('TCP Connection Refused');
});
socket.on('data', (data) => {
if(!this.tcpCallback) return;
if(this.debug) console.log(address+':'+port+" <--TCP "+data.toString('hex'));
received = Buffer.concat([received,data]);
if(this.tcpCallback(received)) {
clearTimeout(this.tcpTimeoutTimer);
this.tcpCallback = false;
received = Buffer.from([]);
}
});
}
tcpSend(buffer,ondata) {
process.nextTick(() => {
if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response');
this._tcpConnect((socket) => {
socket.write(buffer);
});
if(!ondata) return;
this.tcpTimeoutTimer = this.setTimeout(() => {
this.tcpCallback = false;
this.fatal('TCP Watchdog Timeout');
},this.options.tcpTimeout);
this.tcpCallback = ondata;
});
}
udpSend(buffer,onpacket,ontimeout) {
process.nextTick(() => {
if(this.udpCallback) return this.fatal('Attempted to send UDP packet while still waiting on a managed response');
this._udpSendNow(buffer);
if(!onpacket) return;
this.udpTimeoutTimer = this.setTimeout(() => {
this.udpCallback = false;
let timeout = false;
if(!ontimeout || ontimeout() !== true) timeout = true;
if(timeout) this.fatal('UDP Watchdog Timeout');
},this.options.udpTimeout);
this.udpCallback = onpacket;
});
}
_udpSendNow(buffer) {
if(!('port_query' in this.options)) return this.fatal('Attempted to send without setting a port');
if(!('address' in this.options)) return this.fatal('Attempted to send without setting an address');
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
if(this.debug) console.log(this.options.address+':'+this.options.port_query+" UDP--> "+buffer.toString('hex'));
this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address);
}
_udpResponse(buffer) {
if(this.udpCallback) {
const result = this.udpCallback(buffer);
if(result === true) {
// we're done with this udp session
clearTimeout(this.udpTimeoutTimer);
this.udpCallback = false;
}
} else {
this.udpResponse(buffer);
}
}
udpResponse() {}
}
module.exports = Core;