Skip to content

Commit

Permalink
Merge pull request #21 from Taraman17/dev
Browse files Browse the repository at this point in the history
Make the API more RESTful, add versioning and fix various issues
  • Loading branch information
Taraman17 authored Nov 17, 2020
2 parents d9c4fe1 + ef1b071 commit 5bc857b
Show file tree
Hide file tree
Showing 5 changed files with 825 additions and 228 deletions.
67 changes: 44 additions & 23 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ The use of this software is at your own risk.
It exposes control of your server and shell functions to the internet. Although I did everything to secure the API, any bugs may lead to security breaches on your server.
I strongly adivise to use secure connections to prevent possible man-in-the-middle attacks.

## NOTE:
This Release (v0.8) changes the API end Points to facilitate versioning of the API.
The old Endpoints will be available for some time to allow for a transition time. I expect to remove them with Version 1.0.

### Breaking changes:
- mapchange completed reporting via websocket will be changed to the standard command status reporting. See below for details.
- control?action=status is deprecated. use /control/runningstatus & /control/rconauthstatus instead.

## Install
download the script files to and install the dependencies for nodejs
```console
npm install --save rcon-srcds srcds-log-receiver express express-session cors passport passport-steam node-pty ws
npm install --save rcon-srcds srcds-log-receiver express express-session cors passport passport-steam node-pty ws winston winston-daily-rotate-file
```

## Configuration
The CS:GO Server must be configured to send logs to the local IP (not 127.0.0.1): on port 9871
```
log on
sv_logecho 1
mp_logfile 1
mp_logdetail 3
mp_logmessages 1
Expand Down Expand Up @@ -45,65 +54,76 @@ For better readability, $.get() is used in the following examples

### Login / Logout
```javascript
$.get('http://<your-servers-address>:<your-port>/login')
$.get('http://<your-servers-address>:<your-port>/logout')
$.get('http://<your-servers-address>:<your-port>/loginStatus')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/login')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/logout')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/loginStatus')
```

For Authentication the API redirects to the Steam login page by calling '/login'
After authentication there, it will return to '/loginStatus' by default, returning { "login": true/false }.
For Authentication the API redirects to the Steam login page by calling '/csgoapi/v1.0/login'
After authentication there, it will return to '/csgoapi/v1.0/loginStatus' by default, returning { "login": true/false }.
If you use the API in a web interface, you can set 'redirectPage' in the config to your startPage (e.g. http://your-webserver/index.html) This way, you can call up the login page and then be returned to your web application after you got the session cookie in your browser.

If you want to have a manual logout in your client, call '/logout', which will redirect to '/loginStatus' to confirm the success.
If you want to have a manual logout in your client, call '/csgoapi/v1.0/logout', which will redirect to '/csgoapi/v1.0/loginStatus' to confirm the success.

### Server Control
```javascript
$.get('http://<your-servers-address>:<your-port>/control', 'action=<anAction>')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/control/<anAction>')
```
The /control message will return a JSON-String.
'action' can have the following values:
- status -> fetch the servers running status: { "running": true/false }
- update -> update the server (may take quite some time): { "success": true/false }
- start -> optional additional argument "starmap" (action=start&startmap=mapname): { "success": true/false }
If run without startmap, server will be started with famous de_dust2.
- start -> optional additional argument "startmap" (&startmap=mapname): { "success": true/false }
If run without startmap, server will be started with de_dust2.
- stop -> stop the server with RCON 'quit': { "success": true/false }
- kill -> use 'kill' command to shutdown the server, if RCON connection is not working: { "success": true/false }
- changemap -> additional argument "map" (action=changemap&map=mapname):
If you do not use websockets, the answer will be { "success": true/false }. true if start of new map is logged, false in case of error or after a timeout of 30 seconds.
If you use websockets, answer will be { "success": true/false } if the rcon command to server was sauccessful or not. Completion of mapchange is sent via the websocket in the format:
```javascript
{ "type": "mapchange", "payload": { "success": true/false } }
```
- reloadmaplist -> reload the available maps on the server (action=reloadmaplist): { "success": true/false }
- stop -> stop the server: { "success": true/false }

If you do not use websockets, the answer will be sent after completion of the operation.
If you use websockets, answer will be sent right away. Progress and/or completion messages are sent via the websocket. See below for format. Exception is server start, since RCON-authentication is a vital step for the api, 'start' will always return "success" only after authentication finished.

### RCON
```javascript
$.get('http://<your-servers-address>:<your-port>/rcon', 'message=command')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/rcon', 'message=command')
```
'command' is the command as you would enter it in the servers console.
Answer is the response as string.

### Server Info
```javascript
$.get('http://<your-servers-address>:<your-port>/serverInfo')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/info/serverInfo')
```
Gets information on the server as JSON-String. See serverInfo.js for available data.

```javascript
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/info/runstatus')
$.get('http://<your-servers-address>:<your-port>/csgoapi/v1.0/info/rconauthstatus')
```
Query if the server is running or also authenticated for rcon requests. Answer is deleayed if a status change is in progress.

## Information Updates via WebSocket
A websocket is available a configurable port to receive further information.
Currently avialable are update-progress, ServerInfo as any value changes and completion of mapchange (on start of new map).
A websocket is available on a configurable port to receive further information.
Currently avialable are ServerInfo as any value changes, start/stop/fail of commands, update-progress and completion of mapchange (on start of new map).

ServerInfo message looks as follows:
```javascript
{ "type": "serverInfo", "payload": ${JSON.stringify(serverInfo.getAll())}
{ "type": "serverInfo", "payload": {ServerInfoObject}
```
For now, Serverinfo is always sent completely
Start/stop of a command:
```javascript
{ "type": "commandstatus", "payload": { "operation": <string>, "state": <"start"/"stop"/"fail"> } }
```
Operation can be one of the following: start, stop, update, mapchange
UpdateProgress looks as follows:
```javascript
{ "type": "updateProgress", "payload": { "step": <string>, "progress": <int> } }
```
mapchange message:
mapchange message (deprecated, do not use anymore):
```javascript
{ "type": "mapchange", "payload": { "success": true/false }
```
Expand All @@ -112,7 +132,8 @@ false is sent after a 30 sec. timeout when no "Started map" log has been receive
## Example
An example of a webinterface is available in the folder "example"
It assumes, that you run it on the same domain as the API. If you want to change that, you can do so in js/gameserver.js (see comment in the file)
To correctly load the maplist for server start, edit the respective line in gameserver.php.
To correctly load the maplist for server start, edit the respective line at the beginning of gameserver.js.
The Webinterface works with and without websockets.
## Support
If you have any questions, contact me.
Expand Down
2 changes: 1 addition & 1 deletion example/gameserver.htm
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
type="button"
class="text"
value="Update"
onclick="doUpdate(this);"/>
onclick="clickButton(this);"/>
<input id="buttonStart"
type="button"
class="text"
Expand Down
149 changes: 91 additions & 58 deletions example/js/gameserver.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
var address;
// Change here if you don't host the webInterfae on the same host as the NodeJS API
var host = window.location.hostname;
var address =`https://${host}:8090/csgoapi/v1.0`;
var maplistFile = './maplist.txt';

// Titles for throbber window.
var titles = {
'start': 'Starting server',
'stop': 'Stopping server',
'auth': 'Authenticating RCON',
'update': 'Updating server',
'mapchange': 'Changing map'
}
var running = false;
var authenticated = false;
try {
var socket = new WebSocket(`wss://${host}:8091`);
} catch (err) {
console.error('Connection to websocket failed:\n' + err);
}

// Redirect to login page.
function doLogin() {
window.location.href = `${address}/login`;
}

// Sends a get Request with the headers needed for authentication with the seesion cookie.
function sendGet(address, data, callback) {
return $.ajax({
type: "GET",
Expand All @@ -20,17 +41,10 @@ function sendGet(address, data, callback) {

// what to do after document is loaded.
$( document ).ready(() => {
// Change here if you don't host the webInterfae on the same host as the NodeJS API
let ip = window.location.hostname;
address = `https://${ip}:8090`;

loadMaplist();
setupPage();

var socket = new WebSocket(`wss://${ip}:8091`);
socket.onopen = () => {
socket.send('infoRequest');
}

socket.onmessage = (e) => {
let data = JSON.parse(e.data);

Expand All @@ -53,40 +67,47 @@ $( document ).ready(() => {
$(`#${player.team.toLowerCase()}Players`).show(0);
}
}
if ($('#mapList li').length < 1) {
let maplist = data.mapsAvail;
$("#mapList").empty();
for (map of maplist) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(map));
$("#mapList").append(li);
if ($('#mapList li').length < 2) {
if (serverInfo.mapsAvail) {
let maplist = serverInfo.mapsAvail;
$("#mapList").empty();
for (map of maplist) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(map));
$("#mapList").append(li);
}
}
}
} else if (data.type == "updateProgress") {
$('#popupText').html(`${data.payload.step}: ${data.payload.progress}%`);
if (data.payload.step == 'Update Successful!') {
} else if (data.type == "commandstatus") {
if (data.payload.state == 'start') {
$('#popupCaption').text(`${titles[data.payload.operation]}`);
$('#popupText').text('Moment bitte!');
$('.container-popup').css('display', 'flex');
} else if (data.payload.state == 'end' && data.payload.operation != 'start') {
window.setTimeout( () => {
$('.container-popup').css('display', 'none');
}, 1500);
setupPage();
}
} else if (data.type == "progress") {
$('#popupText').html(`${data.payload.step}: ${data.payload.progress}%`);
} else if (data.type == "mapchange") {
if (data.payload.success) {
setupPage();
if (data.payload.success$ && ('#popupCaption').text() == 'Changing Map') {
socket.send('infoRequest');
$('.container-popup').css('display', 'none');
} else {
} else if (!data.payload.success) {
$('#popupText').html(`Mapchange failed!`);
window.setTimeout( () => {
$('.container-popup').css('display', 'none');
}, 2000);
}
}
}
loadMaplist();
setupPage();
});

// Load the maplist for serverstart from maplist.txt
function loadMaplist() {
// The Maplist file can be taken from the csgo folder.
$.get('./maplist.txt', (data) => {
$.get(maplistFile, (data) => {
let lines = data.split(/\r\n|\n/);
lines.forEach( (map) => {
$("#mapAuswahl").append(`<option value="${map}">${map}</option>`);
Expand All @@ -97,22 +118,30 @@ function loadMaplist() {
// Setup the Elements according to server status.
function setupPage() {
$('#popupCaption').text('Querying Server');
function loggedIn() {
return Promise.resolve(sendGet(`${address}/loginStatus`));
getPromise = (path) => {
return Promise.resolve(sendGet(`${address}/${path}`));
}

let loginCheck = loggedIn();
let loginCheck = getPromise('loginStatus');
loginCheck.then((data) => {
if (data.login) {
function running() {
return Promise.resolve(sendGet(`${address}/control`, `action=status`));
}
let serverRunning = running();
serverRunning.then((data) => {
if (data.running) {
let authenticated = getPromise('info/rconauthstatus');
authenticated.then((data) => {
if (data.rconauth) {
setupServerRunning();
} else {
setupServerStopped();
let serverRunning = getPromise('info/runstatus');
serverRunning.then((data) => {
if (data.running) {
if (confirm('Server Running, but RCON not authenticated.\n\nTry to authenticate again?')) {
sendGet(`${address}/info/authenticate`).done((data) => {

});
}
} else {
setupServerStopped();
}
});
}
}).catch((error) => {
setupServerStopped();
Expand All @@ -139,7 +168,11 @@ function setupNotLoggedIn() {
}
function setupServerRunning() {
$('#power-image').attr('src', 'pic/power-on.png');
getMaps();
if (socket.readyState != 1) { // if websocket not connected
getMaps();
} else if ($("#mapList li").length < 2) {
socket.send('infoRequest');
}
$('#startMap').hide(0);
$('#mapList').on( 'click', showPlay);
$('#mapList').on( 'dblclick', changeMap);
Expand All @@ -162,30 +195,30 @@ function setupServerStopped() {
$('#mapSelector').hide('fast');
}

function doUpdate(aButton) {
action = aButton.value.toLowerCase();
$('#popupCaption').text(`Updating Server`);
$('#popupText').text('Moment bitte!');
$('.container-popup').css('display', 'flex');

sendGet(`${address}/control`, `action=update`, ( data ) => {
if(!data.success) {
alert('command' + action + ' failed!');
}
});
}

function clickButton(aButton) {
action = aButton.value.toLowerCase();
$('#popupCaption').text(`${action}ing Server`);
$('#popupCaption').text(`${titles[action]}`);
$('#popupText').text('Moment bitte!');
$('.container-popup').css('display', 'flex');
startMap = document.getElementById('mapAuswahl').value;

sendGet(`${address}/control`, `action=${action}&startmap=${startMap}`, ( data ) => {
setupPage();
if(!data.success) {
alert('command' + action + ' failed!');
sendGet(`${address}/control/${action}`, `startmap=${startMap}`).done(( data ) => {
if (socket.readyState != 1) { // if websocket not connected
if (action != 'update') {
setupPage();
}
$('.container-popup').css('display', 'none');
}
}).fail((err) => {
let errorText = err.responseJSON.error;
if (errorText.indexOf('Another Operation is pending:') != -1) {
let operation = errorText.split(':')[1];
alert(`${operation} running.\nTry again in a moment.`);
} else {
alert(`command ${action} failed!\nError: ${errorText}`);
}
if (socket.readyState != 1) {
$('.container-popup').css('display', 'none');
}
});
}
Expand Down Expand Up @@ -215,7 +248,7 @@ function movePlayer(event) {

function getMaps() {
function getServerInfo() {
return Promise.resolve(sendGet(`${address}/serverInfo`));
return Promise.resolve(sendGet(`${address}/info/serverInfo`));
}
let serverInfo = getServerInfo();
serverInfo.then((data) => {
Expand Down Expand Up @@ -249,9 +282,9 @@ function showPlay(event) {
function changeMap(event) {
let map = event.target.innerText;
$('#mapSelector').hide('fast');
$('#popupCaption').text('Changing Map');
$('#popupCaption').text(titles['mapchange']);
$('.container-popup').css('display', 'flex');
sendGet(`${address}/control`, `action=changemap&map=${map}`, (data) => {
sendGet(`${address}/control/changemap`, `map=${map}`, (data) => {
if (data.success) {
$('#popupText').html(`Changing map to ${map}`);
} else {
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5bc857b

Please sign in to comment.