Skip to content

Commit

Permalink
Merge pull request #40 from Taraman17/dev
Browse files Browse the repository at this point in the history
Prepare v2.0.0-beta.4
  • Loading branch information
Taraman17 authored Feb 26, 2024
2 parents e0e4deb + fdd7110 commit 8556388
Show file tree
Hide file tree
Showing 18 changed files with 1,453 additions and 1,225 deletions.
24 changes: 24 additions & 0 deletions MAPS.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Technical background on map handling

## Read available maps
In the new CS2 dedicated server, there is no single function to list all maps available.
The internal maps are accessible as before with the ```maps *``` command. However there are lots of entries in the answer that are not real maps.
Workshop maps from a hosted collection are available by calling ```ds_workshop_listmaps```. This only lists the filenames of the maps and no workshop id. Unfortunately, the filenames are not available in the stamAPI, so there is currently no way to match the output fo ```ds_workshop_listmaps``` to preview pictures from the steamAPI.
For the Official maps the filenames are available in the API.

Therefore I decided to work with a static list for the official maps and get the workshop maps of a collection directly via the steamAPI.

```ds_workshop_listmaps``` is only a backup in case the API is not reachable for some reason.

## Change maps
To change a map, there are also different commands whether it's a built in or a workshop map.

- ```map <mapfilename>``` is used to change level to a built in map.
- ```ds_workshop_changelevel <mapfilename>``` is used to change level to a map present in the hosted workshop collection.
- ```host_workshop_map``` ist used to change to any workshop map.

This cs2-api takes either the map-filename, the workshop-id or the title from the workshop details and matches it to a map-details object. Depending if it's an official of a workshop map, the respective command is called. If the workshop-id is not available for a workshop map, ```ds_workshop_changelevel``` is used.

For that reason, a workshop collection id has to be set in the config if workshop maps are to be used.

Alternatively one could call ```host_workshop_map``` via rcon command using the ```/rcon``` endpoint.
82 changes: 37 additions & 45 deletions OfficialMaps.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
{
"ar_baggage": 125440026,
"ar_monastery": 125440154,
"ar_shoots": 125440261,
"cs_agency": 1464919827,
"cs_assault": 125432575,
"de_boyard": 2926953717,
"de_chalice": 2926952684,
"cs_climb": 2537983994,
"cs_insertion2": 2650330155,
"cs_italy": 125436057,
"cs_militia": 133256570,
"cs_office": 125444404,
"de_ancient": 2627571649,
"de_anubis": 1984883124,
"de_crete": 1220681096,
"de_bank": 125440342,
"de_basalt": 2627569615,
"de_blagai": 2791116183,
"de_breach": 1258599704,
"de_cache": 2606407435,
"de_canals": 951287718,
"de_cbble": 205239595,
"de_dust2": 125438255,
"de_extraction": 2650340943,
"de_hive": 2539316567,
"de_inferno": 125438669,
"de_iris": 1591780701,
"de_lake": 125440557,
"de_mirage": 152508932,
"de_nuke": 125439125,
"de_overpass": 205240106,
"de_prime": 2831565855,
"de_ravine": 2615546425,
"de_safehouse": 125440714,
"de_shortnuke": 2131550446,
"de_stmarc": 125441004,
"de_sugarcane": 125440847,
"de_train": 125438372,
"de_tuscan": 2458920550,
"de_vertigo": 125439851,
"dz_ember": 2681770529,
"dz_vineyard": 2587298130,
"gd_cbble": 782012846
}
[{
"name": "ar_baggage",
"id": 125440026
},{
"name": "ar_shoots",
"id" : 125440261
},{
"name": "cs_italy",
"id": 125436057
},{
"name": "cs_office",
"id": 125444404
},{
"name": "de_ancient",
"id": 2627571649
},{
"name": "de_anubis",
"id": 1984883124
},{
"name": "de_dust2",
"id": 125438255
},{
"name": "de_inferno",
"id": 125438669
},{
"name": "de_mirage",
"id": 152508932
},{
"name": "de_nuke",
"id": 125439125
},{
"name": "de_overpass",
"id": 205240106
},{
"name": "de_vertigo",
"id": 125439851
}]
11 changes: 8 additions & 3 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# CS2 Server Control with a Nodejs-powered web-API (LINUX only)
this API is used to start/stop/update a CS2 linux dedicated server and control it via rcon.
The backend accepts RESTful api calls authenticated either via steamID or a configurable user with hppt authentication for stateless API calls.
A full featured example webinterface is provided to get a quick start on the application.

## Disclaimer
The use of this software is at your own risk.
Expand All @@ -21,7 +24,7 @@ I strongly adivise to use secure connections to prevent possible man-in-the-midd
## Prerequisites
- steam CLI
- CS2 dedicated server
- NodeJS 14.X or higher
- NodeJS 16.X or higher
- screen

## Install
Expand All @@ -44,6 +47,8 @@ sudo apt install screen
### API:
- Edit the settings in config.js - at least the first 5. They are explained in the file.
- The API uses steam authentication which returns a Steam ID as URL (https://steamcommunity.com/openid/id/{steamid}). The last part is the SteamID64, which can be calculated from the other SteamID formats - various online tools are available. In the configuration, an array needs to be filled with the comma separated IDs of your intended admins as strings (e.g. ['{steamid-1}', '{steamid-2}']).
- To display map preview images, a steamAPI key is needed. See https://steamcommunity.com/dev/apikey how to get one.
It must be copied to the respective config option. To learn more on how the map-handling works see [Maps TL;DR](https://github.com/Taraman17/nodejs-cs2-api/blob/master/MAPS.MD)

### Server update script
If you want to use the update function, you need to provide a script to use with the steamcmd.
Expand All @@ -66,11 +71,11 @@ Start the script with
```console
node serverControl.js
```
In your brower open http://<yourIP>:8090/gameserver.htm
In your brower open http://\<yourIP>:8090/gameserver.htm

The API will detect a running server and connect to it.

To start the API on boot and have it running in the background, I recommend [Forever](https://github.com/foreversd/forever)
To start the API on boot and have it running in the background, I recommend [PM2](https://pm2.keymetrics.io/)

## Usage
*NOTE: For API calls with basic http authentication see below.*
Expand Down
14 changes: 12 additions & 2 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@

/* Optional settings */
// Anything you want your server command line to have additional to:
// -console -usercon -ip 0.0.0.0 +sv_logfile 1 -serverlogging +logaddress_add_http "http://${this._localIp}:${this.logPort}/log"
// -console -usercon -ip 0.0.0.0 +sv_logfile 1 -serverlogging
// +logaddress_add_http "http://${this._localIp}:${this.logPort}/log"
"csgoOptionalArgs": "",
// steam serverToken for public access. To get one see https://steamcommunity.com/dev/managegameservers
"serverToken": "",
// Steam Web API token. Needed to get mapdetails like thumbnail, etc
// See https://steamcommunity.com/dev/apikey how to get one.
"apiToken": "",
// Workshop Collection to host on the server.
"workshopCollection": "",
// List of workshop ids of maps to add to available maps.
"workshopMaps": [ // '23423523523525',
// '37281723987123'
],
// If you want to use a different name / location for the update script (absolute path).
"updateScript": "",
// Time in minutes, after which a new login is needed.
Expand All @@ -42,7 +52,7 @@
// If you use https, add the path to the certificate files here.
"httpsCertificate": "",
"httpsPrivateKey": "",
// Optional: In case your CA is not trusted by default (e.g. letsencrypt), you can add
// In case your CA is not trusted by default (e.g. letsencrypt), you can add
// the CA-Cert here.
"httpsCa": "",
// Change this to any string of your liking to make it harder for attackers to profile your cookies.
Expand Down
51 changes: 35 additions & 16 deletions modules/apiV10.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* @requires node-pty
* @requires express
* @requires ./config.js
* @requires ./emitters.js
* @requires ./serverInfo.js
* @requires ./controlEmitter.js
* @requires ./sharedFunctions.js
*/

Expand Down Expand Up @@ -293,7 +294,7 @@ router.get('/control/start', (req, res) => {
}
let commandLine = `${cfg.serverCommandline} +map ${startMap}`;
logger.info(commandLine);
let serverProcess = exec(commandLine, (error, stdout, stderr) => {
exec(commandLine, (error, stdout, stderr) => {
if (error) {
// node couldn't execute the command.
res.status(501).json({ "error": error.code });
Expand Down Expand Up @@ -442,12 +443,17 @@ router.get('/control/stop', (req, res) => {
controlEmitter.emit('exec', 'stop', 'start');
logger.verbose("sending quit.");
sf.executeRcon('quit').then((answer) => {
// CHostStateMgr::QueueNewRequest( Quitting, 8 )
// TODO: find out if command quit can fail.
serverInfo.serverState.serverRunning = false;
serverInfo.serverState.authenticated = false;
serverInfo.reset();
res.json({ "success": true });
if (answer.indexOf("CHostStateMgr::QueueNewRequest( Quitting") != -1) {
// CHostStateMgr::QueueNewRequest( Quitting, 8 )
// TODO: find out if command quit can fail.
serverInfo.serverState.serverRunning = false;
serverInfo.serverState.authenticated = false;
serverInfo.reset();
res.json({ "success": true });
} else {
res.status(501).json({ "error": `RCON response not correct.` });
logger.warn("Stopping the server failed - rcon command not successful");
}
controlEmitter.emit('exec', 'stop', 'end');
}).catch((err) => {
logger.error('Stopping server Failed: ' + err);
Expand Down Expand Up @@ -483,12 +489,13 @@ router.get('/control/stop', (req, res) => {
router.get('/control/kill', (req, res) => {
exec('/bin/ps -A |grep cs2', (error, stdout, stderr) => {
if (error) {
logger.error(`exec error: ${error}`);
logger.error(`exec error: ${error}, ${stderr}`);
res.status(501).json({ "error": "Could not find csgo server process" });
} else if (stdout.match(/cs2/) != null) {
let pid = stdout.split(/\s+/)[1];
exec(`/bin/kill ${pid}`, (error, stdout, stderr) => {
if (error) {
logger.warn(`Server process could not be killed: ${error}: ${stderr}`);
res.status(501).json({ "error": "Could not kill csgo server process" });
} else {
// reset API-State
Expand Down Expand Up @@ -562,7 +569,7 @@ router.get('/control/update', (req, res) => {
res.json(`{ "success": true }`);
updateProcess.once('close', (code) => {
if (!updateSuccess) {
logger.warn('Update exited without success.');
logger.warn(`Update exited without success. Exit code: ${code}`);
controlEmitter.emit('progress', 'Update failed!', 100);
controlEmitter.emit('exec', 'update', 'fail');
}
Expand All @@ -572,7 +579,7 @@ router.get('/control/update', (req, res) => {
if (updateSuccess) {
res.json({ "success": true });
} else {
logger.warn('Update exited without success.');
logger.warn(`Update exited without success. Exit code: ${code}`);
res.status(501).json({ "error": "Update was not successful" });
}
controlEmitter.emit('exec', 'update', 'end');
Expand Down Expand Up @@ -601,7 +608,7 @@ router.get('/control/update', (req, res) => {
* @apiName changemap
* @apiGroup Control
*
* @apiParam {string} mapname filename of the map without extension (.bsp)
* @apiParam {string/int} map name, title or workshopID of a map.
* @apiParamExample {string} Map-example
* cs_italy
*
Expand All @@ -619,16 +626,28 @@ router.get('/control/changemap', (req, res) => {
if (serverInfo.serverState.operationPending == 'none') {
controlEmitter.emit('exec', 'mapchange', 'start');
// only try to change map, if it exists on the server.
if (serverInfo.mapsAvail.includes(args.map)) {
sf.executeRcon(`map ${args.map}`).then((answer) => {
// Answer on sucess:
let map = sf.getMap(args.map);
if (map != undefined) {
let mapchangeCommand = '';
if (map.official) {
mapchangeCommand = `map ${map.name}`;
} else {
if (map.workshopID != '') {
mapchangeCommand = `host_workshop_map ${map.workshopID}`;
} else {
mapchangeCommand = `ds_workshop_changelevel ${map.name}`;
}
}

sf.executeRcon(mapchangeCommand).then((answer) => {
// Answer on success (unfortunately only available for official maps):
// Changelevel to de_nuke
// changelevel "de_nuke"
// CHostStateMgr::QueueNewRequest( Changelevel (de_nuke), 5 )
//
// Answer on failure:
// changelevel de_italy: invalid map name
if (answer.indexOf(`CHostStateMgr::QueueNewRequest( Changelevel (${args.map})`) == -1) {
if (map.official && answer.indexOf(`CHostStateMgr::QueueNewRequest( Changelevel (${map.name})`) == -1) {
// If the mapchange command fails, return failure immediately
res.status(501).json({ "error": `Mapchange failed: ${answer}` });
controlEmitter.emit('exec', 'mapchange', 'fail');
Expand Down
Loading

0 comments on commit 8556388

Please sign in to comment.