diff --git a/MAPS.MD b/MAPS.MD new file mode 100755 index 0000000..532cde3 --- /dev/null +++ b/MAPS.MD @@ -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 ``` is used to change level to a built in map. +- ```ds_workshop_changelevel ``` 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. \ No newline at end of file diff --git a/OfficialMaps.json b/OfficialMaps.json index 71956c4..7c76b85 100644 --- a/OfficialMaps.json +++ b/OfficialMaps.json @@ -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 -} \ No newline at end of file +[{ + "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 +}] diff --git a/README.MD b/README.MD index 574163f..80eb53d 100644 --- a/README.MD +++ b/README.MD @@ -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. @@ -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 @@ -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. @@ -66,11 +71,11 @@ Start the script with ```console node serverControl.js ``` -In your brower open http://:8090/gameserver.htm +In your brower open http://\: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.* diff --git a/config.js b/config.js index 5d21efe..9f6ddef 100644 --- a/config.js +++ b/config.js @@ -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. @@ -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. diff --git a/modules/apiV10.js b/modules/apiV10.js old mode 100644 new mode 100755 index 63839ad..065a274 --- a/modules/apiV10.js +++ b/modules/apiV10.js @@ -3,7 +3,8 @@ * @requires node-pty * @requires express * @requires ./config.js - * @requires ./emitters.js + * @requires ./serverInfo.js + * @requires ./controlEmitter.js * @requires ./sharedFunctions.js */ @@ -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 }); @@ -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); @@ -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 @@ -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'); } @@ -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'); @@ -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 * @@ -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'); diff --git a/modules/configClass.js b/modules/configClass.js old mode 100644 new mode 100755 index 8a06662..76ac1f8 --- a/modules/configClass.js +++ b/modules/configClass.js @@ -2,130 +2,154 @@ * Config class for CSGO Server API */ class config { - constructor() { - this._userOptions = require('../config.js'); + #userOptions = require('../config.js'); + #screenCommand; + #csgoCommand; + #serverTokenCommand; + #localIp; - this._screenCommand = `${this._userOptions.screen} -L -Logfile ${this._userOptions.screenLog} -dmS ${this._userOptions.screenName}`; - this._csgoCommand = `${this._userOptions.csgoDir}game/bin/linuxsteamrt64/cs2 -dedicated`; - this._serverTokenCommand = `+sv_setsteamaccount ${this._userOptions.serverToken}`; - this._localIp = ''; - } - get _csgoArgs() { - return `-console -usercon -ip 0.0.0.0 +sv_logfile 1 -serverlogging +logaddress_add_http "http://${this._localIp}:${this.logPort}/log" ${this._userOptions.csgoOptionalArgs}`; + constructor() { + this.#screenCommand = `${this.#userOptions.screen} -L -Logfile ${this.#userOptions.screenLog} -dmS ${this.#userOptions.screenName}`; + this.#csgoCommand = `${this.#userOptions.csgoDir}game/bin/linuxsteamrt64/cs2 -dedicated`; + this.#serverTokenCommand = `+sv_setsteamaccount ${this.#userOptions.serverToken}`; + this.#localIp = ''; + } + get #csgoArgs() { + let args = `-console -usercon -ip 0.0.0.0 +sv_logfile 1 -serverlogging +logaddress_add_http "http://${this.#localIp}:${this.#userOptions.logPort}/log" ${this.#userOptions.csgoOptionalArgs}`; + if (this.#userOptions.workshopCollection != '') { + args += ` +host_workshop_collection ${this.#userOptions.workshopCollection}`; + } + return args; } + get apiToken() { + return this.#userOptions.apiToken; + } get rconPass() { - return this._userOptions.rconPass; + return this.#userOptions.rconPass; } get admins() { - return this._userOptions.admins; + return this.#userOptions.admins; + } + + get workshopCollection() { + return this.#userOptions.workshopCollection; + } + set workshopCollection(id) { + this.#userOptions.workshopCollection = id; + } + get workshopMaps() { + return this.#userOptions.workshopMaps; + } + set workshopMaps(maps) { + this.#userOptions.workshopMaps = maps; } get redirectPage() { - if (this._userOptions.redirectPage) { - return this._userOptions.redirectPage; + if (this.#userOptions.redirectPage) { + return this.#userOptions.redirectPage; } else { return ('/gameserver.htm'); } } get loginValidity() { - return this._userOptions.loginValidity * 60000; + return this.#userOptions.loginValidity * 60000; } get httpAuth() { - return this._userOptions.httpAuth; + return this.#userOptions.httpAuth; } get httpUser() { - return this._userOptions.httpUser; + return this.#userOptions.httpUser; } get iface() { - return this._userOptions.iface; + return this.#userOptions.iface; } get localIp() { - return this._localIp; + return this.#localIp; } set localIp(ip) { - this._localIp = ip; + this.#localIp = ip; } get host() { - if (this._userOptions.host != '') { - return this._userOptions.host; + if (this.#userOptions.host != '' && this.#userOptions.useHttps) { + return this.#userOptions.host; } else { - return this._localIp + return this.#localIp } } get apiPort() { - return this._userOptions.apiPort; + return this.#userOptions.apiPort; } get socketPort() { - return this._userOptions.socketPort; + return this.#userOptions.socketPort; } get logPort() { - return this._userOptions.logPort; + return this.#userOptions.logPort; } get serverCommandline() { - let command = `${this._screenCommand} ${this._csgoCommand} ${this._csgoArgs}`; + let command = `${this.#screenCommand} ${this.#csgoCommand} ${this.#csgoArgs}`; if (this._csgoToken != '') { - command = `${command} ${this._serverTokenCommand}`; + command = `${command} ${this.#serverTokenCommand}`; } return command; } get steamCommand() { - return this._userOptions.steamExe + return this.#userOptions.steamExe } get updateScript() { - if (this._userOptions.updateScript != ''){ - return this._userOptions.updateScript; + if (this.#userOptions.updateScript != ''){ + return this.#userOptions.updateScript; } else { - return `${this._userOptions.csgoDir}update_cs2.txt`; + return `${this.#userOptions.csgoDir}update_cs2.txt`; } } get webSockets() { - return this._userOptions.webSockets; + return this.#userOptions.webSockets; } get useHttps() { - return this._userOptions.useHttps; + return this.#userOptions.useHttps; } get scheme() { - return (this._userOptions.useHttps ? 'https' : 'http'); + return (this.#userOptions.useHttps ? 'https' : 'http'); } get httpsCertificate() { - return this._userOptions.httpsCertificate; + return this.#userOptions.httpsCertificate; } get httpsPrivateKey() { - return this._userOptions.httpsPrivateKey; + return this.#userOptions.httpsPrivateKey; } get httpsCa() { - return this._userOptions.httpsCa; + return this.#userOptions.httpsCa; } get corsOrigin() { - return this._userOptions.corsOrigin; + return this.#userOptions.corsOrigin; } get sessionSecret() { - return this._userOptions.sessionSecret; + return this.#userOptions.sessionSecret; } script(type) { - return this._userOptions[`${type}Script`]; + return this.#userOptions[`${type}Script`]; } get logFile() { - return this._userOptions.logFile; + return this.#userOptions.logFile; } get logLevel() { - return this._userOptions.logLevel; + return this.#userOptions.logLevel; } get logDays() { - return this._userOptions.logDays; + return this.#userOptions.logDays; } -}; +} module.exports = new config(); \ No newline at end of file diff --git a/modules/logreceive.js b/modules/logreceive.js old mode 100644 new mode 100755 index d0fbd07..b50782b --- a/modules/logreceive.js +++ b/modules/logreceive.js @@ -1,119 +1,123 @@ -const express = require('express'); -var router = express.Router(); -const { exec } = require('child_process'); -var controlEmitter = require('./controlEmitter.js'); -const logger = require('./logger.js'); -var serverInfo = require('./serverInfo.js'); -const sf = require('./sharedFunctions.js'); -var cfg = require('./configClass.js'); - -router.post('/log', (req, res) => { - const data = req.body; - var logs = data.split(/\r\n|\r|\n/); - - logs.forEach(line => { - if (line.length >= 20) { - // Start authentication, when not authenticated. - if ((line.indexOf('Log file started') != -1) && !serverInfo.serverState.authenticated) { - // Start of logfile - // L 08/13/2020 - 21:48:49: Log file started (file "logs/L000_000_000_000_27015_202008132148_000.log") (game "/home/user/csgo_ds/csgo") (version "7929") - logger.verbose('start authenticating RCON'); - // Since authentication is a vital step for the API to work, we start it automatically - // once the server runs. - sf.authenticate().then((data) => { - logger.verbose(`authentication ${data.authenticated}`); - }).catch((data) => { - logger.verbose(`authentication ${data.authenticated}`); - }); - if (cfg.script('logStart') != '') { - exec(cfg.script('logStart')); - } - } else if (line.indexOf('Loading map ') != -1) { - // Start of map. - // L 10/13/2023 - 14:28:38: Loading map "de_anubis" - let rex = /Loading map \"(\S+)\"/g; - let matches = rex.exec(line); - let mapstring = matches[1]; - mapstring = sf.cutMapName(mapstring); - serverInfo.map = mapstring; - serverInfo.pause = false; - // since 'started map' is also reported on server-start, only emit on mapchange. - if (serverInfo.serverState.operationPending == 'mapchange') { - controlEmitter.emit('exec', 'mapchange', 'end'); - } - logger.verbose(`Started map: ${mapstring}`); - serverInfo.clearPlayers(); - serverInfo.newMatch(); - if (cfg.script('mapStart') != '') { - exec(cfg.script('mapStart')); - } - } else if (line.indexOf('World triggered "Match_Start" on') != -1) { - // Start of a new match. - // L 08/13/2020 - 21:49:26: World triggered "Match_Start" on "de_nuke" - logger.verbose('Detected match start.'); - sf.queryMaxRounds(); - serverInfo.newMatch(); - let rex = /World triggered "Match_Start" on "(.+)"/ - let matches = rex.exec(line) - serverInfo.map = matches[1]; - if (cfg.script('matchStart') != '') { - exec(cfg.script('matchStart')); - } - } else if (line.indexOf('World triggered "Round_Start"') != -1) { - // Start of round. - // L 08/13/2020 - 21:49:28: World triggered "Round_Start" - if (cfg.script('roundStart') != '') { - exec(cfg.script('roundStart')); - } - } else if (/Team \"\S+\" scored/.test(line)) { - // Team scores at end of round. - // L 02/10/2019 - 21:31:15: Team "CT" scored "1" with "2" players - // L 02/10/2019 - 21:31:15: Team "TERRORIST" scored "1" with "2" players - rex = /Team \"(\S)\S+\" scored \"(\d+)\"/g; - let matches = rex.exec(line); - serverInfo.score = matches; - } else if (line.indexOf('World triggered "Round_End"') != -1) { - // End of round. - // L 08/13/2020 - 22:24:22: World triggered "Round_End" - if (cfg.script('roundEnd') != '') { - exec(cfg.script('roundEnd')); - } - } else if (line.indexOf("Game Over:") != -1) { - // End of match. - // L 08/13/2020 - 22:24:22: Game Over: competitive 131399785 de_nuke score 16:9 after 35 min - if (cfg.script('matchEnd') != '') { - exec(cfg.script('matchEnd')); - } - } else if (/\".+<\d+><\[U:\d:\d+\]>/.test(line)) { - // Player join or teamchange. - // 10/12/2023 - 16:06:38: "[Klosser] Taraman<2><[U:1:12610374]><>" entered the game - // 10/12/2023 - 18:57:47: "[Klosser] Taraman<2><[U:1:12610374]>" switched from team to - // 10/12/2023 - 18:59:25: "[Klosser] Taraman<2><[U:1:12610374]>" switched from team to - // 10/16/2023 - 16:31:59.699 - "[Klosser] Taraman<2><[U:1:12610374]>" disconnected (reason "NETWORK_DISCONNECT_DISCONNECT_BY_USER") - let rex = /\"(.+)<\d+><\[(U:\d+:\d+)\]>.*switched from team <\S+> to <(\S+)>/g; - matches = rex.exec(line); - serverInfo.assignPlayer(matches[1], matches[2]); - } - } else if (line.indexOf('Log file closed') != -1) { - // end of current log file. (Usually on mapchange or server quit.) - // L 08/13/2020 - 22:25:00: Log file closed - logger.verbose('logfile closed!'); - if (cfg.script('logEnd') != '') { - exec(cfg.script('logEnd')); - } - } - } - }); - - res.status(200).send("Receiving logs"); -}); - +const express = require('express'); +var router = express.Router(); +const { exec } = require('child_process'); +var controlEmitter = require('./controlEmitter.js'); +const logger = require('./logger.js'); +var serverInfo = require('./serverInfo.js'); +const sf = require('./sharedFunctions.js'); +var cfg = require('./configClass.js'); + +router.post('/log', (req, res) => { + const data = req.body; + var logs = data.split(/\r\n|\r|\n/); + + logs.forEach(line => { + if (line.length >= 20) { + // Start authentication, when not authenticated. + if ((line.indexOf('Log file started') != -1) && !serverInfo.serverState.authenticated) { + // Start of logfile + // L 08/13/2020 - 21:48:49: Log file started (file "logs/L000_000_000_000_27015_202008132148_000.log") (game "/home/user/csgo_ds/csgo") (version "7929") + logger.verbose('start authenticating RCON'); + // Since authentication is a vital step for the API to work, we start it automatically + // once the server runs. + sf.authenticate().then((data) => { + logger.verbose(`authentication ${data.authenticated}`); + }).catch((data) => { + logger.verbose(`authentication ${data.authenticated}`); + }); + if (cfg.script('logStart') != '') { + exec(cfg.script('logStart')); + } + } else if (line.indexOf('Loading map ') != -1) { + // Start of map. + // L 10/13/2023 - 14:28:38: Loading map "de_anubis" + let rex = /Loading map "(\S+)"/g; + let matches = rex.exec(line); + let mapstring = matches[1]; + mapstring = sf.cutMapName(mapstring); + serverInfo.map = mapstring; + serverInfo.pause = false; + // since 'started map' is also reported on server-start, only emit on mapchange. + if (serverInfo.serverState.operationPending == 'mapchange') { + controlEmitter.emit('exec', 'mapchange', 'end'); + } + logger.verbose(`Started map: ${mapstring}`); + serverInfo.clearPlayers(); + serverInfo.newMatch(); + if (cfg.script('mapStart') != '') { + exec(cfg.script('mapStart')); + } + } else if (line.indexOf('World triggered "Match_Start" on') != -1) { + // Start of a new match. + // L 08/13/2020 - 21:49:26: World triggered "Match_Start" on "de_nuke" + logger.verbose('Detected match start.'); + sf.queryMaxRounds(); + serverInfo.newMatch(); + let rex = /World triggered "Match_Start" on "(.+)"/ + let matches = rex.exec(line) + serverInfo.map = matches[1]; + if (cfg.script('matchStart') != '') { + exec(cfg.script('matchStart')); + } + } else if (line.indexOf('World triggered "Round_Start"') != -1) { + // Start of round. + // L 08/13/2020 - 21:49:28: World triggered "Round_Start" + if (cfg.script('roundStart') != '') { + exec(cfg.script('roundStart')); + } + } else if (/Team "\S+" scored/.test(line)) { + // Team scores at end of round. + // L 02/10/2019 - 21:31:15: Team "CT" scored "1" with "2" players + // L 02/10/2019 - 21:31:15: Team "TERRORIST" scored "1" with "2" players + let rex = /Team "(\S)\S+" scored "(\d+)"/g; + let matches = rex.exec(line); + serverInfo.score = matches; + } else if (line.indexOf('World triggered "Round_End"') != -1) { + // End of round. + // L 08/13/2020 - 22:24:22: World triggered "Round_End" + if (cfg.script('roundEnd') != '') { + exec(cfg.script('roundEnd')); + } + } else if (line.indexOf("Game Over:") != -1) { + // End of match. + // L 08/13/2020 - 22:24:22: Game Over: competitive 131399785 de_nuke score 16:9 after 35 min + if (cfg.script('matchEnd') != '') { + exec(cfg.script('matchEnd')); + } + } else if (/".{1,32}<\d{1,3}><\[\w:\d:\d{1,10}\]>/.test(line)) { + // Player join or teamchange. + // 10/12/2023 - 16:06:38: "[Klosser] Taraman<2><[U:1:12610374]><>" entered the game + // 10/12/2023 - 18:57:47: "[Klosser] Taraman<2><[U:1:12610374]>" switched from team to + // 10/12/2023 - 18:59:25: "[Klosser] Taraman<2><[U:1:12610374]>" switched from team to + // 10/16/2023 - 16:31:59.699 - "[Klosser] Taraman<2><[U:1:12610374]>" disconnected (reason "NETWORK_DISCONNECT_DISCONNECT_BY_USER") + // "Strapper<6>" + let rex = /"(.{1,32})<\d{1,3}><\[(\w:\d:\d{1,10})\]><\[(\w:\d:\d{1,10})\]>" switched from team <\S{1,10}> to <(\S{1,10})>/g; + matches = rex.exec(line); + serverInfo.assignPlayer(matches[1], matches[2], matches[3]); + } else if (line.search(/\[\w:\d:\d{1,10}\]><\w{1,10}>" \[.{1,5} .{1,5} .{1,5}\] killed ".{1,32}<\d{1,3}><\[\w:\d:\d{1,10}\]/) != -1) { + rex = /\[(\w:\d:\d{1,10})\]><\w{1,10}>" \[.{1,5} .{1,5} .{1,5}\] killed ".{1,32}<\d{1,3}><\[(\w:\d:\d{1,10})\]/ + matches = rex.exec(line); + serverInfo.recordKill(matches[1], matches[2]); + } + } else if (line.indexOf('Log file closed') != -1) { + // end of current log file. (Usually on mapchange or server quit.) + // L 08/13/2020 - 22:25:00: Log file closed + logger.verbose('logfile closed!'); + if (cfg.script('logEnd') != '') { + exec(cfg.script('logEnd')); + } + } + } + }); + + res.status(200).send("Receiving logs"); +}); + module.exports = router; \ No newline at end of file diff --git a/modules/rcon-srcds/LICENSE b/modules/rcon-srcds/LICENSE index 786d1c5..d3216cf 100644 --- a/modules/rcon-srcds/LICENSE +++ b/modules/rcon-srcds/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Enrique Carpintero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2018 Enrique Carpintero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modules/rcon-srcds/packet.js b/modules/rcon-srcds/packet.js index dee157c..abb4335 100644 --- a/modules/rcon-srcds/packet.js +++ b/modules/rcon-srcds/packet.js @@ -1,37 +1,37 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.decode = exports.encode = void 0; -/** - * Encode data to packet buffer - * @param type Packet Type - * @param id Packet ID - * @param body Packet body (payload) - * @param encoding Body encoding - * @returns Encoded packet buffer - */ -exports.encode = function (type, id, body, encoding) { - if (encoding === void 0) { encoding = 'ascii'; } - var size = Buffer.byteLength(body) + 14; // body size + 10 + 4 (Null) - var buffer = Buffer.alloc(size); - buffer.writeInt32LE(size - 4, 0); - buffer.writeInt32LE(id, 4); - buffer.writeInt32LE(type, 8); - buffer.write(body, 12, size - 2, encoding); - buffer.writeInt16LE(0, size - 2); - return buffer; -}; -/** - * Decodes packet buffer to data - * @param buf Buffer to decode - * @param encoding Body encoding - * @returns Decoded packet object - */ -exports.decode = function (buf, encoding) { - if (encoding === void 0) { encoding = 'ascii'; } - return { - size: buf.readInt32LE(0), - id: buf.readInt32LE(4), - type: buf.readInt32LE(8), - body: buf.toString(encoding, 12, buf.byteLength - 2) - }; -}; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.decode = exports.encode = void 0; +/** + * Encode data to packet buffer + * @param type Packet Type + * @param id Packet ID + * @param body Packet body (payload) + * @param encoding Body encoding + * @returns Encoded packet buffer + */ +exports.encode = function (type, id, body, encoding) { + if (encoding === void 0) { encoding = 'ascii'; } + var size = Buffer.byteLength(body) + 14; // body size + 10 + 4 (Null) + var buffer = Buffer.alloc(size); + buffer.writeInt32LE(size - 4, 0); + buffer.writeInt32LE(id, 4); + buffer.writeInt32LE(type, 8); + buffer.write(body, 12, size - 2, encoding); + buffer.writeInt16LE(0, size - 2); + return buffer; +}; +/** + * Decodes packet buffer to data + * @param buf Buffer to decode + * @param encoding Body encoding + * @returns Decoded packet object + */ +exports.decode = function (buf, encoding) { + if (encoding === void 0) { encoding = 'ascii'; } + return { + size: buf.readInt32LE(0), + id: buf.readInt32LE(4), + type: buf.readInt32LE(8), + body: buf.toString(encoding, 12, buf.byteLength - 2) + }; +}; diff --git a/modules/rcon-srcds/protocol.js b/modules/rcon-srcds/protocol.js index d0e8d67..47cfd21 100644 --- a/modules/rcon-srcds/protocol.js +++ b/modules/rcon-srcds/protocol.js @@ -1,17 +1,17 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -/** - * Packet Types - * Reference: https://developer.valvesoftware.com/wiki/Source_RCON#Requests_and_Responses - * - * @readonly - */ -var protocol = Object.freeze({ - SERVERDATA_AUTH: 0x03, - SERVERDATA_EXECCOMMAND: 0x02, - SERVERDATA_AUTH_RESPONSE: 0x02, - SERVERDATA_RESPONSE_VALUE: 0x00, - ID_AUTH: 0x999, - ID_REQUEST: 0x123, -}); -exports.default = protocol; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Packet Types + * Reference: https://developer.valvesoftware.com/wiki/Source_RCON#Requests_and_Responses + * + * @readonly + */ +var protocol = Object.freeze({ + SERVERDATA_AUTH: 0x03, + SERVERDATA_EXECCOMMAND: 0x02, + SERVERDATA_AUTH_RESPONSE: 0x02, + SERVERDATA_RESPONSE_VALUE: 0x00, + ID_AUTH: 0x999, + ID_REQUEST: 0x123, +}); +exports.default = protocol; diff --git a/modules/rcon-srcds/rcon.js b/modules/rcon-srcds/rcon.js index 469136c..560c034 100644 --- a/modules/rcon-srcds/rcon.js +++ b/modules/rcon-srcds/rcon.js @@ -1,250 +1,250 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (_) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -var net_1 = require("net"); -var protocol_1 = __importDefault(require("./protocol")); -var packets = __importStar(require("./packet")); -var RCON = /** @class */ (function () { - /** - * Source RCON (https://developer.valvesoftware.com/wiki/Source_RCON) - * @param options Connection options - */ - function RCON(options) { - this.host = options.host || '127.0.0.1'; - this.port = options.port || 27015; - this.maxPacketSize = options.maxPacketSize || 4096; - this.encoding = options.encoding || 'ascii'; - this.timeout = options.timeout || 1000; - this.authenticated = false; - this.connected = false; - } - /** - * Authenticates the connection - * @param password Password string - */ - RCON.prototype.authenticate = function (password) { - return __awaiter(this, void 0, void 0, function () { - var _this = this; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - if (!!this.connected) return [3 /*break*/, 2]; - return [4 /*yield*/, this.connect()]; - case 1: - _a.sent(); - _a.label = 2; - case 2: return [2 /*return*/, new Promise(function (resolve, reject) { - if (_this.authenticated) { - reject(Error('Already authenticated')); - return; - } - _this.write(protocol_1.default.SERVERDATA_AUTH, protocol_1.default.ID_AUTH, password) - .then(function (data) { - if (data === true) { - _this.authenticated = true; - resolve(true); - } - else { - _this.disconnect(); - reject(Error('Unable to authenticate')); - } - }).catch(reject); - })]; - } - }); - }); - }; - /** - * Executes command on the server - * @param command Command to execute - */ - RCON.prototype.execute = function (command) { - var _this = this; - return new Promise(function (resolve, reject) { - if (!_this.connected) { - reject(Error('Already disconnected. Please reauthenticate.')); - return; - } - var packetId = Math.floor(Math.random() * (256 - 1) + 1); - if (!_this.connection.writable) { - reject(Error('Unable to write to socket')); - return; - } - if (!_this.authenticated) { - reject(Error('Not authorized')); - return; - } - _this.write(protocol_1.default.SERVERDATA_EXECCOMMAND, packetId, command) - .then(resolve) - .catch(reject); - }); - }; - /** - * Creates a connection to the socket - */ - RCON.prototype.connect = function () { - var _this = this; - return new Promise(function (resolve, reject) { - _this.connection = net_1.createConnection({ - host: _this.host, - port: _this.port - }, function () { - if (_this.connection) - _this.connection.removeListener('error', reject); - _this.connected = true; - resolve(); - }); - _this.connection.once('error', reject); - _this.connection.setTimeout(_this.timeout); - }); - }; - /** - * Destroys the socket connection - */ - RCON.prototype.disconnect = function () { - var _this = this; - this.authenticated = false; - this.connected = false; - this.connection.destroy(); - return new Promise(function (resolve, reject) { - var onError = function (e) { - _this.connection.removeListener('close', onClose); - reject(e); - }; - var onClose = function () { - _this.connection.removeListener('error', onError); - resolve(); - }; - _this.connection.once('close', onClose); - _this.connection.once('error', onError); - }); - }; - RCON.prototype.isConnected = function () { - return this.connected; - }; - RCON.prototype.isAuthenticated = function () { - return this.authenticated; - }; - /** - * Writes to socket connection - * @param type Packet Type - * @param id Packet ID - * @param body Packet payload - */ - RCON.prototype.write = function (type, id, body) { - var _this = this; - return new Promise(function (resolve, reject) { - var response = ''; - var decodedPacket = undefined - var onData = function (packet) { - decodedPacket = packets.decode(packet, _this.encoding); - const fs = require('fs'); - // Server will respond twice (0x00 and 0x02) if we send an auth packet (0x03) - // but we need 0x02 to confirm - if (type === protocol_1.default.SERVERDATA_AUTH && decodedPacket.type !== protocol_1.default.SERVERDATA_AUTH_RESPONSE) { - // TODO: It seems, the CS2 server does not send this additional packet. - console.log('Does this still get called anyway?'); - return; - } else if (type === protocol_1.default.SERVERDATA_AUTH && decodedPacket.type === protocol_1.default.SERVERDATA_AUTH_RESPONSE) { - if (decodedPacket.id === protocol_1.default.ID_AUTH) { - resolve(true); - } - else { - resolve(false); - } - _this.connection.removeListener('data', onData); - } else if (id === decodedPacket.id) { - // if decoded packet has the correct id, and a termination string at the end, - // it is a single packet answer - if (packet.indexOf("0000", -2 , "hex") != -1) { - response = decodedPacket.body.replace(/\n\s*$/, ''); // delete trailing empty line - _this.connection.removeListener('data', onData); - resolve(response); - } - } else { - // If the sent packet is not of type SERVERDATA_AUTH - // AND - // The decoded packet ID does not match the sent id, it is the continuation of a multipacket response, - // so we keep reading until a termination string is detected. - response = response.concat(packet.toString(_this.encoding, 0, packet.byteLength - 2)); - if (packet.indexOf("0000", -2 , "hex") != -1) { - _this.connection.removeListener('data', onData); - resolve(response.replace(/\n\s*$/, '')); // delete trailing empty line - } - } - _this.connection.removeListener('error', onError); - }; - var onError = function (e) { - _this.connection.removeListener('data', onData); - reject(e); - }; - var encodedPacket = packets.encode(type, id, body, _this.encoding); - if (_this.maxPacketSize > 0 && encodedPacket.length > _this.maxPacketSize) { - reject(Error('Packet size too big')); - return; - } - _this.connection.on('data', onData); - _this.connection.on('error', onError); - _this.connection.write(encodedPacket); - }); - }; - return RCON; -}()); -exports.default = RCON; +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var net_1 = require("net"); +var protocol_1 = __importDefault(require("./protocol")); +var packets = __importStar(require("./packet")); +var RCON = /** @class */ (function () { + /** + * Source RCON (https://developer.valvesoftware.com/wiki/Source_RCON) + * @param options Connection options + */ + function RCON(options) { + this.host = options.host || '127.0.0.1'; + this.port = options.port || 27015; + this.maxPacketSize = options.maxPacketSize || 4096; + this.encoding = options.encoding || 'ascii'; + this.timeout = options.timeout || 1000; + this.authenticated = false; + this.connected = false; + } + /** + * Authenticates the connection + * @param password Password string + */ + RCON.prototype.authenticate = function (password) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!!this.connected) return [3 /*break*/, 2]; + return [4 /*yield*/, this.connect()]; + case 1: + _a.sent(); + _a.label = 2; + case 2: return [2 /*return*/, new Promise(function (resolve, reject) { + if (_this.authenticated) { + reject(Error('Already authenticated')); + return; + } + _this.write(protocol_1.default.SERVERDATA_AUTH, protocol_1.default.ID_AUTH, password) + .then(function (data) { + if (data === true) { + _this.authenticated = true; + resolve(true); + } + else { + _this.disconnect(); + reject(Error('Unable to authenticate')); + } + }).catch(reject); + })]; + } + }); + }); + }; + /** + * Executes command on the server + * @param command Command to execute + */ + RCON.prototype.execute = function (command) { + var _this = this; + return new Promise(function (resolve, reject) { + if (!_this.connected) { + reject(Error('Already disconnected. Please reauthenticate.')); + return; + } + var packetId = Math.floor(Math.random() * (256 - 1) + 1); + if (!_this.connection.writable) { + reject(Error('Unable to write to socket')); + return; + } + if (!_this.authenticated) { + reject(Error('Not authorized')); + return; + } + _this.write(protocol_1.default.SERVERDATA_EXECCOMMAND, packetId, command) + .then(resolve) + .catch(reject); + }); + }; + /** + * Creates a connection to the socket + */ + RCON.prototype.connect = function () { + var _this = this; + return new Promise(function (resolve, reject) { + _this.connection = net_1.createConnection({ + host: _this.host, + port: _this.port + }, function () { + if (_this.connection) + _this.connection.removeListener('error', reject); + _this.connected = true; + resolve(); + }); + _this.connection.once('error', reject); + _this.connection.setTimeout(_this.timeout); + }); + }; + /** + * Destroys the socket connection + */ + RCON.prototype.disconnect = function () { + var _this = this; + this.authenticated = false; + this.connected = false; + this.connection.destroy(); + return new Promise(function (resolve, reject) { + var onError = function (e) { + _this.connection.removeListener('close', onClose); + reject(e); + }; + var onClose = function () { + _this.connection.removeListener('error', onError); + resolve(); + }; + _this.connection.once('close', onClose); + _this.connection.once('error', onError); + }); + }; + RCON.prototype.isConnected = function () { + return this.connected; + }; + RCON.prototype.isAuthenticated = function () { + return this.authenticated; + }; + /** + * Writes to socket connection + * @param type Packet Type + * @param id Packet ID + * @param body Packet payload + */ + RCON.prototype.write = function (type, id, body) { + var _this = this; + return new Promise(function (resolve, reject) { + var response = ''; + var decodedPacket = undefined + var onData = function (packet) { + decodedPacket = packets.decode(packet, _this.encoding); + const fs = require('fs'); + // Server will respond twice (0x00 and 0x02) if we send an auth packet (0x03) + // but we need 0x02 to confirm + if (type === protocol_1.default.SERVERDATA_AUTH && decodedPacket.type !== protocol_1.default.SERVERDATA_AUTH_RESPONSE) { + // TODO: It seems, the CS2 server does not send this additional packet. + console.log('Does this still get called anyway?'); + return; + } else if (type === protocol_1.default.SERVERDATA_AUTH && decodedPacket.type === protocol_1.default.SERVERDATA_AUTH_RESPONSE) { + if (decodedPacket.id === protocol_1.default.ID_AUTH) { + resolve(true); + } + else { + resolve(false); + } + _this.connection.removeListener('data', onData); + } else if (id === decodedPacket.id) { + // if decoded packet has the correct id, and a termination string at the end, + // it is a single packet answer + if (packet.indexOf("0000", -2 , "hex") != -1) { + response = decodedPacket.body.replace(/\n\s*$/, ''); // delete trailing empty line + _this.connection.removeListener('data', onData); + resolve(response); + } + } else { + // If the sent packet is not of type SERVERDATA_AUTH + // AND + // The decoded packet ID does not match the sent id, it is the continuation of a multipacket response, + // so we keep reading until a termination string is detected. + response = response.concat(packet.toString(_this.encoding, 0, packet.byteLength - 2)); + if (packet.indexOf("0000", -2 , "hex") != -1) { + _this.connection.removeListener('data', onData); + resolve(response.replace(/\n\s*$/, '')); // delete trailing empty line + } + } + _this.connection.removeListener('error', onError); + }; + var onError = function (e) { + _this.connection.removeListener('data', onData); + reject(e); + }; + var encodedPacket = packets.encode(type, id, body, _this.encoding); + if (_this.maxPacketSize > 0 && encodedPacket.length > _this.maxPacketSize) { + reject(Error('Packet size too big')); + return; + } + _this.connection.on('data', onData); + _this.connection.on('error', onError); + _this.connection.write(encodedPacket); + }); + }; + return RCON; +}()); +exports.default = RCON; diff --git a/modules/serverInfo.js b/modules/serverInfo.js old mode 100644 new mode 100755 index 53a4788..7c28629 --- a/modules/serverInfo.js +++ b/modules/serverInfo.js @@ -1,7 +1,35 @@ const events = require('events'); +const logger = require('./logger'); class serverInfo { - constructor(options = {}) { + #serverState; + + #map = ''; + #mapsAvail = []; + #mapsDetails = [ + //{ 'name': '', + // 'official': true/false, + // 'title': '', + // 'workshopID': '', + // 'description': '', + // 'previewLink': '', + // 'tags': [{ "tag": "" }] } + ]; + #mapFilterType; + #mapFilters; + #maxRounds = 0 + #pause = false; + + #players = [ + //{ 'name': '', + // 'steamID': '', + // 'team': '', + // 'kills': 0, + // 'deaths': 0 } + ]; + #score; + + constructor() { /** * Stores the state of the controlled server-instance. * @typedef serverState @@ -12,7 +40,7 @@ class serverInfo { */ /** @type {serverState} */ - this._serverState = { + this.#serverState = { 'operationPending': 'none', 'serverRunning': false, 'serverRcon': undefined, @@ -20,29 +48,12 @@ class serverInfo { } // data section - this._map = ''; - this._mapsAvail = [] - this._mapsDetails = [ - //{ 'name': '', - // 'title': '', - // 'workshopID': '', - // 'description': '', - // 'previewLink': '', - // 'tags': [{ "tag": "" }] } - ]; - this._mapFilterType = 'exclude'; // 'include / exclude', - this._mapFilters = ['ar_', 'dz_', 'gd_', 'lobby_', 'training1']; // [ {string} ] - this._maxRounds = 0; - this._pause = false // Is the match paused? - this._score = { + this.#mapFilterType = 'exclude'; // 'include / exclude', + this.#mapFilters = ['ar_', 'dz_', 'gd_', 'lobby_', 'training1']; // [ {string} ] + this.#score = { 'T': 0, 'C': 0 }; - this._players = [ - //{ 'name': '', - // 'steamID': '', - // 'team': '' } - ]; // emitter to notify of changes this.serverInfoChanged = new events.EventEmitter(); @@ -50,200 +61,223 @@ class serverInfo { // getter / setter get serverState() { - return this._serverState; + return this.#serverState; } set serverState(newVal) { - this._serverState[expr] = newVal; + this.#serverState = newVal; } get map() { - return this._map; + return this.#map; } set map(newMap) { - this._map = newMap; + this.#map = newMap; this.serverInfoChanged.emit('change'); } get mapsAvail() { - return this._mapsAvail; + return this.#mapsAvail; } set mapsAvail(newMapsAvail) { - this._mapsAvail = newMapsAvail; + this.#mapsAvail = newMapsAvail; this.serverInfoChanged.emit('change'); } mapList() { - if (this._mapFilters.length > 0) { - return this._mapsAvail.filter((map) => { + if (this.#mapFilters.length > 0) { + return this.#mapsAvail.filter((map) => { let found = false; - this._mapFilters.forEach((filter) => { + this.#mapFilters.forEach((filter) => { if (map.includes(filter)) { found = true; } }); - if (this._mapFilterType === 'include') { + if (this.#mapFilterType === 'include') { return found; } else { return !found; } }); } else { - return this._mapsAvail; + return this.#mapsAvail; } } get mapsDetails() { - return this._mapsAvail; + return this.#mapsDetails; } set mapsDetails(newMapsDetails) { - this._mapsDetails = newMapsDetails; + this.#mapsDetails = newMapsDetails; this.serverInfoChanged.emit('change'); } mapDetails() { - if (this._mapFilters.length > 0) { - return this._mapsDetails.filter((map) => { + if (this.#mapFilters.length > 0) { + return this.#mapsDetails.filter((map) => { let found = false; if (map.name) { // sometimes map.name is undefined for some reason. - this._mapFilters.forEach((filter) => { + this.#mapFilters.forEach((filter) => { if (map.name.includes(filter)) { found = true; } }); } - if (this._mapFilterType === 'include') { + if (this.#mapFilterType === 'include') { return found; } else { return !found; } }); } else { - return this._mapsDetails; + return this.#mapsDetails; } } // Map Filter Methods get mapFilterType() { - return this._mapFilterType; + return this.#mapFilterType; } set mapFilterType(type) { if (type === 'include' || type === 'exclude') { - this._mapFilterType = type; + this.#mapFilterType = type; this.serverInfoChanged.emit('change'); } } get mapFilters() { - return this._mapFilters; + return this.#mapFilters; } mapFilterAdd(filter) { - this._mapFilters.push(filter); + this.#mapFilters.push(filter); this.serverInfoChanged.emit('change'); } mapFilterRemove(itemToRemove) { - if (this._mapFilters.length == 0) { + if (this.#mapFilters.length == 0) { return (0); } - if (typeof itemToRemove === 'number' && this._mapFilters.length > parseInt(itemToRemove)) { + if (typeof itemToRemove === 'number' && this.#mapFilters.length > parseInt(itemToRemove)) { console.log("removing number"); - this._mapFilters.splice(parseInt(itemToRemove), 1); + this.#mapFilters.splice(parseInt(itemToRemove), 1); this.serverInfoChanged.emit('change'); } else { - let newFilters = this._mapFilters.filter((currentItem) => { + let newFilters = this.#mapFilters.filter((currentItem) => { return (currentItem != itemToRemove); }); - this._mapFilters = newFilters; + this.#mapFilters = newFilters; this.serverInfoChanged.emit('change'); } - return (this._mapFilters.length); + return (this.#mapFilters.length); } mapFilterReset() { - this._mapFilterType = 'exclude'; - this._mapFilters = []; + this.#mapFilterType = 'exclude'; + this.#mapFilters = []; this.serverInfoChanged.emit('change'); } get maxRounds() { - return this._maxRounds; + return this.#maxRounds; } set maxRounds(newMaxRounds) { - this._maxRounds = newMaxRounds; - this.serverInfoChanged.emit('change'); + if (!Number.isNaN(newMaxRounds)) { + this.#maxRounds = newMaxRounds; + this.serverInfoChanged.emit('change'); + } else { + logger.warn('maxRounds must be a number.'); + } } get score() { - return this._score; + return this.#score; } // Accepts array with team (T or C) and score. set score(newScoreArray) { - this._score[newScoreArray[1]] = parseInt(newScoreArray[2]); + this.#score[newScoreArray[1]] = parseInt(newScoreArray[2]); this.serverInfoChanged.emit('change'); } get pause() { - return this._pause; + return this.#pause; } set pause(state) { - this._pause = state - this.serverInfoChanged.emit('change'); + if (typeof(state) == 'boolean') { + this.#pause = state + this.serverInfoChanged.emit('change'); + } else { + logger.warn('Invalid pause state - must be of type Boolean'); + } } get players() { - return this._players; + return this.#players; } addPlayer(newPlayer) { - newPlayer.team = 'U'; - this._players.push(newPlayer); + if (this.#players.find(x => x.steamID === newPlayer.steamID) != undefined) { + this.#players.find(x => x.steamID === newPlayer.steamID).disconnected = false + } else { + newPlayer.team = 'U'; + newPlayer.kills = 0; + newPlayer.deaths = 0; + newPlayer.disconnected = false; + this.#players.push(newPlayer); + } this.serverInfoChanged.emit('change'); } - assignPlayer(steamID, team) { - for (let i = 0; i < this._players.length; i++) { - if (this._players[i].steamID == steamID) { - this._players[i].team = team.substr(0, 1); - i = this._players.length; - } + assignPlayer(name, steamID, team) { + if (this.#players.find(x => x.steamID === steamID) == undefined ) { + this.addPlayer({'name': name, 'steamID': steamID }); } + let player = this.#players.find(x => x.steamID === steamID); + player.team = team.substr(0, 1); this.serverInfoChanged.emit('change'); } removePlayer(steamID) { - for (let i = 0; i < this._players.length; i++) { - if (this._players[i].steamID == steamID) { - this._players.splice(i, 1); - i = this._players.length; - } - } + this.#players.find(x => x.steamID === steamID).disconnected = true; + // this.#players.splice(this.#players.findIndex(x => x.steamID === steamID), 1); this.serverInfoChanged.emit('change'); } clearPlayers() { - this._players = []; + this.#players = []; + this.serverInfoChanged.emit('change'); + } + recordKill(killer, victim) { + let killPlayer = this.#players.find(x => x.steamID === killer); + if (killPlayer != undefined) + killPlayer.kills += 1; + let victimPlayer = this.#players.find(x => x.steamID === victim); + if (victimPlayer != undefined) + victimPlayer.deaths += 1; this.serverInfoChanged.emit('change'); } // Methods getAll() { return { - 'map': this._map, + 'map': this.#map, 'mapsAvail': this.mapList(), 'mapsDetails': this.mapDetails(), - 'maxRounds': this._maxRounds, - 'score': this._score, - 'pause': this._pause, - 'players': this._players + 'maxRounds': this.#maxRounds, + 'score': this.#score, + 'pause': this.#pause, + 'players': this.#players }; } newMatch() { - this._score.C = 0; - this._score.T = 0; + this.#score.C = 0; + this.#score.T = 0; + for (let i in this.#players) { + this.#players[i].kills = 0; + this.#players[i].deaths = 0; + } this.serverInfoChanged.emit('change'); } reset() { // Method to be called on server quit. - this._map = ''; - this._mapsAvail = []; - this._mapsDetails = []; - this._maxRounds = 0; - this._pause = false; + this.#map = ''; + this.#mapsAvail = []; + this.#mapsDetails = []; + this.#maxRounds = 0; + this.#pause = false; this.clearPlayers(); this.newMatch(); } -}; +} module.exports = new serverInfo(); \ No newline at end of file diff --git a/modules/sharedFunctions.js b/modules/sharedFunctions.js old mode 100644 new mode 100755 index 60fa19c..386762e --- a/modules/sharedFunctions.js +++ b/modules/sharedFunctions.js @@ -6,7 +6,7 @@ var cfg = require('./configClass.js'); var serverInfo = require('./serverInfo.js'); var controlEmitter = require('./controlEmitter.js'); -rconQ = new queue({ "autostart": true, "timeout": 500, "concurrency": 1 }); +const rconQ = new queue({ "autostart": true, "timeout": 500, "concurrency": 1 }); /** * Authenticate rcon with server @@ -71,101 +71,214 @@ function authenticate() { * Get available maps from server and store them in serverInfo * @return {Promise} - Promise object that yields the result of reload. */ -function reloadMaplist() { - return new Promise((resolve, reject) => { - - function _sendApiRequest(_mapName, mapId) { +async function reloadMaplist() { + return new Promise( async (resolve, reject) => { + function getWorkshopCollection(id) { return new Promise((resolve, reject) => { - let workshopInfo = ''; - - const options = { - hostname: 'api.steampowered.com', - path: '/ISteamRemoteStorage/GetPublishedFileDetails/v1/', - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }; - var steamApiRequest = https.request(options, (res) => { + https.get(`https://api.steampowered.com/IPublishedFileService/GetDetails/v1?key=${cfg.apiToken}&publishedfileids[0]=${id}&includechildren=true`, (res) => { let resData = ''; res.on('data', (dataChunk) => { resData += dataChunk; }); res.on('end', () => { try { - resJSON = JSON.parse(resData); - let previewLink = resJSON.response.publishedfiledetails[0].preview_url; - let title = resJSON.response.publishedfiledetails[0].title; - let workshopID = resJSON.response.publishedfiledetails[0].publishedfileid; - let description = resJSON.response.publishedfiledetails[0].description; - let tags = resJSON.response.publishedfiledetails[0].tags; - resolve({ "name": _mapName, "title": title, "workshopID": workshopID, "description": description, "previewLink": previewLink, "tags": tags }); + let colMaps = [] + let resJson = JSON.parse(resData); + resJson.response.publishedfiledetails[0].children.forEach((colMap) => { + colMaps.push(colMap.publishedfileid); + }) + resolve(colMaps); } catch (e) { - reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); + reject(e); } }); + }).on('error', (error) => { + logger.warn(`Steam Workshop Collection request failed: ${error}`); + reject(error); }); + }); + } - steamApiRequest.on('error', error => { - logger.warn(`steamApiRequest not successful: ${error}`); - reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); + function getMapDetails(mapIDs, official) { + return new Promise((resolve, reject) => { + let idString = ''; + let i = 0; + mapIDs.forEach( (mapId) => { + idString += `&publishedfileids[${i}]=${mapId}`; + i++; }); - steamApiRequest.write(`itemcount=1&publishedfileids%5B0%5D=${mapId}`); - steamApiRequest.end(); + https.get(`https://api.steampowered.com/IPublishedFileService/GetDetails/v1?key=${cfg.apiToken}${idString}&appid=730`, (res) => { + let resData = ''; + let returnDetails = []; + res.on('data', (dataChunk) => { + resData += dataChunk; + }); + res.on('end', () => { + if (res.statusCode != 200) { + logger.warn(`getMapDetails api call failed. Status = ${res.statusCode}`); + reject([]); + } else { + try { + let resJson = JSON.parse(resData); + resJson.response.publishedfiledetails.forEach( details => { + let _mapName = ""; + if (details.filename != "") { + let re = /\S+\/(\S+).bsp/; + let matches = details.filename.match(re); + _mapName = matches[1]; + } + returnDetails.push({ + "name": _mapName, + "official": official, + "title": details.title, + "workshopID": details.publishedfileid.toString(), + "description": details.description, + "previewLink": details.preview_url, + "tags": details.tags }) + }); + resolve(returnDetails); + } catch (e) { + logger.warn(`Reading map details failed: ${e}`); + reject([]); + } + } + }); + }).on('error', (error) => { + logger.warn(`Steam Workshop Maps Request failed: ${error}`); + reject(error); + }); + }); } - // Temporarily work with a static maplist. - executeRcon('maps *').then((answer) => { - const officialMaps = require('../OfficialMaps.json'); - let re = /\(fs\) (\S+).bsp/g; - let maplist = []; - let mapdetails = []; - // let mapsArray = getMatches(answer, re, 1); - let mapsArray = ['cs_italy', 'cs_office', 'de_ancient', 'de_anubis', - 'de_dust2', 'de_inferno', 'de_mirage', 'de_nuke', - 'de_overpass', 'de_vertigo'] - let promises = []; - mapsArray.forEach((mapString) => { - let mapName = cutMapName(mapString); - maplist.push(mapName); - if (mapString.includes('workshop/')) { - let mapIdRegex = /workshop\/(\d+)\//; - let workshopId = mapString.match(mapIdRegex)[1]; - promises.push(_sendApiRequest(mapName, workshopId)); - } else { - let workshopId = officialMaps[mapName]; - if (workshopId != undefined) { - promises.push(_sendApiRequest(mapName, workshopId)); - } else { - mapdetails.push({ "name": mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); - } - } + function getWorkshopCollectionMapsFromServer() { + return new Promise((resolve, reject) => { + executeRcon('ds_workshop_listmaps ').then((response) => { + let mapArray = response.split(/\r?\n/); + let details = []; + mapArray.forEach((value) => { + mapdetails.push({ + "name": value, + "official": false, + "title": value, + "workshopID": "", + "description": "", + "previewLink": "", + "tags": [], + }); + }); + + resolve(details); + }).catch((err) => { + logger.warn(`Could not get workshop collection maps from server: ${err}`); + reject(err); + }); }); - Promise.allSettled(promises).then((results) => { - results.forEach((result) => { - mapdetails.push(result.value) - }) - - mapdetails.sort((a, b) => a.name.localeCompare(b.name)); - maplist.sort(); - // Only return, if list has at least one item. - logger.debug(`Maps found: ${maplist.length}`); - if (maplist.length > 0) { - logger.debug('Saving Maplist to ServerInfo'); - serverInfo.mapsAvail = maplist; - serverInfo.mapsDetails = mapdetails; - resolve({ "success": true }); - } else { - reject({ "success": false }); - } + } + + + + // Available maps will be built from OfficialMaps.json static file, + // workshop collection and mapsfrom config. + let officialMapIds = []; + let workshopMapIds = []; + let mapdetails = []; + + let omJson = require('../OfficialMaps.json'); + + omJson.forEach( (om) => { + officialMapIds.push(om.id); + }) + + logger.debug("getting official maps"); + + try { + mapdetails = await getMapDetails(officialMapIds, true); + } catch(error) { + logger.warn(`Getting official maps details failed: ${error}`); + logger.warn('Falling back to name and ID only'); + // As fallback use name and id from local file. + let alternateDetails = []; + omJson.forEach( (map) => { + alternateDetails.push( { + "name": map.name, + "official": true, + "title": map.name, + "workshopID": map.id, + "description": "", + "previewLink": "", + "tags": [], + }); }); - }).catch((err) => { - logger.warn(`Error executing maps rcon: ${err.message}`); - reject({ "success": false }); - }); + mapdetails = alternateDetails; + } + + if (cfg.workshopCollection != '') { + logger.debug("getting collection ids"); + try { + workshopMapIds = await getWorkshopCollection(cfg.workshopCollection); + } catch (error) { + logger.warn(`Getting Workshop map IDs failed: ${error} +Trying to get names from server.`); + // As a fallback try to get workshop maps from server via rcon command. + try { + mapdetails.push(...await getWorkshopCollectionMapsFromServer()); + } catch (err) { + logger.warn(`Loading workshop maps from server failed: ${err} +Workshop maps not available.`); + } + } + } + workshopMapIds.push(...cfg.workshopMaps); + + if(workshopMapIds.length > 0) { + logger.debug("getting workshop maps details"); + try { + mapdetails.push(...await getMapDetails(workshopMapIds, false)); + } catch(error) { + logger.warn(`Getting Workshop maps details failed: ${error}`); + // As a fallback try to get workshop maps from server via rcon command. + try { + mapdetails.push(...await getWorkshopCollectionMapsFromServer()); + } catch (err) { + logger.warn(`Loading workshop maps from server failed: ${err} +Workshop maps not available.`); + } + } + } + if (mapdetails.length > 1) { + mapdetails.sort((a, b) => a.title.localeCompare(b.title)); + } + + serverInfo.mapsDetails = mapdetails; + // TODO: Check if this is still needed. + // serverInfo.mapsAvail = maplist; + if(mapdetails.length > 0) { + logger.info('Maps reloaded'); + resolve({ "success": true }); + } else { + logger.warn('Update maps failed: Maplist is empty.'); + reject( {"success": false}); + } }); } +/** + * Checks if a map is available on the server or not + * @param {string/int} map - a filename, title or workshopID + * @return {boolean} if the map was found in the details. + */ +function getMap(mapToFind) { + let returnMap = undefined; + serverInfo.mapsDetails.forEach( (map) => { + if (map.workshopID == mapToFind || map.name == mapToFind || map.title == mapToFind) { + returnMap = map; + } + }) + return returnMap; +} + /** * Executes a rcon command * @param {string} message - The rcon command to execute @@ -188,23 +301,6 @@ function executeRcon(message) { } /*------------------------- Helper Functions ----------------------------*/ -/** - * Extracts all matches for a regex. - * @param {string} string - String to search. - * @param {regex} regex - Regex to execute on the string. - * @param {integer} index - Optional index which capturing group should be retreived. - * @returns {string[]} matches - Array holaction the found matches. - */ -function getMatches(string, regex, index) { - index || (index = 1); // default to the first capturing group - var matches = []; - var match; - while (match = regex.exec(string)) { - matches.push(match[index]); - } - return matches; -} - /** * Cuts the bare map-name from the various representations in the servers responses. * @param {string} mapstring - The response of mapname(s) from rcon. @@ -212,8 +308,8 @@ function getMatches(string, regex, index) { */ function cutMapName(mapstring) { if (mapstring.search('workshop') != -1) { - re = /(\w+)/g; - matches = mapstring.match(re); + let re = /(\w+)/g; + let matches = mapstring.match(re); mapstring = matches[2]; } if (mapstring.search(".bsp") != -1) { @@ -237,4 +333,4 @@ function queryMaxRounds() { }); } -module.exports = { authenticate, reloadMaplist, executeRcon, cutMapName, queryMaxRounds }; \ No newline at end of file +module.exports = { authenticate, reloadMaplist, getMap, executeRcon, cutMapName, queryMaxRounds }; \ No newline at end of file diff --git a/package.json b/package.json index 957191c..bf227e4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "version": "2.0.0", "main": "serverControl.js", "directories": { - "example": "example" + "public": "public", + "modules": "modules" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/public/js/gameserver.js b/public/js/gameserver.js index 8f6b0ae..d5d1fb0 100644 --- a/public/js/gameserver.js +++ b/public/js/gameserver.js @@ -1,318 +1,315 @@ -// Change here if you don't host the webInterfae on the same host as the NodeJS API -var host = window.location.hostname; -var protocol = window.location.protocol -var address = `${protocol}//${host}:8090/csgoapi`; -var apiPath = `${address}/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', - 'pause': 'Pausing/Unpausing match' -} -var running = false; -var authenticated = false; - -// 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", - url: address, - data: data, - cache: false, - crossDomain: true, - xhrFields: { - withCredentials: true - }, - success: callback - }); -} - -// Load the maplist for serverstart from maplist.txt -function loadMaplist() { - // The Maplist file can be taken from the csgo folder. - $.get(maplistFile, (data) => { - let lines = data.split(/\r\n|\n/); - lines.forEach((map) => { - $("#mapAuswahl").append(``); - }); - }); -} - -// Setup the Elements according to server status. -function setupPage() { - $('#popupCaption').text('Querying Server'); - getPromise = (path) => { - return Promise.resolve(sendGet(`${address}/${path}`)); - } - - let loginCheck = getPromise('loginStatus'); - loginCheck.then((data) => { - if (data.login) { - let authenticated = getPromise('v1.0/info/rconauthstatus'); - authenticated.then((data) => { - if (data.rconauth) { - setupServerRunning(); - } else { - let serverRunning = getPromise('v1.0/info/runstatus'); - serverRunning.then((data) => { - if (data.running) { - window.location.href = './notauth.htm'; - } else { - setupServerStopped(); - } - }); - } - }).catch((error) => { - setupServerStopped(); - }); - } else { - setupNotLoggedIn(); - } - }).catch((error) => { - setupNotLoggedIn(); - }); - - $('#container-popup').css('display', 'none'); -} - -function setupNotLoggedIn() { - $('#power-image').hide(0); - $('#startMap').hide(0); - $('#buttonStop').hide(0); - $('#buttonStart').hide(0); - $('#buttonUpdate').hide(0); - $('#buttonLogin').show(0); - $('#addControl').hide(0); - $('#serverInfo').hide(0); - $('#mapControl').hide(0); -} - -function setupServerRunning() { - $('#power-image').attr('src', 'pic/power-on.png'); - if (socket.readyState != 1) { // if websocket not connected - getMaps(); - } else if ($("#mapSelector div").length < 2) { - socket.send('infoRequest'); - } - $('#startMap').hide(0); - $('#buttonStop').show(0); - $('#buttonStart').hide(0); - $('#buttonUpdate').hide(0); - $('#buttonLogin').hide(0); - $('#addControl').show(0); - $('#serverInfo').css('display', 'flex'); - $('#mapControl').show(0); -} - -function setupServerStopped() { - $('#power-image').attr('src', 'pic/power-off.png'); - $('#startMap').show(0); - $('#buttonStart').show(0); - $('#buttonStop').hide(0); - $('#buttonUpdate').show(0); - $('#buttonLogin').hide(0); - $('#serverInfo').hide(0); - $('#addControl').hide(0); - $('#mapControl').hide(0); - $('#mapSelector').hide('fast'); -} - -function clickButton(aButton) { - action = aButton.value.toLowerCase(); - $('#popupCaption').text(`${titles[action]}`); - $('#popupText').text('Moment bitte!'); - $('#container-popup').css('display', 'flex'); - startMap = document.getElementById('mapAuswahl').value; - - sendGet(`${apiPath}/control/${action}`, `startmap=${startMap}`).done((data) => { - if (socket.readyState != 1) { // if websocket not connected - if (action != 'update') { - setupPage(); - } - $('#container-popup').hide(); - } - }).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}`); - window.location.href = './notauth.htm'; - } - if (socket.readyState != 1) { - $('#container-popup').css('display', 'none'); - } - }); -} - -function showPlayerMenu(event) { - $('#playerDropdown').css({ 'top': event.pageY, 'left': event.pageX, 'display': 'block' }); - $('#playerDropdown').attr('player', event.target.textContent); - // Close the dropdown menu if the user clicks outside of it - window.onclick = function(event) { - if (!event.target.matches('.dropbtn')) { - $('#playerDropdown').css('display', 'none'); - window.onclick = ''; - } - } -} - -function movePlayer(event) { - // This function uses sourcemod plugin "moveplayers" -> https://forums.alliedmods.net/showthread.php?p=2471466 - /* "sm_movect" - Move a player to the counter-terrorist team. - "sm_movespec" - Move a player to the spectators team. - "sm_movet" - Move a player to the terrorist team. */ - let player = event.target.parentElement.getAttribute('player') - let command = event.target.getAttribute('command'); - sendGet(`${apiPath}/rcon`, `message=sm_move${command} "${player}"`, (data) => { - // no actions for now. - }); -} - -function getMaps() { - function getServerInfo() { - return Promise.resolve(sendGet(`${apiPath}/info/serverInfo`)); - } - let serverInfo = getServerInfo(); - serverInfo.then((data) => { - $("#currentMap").html(`Current map: ${data.map}`); - maplist = data.mapsDetails; - $("#mapSelector").empty(); - maplist.forEach((map) => { - if ('content' in document.createElement('template')) { - var mapDiv = document.querySelector('#maptemplate'); - mapDiv.content.querySelector('.mapname').textContent = map.name; - mapDiv.content.querySelector('.mapimg').setAttribute("src", map.previewLink); - $('#mapSelector').append(document.importNode(mapDiv.content, true)); - } else { - let alttext = createElement('h2'); - text.html("Your browser does not have HTML template support - please use another browser."); - $('#mapSelector').append(alttext); - } - }); - }).catch((error) => { - // do nothing for now - }); -} - -function toggleMaplist() { - $('#mapSelector').toggle('fast'); -} - -function showPlay(event) { - if (event.currentTarget.classList.contains('active')) { - changeMap(event); - $('.map').removeClass('active'); - } else { - $('.active > .playicon').hide(0); - $('.active').removeClass('active'); - event.currentTarget.classList.add('active'); - event.currentTarget.children[1].style.display = 'block'; - } -} - -function changeMap(event) { - let map = event.currentTarget.firstElementChild.textContent; - $('#mapSelector').hide('fast'); - $('#popupCaption').text(titles['mapchange']); - $('#container-popup').css('display', 'flex'); - sendGet(`${apiPath}/control/changemap`, `map=${map}`, (data) => { - if (data.success) { - $('#popupText').html(`Changing map to ${map}`); - } else { - $('#popupText').html(`Mapchange failed!`); - window.setTimeout(() => { - $('#container-popup').css('display', 'none'); - window.location.href = './notauth.htm'; - }, 2000); - - } - }); -} - -function restartRound() { - sendGet(`${apiPath}/rcon`, `message=mp_restartgame 1`, (data) => { - $('#popupCaption').text(`Restart Round`); - $('#popupText').html(`Round Restarted!`); - $('#container-popup').css('display', 'flex'); - window.setTimeout(() => { - $('#container-popup').css('display', 'none'); - }, 1000); - }); -} - -function pauseGame() { - sendGet(`${apiPath}/control/pause`).done((data) => { - if (data.success) { - if (socket.readyState != 1) { // if websocket not connected - $('#pause-overlay').css('top', $('#serverControl').position().top); - $('#pause-overlay').css('height', $('#serverInfo').height() + $('#serverControl').height()); - $('#pause-overlay').css('display', 'flex'); - } - } else { - alert('Pausing the match failed!'); - } - }); -} - -function resumeGame() { - sendGet(`${apiPath}/control/unpause`).done((data) => { - if (data.success) { - if (socket.readyState != 1) { // if websocket not connected - $('#pause-overlay').hide(); - } - } else { - alert('Unpausing the match failed!'); - } - }); -} - -function authenticate(caller) { - sendGet(`${apiPath}/authenticate`).done((data) => { - if (data.authenticated) { - window.location.href = './gameserver.htm'; - } else { - caller.disabled = true; - $('#autherror').show('fast'); - } - }); -} - -function kill(caller) { - - sendGet(`${apiPath}/control/kill`).done((data) => { - window.location.href = './gameserver.htm'; - }).fail((error) => { - caller.disabled = true; - $('#killerror').show('fast'); - }); -} - - -// Bot Training functions -function setBotRules() { - sendGet(`${apiPath}/rcon`, `message=mp_autoteambalance 0`); - sendGet(`${apiPath}/rcon`, `message=mp_limitteams 0`); - sendGet(`${apiPath}/rcon`, `message=bot_difficulty 3`); -} - -function addBots(team, quantity) { - for (let i = 0; i < quantity; i++) { - setTimeout(sendGet(`${apiPath}/rcon`, `message=bot_add_${team}`), 100); - } -} - -function kickBots() { - sendGet(`${apiPath}/rcon`, `message=bot_kick all`); +// Change here if you don't host the webInterfae on the same host as the NodeJS API +var host = window.location.hostname; +var protocol = window.location.protocol +var address = `${protocol}//${host}:8090/csgoapi`; +var apiPath = `${address}/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', + 'pause': 'Pausing/Unpausing match' +} + +// 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", + url: address, + data: data, + cache: false, + crossDomain: true, + xhrFields: { + withCredentials: true + }, + success: callback + }); +} + +// Load the maplist for serverstart from maplist.txt +function loadMaplist() { + // The Maplist file can be taken from the csgo folder. + $.get(maplistFile, (data) => { + let lines = data.split(/\r\n|\n/); + lines.forEach((map) => { + $("#mapAuswahl").append(``); + }); + }); +} + +// Setup the Elements according to server status. +function setupPage() { + $('#popupCaption').text('Querying Server'); + let getPromise = (path) => { + return Promise.resolve(sendGet(`${address}/${path}`)); + } + + let loginCheck = getPromise('loginStatus'); + loginCheck.then((data) => { + if (data.login) { + let authenticated = getPromise('v1.0/info/rconauthstatus'); + authenticated.then((data) => { + if (data.rconauth) { + setupServerRunning(); + } else { + let serverRunning = getPromise('v1.0/info/runstatus'); + serverRunning.then((data) => { + if (data.running) { + window.location.href = './notauth.htm'; + } else { + setupServerStopped(); + } + }); + } + }).catch(() => { + setupServerStopped(); + }); + } else { + setupNotLoggedIn(); + } + }).catch(() => { + setupNotLoggedIn(); + }); + + $('#container-popup').css('display', 'none'); +} + +function setupNotLoggedIn() { + $('#power-image').hide(0); + $('#startMap').hide(0); + $('#buttonStop').hide(0); + $('#buttonStart').hide(0); + $('#buttonUpdate').hide(0); + $('#buttonLogin').show(0); + $('#addControl').hide(0); + $('#serverInfo').hide(0); + $('#mapControl').hide(0); +} + +function setupServerRunning() { + $('#power-image').attr('src', 'pic/power-on.png'); + if (socket.readyState != 1) { // if websocket not connected + getMaps(); + } else if ($("#mapSelector div").length < 2) { + socket.send('infoRequest'); + } + $('#startMap').hide(0); + $('#buttonStop').show(0); + $('#buttonStart').hide(0); + $('#buttonUpdate').hide(0); + $('#buttonLogin').hide(0); + $('#addControl').show(0); + $('#serverInfo').css('display', 'flex'); + $('#mapControl').show(0); +} + +function setupServerStopped() { + $('#power-image').attr('src', 'pic/power-off.png'); + $('#startMap').show(0); + $('#buttonStart').show(0); + $('#buttonStop').hide(0); + $('#buttonUpdate').show(0); + $('#buttonLogin').hide(0); + $('#serverInfo').hide(0); + $('#addControl').hide(0); + $('#mapControl').hide(0); + $('#mapSelector').hide('fast'); +} + +function clickButton(aButton) { + let action = aButton.value.toLowerCase(); + $('#popupCaption').text(`${titles[action]}`); + $('#popupText').text('Moment bitte!'); + $('#container-popup').css('display', 'flex'); + let startMap = document.getElementById('mapAuswahl').value; + + sendGet(`${apiPath}/control/${action}`, `startmap=${startMap}`).done((data) => { + if (socket.readyState != 1) { // if websocket not connected + if (action != 'update') { + setupPage(); + } + $('#container-popup').hide(); + } + }).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}`); + window.location.href = './notauth.htm'; + } + if (socket.readyState != 1) { + $('#container-popup').css('display', 'none'); + } + }); +} + +function showPlayerMenu(event) { + $('#playerDropdown').css({ 'top': event.pageY, 'left': event.pageX, 'display': 'block' }); + $('#playerDropdown').attr('player', event.target.textContent); + // Close the dropdown menu if the user clicks outside of it + window.onclick = function(event) { + if (!event.target.matches('.dropbtn')) { + $('#playerDropdown').css('display', 'none'); + window.onclick = ''; + } + } +} + +function movePlayer(event) { + // This function uses sourcemod plugin "moveplayers" -> https://forums.alliedmods.net/showthread.php?p=2471466 + /* "sm_movect" - Move a player to the counter-terrorist team. + "sm_movespec" - Move a player to the spectators team. + "sm_movet" - Move a player to the terrorist team. */ + let player = event.target.parentElement.getAttribute('player') + let command = event.target.getAttribute('command'); + sendGet(`${apiPath}/rcon`, `message=sm_move${command} "${player}"`, (data) => { + // no actions for now. + }); +} + +function getMaps() { + function getServerInfo() { + return Promise.resolve(sendGet(`${apiPath}/info/serverInfo`)); + } + let serverInfo = getServerInfo(); + serverInfo.then((data) => { + $("#currentMap").html(`Current map: ${data.map}`); + maplist = data.mapsDetails; + $("#mapSelector").empty(); + maplist.forEach((map) => { + if ('content' in document.createElement('template')) { + var mapDiv = document.querySelector('#maptemplate'); + mapDiv.content.querySelector('.mapname').textContent = map.title; + mapDiv.content.querySelector('.mapimg').setAttribute("src", map.previewLink); + mapDiv.content.querySelector('.map').setAttribute("id", map.workshopID); + $('#mapSelector').append(document.importNode(mapDiv.content, true)); + } else { + let alttext = createElement('h2'); + text.html("Your browser does not have HTML template support - please use another browser."); + $('#mapSelector').append(alttext); + } + }); + }).catch((error) => { + // do nothing for now + }); +} + +function toggleMaplist() { + $('#mapSelector').toggle('fast'); +} + +function showPlay(event) { + if (event.currentTarget.classList.contains('active')) { + changeMap(event); + $('.map').removeClass('active'); + } else { + $('.active > .playicon').hide(0); + $('.active').removeClass('active'); + event.currentTarget.classList.add('active'); + event.currentTarget.children[1].style.display = 'block'; + } +} + +function changeMap(event) { + let map = event.currentTarget.firstElementChild.textContent; + $('#mapSelector').hide('fast'); + //$('#popupCaption').text(titles['mapchange']); + //$('#container-popup').css('display', 'flex'); + sendGet(`${apiPath}/control/changemap`, `map=${map}`, (data) => { + if (data.success) { + $('#popupText').html(`Changing map to ${map}`); + } else { + $('#popupText').html(`Mapchange failed!`); + window.setTimeout(() => { + $('#container-popup').css('display', 'none'); + }, 2000); + + } + }); +} + +function restartRound() { + sendGet(`${apiPath}/rcon`, `message=mp_restartgame 1`, (data) => { + $('#popupCaption').text(`Restart Round`); + $('#popupText').html(`Round Restarted!`); + $('#container-popup').css('display', 'flex'); + window.setTimeout(() => { + $('#container-popup').css('display', 'none'); + }, 1000); + }); +} + +function pauseGame() { + sendGet(`${apiPath}/control/pause`).done((data) => { + if (data.success) { + if (socket.readyState != 1) { // if websocket not connected + $('#pause-overlay').css('top', $('#serverControl').position().top); + $('#pause-overlay').css('height', $('#serverInfo').height() + $('#serverControl').height()); + $('#pause-overlay').css('display', 'flex'); + } + } else { + alert('Pausing the match failed!'); + } + }); +} + +function resumeGame() { + sendGet(`${apiPath}/control/unpause`).done((data) => { + if (data.success) { + if (socket.readyState != 1) { // if websocket not connected + $('#pause-overlay').hide(); + } + } else { + alert('Unpausing the match failed!'); + } + }); +} + +function authenticate(caller) { + sendGet(`${apiPath}/authenticate`).done((data) => { + if (data.authenticated) { + window.location.href = './gameserver.htm'; + } else { + caller.disabled = true; + $('#autherror').show('fast'); + } + }); +} + +function kill(caller) { + sendGet(`${apiPath}/control/kill`).done((data) => { + window.location.href = './gameserver.htm'; + }).fail((error) => { + caller.disabled = true; + $('#killerror').show('fast'); + }); +} + + +// Bot Training functions +function setBotRules() { + sendGet(`${apiPath}/rcon`, `message=mp_autoteambalance 0`); + sendGet(`${apiPath}/rcon`, `message=mp_limitteams 0`); + sendGet(`${apiPath}/rcon`, `message=bot_difficulty 3`); +} + +function addBots(team, quantity) { + for (let i = 0; i < quantity; i++) { + setTimeout(sendGet(`${apiPath}/rcon`, `message=bot_add_${team}`), 100); + } +} + +function kickBots() { + sendGet(`${apiPath}/rcon`, `message=bot_kick all`); } \ No newline at end of file diff --git a/public/js/onload.js b/public/js/onload.js index 110f4c0..c1f3c90 100644 --- a/public/js/onload.js +++ b/public/js/onload.js @@ -1,107 +1,121 @@ -// what to do after document is loaded. -var socket = null; -$(document).ready(() => { - let startSocket = () => { - try { - if (protocol == 'https:') { - socket = new WebSocket(`wss://${host}:8091`); - } else { - socket = new WebSocket(`ws://${host}:8091`); - } - } catch (err) { - console.error('Connection to websocket failed:\n' + err); - } - - socket.onopen = () => { - socket.send('infoRequest'); - } - - socket.onmessage = (e) => { - let data = JSON.parse(e.data); - - if (data.type == "serverInfo") { - let serverInfo = data.payload; - - $("#currentMap").html(`Current map: ${serverInfo.map}`); - $('#scoreT').text(serverInfo.score.T); - $('#scoreCT').text(serverInfo.score.C); - $('#rounds').html( - `Rounds: ${serverInfo.maxRounds} / Left: ${serverInfo.maxRounds - (serverInfo.score.T + serverInfo.score.C)}` - ); - - $('.playerDiv ul').empty(); - $('.playerDiv').hide(0); - if (serverInfo.players.length > 0) { - for (let i = 0; i < serverInfo.players.length; i++) { - let player = serverInfo.players[i]; - $(`#${player.team.toLowerCase()}List`).append(`
  • ${player.name}
  • `); - $(`#${player.team.toLowerCase()}Players`).show(0); - } - } - if (serverInfo.pause) { - $('#pause-overlay').css('top', $('#serverControl').position().top); - $('#pause-overlay').css('height', $('#serverInfo').height() + $('#serverControl').height()); - $('#pause-overlay').css('display', 'flex'); - } else { - $('#pause-overlay').hide(); - } - if ($('#mapSelector .map').length != serverInfo.mapsDetails.length) { - if (serverInfo.mapsDetails) { - let maplist = serverInfo.mapsDetails; - $("#mapSelector").empty(); - maplist.forEach((map) => { - if ('content' in document.createElement('template')) { - var mapDiv = document.querySelector('#maptemplate'); - mapDiv.content.querySelector('.mapname').textContent = map.name; - mapDiv.content.querySelector('.mapimg').setAttribute("src", map.previewLink ? map.previewLink : ''); - $('#mapSelector').append(document.importNode(mapDiv.content, true)); - } else { - let alttext = document.createElement('h2'); - text.html("Your browser does not have HTML template support - please use another browser."); - $('#mapSelector').append(alttext); - } - }); - } - } - } 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') { - $('#popupText').html(`${data.payload.operation} success!`); - setTimeout(() => { - $('#container-popup').css('display', 'none'); - setupPage(); - }, 1500); - } else if (data.payload.state == 'fail') { - $('#popupText').html(`${data.payload.operation} failed!`); - setTimeout(() => { - $('#container-popup').css('display', 'none'); - if (data.payload.operation != 'update') { - window.location.href = './notauth.htm'; - } - }, 3000); - } - } else if (data.type == "progress") { - $('#popupText').html(`${data.payload.step}: ${data.payload.progress}%`); - } else if (data.type == "mapchange") { - if (data.payload.success$ && $('#popupCaption').text() == 'Changing Map') { - socket.send('infoRequest'); - $('#container-popup').css('display', 'none'); - } else if (!data.payload.success) { - $('#popupText').html(`Mapchange failed!`); - } - } - } - - socket.onclose = () => { - // connection closed, discard old websocket and create a new one in 5s - socket = null; - setTimeout(startSocket, 5000); - } - } - startSocket(); - loadMaplist(); - setupPage(); +// what to do after document is loaded. +var socket = null; +$(document).ready(() => { + let startSocket = () => { + try { + if (protocol == 'https:') { + socket = new WebSocket(`wss://${host}:8091`); + } else { + socket = new WebSocket(`ws://${host}:8091`); + } + } catch (err) { + console.error('Connection to websocket failed:\n' + err); + } + + socket.onopen = () => { + socket.send('infoRequest'); + } + + socket.onmessage = (e) => { + let data = JSON.parse(e.data); + + if (data.type == "serverInfo") { + let serverInfo = data.payload; + + $("#currentMap").html(`Current map: ${serverInfo.map}`); + $('#scoreT').text(serverInfo.score.T); + $('#scoreCT').text(serverInfo.score.C); + $('#rounds').html( + `Rounds: ${serverInfo.maxRounds} / Left: ${serverInfo.maxRounds - (serverInfo.score.T + serverInfo.score.C)}` + ); + + $('.playerDiv ul').empty(); + $('.playerDiv').hide(0); + if (serverInfo.players.length > 0) { + for (let i = 0; i < serverInfo.players.length; i++) { + let player = serverInfo.players[i]; + if (player.disconnected) { + break; + } + if ('content' in document.createElement('template')) { + var playerLi = document.querySelector('#playerTemplate'); + playerLi.content.querySelector('.playerName').textContent = player.name; + playerLi.content.querySelector('.playerKills').textContent = `K: ${player.kills}`; + playerLi.content.querySelector('.playerDeaths').textContent = `D: ${player.deaths}`; + $(`#${player.team.toLowerCase()}List`).append(document.importNode(playerLi.content, true)); + } else { + let alttext = document.createElement('li'); + alttext.html("Your browser does not have HTML template support - please use another browser."); + $(`#${player.team.toLowerCase()}List`).append(alttext); + } + $(`#${player.team.toLowerCase()}Players`).show(0); + } + } + if (serverInfo.pause) { + $('#pause-overlay').css('top', $('#serverControl').position().top); + $('#pause-overlay').css('height', $('#serverInfo').height() + $('#serverControl').height()); + $('#pause-overlay').css('display', 'flex'); + } else { + $('#pause-overlay').hide(); + } + if ($('#mapSelector .map').length != serverInfo.mapsDetails.length) { + if (serverInfo.mapsDetails) { + let maplist = serverInfo.mapsDetails; + $("#mapSelector").empty(); + maplist.forEach((map) => { + if ('content' in document.createElement('template')) { + var mapDiv = document.querySelector('#maptemplate'); + mapDiv.content.querySelector('.mapname').textContent = map.title; + mapDiv.content.querySelector('.mapimg').setAttribute("src", map.previewLink ? map.previewLink : ''); + mapDiv.content.querySelector('.map').setAttribute("id", map.workshopID); + $('#mapSelector').append(document.importNode(mapDiv.content, true)); + } else { + let alttext = document.createElement('h2'); + alttext.html("Your browser does not have HTML template support - please use another browser."); + $('#mapSelector').append(alttext); + } + }); + } + } + } 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') { + $('#popupText').html(`${data.payload.operation} success!`); + setTimeout(() => { + $('#container-popup').css('display', 'none'); + setupPage(); + }, 1500); + } else if (data.payload.state == 'fail') { + $('#popupText').html(`${data.payload.operation} failed!`); + setTimeout(() => { + $('#container-popup').css('display', 'none'); + if (data.payload.operation != 'update' && data.payload.operation != 'mapchange') { + window.location.href = './notauth.htm'; + } + }, 3000); + } + } else if (data.type == "progress") { + $('#popupText').html(`${data.payload.step}: ${data.payload.progress}%`); + } else if (data.type == "mapchange") { + if (data.payload.success$ && $('#popupCaption').text() == 'Changing Map') { + socket.send('infoRequest'); + $('#container-popup').css('display', 'none'); + } else if (!data.payload.success) { + $('#popupText').html(`Mapchange failed!`); + } + } + } + + socket.onclose = () => { + // connection closed, discard old websocket and create a new one in 5s + socket = null; + setTimeout(startSocket, 5000); + } + } + startSocket(); + loadMaplist(); + setupPage(); }); \ No newline at end of file diff --git a/public/notauth.htm b/public/notauth.htm index 127b37c..292fe04 100644 --- a/public/notauth.htm +++ b/public/notauth.htm @@ -1,36 +1,36 @@ - - - - Klosser Gameserver - - - - - -

    Server not running correctly

    -

    - The CS:GO Server is started, but RCON is not authenticated.
    - You can either try to authenticate again or kill the server-process to start over. -

    -

    - The attempt was also unsuccessful. See your server logs for details and try to fix there. -

    -

    - Kill command failed! Further troubleshooting needs to take place on your server. -

    - -
    - - -
    - - + + + + Klosser Gameserver + + + + + +

    Server not running correctly

    +

    + The CS:GO Server is started, but RCON is not authenticated.
    + You can either try to authenticate again or kill the server-process to start over. +

    +

    + The attempt was also unsuccessful. See your server logs for details and try to fix there. +

    +

    + Kill command failed! Further troubleshooting needs to take place on your server. +

    + +
    + + +
    + + \ No newline at end of file diff --git a/serverControl.js b/serverControl.js old mode 100644 new mode 100755 index 7e07f46..819ca3b --- a/serverControl.js +++ b/serverControl.js @@ -1,7 +1,7 @@ /** * @file CS:GO Dedicated Server Control * @author Markus Adrario - * @version 1.0 + * @version 2.0 * @requires express * @requires express-session * @requires express-rate-limit @@ -11,8 +11,6 @@ * @requires http * @requires https * @requires ws - * @requires url - * @requires events * @requires child_process * @requires rcon-srcds * @requires ./modules/logger.js @@ -29,9 +27,7 @@ const passport = require('passport'); const SteamStrategy = require('passport-steam').Strategy; const BasicStrategy = require('passport-http').BasicStrategy; const webSocket = require('ws'); -const url = require('url'); const fs = require('fs'); -const events = require('events'); const { exec } = require('child_process'); const logger = require('./modules/logger.js'); var serverInfo = require('./modules/serverInfo.js'); @@ -59,6 +55,7 @@ if (cfg.useHttps) { exec('/bin/ps -A', (error, stdout, stderr) => { if (error) { logger.error(`exec error: ${error}`); + logger.error(stderr) return; } if (stdout.match(/cs2/) != null) { @@ -66,7 +63,8 @@ exec('/bin/ps -A', (error, stdout, stderr) => { logger.verbose('Found running server'); sf.authenticate().then((data) => { logger.verbose(`authentication ${data.authenticated}`); - sf.executeRcon(`logaddress_add_http "http://${cfg.localIp}:${cfg.logPort}/log`) + sf.executeRcon(`logaddress_add_http "http://${cfg.localIp}:${cfg.logPort}/log`); + sf.executeRcon(`host_workshop_collection ${cfg.workshopCollection}`); }).catch((data) => { logger.verbose(`authentication ${data.authenticated}`); }); @@ -94,10 +92,10 @@ controlEmitter.on('exec', (operation, action) => { let mapstring = matches[1]; serverInfo.map = sf.cutMapName(mapstring); }); - sf.reloadMaplist().then((answer) => { - logger.info('Maps reloaded'); + sf.reloadMaplist().then(() => { + // Be happy and do nothing }).catch((err) => { - logger.warn("Maps could not be loaded"); + logger.warn(`Maps could not be loaded: ${err}`); }); sf.queryMaxRounds(); } @@ -199,7 +197,7 @@ function ensureAuthenticated(req, res, next) { */ app.get('/csgoapi/login', passport.authenticate('steam'), - (req, res) => { + () => { // The request will be redirected to Steam for authentication, so // this function will not be called. } @@ -229,11 +227,18 @@ app.get('/csgoapi/login/return', */ app.get('/csgoapi/logout', (req, res) => { logger.http({ - 'user': `${steamID64}`, + 'user': `${req.user.identifier}`, 'message': 'logged out' }); - req.logout(); - res.redirect(cfg.redirectPage); + req.logout((err) => { + if (err) { + logger.warn({ + 'user': `${req.user.identifier}`, + 'message': `logout failed: ${err}` + }); + } + res.redirect(cfg.redirectPage); + }); }); /** @@ -292,25 +297,27 @@ if (cfg.httpAuth) { } //--------------------- END Basic authentication --------------------------// +let server; if (cfg.useHttps) { - var server = http.createServer(httpsCredentials, app); + server = http.createServer(httpsCredentials, app); } else { - var server = http.createServer(app); + server = http.createServer(app); } server.listen(cfg.apiPort); //------------------------------- Log receiver ----------------------------// -logreceive = express(); +var logreceive = express(); logreceive.use(express.text({ limit: "50mb" })); +let logserver; if (cfg.useHttps) { - loghttp = require('http'); + let loghttp = require('http'); logserver = loghttp.createServer(logreceive); } else { logserver = http.createServer(logreceive); } -logroute = require('./modules/logreceive.js'); +const logroute = require('./modules/logreceive.js'); logreceive.use('/', logroute); logserver.listen(cfg.logPort, () => { @@ -320,9 +327,20 @@ logserver.listen(cfg.logPort, () => { /*----------------- WebSockets Code -------------------*/ if (cfg.webSockets) { - const wssServer = http.createServer(httpsCredentials); + let wssServer + if (cfg.useHttps) { + wssServer = http.createServer(httpsCredentials); + } else { + wssServer = http.createServer(); + } const wss = new webSocket.Server({ server: wssServer }); + wssServer.listen(cfg.socketPort, () => { + let host = cfg.host; + logger.verbose(host); + }); + + /** * Websocket to send data updates to a webClient. * @listens ws#connection @@ -389,17 +407,7 @@ if (cfg.webSockets) { serverInfo.serverInfoChanged.removeListener('change', sendUpdate); controlEmitter.removeListener('exec', sendControlNotification); controlEmitter.removeListener('progress', reportProgress); + logger.info(`websocket closed with code ${code}. Reason: ${reason}`); }); }); - - wssServer.listen(cfg.socketPort, () => { - let host = cfg.host; - logger.verbose(host); - - if (cfg.useHttps) { - const ws = new webSocket(`wss://${host}:${wssServer.address().port}`); - } else { - const ws = new webSocket(`ws://${host}:${wssServer.address().port}`); - } - }); }