mirror of
https://codeberg.org/spire/webrcon.git
synced 2026-06-03 09:14:06 +02:00
initial jsclient
This commit is contained in:
parent
4e4fce92e5
commit
d921430bd9
10 changed files with 319 additions and 0 deletions
3
.babelrc
Normal file
3
.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"stage": 0
|
||||||
|
}
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
test.js
|
||||||
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "webrcon",
|
||||||
|
"description": "Create rcon connections using websockets",
|
||||||
|
"author": "Robin Appelman <robin@icewind.nl>",
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "src/rcon.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spiretf/webrcon"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "^3.0.5",
|
||||||
|
"ws": "^0.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/command/changelevel.js
Normal file
11
src/command/changelevel.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default class ChangeLevel {
|
||||||
|
constructor (level) {
|
||||||
|
// force response
|
||||||
|
this.command = 'changelevel ' + level + '; echo ' + level;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler () {
|
||||||
|
return this.level;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/command/getsvar.js
Normal file
16
src/command/getsvar.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default class GetSVar {
|
||||||
|
constructor (name) {
|
||||||
|
this.command = name;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler (response) {
|
||||||
|
var regex = '"' + this.name + '" = "([^"]*)"';
|
||||||
|
var matches = response.match(new RegExp(regex));
|
||||||
|
if (matches) {
|
||||||
|
return matches[1];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/command/setsvar.js
Normal file
8
src/command/setsvar.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import GetSVar from './getsvar.js'
|
||||||
|
|
||||||
|
export default class SetSVar extends GetSVar {
|
||||||
|
constructor (name, value) {
|
||||||
|
super();
|
||||||
|
this.command = name + " " + value + "; " + (this.command) ? this.command : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/command/status.js
Normal file
58
src/command/status.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
export default class Status {
|
||||||
|
command = 'status';
|
||||||
|
|
||||||
|
handler (response) {
|
||||||
|
var status = {
|
||||||
|
name: '',
|
||||||
|
map: '',
|
||||||
|
players: []
|
||||||
|
};
|
||||||
|
var playerIds = {};
|
||||||
|
var lines = response.split("\n");
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
let parts;
|
||||||
|
var line = lines[i].trim();
|
||||||
|
if (line[0] !== '#' && line.indexOf(':')) {
|
||||||
|
parts = line.split(':');
|
||||||
|
parts = parts.map((part)=> {
|
||||||
|
return part.trim()
|
||||||
|
});
|
||||||
|
if (parts[0] === 'hostname') {
|
||||||
|
status.name = parts[1];
|
||||||
|
} else if (parts[0] === 'map') {
|
||||||
|
status.map = parts[1].substr(0, parts[1].indexOf(' '));
|
||||||
|
}
|
||||||
|
} else if (line[0] === '#' && line.substr(0, 8) !== '# userid') {
|
||||||
|
var playerLine = line.substr(2).trim();
|
||||||
|
parts = playerLine.split('"');
|
||||||
|
parts = parts.map((part)=> {
|
||||||
|
return part.trim()
|
||||||
|
});
|
||||||
|
var id = parseInt(parts[0], 10);
|
||||||
|
if (playerIds[id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
playerIds[id] = true;
|
||||||
|
var name = parts[1];
|
||||||
|
if (!parts[2]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parts = parts[2].replace(/\s+/g, ' ').split(' ');
|
||||||
|
var steamId = parts[0];
|
||||||
|
var player = {
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
steamId: steamId
|
||||||
|
};
|
||||||
|
if (parts.length > 2 && steamId !== 'BOT') {
|
||||||
|
player.ping = parseInt(parts[2], 10);
|
||||||
|
player.ip = parts[5];
|
||||||
|
var timeParts = parts[1].split(':');
|
||||||
|
player.connected = parseInt(timeParts[0], 10) * 60 + parseInt(timeParts[1], 10);
|
||||||
|
}
|
||||||
|
status.players.push(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/connection.js
Normal file
109
src/connection.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import Promise from 'bluebird';
|
||||||
|
import WebRcon from './webrcon.js';
|
||||||
|
|
||||||
|
export default class Connection {
|
||||||
|
connectPromise = null;
|
||||||
|
messageHandlers = [];
|
||||||
|
receivedTimer = 0;
|
||||||
|
receivedBuffer = '';
|
||||||
|
errorCount = 0;
|
||||||
|
|
||||||
|
constructor (host, password) {
|
||||||
|
this.host = host;
|
||||||
|
this.password = password;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRcon () {
|
||||||
|
if (this.rcon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rcon = new WebRcon(this.host, this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
this.buildRcon();
|
||||||
|
this.connected = false;
|
||||||
|
this.connectPromise = null;
|
||||||
|
this.rcon.on('response', (response) => {
|
||||||
|
clearTimeout(this.receivedTimer);
|
||||||
|
this.receivedBuffer += response;
|
||||||
|
// buffer the response
|
||||||
|
this.receivedTimer = setTimeout(() => {
|
||||||
|
var response = this.receivedBuffer;
|
||||||
|
this.receivedBuffer = '';
|
||||||
|
if (this.messageHandlers.length) {
|
||||||
|
while (this.messageHandlers.length > 0) {
|
||||||
|
var handler = this.messageHandlers.shift();
|
||||||
|
handler(response);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('unhandled response:');
|
||||||
|
console.log("'" + response + "'");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
this.rcon.on('error', (e) => {
|
||||||
|
this.errorCount++;
|
||||||
|
console.log('failed to connect ' + this.errorCount + ' times (' + e + ')');
|
||||||
|
Promise.delay(2500).then(() => {
|
||||||
|
if (this.connectPromise) {
|
||||||
|
this.rcon.connect();
|
||||||
|
} else {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.rcon.on('end', () => {
|
||||||
|
console.log('lost rcon');
|
||||||
|
this.connectPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
if (!this.connectPromise) {
|
||||||
|
this.connectPromise = new Promise((resolve) => {
|
||||||
|
this.rcon.on('connect', () => {
|
||||||
|
resolve(null);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
this.rcon.connect();
|
||||||
|
}
|
||||||
|
return this.connectPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendString (command) {
|
||||||
|
await this.connect();
|
||||||
|
// give existing command a chance to finish
|
||||||
|
await this.waitFor(() => {
|
||||||
|
return this.messageHandlers.length === 0;
|
||||||
|
}, 500);
|
||||||
|
var promise = new Promise((resolve) => {
|
||||||
|
this.messageHandlers.push(resolve);
|
||||||
|
});
|
||||||
|
this.rcon.send(command);
|
||||||
|
return await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send (command) {
|
||||||
|
var response = await this.sendString(command.command);
|
||||||
|
return command.handler(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect () {
|
||||||
|
this.rcon.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitFor (cb, timeout) {
|
||||||
|
var waited = 0;
|
||||||
|
while (waited < timeout) {
|
||||||
|
if (cb()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await Promise.delay(250);
|
||||||
|
waited += 250;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/rcon.js
Normal file
31
src/rcon.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Connection from './connection';
|
||||||
|
import GetSVar from './command/getsvar';
|
||||||
|
import SetSVar from './command/setsvar';
|
||||||
|
import ChangeLevel from './command/changelevel';
|
||||||
|
import Status from './command/status';
|
||||||
|
|
||||||
|
export default class Rcon {
|
||||||
|
constructor (host, password) {
|
||||||
|
this.connection = new Connection(host, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendString (command) {
|
||||||
|
return this.connection.sendString(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
status () {
|
||||||
|
return this.connection.send(new Status());
|
||||||
|
}
|
||||||
|
|
||||||
|
getSVar (name) {
|
||||||
|
return this.connection.send(new GetSVar(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
setSVar (name, value) {
|
||||||
|
return this.connection.send(new SetSVar(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLevel (level) {
|
||||||
|
return this.connection.send(new ChangeLevel(level));
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/webrcon.js
Normal file
64
src/webrcon.js
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
export default class WebRcon extends EventEmitter {
|
||||||
|
constructor (host, password, port = 27021) {
|
||||||
|
super();
|
||||||
|
this.host = host;
|
||||||
|
this.password = password;
|
||||||
|
this.port = port;
|
||||||
|
this.socket = null;
|
||||||
|
this.queue = [];
|
||||||
|
this.authenticated = false;
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
if (this.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.socket = new WebSocket('ws://' + this.host + ':' + this.port, 'foo');
|
||||||
|
this.socket.onopen = () => {
|
||||||
|
if (this.socket.readyState === 1) {
|
||||||
|
this.socket.send(this.password);
|
||||||
|
this.connected = true;
|
||||||
|
this.emit('connect');
|
||||||
|
setTimeout(() => {
|
||||||
|
for (var message of this.queue) {
|
||||||
|
this.socket.send(message);
|
||||||
|
}
|
||||||
|
this.queue = [];
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.socket.onmessage = (data) => {
|
||||||
|
if (data.data === 'authenticated') {
|
||||||
|
this.authenticated = true;
|
||||||
|
} else {
|
||||||
|
if (!this.authenticated) {
|
||||||
|
this.emit('error', 'not authenticated');
|
||||||
|
}
|
||||||
|
this.emit('response', data.data)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.socket.onerror = (error) => {
|
||||||
|
this.emit('error', error);
|
||||||
|
};
|
||||||
|
this.socket.onclose = () => {
|
||||||
|
this.emit('close');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
send (string) {
|
||||||
|
this.connect();
|
||||||
|
if (this.socket.readyState !== 1) {
|
||||||
|
this.queue.push(string);
|
||||||
|
} else {
|
||||||
|
this.socket.send(string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect () {
|
||||||
|
this.socket.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue