node-gamedig/protocols/core.js

382 lines
12 KiB
JavaScript
Raw Normal View History

const EventEmitter = require('events').EventEmitter,
2017-08-09 12:32:09 +02:00
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'),
2019-01-12 11:43:36 +01:00
requestAsync = require('request-promise'),
2019-01-20 10:45:57 +01:00
Promises = require('../lib/Promises'),
Logger = require('../lib/Logger'),
DnsResolver = require('../lib/DnsResolver');
2014-10-29 08:02:03 +01:00
2019-02-14 05:46:13 +01:00
let uid = 0;
class Core extends EventEmitter {
2017-08-09 12:32:09 +02:00
constructor() {
super();
this.encoding = 'utf8';
this.byteorder = 'le';
this.delimiter = '\0';
this.srvRecord = null;
2019-01-12 11:43:36 +01:00
this.abortedPromise = null;
2019-01-20 10:45:57 +01:00
this.logger = new Logger();
this.dnsResolver = new DnsResolver(this.logger);
2017-08-09 12:32:09 +02:00
2019-01-12 11:43:36 +01:00
// Sent to us by QueryRunner
this.options = null;
2019-01-20 11:21:40 +01:00
/** @type GlobalUdpSocket */
2019-01-12 11:43:36 +01:00
this.udpSocket = null;
this.shortestRTT = 0;
this.usedTcp = false;
2017-08-09 12:32:09 +02:00
}
2019-02-14 05:46:13 +01:00
// Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe() {
2019-01-20 10:45:57 +01:00
if (this.options.debug) {
this.logger.debugEnabled = true;
}
2019-02-14 05:46:13 +01:00
this.logger.prefix = 'Q#' + (uid++);
2019-01-20 10:45:57 +01:00
2019-02-14 05:46:13 +01:00
this.logger.debug("Query is running with options:", this.options);
2017-08-09 12:32:09 +02:00
2019-01-12 11:43:36 +01:00
let abortCall = null;
this.abortedPromise = new Promise((resolve,reject) => {
abortCall = () => reject(new Error("Query is finished -- cancelling outstanding promises"));
2019-01-12 11:43:36 +01:00
});
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
this.abortedPromise.catch(() => {});
let timeout;
2019-01-07 07:52:29 +01:00
try {
2019-01-12 11:43:36 +01:00
const promise = this.runOnce();
timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt");
2019-02-14 05:46:13 +01:00
const result = await Promise.race([promise, timeout]);
this.logger.debug("Query was successful");
return result;
} catch(e) {
this.logger.debug("Query failed with error", e);
throw e;
2019-01-07 07:52:29 +01:00
} finally {
2019-01-12 11:43:36 +01:00
timeout && timeout.cancel();
try {
abortCall();
} catch(e) {
2019-02-14 05:46:13 +01:00
this.logger.debug("Error during abort cleanup: " + e.stack);
2019-01-07 07:52:29 +01:00
}
}
}
2017-08-09 12:32:09 +02:00
2019-01-07 07:52:29 +01:00
async runOnce() {
const options = this.options;
if (('host' in options) && !('address' in options)) {
2019-01-20 10:45:57 +01:00
const resolved = await this.dnsResolver.resolve(options.host, this.srvRecord);
options.address = resolved.address;
if (resolved.port) options.port = resolved.port;
2017-08-09 12:32:09 +02:00
}
2019-01-12 11:43:36 +01:00
const state = {
name: '',
map: '',
password: false,
raw: {},
maxplayers: 0,
players: [],
bots: []
};
2019-01-07 07:52:29 +01:00
await this.run(state);
2017-08-09 12:32:09 +02:00
2019-01-12 11:43:36 +01:00
// because lots of servers prefix with spaces to try to appear first
2019-01-12 12:45:09 +01:00
state.name = (state.name || '').trim();
2019-01-12 11:43:36 +01:00
2019-02-04 08:11:28 +01:00
if (typeof state.players === 'number') {
const num = state.players;
state.players = [];
2019-02-05 10:19:06 +01:00
state.raw.rcvNumPlayers = num;
if (num < 10000) {
for (let i = 0; i < num; i++) {
state.players.push({});
}
2019-02-04 08:11:28 +01:00
}
}
if (typeof state.bots === 'number') {
const num = state.bots;
state.bots = [];
2019-02-05 10:19:06 +01:00
state.raw.rcvNumBots = num;
if (num < 10000) {
for (let i = 0; i < num; i++) {
state.bots.push({});
}
2019-02-04 08:11:28 +01:00
}
}
2019-01-12 11:43:36 +01:00
if (!('connect' in state)) {
state.connect = ''
+ (state.gameHost || this.options.host || this.options.address)
+ ':'
+ (state.gamePort || this.options.port)
}
state.ping = this.shortestRTT;
2019-01-12 11:43:36 +01:00
delete state.gameHost;
delete state.gamePort;
this.logger.debug(log => {
log("Size of players array: " + state.players.length);
log("Size of bots array: " + state.bots.length);
});
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) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt(param) {
if (param.then) {
const start = Date.now();
param.then(() => {
const end = Date.now();
const rtt = end - start;
this.registerRtt(rtt);
}).catch(() => {});
} else {
this.debugLog("Registered RTT: " + param + "ms");
if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param;
}
}
}
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-12 11:43:36 +01:00
assertValidPort(port) {
if (!port || port < 1 || port > 65535) {
throw new Error("Invalid tcp/ip port: " + port);
}
}
2019-01-07 07:52:29 +01:00
/**
2019-01-10 13:03:07 +01:00
* @template T
* @param {function(NodeJS.Socket):Promise<T>} fn
* @param {number=} port
2019-01-10 13:03:07 +01:00
* @returns {Promise<T>}
2019-01-07 07:52:29 +01:00
*/
2019-01-12 12:45:09 +01:00
async withTcp(fn, port) {
this.usedTcp = true;
2017-08-09 12:32:09 +02:00
const address = this.options.address;
2019-01-12 12:45:09 +01:00
if (!port) port = this.options.port;
2019-01-12 11:43:36 +01:00
this.assertValidPort(port);
2017-08-09 12:32:09 +02:00
2019-01-12 11:43:36 +01:00
let socket, connectionTimeout;
try {
socket = net.connect(port,address);
socket.setNoDelay(true);
// Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {});
2019-01-12 11:43:36 +01:00
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);
};
2019-01-20 11:21:40 +01:00
socket.on('error', e => log('TCP Error:', e));
2019-01-12 11:43:36 +01:00
socket.on('close', () => log('TCP Closed'));
socket.on('data', (data) => {
2019-01-09 12:50:30 +01:00
log(address+':'+port+" <--TCP");
log(data);
2019-01-12 11:43:36 +01:00
});
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
2019-01-07 07:52:29 +01:00
});
2017-08-09 12:32:09 +02:00
2019-01-12 11:43:36 +01:00
const connectionPromise = new Promise((resolve,reject) => {
socket.on('ready', resolve);
socket.on('close', () => reject(new Error('TCP Connection Refused')));
});
this.registerRtt(connectionPromise);
2019-01-12 11:43:36 +01:00
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening');
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
]);
2019-01-07 07:52:29 +01:00
return await fn(socket);
} finally {
2019-01-12 11:43:36 +01:00
socket && socket.destroy();
connectionTimeout && connectionTimeout.cancel();
2019-01-07 07:52:29 +01:00
}
2017-08-09 12:32:09 +02:00
}
2014-10-29 08:02:03 +01:00
2019-01-07 07:52:29 +01:00
/**
2019-01-10 13:03:07 +01:00
* @template T
* @param {NodeJS.Socket} socket
2019-01-10 13:03:07 +01:00
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
2019-01-07 07:52:29 +01:00
*/
async tcpSend(socket,buffer,ondata) {
2019-01-12 11:43:36 +01:00
let timeout;
try {
const promise = new Promise(async (resolve, reject) => {
2019-01-07 07:52:29 +01:00
let received = Buffer.from([]);
const onData = (data) => {
received = Buffer.concat([received, data]);
const result = ondata(received);
if (result !== undefined) {
socket.removeListener('data', onData);
2019-01-07 07:52:29 +01:00
resolve(result);
}
};
socket.on('data', onData);
socket.write(buffer);
2019-01-12 11:43:36 +01:00
});
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP');
return await Promise.race([promise, timeout, this.abortedPromise]);
2019-01-07 07:52:29 +01:00
} finally {
2019-01-12 11:43:36 +01:00
timeout && timeout.cancel();
2019-01-07 07:52:29 +01:00
}
}
2017-08-09 12:32:09 +02:00
2019-01-07 07:52:29 +01:00
/**
* @param {Buffer|string} buffer
2019-01-14 06:54:36 +01:00
* @param {function(Buffer):T=} onPacket
2019-01-07 07:52:29 +01:00
* @param {(function():T)=} onTimeout
* @returns Promise<T>
* @template T
*/
async udpSend(buffer,onPacket,onTimeout) {
2019-01-12 11:43:36 +01:00
const address = this.options.address;
const port = this.options.port;
this.assertValidPort(port);
2019-01-07 07:52:29 +01:00
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
2019-01-09 12:50:30 +01:00
this.debugLog(log => {
2019-01-12 11:43:36 +01:00
log(address+':'+port+" UDP-->");
2019-01-09 12:50:30 +01:00
log(HexUtil.debugDump(buffer));
});
2019-01-07 07:52:29 +01:00
2019-01-12 11:43:36 +01:00
const socket = this.udpSocket;
socket.send(buffer, address, port);
2019-01-07 07:52:29 +01:00
2019-01-14 06:54:36 +01:00
if (!onPacket && !onTimeout) {
return null;
}
2019-01-12 11:43:36 +01:00
let socketCallback;
let timeout;
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now();
let end = null;
2019-01-12 11:43:36 +01:00
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return;
if (fromPort !== port) return;
if (end === null) {
end = Date.now();
const rtt = end-start;
this.registerRtt(rtt);
}
2019-01-12 11:43:36 +01:00
const result = onPacket(buffer);
2019-01-07 07:52:29 +01:00
if (result !== undefined) {
2019-01-12 11:43:36 +01:00
this.debugLog("UDP send finished by callback");
2019-01-07 07:52:29 +01:00
resolve(result);
}
2019-01-12 11:43:36 +01:00
} catch(e) {
reject(e);
2019-01-07 07:52:29 +01:00
}
};
socket.addCallback(socketCallback, this.options.debug);
2019-01-07 07:52:29 +01:00
});
2019-01-12 11:43:36 +01:00
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP');
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.debugLog("UDP timeout detected");
if (onTimeout) {
try {
const result = onTimeout();
if (result !== undefined) {
this.debugLog("UDP timeout resolved by callback");
resolve(result);
return;
}
} catch(e) {
reject(e);
}
}
reject(e);
});
});
return await Promise.race([promise, wrappedTimeout, this.abortedPromise]);
} finally {
timeout && timeout.cancel();
socketCallback && socket.removeCallback(socketCallback);
}
2017-08-09 12:32:09 +02:00
}
2019-01-09 12:35:11 +01:00
2019-01-12 11:43:36 +01:00
async request(params) {
// If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it.
// This will give us a much more accurate RTT than using the rtt of the http request.
if (!this.usedTcp) {
await this.withTcp(() => {});
}
2019-01-12 11:43:36 +01:00
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))
2019-01-12 12:45:09 +01:00
.catch(() => {});
2019-01-12 11:43:36 +01:00
});
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode);
return response.body;
});
2019-01-12 11:43:36 +01:00
return await Promise.race([wrappedPromise, this.abortedPromise]);
} finally {
requestPromise && requestPromise.cancel();
}
2019-01-09 12:35:11 +01:00
}
2019-01-09 12:50:30 +01:00
2019-01-20 10:45:57 +01:00
/** @deprecated */
2019-01-09 12:50:30 +01:00
debugLog(...args) {
2019-01-20 10:45:57 +01:00
this.logger.debug(...args);
2019-01-09 12:50:30 +01:00
}
}
module.exports = Core;