Skip to content

Instantly share code, notes, and snippets.

@Darkhogg
Created December 30, 2017 15:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Darkhogg/0cda635462eaa3de07c1da6ffcb3be19 to your computer and use it in GitHub Desktop.
Save Darkhogg/0cda635462eaa3de07c1da6ffcb3be19 to your computer and use it in GitHub Desktop.
Factorio RCON tests
const net = require('net');
function connect () {
return new Promise((accept, reject) => {
const socket = net.createConnection(27015, '127.0.0.1');
socket.on('connect', () => accept(socket));
socket.on('error', (err) => reject(err));
});
}
function disconnect (socket) {
return new Promise((accept, reject) => {
socket.end();
socket.on('end', () => accept());
socket.on('error', () => reject());
})
}
function buildMessage (id, type, data) {
const length = data.length;
const buf = Buffer.alloc(length + 14)
buf.writeInt32LE(length + 10, 0);
buf.writeInt32LE(id, 4);
buf.writeInt32LE(type, 8);
buf.write(data, 12, 'utf-8');
buf.writeInt16LE(0, length + 12);
return buf;
}
function buildAuth (id, password) {
return buildMessage(id, 3, password);
}
function buildCommand (id, command) {
return buildMessage(id, 2, command);
}
function collectResponses (socket, timeout=1000) {
return new Promise((accept, reject) => {
socket.on('end', () => accept(collector));
socket.on('close', () => reject(new Error('socket closed')));
socket.on('error', (err) => reject(err));
setTimeout(() => {socket.destroy(); reject(new Error('timeout'))}, timeout)
const collector = [];
let oldData = null;
const onData = (data) => {
const allData = oldData ? Buffer.concat(oldData, data) : data;
const dataLength = (allData.length < 4) ? Infinity : allData.readInt32LE(0);
if (dataLength + 4 > allData.length) {
oldData = allData;
return;
}
oldData = null;
const dataId = allData.readInt32LE(4);
const dataType = allData.readInt32LE(8);
const dataStr = allData.toString('utf8', 12, 12 + dataLength - 10);
collector.push({'id': dataId, 'type': dataType, 'data': dataStr});
if (allData.length > dataLength + 4) {
onData(allData.slice(dataLength + 4));
}
};
socket.on('data', onData);
});
}
function sendThenExpect (packetGroups, timeout = 100, expected = null) {
return connect().then((socket) => {
let messagesSent = 0;
const collectProm = collectResponses(socket, timeout + 50);
let sendProm = Promise.resolve();
for (const group of packetGroups) {
messagesSent += group.length;
sendProm = sendProm.then(() => {
socket.write(Buffer.concat(group));
return new Promise((accept, reject) => setTimeout(accept, 1));
});
}
const expectedResponses = expected || messagesSent
setTimeout(() => disconnect(socket), timeout);
return sendProm.then(() => collectProm).then(result => {
return {
'expected': expectedResponses,
'responded': result.length,
'passes': expectedResponses == result.length
};
}).catch(err => ({
'expected': expectedResponses,
'responded': 'E',
'passes': false
}));
});
}
function makeTest (text, packets, expected = null) {
return function () {
return sendThenExpect(packets, 300, expected)
.then(result => {
console.log(' [%s] (%s/%s) %s', result.passes ? ' OK ' : 'FAIL', result.responded, result.expected, text);
return result.passes;
});
};
}
const TESTS = [
makeTest('should authenticate', [
[buildAuth(0, 'rcon')],
]),
makeTest('should respond to single command', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed')],
]),
makeTest('should respond to multiple separate commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed')],
[buildCommand(2, '/seed')],
]),
makeTest('should respond to multiple combined commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed'), buildCommand(2, '/seed')],
]),
makeTest('should respond to commands after grouped commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed'), buildCommand(2, '/seed')],
[buildCommand(3, '/seed')],
]),
makeTest('should respond to split commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed').slice(0, 10)],
[buildCommand(1, '/seed').slice(10)],
], 2),
makeTest('should respond to commands after split commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed').slice(0, 10)],
[buildCommand(1, '/seed').slice(10)],
[buildCommand(2, '/seed')],
], 3),
makeTest('should respond to commands grouped before split commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed'), buildCommand(2, '/seed').slice(0, 10)],
[buildCommand(2, '/seed').slice(10)],
], 3),
makeTest('should respond to commands grouped after split commands', [
[buildAuth(0, 'rcon')],
[buildCommand(1, '/seed').slice(0, 10)],
[buildCommand(1, '/seed').slice(10), buildCommand(2, '/seed')],
], 3),
];
function main () {
let lastPromise = Promise.resolve();
for (const test of TESTS) {
lastPromise = lastPromise.then(() => test());
}
return lastPromise;
}
main().catch((err) => console.error(err.stack || err));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment