node-gamedig/protocols/core.js

311 lines
7.5 KiB
JavaScript
Raw Normal View History

const EventEmitter = require('events').EventEmitter,
2014-10-29 08:02:03 +01:00
dns = require('dns'),
net = require('net'),
async = require('async'),
2015-08-06 23:12:37 +02:00
Reader = require('../lib/reader');
2014-10-29 08:02:03 +01:00
class Core extends EventEmitter {
constructor() {
super();
2014-10-29 08:02:03 +01:00
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');
2014-10-29 08:02:03 +01:00
},10000);
}
2014-10-29 08:02:03 +01:00
fatal(err,noretry) {
2014-10-29 08:02:03 +01:00
if(!noretry && this.attempt < this.maxAttempts) {
this.attempt++;
this.start();
return;
}
this.done({error: err.toString()});
}
initState() {
2014-10-29 08:02:03 +01:00
return {
name: '',
map: '',
password: false,
2014-10-29 08:02:03 +01:00
raw: {},
maxplayers: 0,
players: [],
bots: []
};
}
finalizeState(state) {}
2014-10-29 08:02:03 +01:00
finish(state) {
2014-10-29 08:02:03 +01:00
this.finalizeState(state);
this.done(state);
}
2014-10-29 08:02:03 +01:00
done(state) {
2014-10-29 08:02:03 +01:00
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);
}
2014-10-29 08:02:03 +01:00
reset() {
2014-10-29 08:02:03 +01:00
if(this.timers) {
for (const timer of this.timers) {
2014-10-29 08:02:03 +01:00
clearTimeout(timer);
}
2014-10-29 08:02:03 +01:00
}
this.timers = [];
2014-10-29 08:02:03 +01:00
if(this.tcpSocket) {
this.tcpSocket.destroy();
delete this.tcpSocket;
}
this.udpTimeoutTimer = false;
this.udpCallback = false;
}
start() {
const options = this.options;
2014-10-29 08:02:03 +01:00
this.reset();
async.series([
(c) => {
2014-10-29 08:02:03 +01:00
// 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);
2014-10-29 08:02:03 +01:00
}
},
(c) => {
2014-10-29 08:02:03 +01:00
// calculate query port if needed
if(!('port_query' in options) && 'port' in options) {
const offset = options.port_query_offset || 0;
2014-10-29 08:02:03 +01:00
options.port_query = options.port + offset;
}
c();
},
(c) => {
2014-10-29 08:02:03 +01:00
// run
this.run(this.initState());
2014-10-29 08:02:03 +01:00
}
]);
}
parseDns(host,c) {
const resolveStandard = (host,c) => {
dns.lookup(host, (err,address,family) => {
if(err) return this.fatal(err);
this.options.address = address;
2014-10-29 08:02:03 +01:00
c();
});
};
const resolveSrv = (srv,host,c) => {
dns.resolve(srv+'.'+host, 'SRV', (err,addresses) => {
2014-10-29 08:02:03 +01:00
if(err) return resolveStandard(host,c);
if(addresses.length >= 1) {
const line = addresses[0];
this.options.port = line.port;
const srvhost = line.name;
2014-10-29 08:02:03 +01:00
if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) {
this.options.address = srvhost;
2014-10-29 08:02:03 +01:00
c();
} else {
// resolve yet again
resolveStandard(srvhost,c);
}
return;
}
return resolveStandard(host,c);
});
};
2014-10-29 08:02:03 +01:00
if(this.srvRecord) resolveSrv(this.srvRecord,host,c);
else resolveStandard(host,c);
}
2014-10-29 08:02:03 +01:00
// utils
/** @returns {Reader} */
reader(buffer) {
2014-10-29 08:02:03 +01:00
return new Reader(this,buffer);
}
translate(obj,trans) {
for(const from of Object.keys(trans)) {
const to = trans[from];
2014-10-29 08:02:03 +01:00
if(from in obj) {
if(to) obj[to] = obj[from];
delete obj[from];
}
}
}
setTimeout(c,t) {
2014-10-29 08:02:03 +01:00
if(this.finished) return 0;
const id = setTimeout(c,t);
2014-10-29 08:02:03 +01:00
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;
2014-10-29 08:02:03 +01:00
}
return false;
}
debugBuffer(buffer) {
let out = '';
let out2 = '';
for(let i = 0; i < buffer.length; i++) {
const sliced = buffer.slice(i,i+1);
2014-10-29 08:02:03 +01:00
out += sliced.toString('hex')+' ';
let chr = sliced.toString();
2014-10-29 08:02:03 +01:00
if(chr < ' ' || chr > '~') chr = ' ';
out2 += chr+' ';
if(out.length > 60) {
console.log(out);
console.log(out2);
out = out2 = '';
}
}
console.log(out);
console.log(out2);
}
2014-10-29 08:02:03 +01:00
_tcpConnect(c) {
2014-10-29 08:02:03 +01:00
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;
2014-10-29 08:02:03 +01:00
const socket = this.tcpSocket = net.connect(port,address,() => {
if(this.debug) console.log(address+':'+port+" TCPCONNECTED");
2014-10-29 08:02:03 +01:00
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);
};
2014-10-29 08:02:03 +01:00
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');
2014-10-29 08:02:03 +01:00
});
socket.on('data', (data) => {
if(!this.tcpCallback) return;
if(this.debug) console.log(address+':'+port+" <--TCP "+data.toString('hex'));
2014-10-29 08:02:03 +01:00
received = Buffer.concat([received,data]);
if(this.tcpCallback(received)) {
clearTimeout(this.tcpTimeoutTimer);
this.tcpCallback = false;
received = Buffer.from([]);
2014-10-29 08:02:03 +01:00
}
});
}
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) => {
2014-10-29 08:02:03 +01:00
socket.write(buffer);
});
if(!ondata) return;
this.tcpTimeoutTimer = this.setTimeout(() => {
this.tcpCallback = false;
this.fatal('TCP Watchdog Timeout');
},this.options.tcpTimeout);
this.tcpCallback = ondata;
2014-10-29 08:02:03 +01:00
});
}
2014-10-29 08:02:03 +01:00
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);
2014-10-29 08:02:03 +01:00
if(!onpacket) return;
this.udpTimeoutTimer = this.setTimeout(() => {
this.udpCallback = false;
let timeout = false;
2014-10-29 08:02:03 +01:00
if(!ontimeout || ontimeout() !== true) timeout = true;
if(timeout) this.fatal('UDP Watchdog Timeout');
},this.options.udpTimeout);
this.udpCallback = onpacket;
2014-10-29 08:02:03 +01:00
});
}
_udpSendNow(buffer) {
2014-10-29 08:02:03 +01:00
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');
2014-10-29 08:02:03 +01:00
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) {
2014-10-29 08:02:03 +01:00
if(this.udpCallback) {
const result = this.udpCallback(buffer);
2014-10-29 08:02:03 +01:00
if(result === true) {
// we're done with this udp session
clearTimeout(this.udpTimeoutTimer);
this.udpCallback = false;
}
} else {
this.udpResponse(buffer);
}
}
udpResponse() {}
}
module.exports = Core;