From ca62e47b897dffc90a26847bf3439ccf5143fe11 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Sun, 10 Apr 2022 15:42:39 +0200 Subject: [PATCH 1/8] Update README.MD Small updates on readme --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index c8f1fcf..4d0a0dd 100644 --- a/README.MD +++ b/README.MD @@ -6,7 +6,7 @@ It exposes control of your server and shell functions to the internet. Although I strongly adivise to use secure connections to prevent possible man-in-the-middle attacks. ## NOTE: -This Release (v0.8) changes the API end Points to facilitate versioning of the API. +Release 0.8 changed the API end Points to facilitate versioning of the API. The old Endpoints will be available for some time to allow for a transition time. I expect to remove them with Version 1.0. ### Breaking changes: From 23b83d8f3e38aea81a6367a6c20f2074e06208e1 Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Sun, 10 Apr 2022 23:31:00 +0200 Subject: [PATCH 2/8] Refactor and modularize code, remove old api 0.X --- README.MD | 21 +- config.js | 237 ++--- example/js/gameserver.js | 69 +- modules/apiV10.js | 620 +++++++++++ modules/configClass.js | 95 ++ modules/controlEmitter.js | 20 + modules/logger.js | 36 + serverInfo.js => modules/serverInfo.js | 70 +- modules/sharedFunctions.js | 212 ++++ serverControl.js | 1333 ++---------------------- 10 files changed, 1258 insertions(+), 1455 deletions(-) create mode 100755 modules/apiV10.js create mode 100755 modules/configClass.js create mode 100755 modules/controlEmitter.js create mode 100755 modules/logger.js rename serverInfo.js => modules/serverInfo.js (70%) create mode 100755 modules/sharedFunctions.js diff --git a/README.MD b/README.MD index c8f1fcf..ea87cf9 100644 --- a/README.MD +++ b/README.MD @@ -6,12 +6,13 @@ It exposes control of your server and shell functions to the internet. Although I strongly adivise to use secure connections to prevent possible man-in-the-middle attacks. ## NOTE: -This Release (v0.8) changes the API end Points to facilitate versioning of the API. -The old Endpoints will be available for some time to allow for a transition time. I expect to remove them with Version 1.0. +Release 1.0 changed structure of the code to enable further development. +See below for changes. ### Breaking changes: -- mapchange completed reporting via websocket will be changed to the standard command status reporting. See below for details. -- control?action=status is deprecated. use /control/runningstatus & /control/rconauthstatus instead. +- Config logic has been separated from configClass - make sure to backup your config! You can copy the config part from the old file to the new "./config.js" +- steam account used for updating the server has been deleted from the config - it has to go in your update-Script. +- login code has been seperated from the api calls. New location is "http://:/csgoapi/login" ISO "http://:/csgoapi/v1.0/login" ## Install download the script files to and install the dependencies for nodejs @@ -54,16 +55,16 @@ For better readability, $.get() is used in the following examples ### Login / Logout ```javascript -$.get('http://:/csgoapi/v1.0/login') -$.get('http://:/csgoapi/v1.0/logout') -$.get('http://:/csgoapi/v1.0/loginStatus') +$.get('http://:/csgoapi/login') +$.get('http://:/csgoapi/logout') +$.get('http://:/csgoapi/loginStatus') ``` -For Authentication the API redirects to the Steam login page by calling '/csgoapi/v1.0/login' -After authentication there, it will return to '/csgoapi/v1.0/loginStatus' by default, returning { "login": true/false }. +For Authentication the API redirects to the Steam login page by calling '/csgoapi/login' +After authentication there, it will return to '/csgoapi/loginStatus' by default, returning { "login": true/false }. If you use the API in a web interface, you can set 'redirectPage' in the config to your startPage (e.g. http://your-webserver/index.html) This way, you can call up the login page and then be returned to your web application after you got the session cookie in your browser. -If you want to have a manual logout in your client, call '/csgoapi/v1.0/logout', which will redirect to '/csgoapi/v1.0/loginStatus' to confirm the success. +If you want to have a manual logout in your client, call '/csgoapi/logout', which will redirect to '/csgoapi/loginStatus' to confirm the success. ### Map filtering Filters are used to limit the maps that are transmitted to clients with the serverInfo. Filtering here consists of filter strings and a filter type. The strings are matched as parts against the maps filenames. 'cs_' would match all hostage rescue maps, 'dust' would match all maps that have dust in their names (de_dust2, de_dust, etc.). diff --git a/config.js b/config.js index ebf67c5..86a1e61 100755 --- a/config.js +++ b/config.js @@ -1,166 +1,73 @@ -/** - * Config class for CSGO Server API - */ -module.exports = class config { - constructor () { - this._userOptions = { - // Network interface over which the server is communicating. We set this and not the - // IP-address, in case the server is using DHCP in a LAN and not a static address. - 'iface': 'eth0', - // Hostname of the machine, this script runs on (e.g.: yourdomain.org). - // Leave empty if you use the IP of iface. - 'host': '', - // steam serverToken. To get one see https://steamcommunity.com/dev/managegameservers - 'serverToken': '', - // Well, the rcon password... - 'rconPass': 'YourRconPass', - // SteamID64 of Users who are allowed to control the server. For info on SteamID64 see: - // https://steamcommunity.com/discussions/forum/1/364039785160857002/ - 'admins': [], - // The Page the client is redirected to after login - see README for more info. - 'redirectPage': '/loginStatus', - // Time in minutes, after which a new login is needed. - 'loginValidity': 300, - // Port, the webserver for API calls listens on. - 'apiPort': 8090, - // Set to true if you use Websockets for status updates. - 'webSockets': false, - // Port, the websocket is listening on. - 'socketPort': 8091, - // https settings - 'useHttps': false, - // Optional: 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 - // the CA-Cert here. - 'httpsCa': '', - // CORS origin setting. '*' is not allowed since login credentials are sent with requests. - // For possible values see: - // https://expressjs.com/en/resources/middleware/cors.html#configuration-options - 'corsOrigin': 'localhost', - // Change this to any string of your liking to make it harder for attackers to profile your cookies. - 'sessionSecret': 'nodejs-csgo-api', - // The folder, where your srcds_run is located. - 'csgoDir': '/home/csgo/csgo_ds', - // Anything you want your server command line to have additional to: - // -game csgo -console -usercon +sv_setsteamaccount - 'csgoOptionalArgs': '-insecure +sv_lan 1 +sv_pure 0 -ip 0.0.0.0 +mapgroup mg_all', - // The path to screen. - 'screen': '/usr/bin/screen', - // The name screen will give the process (no spaces allowed). - 'screenName': 'csgoServer', - // The screen Logfile where the console output of screen and the server will be logged. - // New logs are appended, so you may need to delete or rotate this log periodically. - 'screenLog': '/home/csgo/screen.log', - // Path to steamcmd, can stay like this if installed via package manager. - 'steamExe': 'steamcmd', - // Steam Account to update the server with steamcmd. - 'steamAccount': ' ', - // Script to pass into steamcmd to update. - // See https://steamcommunity.com/discussions/forum/1/492379159713970561/ for more info. - 'updateScript': '/home/csgo/update_csgo.txt', - // Scripts to run on various events. Use absolute path. - 'logStartScript': '', - 'mapStartScript': '', - 'matchStartScript': '', - 'roundStartScript': '', - 'roundEndScript': '', - 'matchEndScript': '', - //'mapEndScript': '', // For the moment I have no definite way to sense the end of map. - 'logEndScript': '', - // Logfile for API - 'logFile': './logs/csgoapi', - // logLevel for API-Logfiles. In case 'debug' is set, logs will also be written to console. - 'logLevel': 'http', - // how many Days should logfiles be kept? - 'logDays': '14' - }; - - this.screenCommand = `${this._userOptions.screen} -L -Logfile ${this._userOptions.screenLog} -dmS ${this._userOptions.screenName}`; - this.csgoCommand = `${this._userOptions.csgoDir}/srcds_run`; - this.csgoArgs = `-game csgo -console -usercon +sv_setsteamaccount ${this._userOptions.serverToken} ${this._userOptions.csgoOptionalArgs}`; - } - - get rconPass() { - return this._userOptions.rconPass; - } - - get admins() { - return this._userOptions.admins; - } - - get redirectPage() { - return this._userOptions.redirectPage; - } - - get loginValidity() { - return this._userOptions.loginValidity * 60000; - } - - get iface() { - return this._userOptions.iface; - } - - get host() { - return this._userOptions.host; - } - - get apiPort(){ - return this._userOptions.apiPort; - } - get socketPort(){ - return this._userOptions.socketPort; - } - - get serverCommandline() { - return `${this.screenCommand} ${this.csgoCommand} ${this.csgoArgs}`; - } - get updateCommand() { - return this._userOptions.steamExe - } - get updateArguments() { - return [`+login ${this._userOptions.steamAccount}`, - `+runscript ${this._userOptions.updateScript}`]; - } - - get webSockets() { - return this._userOptions.webSockets; - } - get useHttps() { - return this._userOptions.useHttps; - } - get scheme() { - return (this._userOptions.useHttps ? 'https' : 'http'); - } - get httpsCertificate() { - return this._userOptions.httpsCertificate; - } - get httpsPrivateKey() { - return this._userOptions.httpsPrivateKey; - } - get httpsCa() { - return this._userOptions.httpsCa; - } - - get corsOrigin() { - return this._userOptions.corsOrigin; - } - get sessionSecret() { - return this._userOptions.sessionSecret; - } - - script(type) { - return this._userOptions[`${type}Script`]; - } - - get logFile() { - return this._userOptions.logFile; - } - get logLevel() { - return this._userOptions.logLevel; - } - get logDays() { - return this._userOptions.logDays; - } +var config = { + // Network interface over which the server is communicating. We set this and not the + // IP-address, in case the server is using DHCP in a LAN and not a static address. + "iface": "eth0", + // Hostname of the machine, this script runs on (e.g.: yourdomain.org). + // Leave empty if you use the IP of iface. + "host": "", + // steam serverToken. To get one see https://steamcommunity.com/dev/managegameservers + "serverToken": "", + // Well, the rcon password... + "rconPass": "YourRconPass", + // SteamID64 of Users who are allowed to control the server. For info on SteamID64 see: + // https://steamcommunity.com/discussions/forum/1/364039785160857002/ + "admins": [], + // The Page the client is redirected to after login - see README for more info. + "redirectPage": "/loginStatus", + // Time in minutes, after which a new login is needed. + "loginValidity": 300, + // Port, the webserver for API calls listens on. + "apiPort": 8090, + // Set to true if you use Websockets for status updates. + "webSockets": false, + // Port, the websocket is listening on. + "socketPort": 8091, + // https settings + "useHttps": false, + // Optional: 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 + // the CA-Cert here. + "httpsCa": "", + // CORS origin setting. "*" is not allowed since login credentials are sent with requests. + // For possible values see: + // https://expressjs.com/en/resources/middleware/cors.html#configuration-options + "corsOrigin": "localhost", + // Change this to any string of your liking to make it harder for attackers to profile your cookies. + "sessionSecret": "nodejs-csgo-api", + // The folder, where your srcds_run is located. + "csgoDir": "/home/csgo/csgo_ds", + // Anything you want your server command line to have additional to: + // -game csgo -console -usercon +sv_setsteamaccount + "csgoOptionalArgs": "-insecure +sv_lan 1 +sv_pure 0 -ip 0.0.0.0 +mapgroup mg_all", + // The path to screen. + "screen": "/usr/bin/screen", + // The name screen will give the process (no spaces allowed). + "screenName": "csgoServer", + // The screen Logfile where the console output of screen and the server will be logged. + // New logs are appended, so you may need to delete or rotate this log periodically. + "screenLog": "/home/csgo/screen.log", + // Path to steamcmd, can stay like this if installed via package manager. + "steamExe": "steamcmd", + // Script to pass into steamcmd to update. + // See https://developer.valvesoftware.com/wiki/SteamCMD under heading "Automating SteamCMD" for more info. + "updateScript": "/home/csgo/update_csgo.txt", + // Scripts to run on various events. Use absolute path. + "logStartScript": "", + "mapStartScript": "", + "matchStartScript": "", + "roundStartScript": "", + "roundEndScript": "", + "matchEndScript": "", + //"mapEndScript": "", // For the moment I have no definite way to sense the end of map. + "logEndScript": "", + // Logfile for API + "logFile": "./logs/csgoapi", + // logLevel for API-Logfiles. In case "debug" is set, logs will also be written to console. + "logLevel": "http", + // how many Days should logfiles be kept? + "logDays": "14" }; + +module.exports = config; \ No newline at end of file diff --git a/example/js/gameserver.js b/example/js/gameserver.js index 5eb772f..8cc8c63 100755 --- a/example/js/gameserver.js +++ b/example/js/gameserver.js @@ -1,10 +1,11 @@ // Change here if you don't host the webInterfae on the same host as the NodeJS API var host = window.location.hostname; -var address =`https://${host}:8090/csgoapi/v1.0`; +var address = `https://${host}:8090/csgoapi`; +var apiPath = `${address}/v1.0` var maplistFile = './maplist.txt'; // Titles for throbber window. -var titles = { +var titles = { 'start': 'Starting server', 'stop': 'Stopping server', 'auth': 'Authenticating RCON', @@ -16,7 +17,7 @@ var authenticated = false; // Redirect to login page. function doLogin() { - window.location.href = `${address}/login`; + window.location.href = `${address}/login`; } // Sends a get Request with the headers needed for authentication with the seesion cookie. @@ -39,7 +40,7 @@ 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) => { + lines.forEach((map) => { $("#mapAuswahl").append(``); }); }); @@ -55,12 +56,12 @@ function setupPage() { let loginCheck = getPromise('loginStatus'); loginCheck.then((data) => { if (data.login) { - let authenticated = getPromise('info/rconauthstatus'); + let authenticated = getPromise('v1.0/info/rconauthstatus'); authenticated.then((data) => { if (data.rconauth) { setupServerRunning(); } else { - let serverRunning = getPromise('info/runstatus'); + let serverRunning = getPromise('v1.0/info/runstatus'); serverRunning.then((data) => { if (data.running) { window.location.href = './notauth.htm'; @@ -93,6 +94,7 @@ function setupNotLoggedIn() { $('#serverInfo').hide(0); $('#mapControl').hide(0); } + function setupServerRunning() { $('#power-image').attr('src', 'pic/power-on.png'); if (socket.readyState != 1) { // if websocket not connected @@ -109,6 +111,7 @@ function setupServerRunning() { $('#serverInfo').css('display', 'flex'); $('#mapControl').show(0); } + function setupServerStopped() { $('#power-image').attr('src', 'pic/power-off.png'); $('#startMap').show(0); @@ -129,7 +132,7 @@ function clickButton(aButton) { $('.container-popup').css('display', 'flex'); startMap = document.getElementById('mapAuswahl').value; - sendGet(`${address}/control/${action}`, `startmap=${startMap}`).done(( data ) => { + sendGet(`${apiPath}/control/${action}`, `startmap=${startMap}`).done((data) => { if (socket.readyState != 1) { // if websocket not connected if (action != 'update') { setupPage(); @@ -156,12 +159,13 @@ function showPlayerMenu(event) { $('#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 = ''; - } + 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. @@ -169,22 +173,22 @@ function movePlayer(event) { "sm_movet" - Move a player to the terrorist team. */ let player = event.target.parentElement.getAttribute('player') let command = event.target.getAttribute('command'); - sendGet(`${address}/rcon`, `message=sm_move${command} "${player}"`, ( data ) => { + sendGet(`${apiPath}/rcon`, `message=sm_move${command} "${player}"`, (data) => { // no actions for now. }); } function getMaps() { function getServerInfo() { - return Promise.resolve(sendGet(`${address}/info/serverInfo`)); + 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')) { + 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); @@ -221,33 +225,33 @@ function changeMap(event) { $('#mapSelector').hide('fast'); $('#popupCaption').text(titles['mapchange']); $('.container-popup').css('display', 'flex'); - sendGet(`${address}/control/changemap`, `map=${map}`, (data) => { + sendGet(`${apiPath}/control/changemap`, `map=${map}`, (data) => { if (data.success) { $('#popupText').html(`Changing map to ${map}`); } else { $('#popupText').html(`Mapchange failed!`); - window.setTimeout( () => { + window.setTimeout(() => { $('.container-popup').css('display', 'none'); window.location.href = './notauth.htm'; }, 2000); - + } }); } function restartRound() { - sendGet(`${address}/rcon`, `message=mp_restartgame 1`, ( data ) => { + sendGet(`${apiPath}/rcon`, `message=mp_restartgame 1`, (data) => { $('#popupCaption').text(`Restart Round`); $('#popupText').html(`Round Restarted!`); $('.container-popup').css('display', 'flex'); - window.setTimeout( () => { + window.setTimeout(() => { $('.container-popup').css('display', 'none'); }, 1000); }); } -function authenticate (caller) { - sendGet(`${address}/authenticate`).done((data) => { +function authenticate(caller) { + sendGet(`${apiPath}/authenticate`).done((data) => { if (data.authenticated) { window.location.href = './gameserver.htm'; } else { @@ -256,11 +260,12 @@ function authenticate (caller) { } }); } + function kill(caller) { - - sendGet(`${address}/control/kill`).done((data) => { + + sendGet(`${apiPath}/control/kill`).done((data) => { window.location.href = './gameserver.htm'; - }).fail ((error) => { + }).fail((error) => { caller.disabled = true; $('#killerror').show('fast'); }); @@ -269,15 +274,17 @@ function kill(caller) { // Bot Training functions function setBotRules() { - sendGet(`${address}/rcon`, `message=mp_autoteambalance 0`); - sendGet(`${address}/rcon`, `message=mp_limitteams 0`); - sendGet(`${address}/rcon`, `message=bot_difficulty 3`); + 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(`${address}/rcon`, `message=bot_add_${team}`), 100); + for (let i = 0; i < quantity; i++) { + setTimeout(sendGet(`${apiPath}/rcon`, `message=bot_add_${team}`), 100); } } + function kickBots() { - sendGet(`${address}/rcon`, `message=bot_kick all`); + sendGet(`${apiPath}/rcon`, `message=bot_kick all`); } \ No newline at end of file diff --git a/modules/apiV10.js b/modules/apiV10.js new file mode 100755 index 0000000..fc0ddb9 --- /dev/null +++ b/modules/apiV10.js @@ -0,0 +1,620 @@ +/** + * @requires child_process + * @requires node-pty + * @requires express + * @requires ./config.js + * @requires ./emitters.js + * @requires ./sharedFunctions.js + */ + +const { exec } = require('child_process'); +const pty = require('node-pty'); +const express = require('express'); +var router = express.Router(); +const logger = require('./logger.js'); +var cfg = require('./configClass.js'); +var serverInfo = require('./serverInfo.js'); +var controlEmitter = require('./controlEmitter.js'); +const sf = require('./sharedFunctions.js'); + + +//--------------------------- V1.0 ----------------------------// +/** + * @apiDescription Manually Authenticate RCON + * + * @api {get} /authenticate + * @apiVersion 1.0 + * @apiName Authenticate + * @apiGroup RCON + * + * @ApiSuccess {boolean} authneticated + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "authenticated": true/false } + */ +router.get('/authenticate', (req, res) => { + sf.authenticate().then((data) => { + res.json(data); + }).catch((data) => { + res.json(data); + }); +}); + +/** + * @apiDescription serverData request + * + * @api {get} /info/serverInfo + * @apiVersion 1.0 + * @apiName serverInfo + * @apiGroup Info + * + * @apiSuccess {json} serverInfo object (see './serverInfo.js' for example) + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 503 Service Unavailable + * { "error": "RCON not authenticated" } + */ +router.get('/info/serverInfo', (req, res) => { + logger.verbose('Processing Serverinfo request.'); + if (serverInfo.serverState.authenticated) { + res.json(serverInfo.getAll()); + } else if (!serverInfo.serverState.serverRunning) { + res.status(503).json({ "error": "CS:GO Server not running." }); + } else if (!serverInfo.serverState.authenticated) { + res.status(503).json({ "error": "RCON not authenticated." }); + } +}); + +/** + * @apiDescription Query if CS:GO server is running. + * + * @api {get} /info/runstatus + * @apiVersion 1.0 + * @apiName RunStatus + * @apiGroup Info + * + * @apiSuccess {boolean} running + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "running": true/false} + */ +router.get('/info/runstatus', (req, res) => { + if (serverInfo.serverState.operationPending == 'start' || serverInfo.serverState.operationPending == 'stop') { + let sendResponse = (type, action) => { + if (type == 'auth' && action == 'end') { + res.json({ "running": serverInfo.serverState.serverRunning }); + controlEmitter.removeListener('exec', sendResponse); + } + } + controlEmitter.on('exec', sendResponse) + } else { + res.json({ "running": serverInfo.serverState.serverRunning }); + } +}); + +/** + * @apiDescription Query if RCON is authenticated + * + * @api {get} /info/rconauthstatus + * @apiVersion 1.0 + * @apiName RconAuthStatus + * @apiGroup Info + * + * @apiSuccess {boolean} rconauth + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "rconauth": true/false} + */ +router.get('/info/rconauthstatus', (req, res) => { + if (serverInfo.serverState.operationPending == 'auth') { + let sendResponse = (type, action) => { + if (type == 'auth' && action == 'end') { + res.json({ "rconauth": serverInfo.serverState.authenticated }); + controlEmitter.removeListener('exec', sendResponse); + } + } + controlEmitter.on('exec', sendResponse) + } else { + res.json({ "rconauth": serverInfo.serverState.authenticated }); + } +}); + +/** + * @apiDescription Get filter info. + * + * @api {get} /filter + * @apiVersion 1.0 + * @apiName Filter Info + * @apiGroup filter + * + * @apiSuccess {json} Filters + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "type": {string}, "filters": {array of strings} } + */ +router.get('/filter', (req, res) => { + res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); +}); + +/** + * @apiDescription Reset filter to empty. + * + * @api {get} /filter/reset + * @apiVersion 1.0 + * @apiName Reset Filters + * @apiGroup filter + * + * @apiSuccess {json} filters + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "type": {string}, "filters": {array of strings} } + */ +router.get('/filter/reset', (req, res) => { + serverInfo.mapFilterReset(); + res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); +}); + +/** + * @apiDescription Add a Filter + * + * @api {post} /filter/add + * @apiVersion 1.0 + * @apiName Add filter + * @apiGroup filter + * + * @apiParam {string} filter Filter text + * @apiParamExample {string} filter + * 'dz_' + * + * @apiSuccess {json} Filters + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "type": {string}, "filters": {array of strings} } + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 400 Bad Request + * { "error": "Submitted filter text not safe." } + */ +router.post('/filter/add', (req, res) => { + if (!req.query.filter) { + return res.status(400).json({ "error": "Required parameter 'filter' is missing" }); + } + + const safe = /^[a-zA-Z0-9-_]*$/; + if (!safe.test(req.query.filter)) { + return res.status(400).json({ "error": "Submitted filter text not safe." }); + } else { + serverInfo.mapFilters.push(req.query.filter); + } + res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); +}); + +/** + * @apiDescription Remove a Filter + * + * @api {post} /filter/remove + * @apiVersion 1.0 + * @apiName Remove filter + * @apiGroup filter + * + * @apiParam {string} filter Filter text + * @apiParamExample {string} filter + * 'dz_' + * + * @apiSuccess {json} Filters + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "type": {string}, "filters": {array of strings} } + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 400 Bad Request + * { "error": "No filter was removed." } + */ +router.post('/filter/remove', (req, res) => { + if (!req.query.filter) { + return res.status(400).json({ "error": "Required parameter 'filter' is missing" }); + } + + let oldLength = serverInfo.mapFilters.length; + serverInfo.mapFilterRemove(req.query.filter); + if (oldLength == serverInfo.mapFilters.length) { + return res.status(400).json({ "error": "No filter was removed." }); + } + res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); +}); + +/** + * @apiDescription Set filter type. + * + * @api {post} /filter/type + * @apiVersion 1.0 + * @apiName Set filter type + * @apiGroup filter + * + * @apiParam {string} type Filter type ('include' / 'exclude') + * @apiParamExample {string} type + * "include" + * + * @apiSuccess {json} Filters + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "type": {string}, "filters": {array of strings} } + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 400 Bad Request + * { "error": "Invalid type string." } + */ +router.post('/filter/type', (req, res) => { + if (!req.query.type) { + return res.status(400).json({ "error": "Required parameter 'type' is missing" }); + } + + if (req.query.type === 'include' || req.query.type === 'exclude') { + serverInfo.mapFilterType = req.query.type; + } else { + return res.status(400).json({ "error": "Invalid type string." }); + } + res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); +}); + +/** + * @apiDescription Start CS:GO Server + * + * @api {get} /control/start + * @apiVersion 1.0 + * @apiName Start + * @apiGroup Control + * + * @apiParam {string} mapname filename of the map without extension (.bsp) + * @apiParamExample {string} Map-example + * cs_italy + * + * @apiSuccess {boolean} success + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "success": true } + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 503 Service Unavailable + * { "error": "Server already running" } + */ +router.get('/control/start', (req, res) => { + var args = req.query; + + if (!serverInfo.serverState.serverRunning && serverInfo.serverState.operationPending == 'none') { + controlEmitter.emit('exec', 'start', 'start'); + logger.verbose('Starting server.'); + let startMap = 'de_dust2'; + const safe = /^[a-zA-Z0-9-_]*$/; + if (!safe.test(args.startmap)) { + logger.warn(`Supplied mapname ${args.startmap} is not safe, using de_dust2`); + } else { + startMap = args.startmap; + } + let commandLine = `${cfg.serverCommandline} +map ${startMap}`; + let serverProcess = exec(commandLine, (error, stdout, stderr) => { + if (error) { + // node couldn't execute the command. + res.status(501).json({ "error": error.code }); + logger.error('Error Code: ' + error.code); + logger.error('Signal received: ' + error.signal); + logger.error(stderr); + serverInfo.serverState.serverRunning = false; + controlEmitter.emit('exec', 'start', 'fail'); + } else { + logger.verbose('screen started'); + controlEmitter.on('exec', function startCallback(operation, action) { + if (operation == 'auth' && action == 'end' && serverInfo.serverState.authenticated == true) { + controlEmitter.emit('exec', 'start', 'end'); + res.json({ "success": true }); + controlEmitter.removeListener('exec', startCallback); + } else if (operation == 'auth' && action == 'end' && serverInfo.serverState.authenticated == false) { + res.status(501).json({ "error": "RCON Authentication failed." }); + controlEmitter.emit('exec', 'start', 'fail'); + controlEmitter.removeListener('exec', startCallback); + } + }); + serverInfo.serverState.serverRunning = true; + } + }); + } else if (serverInfo.serverState.serverRunning) { + logger.warn('Start triggered with server already running'); + res.status(503).json({ "error": "Server already running." }); + } else if (serverInfo.serverState.operationPending != 'none') { + logger.warn(`Server Start triggered, while ${serverInfo.serverState.operationPending} pending.`); + res.status(503).json({ "error": `Another Operation is Pending: ${serverInfo.serverState.operationPending}` }); + } +}); + +/** +* @apiDescription Stop CS:GO Server +* +* @api {get} /control/stop +* @apiVersion 1.0 +* @apiName Stop +* @apiGroup Control + +* @apiSuccess {boolean} success +* @apiSuccessExample {json} +* HTTP/1.1 200 OK +* { "success": true } +* @apiError {string} error +* @apiErrorExample {json} +* HTTP/1.1 503 Service Unavailable +* { "error": "Server not running" } +*/ +router.get('/control/stop', (req, res) => { + if (serverInfo.serverState.serverRunning && serverInfo.serverState.operationPending == 'none') { + controlEmitter.emit('exec', 'stop', 'start'); + logger.verbose("sending quit."); + sf.executeRcon('quit').then((answer) => { + serverInfo.serverState.serverRunning = false; + serverInfo.serverState.authenticated = false; + res.json({ "success": true }); + controlEmitter.emit('exec', 'stop', 'end'); + }).catch((err) => { + logger.error('Stopping server Failed: ' + err); + res.status(501).json({ "error": `RCON Error: ${err.toString()}` }); + controlEmitter.emit('exec', 'stop', 'end'); + }); + } else if (!serverInfo.serverState.serverRunning) { + logger.warn('Stop triggered, although server not running'); + res.status(503).json({ "error": "Server not running." }); + } else if (serverInfo.serverState.operationPending != 'none') { + logger.warn(`Stop triggered, while ${serverInfo.serverState.operationPending} pending.`); + res.status(503).json({ "error": `Another Operation is Pending: ${serverInfo.serverState.operationPending}` }); + } +}); + +/** +* @apiDescription Kill CS:GO Server Process in case no RCON connection. +* +* @api {get} /control/kill +* @apiVersion 1.0 +* @apiName Kill +* @apiGroup Control + +* @apiSuccess {boolean} success +* @apiSuccessExample {json} +* HTTP/1.1 200 OK +* { "success": true } +* @apiError {string} error +* @apiErrorExample {json} +* HTTP/1.1 501 Service Unavailable +* { "error": "Could not find csgo server process" } +*/ +router.get('/control/kill', (req, res) => { + exec('/bin/ps -a |grep srcds_linux', (error, stdout, stderr) => { + if (error) { + logger.error(`exec error: ${error}`); + res.status(501).json({ "error": "Could not find csgo server process" }); + } else if (stdout.match(/srcds_linux/) != null) { + let pid = stdout.split(/\s+/)[1]; + exec(`/bin/kill ${pid}`, (error, stdout, stderr) => { + if (error) { + res.status(501).json({ "error": "Could not kill csgo server process" }); + } else { + // reset API-State + serverInfo.serverState.serverRunning = false; + serverInfo.serverState.authenticated = false; + serverInfo.serverState.serverRcon = undefined; + logger.verbose('Server process killed.') + res.json({ "success": true }); + } + }); + } + }); +}); + +/** +* @apiDescription Update CS:GO Server +* +* @api {get} /control/update +* @apiVersion 1.0 +* @apiName Update +* @apiGroup Control + +* @apiSuccess {boolean} success +* @apiSuccessExample {json} +* HTTP/1.1 200 OK +* { "success": true } +* @apiError {string} error +* @apiErrorExample {json} +* HTTP/1.1 501 Internal Server Error +* { "error": "Update could not be started." } +*/ +router.get('/control/update', (req, res) => { + if (!serverInfo.serverState.serverRunning && serverInfo.serverState.operationPending == 'none') { + controlEmitter.emit('exec', 'update', 'start'); + let updateSuccess = false; + logger.verbose('Updating Server.'); + let updateProcess = pty.spawn(cfg.updateCommand, [`+runscript ${cfg.updateScript}`]); + + updateProcess.on('data', (data) => { + logger.debug(data); + if (data.indexOf('Checking for available updates') != -1) { + controlEmitter.emit('progress', 'Checking Steam client updates', 0); + } else if (data.indexOf('Verifying installation') != -1) { + controlEmitter.emit('progress', 'Verifying client installation', 0); + } else if (data.indexOf('Logging in user') != -1) { + controlEmitter.emit('progress', 'Logging in steam user', 0); + } else if (data.indexOf('Logged in OK') != -1) { + controlEmitter.emit('progress', 'Login OK', 100); + } else if (data.indexOf('Update state (0x') != -1) { + let rex = /Update state \(0x\d+\) (.+), progress: (\d{1,3})\.\d{2}/; + let matches = rex.exec(data); + controlEmitter.emit('progress', matches[1], matches[2]); + } else if (data.indexOf('Downloaaction update (') != -1) { + let rex = /\[(.+)] Downloaaction update/; + let matches = rex.exec(data); + controlEmitter.emit('progress', 'Updating Steam client', matches[1].slice(0, -1)); + } else if (data.indexOf('Success!') != -1) { + controlEmitter.emit('progress', 'Update successful!', 100); + logger.verbose('Update succeeded'); + updateSuccess = true; + controlEmitter.emit('exec', 'update', 'end'); + } + }); + + if (updateProcess) { + if (cfg.webSockets) { + res.json(`{ "success": true }`); + updateProcess.once('close', (code) => { + if (!updateSuccess) { + logger.warn('Update exited without success.'); + controlEmitter.emit('progress', 'Update failed!', 100); + controlEmitter.emit('exec', 'update', 'end'); + } + }); + } else { + updateProcess.once('close', (code) => { + if (updateSuccess) { + res.json({ "success": true }); + } else { + logger.warn('Update exited without success.'); + res.status(501).json({ "error": "Update was not successful" }); + } + controlEmitter.emit('exec', 'update', 'end'); + }); + } + } else { + logger.error('Update could not be started.'); + res.status(501).json({ "error": "Update could not be started." }); + controlEmitter.emit('exec', 'update', 'end'); + } + } else if (serverInfo.serverState.serverRunning) { + logger.warn('Update triggered, while server running.'); + res.status(503).json({ "error": "Server is running - stop before updating" }); + } else if (serverInfo.serverState.operationPending != 'none') { + logger.warn(`Update triggered, while ${serverInfo.serverState.operationPending} pending`); + res.status(503).json({ "error": `Another Operation is Pending: ${serverInfo.serverState.operationPending}` }); + } +}); + +//change map +/** + * @apiDescription Change Map + * + * @api {get} /control/changemap + * @apiVersion 1.0 + * @apiName changemap + * @apiGroup Control + * + * @apiParam {string} mapname filename of the map without extension (.bsp) + * @apiParamExample {string} Map-example + * cs_italy + * + * @apiSuccess {boolean} success + * @apiSuccessExample {json} + * HTTP/1.1 200 OK + * { "success": true } + * @apiError {string} error + * @apiErrorExample {json} + * HTTP/1.1 501 Internal Server Error + * { "error": "RCON error: Unable to write to socket" } + */ +router.get('/control/changemap', (req, res) => { + var args = req.query; + 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) => { + if (!cfg.webSockets) { + // If the mapchange completed, send success and cancel timeout. + let sendCompleted = (operation, action) => { + if (operation == 'mapchange' && action == 'end') { + res.json({ "success": true }); + clearTimeout(mapchangeTimeout); + } + }; + controlEmitter.once('exec', sendCompleted); + + // Failure of a mapchange is unfortunately not logged by the server, + // so we use a timeout after 30 sec. + let mapchangeTimeout = setTimeout(() => { + res.status(501).json({ "error": "Mapchange failed - timeout" }); + controlEmitter.emit('exec', 'mapchange', 'fail'); + }, 30000); + } else { + res.json({ "success": true }); + // If the mapchange is successful, cancel the timeout. + let removeTimeout = (operation, action) => { + if (operation == 'mapchange' && action == 'end') { + clearTimeout(mapchangeTimeout); + } + }; + controlEmitter.once('exec', removeTimeout); + + // Failure of a mapchange is unfortunately not logged by the server, + // so we use a timeout after 30 sec. + let mapchangeTimeout = setTimeout(() => { + controlEmitter.emit('exec', 'mapchange', 'fail'); + }, 30000); + } + }).catch((err) => { + res.status(501).json({ "error": `RCON error: ${err.toString()}` }); + controlEmitter.emit('exec', 'mapchange', 'fail'); + }); + } else { + res.status(501).json({ "error": `Map ${args.map} not available` }); + controlEmitter.emit('exec', 'mapchange', 'fail'); + } + } else { + logger.warn(`Mapchange triggered, while ${serverInfo.serverState.operationPending} pending.`); + res.status(503).json({ "error": `Another Operation is Pending: ${serverInfo.serverState.operationPending}` }); + } +}); + +/** +* @apiDescription Reload availbale maps from server. +* +* @api {get} /control/reloadMaplist +* @apiVersion 1.0 +* @apiName reloadMaplist +* @apiGroup Control + +* @apiSuccess {boolean} success +* @apiSuccessExample {json} +* HTTP/1.1 200 OK +* { "success": true } +*/ +router.get('/control/reloadMaplist', (req, res) => { + sf.reloadMaplist().then((answer) => { + res.json(answer); + }); +}); + +/** + * @apiDescription Process rcon requests + * + * @api /rcon + * @apiVersion 1.0 + * @apiName Rcon + * @apiGroup Rcon + * + * @apiParam {string} message RCON Command to execute. + * @apiParamExample {string} + * 'mp_limitteams 0' + * + * @apiSuccess {string} answer Servers answer string to RCON request + * @apiSuccessExample {string} + * "mp_maxrounds" = "30" ( def. "0" ) min. 0.000000 game notify replicated - max number of rounds to play before server changes maps + * L 11/06/2020 - 19:05:14: rcon from "127.0.0.1:54598": command "mp_maxrounds" + * @apiError {string} errortext + * @apiErrorExample {string} + * 'Error, check server logs for details.' + */ +router.get('/rcon', (req, res) => { + var message = req.query.message; + res.set('Content-Type', 'text/plain'); + sf.executeRcon(message).then((answer) => { + res.send(answer); + }).catch((err) => { + res.status(501).send('Error, check server logs for details.'); + logger.error(err); + }); +}); +//------------------------ END V1.0 ----------------------------// + +module.exports = router; \ No newline at end of file diff --git a/modules/configClass.js b/modules/configClass.js new file mode 100755 index 0000000..edb8190 --- /dev/null +++ b/modules/configClass.js @@ -0,0 +1,95 @@ +/** + * Config class for CSGO Server API + */ +class config { + constructor() { + this._userOptions = require('../config.js'); + + this.screenCommand = `${this._userOptions.screen} -L -Logfile ${this._userOptions.screenLog} -dmS ${this._userOptions.screenName}`; + this.csgoCommand = `${this._userOptions.csgoDir}/srcds_run`; + this.csgoArgs = `-game csgo -console -usercon +sv_setsteamaccount ${this._userOptions.serverToken} ${this._userOptions.csgoOptionalArgs}`; + } + + get rconPass() { + return this._userOptions.rconPass; + } + + get admins() { + return this._userOptions.admins; + } + + get redirectPage() { + return this._userOptions.redirectPage; + } + + get loginValidity() { + return this._userOptions.loginValidity * 60000; + } + + get iface() { + return this._userOptions.iface; + } + + get host() { + return this._userOptions.host; + } + + get apiPort() { + return this._userOptions.apiPort; + } + get socketPort() { + return this._userOptions.socketPort; + } + + get serverCommandline() { + return `${this.screenCommand} ${this.csgoCommand} ${this.csgoArgs}`; + } + get updateCommand() { + return this._userOptions.steamExe + } + get updateScript() { + return this._userOptions.updateScript; + } + + get webSockets() { + return this._userOptions.webSockets; + } + get useHttps() { + return this._userOptions.useHttps; + } + get scheme() { + return (this._userOptions.useHttps ? 'https' : 'http'); + } + get httpsCertificate() { + return this._userOptions.httpsCertificate; + } + get httpsPrivateKey() { + return this._userOptions.httpsPrivateKey; + } + get httpsCa() { + return this._userOptions.httpsCa; + } + + get corsOrigin() { + return this._userOptions.corsOrigin; + } + get sessionSecret() { + return this._userOptions.sessionSecret; + } + + script(type) { + return this._userOptions[`${type}Script`]; + } + + get logFile() { + return this._userOptions.logFile; + } + get logLevel() { + return this._userOptions.logLevel; + } + get logDays() { + return this._userOptions.logDays; + } +}; + +module.exports = new config(); \ No newline at end of file diff --git a/modules/controlEmitter.js b/modules/controlEmitter.js new file mode 100755 index 0000000..472c4d0 --- /dev/null +++ b/modules/controlEmitter.js @@ -0,0 +1,20 @@ +const events = require('events'); + +/** + * Emits information on control operations. + */ +var controlEmitter = new events.EventEmitter(); +/** + * Control execution event. Tells the start and end of control routines. + * @event controlEmitter#exec + * @property {string} operation (start, stop, update, mapchange) + * @property {string} action (start, end, fail) + */ +/** + * Tracks progress of control routines. + * @event controlEmitter#progress + * @property {string} step - descripbes which step of an operation is reported. + * @property {int} progress - the percentage of the step that is completed. + */ + +module.exports = controlEmitter; \ No newline at end of file diff --git a/modules/logger.js b/modules/logger.js new file mode 100755 index 0000000..f97695e --- /dev/null +++ b/modules/logger.js @@ -0,0 +1,36 @@ +/** + * @requires winston + * @requires winston-daily-rotate-file + * @requires ./config.js + */ + +const winston = require('winston'); +require('winston-daily-rotate-file'); +var cfg = require('./configClass.js'); + +// Setup the logger. +var logger = winston.createLogger({ + level: cfg.logLevel, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + new winston.transports.DailyRotateFile({ + filename: `${cfg.logFile}-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + maxFiles: `${cfg.logDays}d` + }) + ] +}); +// If level is 'debug', also log to console. +if (cfg.logLevel == 'debug') { + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + })); +} + +module.exports = logger; \ No newline at end of file diff --git a/serverInfo.js b/modules/serverInfo.js similarity index 70% rename from serverInfo.js rename to modules/serverInfo.js index 0904f6d..85949da 100755 --- a/serverInfo.js +++ b/modules/serverInfo.js @@ -1,7 +1,24 @@ const events = require('events'); -module.exports = class serverInfo { - constructor (options = {}) { +class serverInfo { + constructor(options = {}) { + /** + * Stores the state of the controlled server-instance. + * @typedef serverState + * @property {string} operationPending - 1 of: none, start, stop, mapchange, update, auth. + * @property {boolean} serverRunning - Is the server process running. + * @property {object} serverRcon - rcon-srcds instance for the server. + * @property {boolean} authenticated - Is the rcon instance authenticated with the server. + */ + + /** @type {serverState} */ + this._serverState = { + 'operationPending': 'none', + 'serverRunning': false, + 'serverRcon': undefined, + 'authenticated': false + } + // data section this._map = ''; this._mapsAvail = [] @@ -14,7 +31,7 @@ module.exports = class serverInfo { // 'tags': [{ "tag": "" }] } ]; this._mapFilterType = 'exclude'; // 'include / exclude', - this._mapFilters = ['ar_', 'dz_', 'gd_', 'lobby_', 'training1']; // [ {string} ] + this._mapFilters = ['ar_', 'dz_', 'gd_', 'lobby_', 'training1']; // [ {string} ] this._maxRounds = 0; this._score = { 'T': 0, @@ -30,7 +47,14 @@ module.exports = class serverInfo { this.serverInfoChanged = new events.EventEmitter(); } -// getter / setter + // getter / setter + get serverState() { + return this._serverState; + } + set serverState(newVal) { + this._serverState[expr] = newVal; + } + get map() { return this._map; } @@ -47,12 +71,12 @@ module.exports = class serverInfo { 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; + found = true; } }); if (this._mapFilterType === 'include') { @@ -74,12 +98,12 @@ module.exports = class serverInfo { 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; - this._mapFilters.forEach( (filter) => { + this._mapFilters.forEach((filter) => { if (map.name.includes(filter)) { - found = true; + found = true; } }); if (this._mapFilterType === 'include') { @@ -93,7 +117,7 @@ module.exports = class serverInfo { } } -// Map Filter Methods + // Map Filter Methods get mapFilterType() { return this._mapFilterType; } @@ -112,14 +136,14 @@ module.exports = class serverInfo { } mapFilterRemove(itemToRemove) { if (this._mapFilters.length == 0) { - return(0); + return (0); } if (typeof itemToRemove === 'number' && this._mapFilters.length > parseInt(itemToRemove)) { console.log("removing number"); 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; @@ -142,9 +166,9 @@ module.exports = class serverInfo { } get score() { - return this._score; - } - // Accepts array with team (T or C) and score. + return this._score; + } + // Accepts array with team (T or C) and score. set score(newScoreArray) { this._score[newScoreArray[1]] = parseInt(newScoreArray[2]); this.serverInfoChanged.emit('change'); @@ -159,16 +183,16 @@ module.exports = class serverInfo { this.serverInfoChanged.emit('change'); } assignPlayer(steamID, team) { - for (let i=0; i < this._players.length; i++) { + for (let i = 0; i < this._players.length; i++) { if (this._players[i].steamID == steamID) { - this._players[i].team = team.substr(0,1); + this._players[i].team = team.substr(0, 1); i = this._players.length; } } this.serverInfoChanged.emit('change'); } removePlayer(steamID) { - for (let i=0; i < this._players.length; i++) { + for (let i = 0; i < this._players.length; i++) { if (this._players[i].steamID == steamID) { this._players.splice(i, 1); i = this._players.length; @@ -198,4 +222,6 @@ module.exports = class serverInfo { this._score.T = 0; this.serverInfoChanged.emit('change'); } -}; \ No newline at end of file +}; + +module.exports = new serverInfo(); \ No newline at end of file diff --git a/modules/sharedFunctions.js b/modules/sharedFunctions.js new file mode 100755 index 0000000..7b6d734 --- /dev/null +++ b/modules/sharedFunctions.js @@ -0,0 +1,212 @@ +const https = require('https'); +const rcon = require('rcon-srcds').default; +const logger = require('./logger.js'); +var cfg = require('./configClass.js'); +var serverInfo = require('./serverInfo.js'); +var controlEmitter = require('./controlEmitter.js'); + +/** + * Authenticate rcon with server + * @return {Promise} - Promise object that yields the result of authentication. + * @fires controlEmitter.exec + */ +function authenticate() { + if (serverInfo.serverState.operationPending != 'auth') { + controlEmitter.emit('exec', 'auth', 'start'); + return new Promise((resolve, reject) => { + if (!serverInfo.serverState.authenticated) { + logger.verbose("RCON authenticating..."); + // since this API is designed to run on the same machine as the server keeping + // default here which is 'localhost' + let authTimeout = setTimeout(() => { + logger.error('Authentication timed out'); + controlEmitter.emit('exec', 'auth', 'fail'); + reject({ "authenticated": false }); + }, 60000); + serverInfo.serverState.serverRcon = new rcon({}); + logger.debug('sending authentication request'); + serverInfo.serverState.serverRcon.authenticate(cfg.rconPass).then(() => { + logger.debug('received authentication'); + controlEmitter.emit('exec', 'auth', 'end'); + clearTimeout(authTimeout); + resolve({ "authenticated": true }); + }).catch((err) => { + if (err == 'Already authenticated') { + logger.verbose('Already authenticated.'); + controlEmitter.emit('exec', 'auth', 'end'); + clearTimeout(authTimeout); + resolve({ "authenticated": true }); + } else { + logger.error("authentication error: " + err); + controlEmitter.emit('exec', 'auth', 'fail'); + clearTimeout(authTimeout); + reject({ "authenticated": false }); + } + }); + + } else { + logger.info('Already authenticated.'); + controlEmitter.emit('exec', 'auth', 'end'); + resolve({ "authenticated": true }); + } + }); + } else { + return new Promise((resolve, reject) => { + if (serverInfo.serverState.authenticated) { + logger.verbose('Already authenticated.'); + resolve({ "authenticated": true }); + } else { + logger.verbose(`Rcon authentication cancelled due to other operation Pending: ${serverInfo.serverState.operationPending}`); + reject({ "authenticated": false }); + } + }); + } + +} + +/** + * 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) { + 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) => { + 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 }); + } catch (e) { + reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); + } + }); + }); + + steamApiRequest.on('error', error => { + logger.warn(`steamApiRequest not successful: ${error}`); + reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); + }); + + steamApiRequest.write(`itemcount=1&publishedfileids%5B0%5D=${mapId}`); + steamApiRequest.end(); + }); + } + + 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 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": "" }); + } + } + }); + 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. + if (maplist.length > 0) { + logger.debug("Saving Maplist to ServerInfo"); + serverInfo.mapsAvail = maplist; + serverInfo.mapsDetails = mapdetails; + resolve({ "success": true }); + } else { + resolve({ "success": false }); + } + }); + }).catch((err) => { + resolve({ "success": false }); + }); + }); +} + +/** + * Executes a rcon command + * @param {string} message - The rcon command to execute + * @return {Promise} - Promise Object that contains the rcon response or an error message. + */ +function executeRcon(message) { + logger.debug(`Executing rcon: ${message}`); + return new Promise((resolve, reject) => { + serverInfo.serverState.serverRcon.execute(message).then((answer) => { + resolve(answer); + }).catch((err) => { + logger.error(`RCON Error: ${err}`); + reject(err.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. + * @returns {string} mapstring - The mapname without workshop path or .bsp + */ +function cutMapName(mapstring) { + if (mapstring.search('workshop') != -1) { + re = /(\w+)/g; + matches = mapstring.match(re); + mapstring = matches[2]; + } + if (mapstring.search(".bsp") != -1) { + mapstring = mapstring.substring(0, mapstring.length - 4); + } + return mapstring; +} + +module.exports = { authenticate, reloadMaplist, executeRcon, cutMapName }; \ No newline at end of file diff --git a/serverControl.js b/serverControl.js index 5766cc4..db8cdf1 100755 --- a/serverControl.js +++ b/serverControl.js @@ -1,8 +1,7 @@ /** * @file CS:GO Dedicated Server Control * @author Markus Adrario - * @version 0.8 - * @requires rcon-srcds + * @version 1.0 * @requires srcds-log-receiver * @requires express * @requires express-session @@ -15,15 +14,13 @@ * @requires ws * @requires url * @requires events - * @requires node-pty * @requires child_process - * @requires winston - * @requires winston-daily-rotate-file - * @requires ./serverInfo.js - * @requires ./config.js + * @requires ./modules/logger.js + * @requires ./modules/serverInfo.js + * @requires ./modules/configClass.js + * @requires ./modules/sharedFunctions.js */ -const rcon = require('rcon-srcds').default; const logReceiver = require('srcds-log-receiver'); const express = require('express'); const session = require('express-session'); @@ -33,42 +30,21 @@ const passport = require('passport'); const SteamStrategy = require('passport-steam').Strategy; const webSocket = require('ws'); const url = require('url'); -const https = require('https'); const fs = require('fs'); const events = require('events'); -const pty = require('node-pty'); -const { exec, spawn } = require('child_process'); -const winston = require('winston'); -require('winston-daily-rotate-file'); -const si = require('./serverInfo.js'); -const config = require('./config.js'); +const { exec } = require('child_process'); +const logger = require('./modules/logger.js'); +var serverInfo = require('./modules/serverInfo.js'); +var cfg = require('./modules/configClass.js'); +const sf = require('./modules/sharedFunctions.js'); -/** - * Stores the state of the controlled server-instance. - * @typedef nodejsapiState - * @property {string} operationPending - 1 of: none, start, stop, mapchange, update, auth. - * @property {boolean} serverRunning - Is the server process running. - * @property {object} serverRcon - rcon-srcds instance for the server. - * @property {boolean} authenticated - Is the rcon instance authenticated with the server. - */ - -/** @type {nodejsapiState} */ -var nodejsapiState = { - 'operationPending': 'none', - 'serverRunning': false, - 'serverRcon': undefined, - 'authenticated': false -} - -var serverInfo = new si(); -var cfg = new config(); var localIP = require('local-ip')(cfg.iface); var http = undefined; var httpsCredentials = {}; // if configured for https, we fork here. if (cfg.useHttps) { http = require('https'); - httpsCredentials = { + httpsCredentials = { key: fs.readFileSync(cfg.httpsPrivateKey), cert: fs.readFileSync(cfg.httpsCertificate), }; @@ -79,31 +55,6 @@ if (cfg.useHttps) { http = require('http'); } -// Setup the logger. -const logger = winston.createLogger({ - level: cfg.logLevel, - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.DailyRotateFile({ - filename: `${cfg.logFile}-%DATE%.log`, - datePattern: 'YYYY-MM-DD', - maxFiles: `${cfg.logDays}d` - }) - ] -}); -// If level is 'debug', also log to console. -if (cfg.logLevel == 'debug') { - logger.add (new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.simple() - ) - })); -} - // check for running Server on Startup exec('/bin/ps -a', (error, stdout, stderr) => { if (error) { @@ -111,9 +62,9 @@ exec('/bin/ps -a', (error, stdout, stderr) => { return; } if (stdout.match(/srcds_linux/) != null) { - nodejsapiState.serverRunning = true; + serverInfo.serverState.serverRunning = true; logger.verbose('Found running server'); - authenticate().then((data) => { + sf.authenticate().then((data) => { logger.verbose(`authentication ${data.authenticated}`); }).catch((data) => { logger.verbose(`authentication ${data.authenticated}`); @@ -121,138 +72,29 @@ exec('/bin/ps -a', (error, stdout, stderr) => { } }); -/** - * 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) { - 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) => { - 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 }); - } - catch (e) { - reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); - } - }); - }); - - steamApiRequest.on('error', error => { - logger.warn(`steamApiRequest not successful: ${error}`); - reject({ "name": _mapName, "title": "", "workshopID": "", "description": "", "previewLink": "", "tags": "" }); - }); - - steamApiRequest.write(`itemcount=1&publishedfileids%5B0%5D=${mapId}`); - steamApiRequest.end(); - }); - } - - 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 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": "" }); - } - } - }); - 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. - if (maplist.length > 0) { - logger.debug("Saving Maplist to ServerInfo"); - serverInfo.mapsAvail = maplist; - serverInfo.mapsDetails = mapdetails; - resolve({ "success": true }); - } else { - resolve({ "success": false }); - } - }); - }).catch((err) => { - resolve({ "success": false }); - }); - }); -} - // Event Emitters -/** - * Emits information on control operations. - */ -var controlEmitter = new events.EventEmitter(); -/** - * Control execution event. Tells the start and end of control routines. - * @event controlEmitter#exec - * @property {string} operation (start, stop, update, mapchange) - * @property {string} action (start, end, fail) - */ -/** - * Tracks progress of control routines. - * @event controlEmitter#progress - * @property {string} step - descripbes which step of an operation is reported. - * @property {int} progress - the percentage of the step that is completed. - */ +var controlEmitter = require('./modules/controlEmitter.js'); /** * Sets the operationPending variable on events. Gathers Information on RCON authentication. * @listens controlEmitter#exec */ controlEmitter.on('exec', (operation, action) => { - nodejsapiState.operationPending = (action == 'start') ? operation : 'none'; - logger.debug('nodejsapiState.operationPending = ' + nodejsapiState.operationPending); + serverInfo.serverState.operationPending = (action == 'start') ? operation : 'none'; + logger.debug('serverInfo.serverState.operationPending = ' + serverInfo.serverState.operationPending); if (operation == 'auth' && action == 'end') { - nodejsapiState.authenticated = true; - logger.debug('nodejsapiState.authenticated = ' + nodejsapiState.authenticated); + serverInfo.serverState.authenticated = true; + logger.debug('serverInfo.serverState.authenticated = ' + serverInfo.serverState.authenticated); logger.verbose("RCON Authenticate success"); queryMaxRounds(); // Get current and available maps and store them. - executeRcon('host_map').then((answer) => { + sf.executeRcon('host_map').then((answer) => { let re = /map" = "(\S+)"/; let matches = re.exec(answer); let mapstring = matches[1]; - serverInfo.map = cutMapName(mapstring); + serverInfo.map = sf.cutMapName(mapstring); }); - reloadMaplist().then((answer) => { + sf.reloadMaplist().then((answer) => { if (answer == '{ "success": false }') { logger.warn("Maps could not be loaded"); } @@ -260,141 +102,13 @@ controlEmitter.on('exec', (operation, action) => { } }); -/** - * Authenticate rcon with server - * @return {Promise} - Promise object that yields the result of authentication. - * @fires controlEmitter.exec - */ -function authenticate() { - if (nodejsapiState.operationPending != 'auth') { - controlEmitter.emit('exec', 'auth', 'start'); - return new Promise((resolve, reject) => { - if (!nodejsapiState.authenticated) { - logger.verbose("RCON authenticating..."); - // since this API is designed to run on the same machine as the server keeping - // default here which is 'localhost' - let authTimeout = setTimeout( () => { - logger.error('Authentication timed out'); - controlEmitter.emit('exec', 'auth', 'fail'); - reject({ "authenticated": false }); - }, 60000); - nodejsapiState.serverRcon = new rcon({}); - logger.debug('sending authentication request'); - nodejsapiState.serverRcon.authenticate(cfg.rconPass).then(() => { - logger.debug('received authentication'); - controlEmitter.emit('exec', 'auth', 'end'); - clearTimeout(authTimeout); - resolve({ "authenticated": true }); - }).catch((err) => { - if (err == 'Already authenticated') { - logger.verbose('Already authenticated.'); - controlEmitter.emit('exec', 'auth', 'end'); - clearTimeout(authTimeout); - resolve({ "authenticated": true }); - } else { - logger.error("authentication error: " + err); - controlEmitter.emit('exec', 'auth', 'fail'); - clearTimeout(authTimeout); - reject({ "authenticated": false }); - } - }); - - } else { - logger.info('Already authenticated.'); - controlEmitter.emit('exec', 'auth', 'end'); - resolve({ "authenticated": true }); - } - }); - } else { - return new Promise((resolve, reject) => { - if (nodejsapiState.authenticated) { - logger.verbose('Already authenticated.'); - resolve({ "authenticated": true }); - } else { - logger.verbose(`Rcon authentication cancelled due to other operation Pending: ${nodejsapiState.operationPending}`); - reject({ "authenticated": false }); - } - }); - } - -} - -/** - * Executes a rcon command - * @param {string} message - The rcon command to execute - * @return {Promise} - Promise Object that contains the rcon response or an error message. - */ -function executeRcon (message) { - logger.debug(`Executing rcon: ${message}`); - return new Promise((resolve, reject) => { - nodejsapiState.serverRcon.execute(message).then((answer) => { - resolve(answer); - }).catch((err) => { - logger.error(`RCON Error: ${err}`); - reject(err.message); - }); - }); -} - - /*----------------- HTTP Server Code -------------------*/ -// Setup Passport for SteamStrategy -passport.serializeUser((user, done) => { - done(null, user); -}); -passport.deserializeUser((obj, done) => { - done(null, obj); -}); - -passport.use( - new SteamStrategy({ - returnURL: `${cfg.scheme}://${cfg.host}:${cfg.apiPort}/login/return`, - realm: `${cfg.scheme}://${cfg.host}:${cfg.apiPort}/`, - profile: false - }, - (identifier, profile, done) => { - process.nextTick(function () { - - // Cut the SteamID64 from the returned User-URI - let steamID64 = identifier.split('/')[5]; - profile.identifier = steamID64; - logger.http({ - 'user': `${steamID64}`, - 'message': 'logged in' - }); - return done(null, profile); - }); - } -)); -function ensureAuthenticated(req, res, next) { - if (req.isAuthenticated()) { - if (cfg.admins.includes(req.user.identifier)) { - logger.http({ - 'user': `${req.user.identifier}`, - 'message':`${req.method}:${req.url}` - }); - return next(); - } else { - logger.info({ - 'user': `${req.user.identifier}`, - 'message': 'User not in Admin list.' - }); - return res.status(401).send('User not in Admin list.'); - } - } - logger.info({ - 'user': 'unknown', - 'message': `Unauthorized Access from ${req.ip}.` - }); - return res.status(401).send('Not logged in.'); -} - /** * Creates an express server to handle the API requests */ const app = express(); const limit = rateLimit({ - max: 50,// max requests + max: 50, // max requests windowMs: 60 * 1000, // 1 Minute message: 'Too many requests' // message to send }); @@ -418,313 +132,102 @@ app.use(passport.session()); app.disable('x-powered-by'); -//------------------- Version 0.X --------------------------// -// Handle authentication. -app.get('/login', - passport.authenticate('steam', { failureRedirect: '/loginStatus' }), - (req, res) => { - res.redirect(cfg.redirectPage); - } -); -app.get('/login/return', - passport.authenticate('steam', { failureRedirect: '/loginStatus' }), - (req, res) => { - res.redirect(cfg.redirectPage); - } -); -app.get('/logout', (req, res) => { - logger.http({ - 'user': `${steamID64}`, - 'message': 'logged out' - }); - req.logout(); - res.redirect(cfg.redirectPage); +//--------------------------- Steam authentication ----------------------------// +// Setup Passport for SteamStrategy +passport.serializeUser((user, done) => { + done(null, user); }); - -// Return the current login status -app.get("/loginStatus", (req, res) => { - res.writeHeader(200, {"Content-Type": "application/json"}); - if(req.user && cfg.admins.includes(req.user.identifier)) { - res.write('{ "login": true }'); - } else { - res.write('{ "login": false }'); - } - res.end(); +passport.deserializeUser((obj, done) => { + done(null, obj); }); -// Process "control" messages. -app.get("/control", ensureAuthenticated, (req, res) => { - var args = req.query; - - // Start Server - if (args.action == "start" && !nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - nodejsapiState.operationPending = 'start'; - logger.verbose('Starting server.'); - let startMap = "de_dust2"; - const safe = /^[a-zA-Z0-9-_]*$/; - if (!safe.test(args.startmap)) { - logger.warn(`Supplied mapname ${args.startmap} is not safe, using de_dust2`); - } else { - startMap = args.startmap; - } - let commandLine = `${cfg.serverCommandline} +map ${startMap}`; - var serverProcess = exec(commandLine, (error, stdout, stderr) => { - if (error) { - // node couldn't execute the command. - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write('{ "success": false }'); - res.end(); - logger.error('Error Code: '+error.code); - logger.error('Signal received: '+error.signal); - logger.error(stderr); - nodejsapiState.serverRunning = false; - nodejsapiState.operationPending = 'none'; - } else { - logger.verbose('screen started'); - controlEmitter.on('exec', function callback (operation, action) { - if (operation == 'auth' && action == 'end') { - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write('{ "success": true }'); - res.end(); - controlEmitter.removeListener('exec', callback); - } +passport.use( + new SteamStrategy({ + returnURL: `${cfg.scheme}://${cfg.host}:${cfg.apiPort}/csgoapi/login/return`, + realm: `${cfg.scheme}://${cfg.host}:${cfg.apiPort}/`, + profile: false + }, + (identifier, profile, done) => { + process.nextTick(function() { + + // Cut the SteamID64 from the returned User-URI + let steamID64 = identifier.split('/')[5]; + profile.identifier = steamID64; + logger.http({ + 'user': `${steamID64}`, + 'message': 'logged in' }); - nodejsapiState.serverRunning = true; - nodejsapiState.operationPending = 'none'; - } - }); - - // Stop Server - } else if (args.action == "stop" && nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - nodejsapiState.operationPending = 'stop'; - logger.verbose("sending quit."); - executeRcon('quit').then((answer) => { - nodejsapiState.serverRunning = false; - nodejsapiState.authenticated = false; - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write(`{ "success": ${!nodejsapiState.serverRunning} }`); - res.end(); - nodejsapiState.operationPending = 'none'; - }).catch((err) => { - logger.error('Stopping server Failed: ' + err); - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write(`{ "success": ${!nodejsapiState.serverRunning} }`); - res.end(); - nodejsapiState.operationPending = 'none'; - }); - - //Update Server - } else if (args.action == "update" && !nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - nodejsapiState.operationPending = 'update'; - let updateSuccess = false; - logger.verbose('Updating Server.'); - let updateProcess = pty.spawn(cfg.updateCommand, cfg.updateArguments); - - updateProcess.on('data', (data) => { - logger.debug(data); - if (data.indexOf('Checking for available updates') != -1) { - controlEmitter.emit('progress', 'Checking Steam client updates', 0); - } else if (data.indexOf('Verifying installation') != -1) { - controlEmitter.emit('progress', 'Verifying client installation', 0); - } else if (data.indexOf('Logging in user') != -1) { - controlEmitter.emit('progress', 'Logging in steam user', 0); - } else if (data.indexOf('Logged in OK') != -1) { - controlEmitter.emit('progress', 'Login OK', 100); - } else if(data.indexOf('Update state (0x') != -1) { - let rex = /Update state \(0x\d+\) (.+), progress: (\d{1,3})\.\d{2}/; - let matches = rex.exec(data); - controlEmitter.emit('progress', matches[1], matches[2]); - } else if (data.indexOf('Downloaaction update (') != -1) { - let rex = /\[(.+)] Downloaaction update/; - let matches = rex.exec(data); - controlEmitter.emit('progress', 'Updating Steam client', matches[1].slice(0, -1)); - } else if (data.indexOf('Success!') != -1) { - controlEmitter.emit('progress', 'Update Successful!', 100); - logger.verbose('update succeeded'); - updateSuccess = true; - nodejsapiState.operationPending = 'none'; - } - }); - - if (cfg.webSockets) { - res.writeHeader(200, {"Content-Type": "application/json"}); - if (updateProcess) { - res.write(`{ "success": true }`); - } else { - res.write(`{ "success": false }`); - } - res.end(); - updateProcess.removeAllListeners(); - nodejsapiState.operationPending = 'none'; - } else { - updateProcess.once('close', (code) => { - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write(`{ "success": ${updateSuccess} }`); - res.end(); - updateProcess.removeAllListeners(); - nodejsapiState.operationPending = 'none'; + return done(null, profile); }); } + )); - // Send Status - } else if (args.action == "status") { - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write(`{ "running": ${nodejsapiState.serverRunning && nodejsapiState.authenticated} }`); - res.end(); - - //change map - } else if (args.action == "changemap" && !nodejsapiState.operationPending == 'none') { - nodejsapiState.operationPending = 'mapchange'; - res.writeHeader(200, { 'Content-Type': 'application/json' }); - // only try to change map, if it exists on the server. - if (serverInfo.mapsAvail.includes(args.map)) { - executeRcon(`map ${args.map}`).then((answer) => { - if (!cfg.webSockets) { - // If the mapchange completed event is fired, send success and cancel timeout. - var sendCompleted = (result) => { - res.write(`{ "success": ${result == 'success'} }`); - res.end(); - clearTimeout(mapchangeTimeout); - nodejsapiState.operationPending = 'none'; - }; - mapChangeEmitter.once('result', sendCompleted); - - // A mapchange should not take longer than 30 sec. - let mapchangeTimeout = setTimeout( () => { - mapChangeEmitter.emit('result', 'timeout'); - res.write(`{ "success": false }`); - res.end(); - nodejsapiState.operationPending = 'none'; - }, 30000); - } else { - res.write(`{ "success": true }`); - res.end(); - // If the mapchange is successful, cancel the timeout. - var removeTimeout = (result) => { - clearTimeout(mapchangeTimeout); - nodejsapiState.operationPending = 'none'; - }; - mapChangeEmitter.once('result', removeTimeout); - - // A mapchange should not take longer than 30 sec. - let mapchangeTimeout = setTimeout( () => { - mapChangeEmitter.emit('result', 'timeout'); - nodejsapiState.operationPending = 'none'; - }, 30000); - } - }).catch((err) => { - res.write(`{ "success": false }`); - res.end(); - nodejsapiState.operationPending = 'none'; +function ensureAuthenticated(req, res, next) { + if (req.isAuthenticated()) { + if (cfg.admins.includes(req.user.identifier)) { + logger.http({ + 'user': `${req.user.identifier}`, + 'message': `${req.method}:${req.url}` }); + return next(); } else { - res.write(`{ "success": false }`); - res.end(); - nodejsapiState.operationPending = 'none'; + logger.info({ + 'user': `${req.user.identifier}`, + 'message': 'User not in Admin list.' + }); + return res.status(401).send('User not in Admin list.'); } - - // DEPRECATED - will be removed in future release, do not use. - // follow mapchange - } else if (args.action == "mapstart") { - mapChangeEmitter.once('result', (result) => { - res.writeHeader(200, {"Content-Type": "application/json"}); - res.write(`{ "completed": ${result == 'success'} }`); - res.end(); - }); - - // Update Maps available on server - } else if (args.action == "reloadmaplist") { - reloadMaplist().then( (answer) => { - res.json(answer); - }); } -}); - -// Process "authenticate" message. -app.get("/authenticate", ensureAuthenticated, (req, res) => { - res.writeHeader(200, {"Content-Type": "application/json"}); - authenticate().then((data) => { - res.write(data); - res.end(); - }).catch((data) => { - res.write(data); - res.end(); - }); -}); - -// Process rcon requests -app.get("/rcon", ensureAuthenticated, (req, res) => { - var message = req.query.message; - executeRcon(message).then((answer) => { - res.writeHeader(200, { 'Content-Type': 'text/plain' }); - res.write(answer); - res.end(); - }).catch( (err) => { - res.writeHeader(200, { 'Content-Type': 'text/plain' }); - res.write("Error, check logs for details"); - res.end(); - logger.error(err); + logger.info({ + 'user': 'unknown', + 'message': `Unauthorized Access from ${req.ip}.` }); -}); - -// Process serverData request -app.get("/serverInfo", ensureAuthenticated, (req, res) => { - logger.verbose('Processing Serverinfo request.'); - res.writeHeader(200, {"Content-Type": "application/json"}); - if (nodejsapiState.authenticated) { - res.write(JSON.stringify(serverInfo.getAll())); - res.end(); - } else { - res.write('{ "error": true }'); - res.end(); - } -}); -//------------------------ END V0.X ----------------------------// + return res.status(401).send('Not logged in.'); +} -//--------------------------- V1.0 ----------------------------// /** - * @api {get} /csgoapi/v1.0/login + * @api {get} /csgoapi/login * @apiVersion 1.0 * @apiName Login * @apiGroup Auth * * @apiSuccess (302) Redirect to confiured page. - * @apiError (302) Redirect to /csgoapi/v1.0/loginStatus + * @apiError (302) Redirect to /csgoapi/loginStatus */ -app.get('/csgoapi/v1.0/login', - passport.authenticate('steam', { failureRedirect: '/csgoapi/v1.0/loginStatus' }), +app.get('/csgoapi/login', + passport.authenticate('steam', { failureRedirect: '/csgoapi/loginStatus' }), (req, res) => { res.redirect(cfg.redirectPage); } ); /** - * @api {get} /csgoapi/v1.0/login/return + * @api {get} /csgoapi/login/return * @apiVersion 1.0 * @apiName Login Return * @apiGroup Auth * * @apiSuccess (302) Redirect to confiured page. - * @apiError (302) Redirect to /csgoapi/v1.0/loginStatus + * @apiError (302) Redirect to /csgoapi/loginStatus */ -app.get('/csgoapi/v1.0/login/return', - passport.authenticate('steam', { failureRedirect: '/csgoapi/v1.0/loginStatus' }), +app.get('/csgoapi/login/return', + passport.authenticate('steam', { failureRedirect: '/csgoapi/loginStatus' }), (req, res) => { res.redirect(cfg.redirectPage); } ); /** - * @api {get} /csgoapi/v1.0/logout + * @api {get} /csgoapi/logout * @apiVersion 1.0 * @apiName Logout * @apiGroup Auth * * @apiSuccess (302) Redirect to confiured page. */ -app.get('/csgoapi/v1.0/logout', (req, res) => { +app.get('/csgoapi/logout', (req, res) => { logger.http({ - 'user': `${steamID64}`, - 'message': 'logged out' - }); + 'user': `${steamID64}`, + 'message': 'logged out' + }); req.logout(); res.redirect(cfg.redirectPage); }); @@ -732,7 +235,7 @@ app.get('/csgoapi/v1.0/logout', (req, res) => { /** * @apiDescription Return the status of login to client. * - * @api {get} /csgoapi/v1.0/loginStatus + * @api {get} /csgoapi/loginStatus * @apiVersion 1.0 * @apiName LoginStatus * @apiGroup Auth @@ -742,611 +245,21 @@ app.get('/csgoapi/v1.0/logout', (req, res) => { * HTTP/1.1 200 OK * { "login": true/false } */ -app.get('/csgoapi/v1.0/loginStatus', (req, res) => { - if(req.user && cfg.admins.includes(req.user.identifier)) { +app.get('/csgoapi/loginStatus', (req, res) => { + if (req.user && cfg.admins.includes(req.user.identifier)) { res.json({ "login": true }); } else { res.json({ "login": false }); } }); +//------------------------ END Steam authentication ----------------------------// -/** - * @apiDescription Manually Authenticate RCON - * - * @api {get} /csgoapi/v1.0/authenticate - * @apiVersion 1.0 - * @apiName Authenticate - * @apiGroup RCON - * - * @ApiSuccess {boolean} authneticated - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "authenticated": true/false } - */ -app.get('/csgoapi/v1.0/authenticate', ensureAuthenticated, (req, res) => { - authenticate().then((data) => { - res.json(data); - }).catch((data) => { - res.json(data); - }); -}); +//------------------------ Routes with steam auth ----------------------------// +var apiV10 = require('./modules/apiV10.js'); +app.use('/csgoapi/v1.0/', ensureAuthenticated, apiV10); +//------------------------ END Routes with steam auth ----------------------------// -/** - * @apiDescription serverData request - * - * @api {get} /csgoapi/v1.0/info/serverInfo - * @apiVersion 1.0 - * @apiName serverInfo - * @apiGroup Info - * - * @apiSuccess {json} serverInfo object (see './serverInfo.js' for example) - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 503 Service Unavailable - * { "error": "RCON not authenticated" } - */ -app.get('/csgoapi/v1.0/info/serverInfo', ensureAuthenticated, (req, res) => { - logger.verbose('Processing Serverinfo request.'); - if (nodejsapiState.authenticated) { - res.json(serverInfo.getAll()); - } else if (!nodejsapiState.serverRunning) { - res.status(503).json({ "error": "CS:GO Server not running." }); - } else if (!nodejsapiState.authenticated) { - res.status(503).json({ "error": "RCON not authenticated." }); - } -}); -/** - * @apiDescription Query if CS:GO server is running. - * - * @api {get} /csgoapi/v1.0/info/runstatus - * @apiVersion 1.0 - * @apiName RunStatus - * @apiGroup Info - * - * @apiSuccess {boolean} running - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "running": true/false} - */ -app.get('/csgoapi/v1.0/info/runstatus', ensureAuthenticated, (req, res) => { - if (nodejsapiState.operationPending == 'start' || nodejsapiState.operationPending == 'stop') { - let sendResponse = (type, action) => { - if (type == 'auth' && action == 'end') { - res.json({ "running": nodejsapiState.serverRunning }); - controlEmitter.removeListener('exec', sendResponse); - } - } - controlEmitter.on('exec', sendResponse) - } else { - res.json({ "running": nodejsapiState.serverRunning }); - } -}); - -/** - * @apiDescription Query if RCON is authenticated - * - * @api {get} /csgoapi/v1.0/info/rconauthstatus - * @apiVersion 1.0 - * @apiName RconAuthStatus - * @apiGroup Info - * - * @apiSuccess {boolean} rconauth - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "rconauth": true/false} - */ -app.get('/csgoapi/v1.0/info/rconauthstatus', ensureAuthenticated, (req, res) => { - if (nodejsapiState.operationPending == 'auth') { - let sendResponse = (type, action) => { - if (type == 'auth' && action == 'end') { - res.json({ "rconauth": nodejsapiState.authenticated }); - controlEmitter.removeListener('exec', sendResponse); - } - } - controlEmitter.on('exec', sendResponse) - } else { - res.json({ "rconauth": nodejsapiState.authenticated }); - } -}); - -/** - * @apiDescription Get filter info. - * - * @api {get} /csgoapi/v1.0/filter - * @apiVersion 1.0 - * @apiName Filter Info - * @apiGroup filter - * - * @apiSuccess {json} Filters - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "type": {string}, "filters": {array of strings} } - */ -app.get('/csgoapi/v1.0/filter', ensureAuthenticated, (req, res) => { - res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); -}); - -/** - * @apiDescription Reset filter to empty. - * - * @api {get} /csgoapi/v1.0/filter/reset - * @apiVersion 1.0 - * @apiName Reset Filters - * @apiGroup filter - * - * @apiSuccess {json} filters - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "type": {string}, "filters": {array of strings} } - */ -app.get('/csgoapi/v1.0/filter/reset', ensureAuthenticated, (req, res) => { - serverInfo.mapFilterReset(); - res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); -}); - -/** - * @apiDescription Add a Filter - * - * @api {post} /csgoapi/v1.0/filter/add - * @apiVersion 1.0 - * @apiName Add filter - * @apiGroup filter - * - * @apiParam {string} filter Filter text - * @apiParamExample {string} filter - * 'dz_' - * - * @apiSuccess {json} Filters - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "type": {string}, "filters": {array of strings} } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 400 Bad Request - * { "error": "Submitted filter text not safe." } - */ -app.post('/csgoapi/v1.0/filter/add', ensureAuthenticated, (req, res) => { - if(!req.query.filter) { - return res.status(400).json({ "error": "Required parameter 'filter' is missing" }); - } - - const safe = /^[a-zA-Z0-9-_]*$/; - if (!safe.test(req.query.filter)) { - return res.status(400).json({ "error": "Submitted filter text not safe." }); - } else { - serverInfo.mapFilters.push(req.query.filter); - } - res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); -}); - -/** - * @apiDescription Remove a Filter - * - * @api {post} /csgoapi/v1.0/filter/remove - * @apiVersion 1.0 - * @apiName Remove filter - * @apiGroup filter - * - * @apiParam {string} filter Filter text - * @apiParamExample {string} filter - * 'dz_' - * - * @apiSuccess {json} Filters - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "type": {string}, "filters": {array of strings} } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 400 Bad Request - * { "error": "No filter was removed." } - */ -app.post('/csgoapi/v1.0/filter/remove', ensureAuthenticated, (req, res) => { - if(!req.query.filter) { - return res.status(400).json({ "error": "Required parameter 'filter' is missing" }); - } - - let oldLength = serverInfo.mapFilters.length; - serverInfo.mapFilterRemove(req.query.filter); - if ( oldLength == serverInfo.mapFilters.length) { - return res.status(400).json({ "error": "No filter was removed." }); - } - res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); -}); - -/** - * @apiDescription Set filter type. - * - * @api {post} /csgoapi/v1.0/filter/type - * @apiVersion 1.0 - * @apiName Set filter type - * @apiGroup filter - * - * @apiParam {string} type Filter type ('include' / 'exclude') - * @apiParamExample {string} type - * "include" - * - * @apiSuccess {json} Filters - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "type": {string}, "filters": {array of strings} } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 400 Bad Request - * { "error": "Invalid type string." } - */ -app.post('/csgoapi/v1.0/filter/type', ensureAuthenticated, (req, res) => { - if(!req.query.type) { - return res.status(400).json({ "error": "Required parameter 'type' is missing" }); - } - - if (req.query.type === 'include' || req.query.type === 'exclude') { - serverInfo.mapFilterType = req.query.type; - } else { - return res.status(400).json({ "error": "Invalid type string." }); - } - res.json({ "type": serverInfo.mapFilterType, "filters": serverInfo.mapFilters }); -}); - -/** - * @apiDescription Start CS:GO Server - * - * @api {get} /csgoapi/v1.0/control/start - * @apiVersion 1.0 - * @apiName Start - * @apiGroup Control - * - * @apiParam {string} mapname filename of the map without extension (.bsp) - * @apiParamExample {string} Map-example - * cs_italy - * - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 503 Service Unavailable - * { "error": "Server already running" } - */ -app.get('/csgoapi/v1.0/control/start', ensureAuthenticated, (req, res) => { - var args = req.query; - - if (!nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - controlEmitter.emit('exec', 'start', 'start'); - logger.verbose('Starting server.'); - let startMap = 'de_dust2'; - const safe = /^[a-zA-Z0-9-_]*$/; - if (!safe.test(args.startmap)) { - logger.warn(`Supplied mapname ${args.startmap} is not safe, using de_dust2`); - } else { - startMap = args.startmap; - } - let commandLine = `${cfg.serverCommandline} +map ${startMap}`; - let serverProcess = exec(commandLine, (error, stdout, stderr) => { - if (error) { - // node couldn't execute the command. - res.status(501).json({ "error": error.code }); - logger.error('Error Code: '+error.code); - logger.error('Signal received: '+error.signal); - logger.error(stderr); - nodejsapiState.serverRunning = false; - controlEmitter.emit('exec', 'start', 'fail'); - } else { - logger.verbose('screen started'); - controlEmitter.on('exec', function startCallback (operation, action) { - if (operation == 'auth' && action == 'end' && nodejsapiState.authenticated == true) { - controlEmitter.emit('exec', 'start', 'end'); - res.json({ "success": true }); - controlEmitter.removeListener('exec', startCallback); - } else if (operation == 'auth' && action == 'end' && nodejsapiState.authenticated == false) { - res.status(501).json({ "error": "RCON Authentication failed." }); - controlEmitter.emit('exec', 'start', 'fail'); - controlEmitter.removeListener('exec', startCallback); - } - }); - nodejsapiState.serverRunning = true; - } - }); - } else if (nodejsapiState.serverRunning) { - logger.warn('Start triggered with server already running'); - res.status(503).json({ "error": "Server already running." }); - } else if (nodejsapiState.operationPending != 'none') { - logger.warn(`Server Start triggered, while ${nodejsapiState.operationPending} pending.`); - res.status(503).json({ "error": `Another Operation is Pending: ${nodejsapiState.operationPending}` }); - } -}); - -/** - * @apiDescription Stop CS:GO Server - * - * @api {get} /csgoapi/v1.0/control/stop - * @apiVersion 1.0 - * @apiName Stop - * @apiGroup Control - - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 503 Service Unavailable - * { "error": "Server not running" } - */ -app.get('/csgoapi/v1.0/control/stop', ensureAuthenticated, (req, res) => { - if (nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - controlEmitter.emit('exec', 'stop', 'start'); - logger.verbose("sending quit."); - executeRcon('quit').then((answer) => { - nodejsapiState.serverRunning = false; - nodejsapiState.authenticated = false; - res.json({ "success": true }); - controlEmitter.emit('exec', 'stop', 'end'); - }).catch((err) => { - logger.error('Stopping server Failed: ' + err); - res.status(501).json({ "error": `RCON Error: ${err.toString()}` }); - controlEmitter.emit('exec', 'stop', 'end'); - }); - } else if (!nodejsapiState.serverRunning) { - logger.warn('Stop triggered, although server not running'); - res.status(503).json({ "error": "Server not running." }); - } else if (nodejsapiState.operationPending != 'none') { - logger.warn(`Stop triggered, while ${nodejsapiState.operationPending} pending.`); - res.status(503).json({ "error": `Another Operation is Pending: ${nodejsapiState.operationPending}` }); - } -}); - -/** - * @apiDescription Kill CS:GO Server Process in case no RCON connection. - * - * @api {get} /csgoapi/v1.0/control/kill - * @apiVersion 1.0 - * @apiName Kill - * @apiGroup Control - - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 501 Service Unavailable - * { "error": "Could not find csgo server process" } - */ - app.get('/csgoapi/v1.0/control/kill', ensureAuthenticated, (req, res) => { - exec('/bin/ps -a |grep srcds_linux', (error, stdout, stderr) => { - if (error) { - logger.error(`exec error: ${error}`); - res.status(501).json({ "error": "Could not find csgo server process" }); - } else if (stdout.match(/srcds_linux/) != null) { - let pid = stdout.split(/\s+/)[1]; - exec(`/bin/kill ${pid}`, (error, stdout, stderr) => { - if (error) { - res.status(501).json({ "error": "Could not kill csgo server process" }); - } else { - // reset API-State - nodejsapiState.serverRunning = false; - nodejsapiState.authenticated = false; - nodejsapiState.serverRcon = undefined; - logger.verbose('Server process killed.') - res.json({ "success": true }); - } - }); - } - }); -}); - -/** - * @apiDescription Update CS:GO Server - * - * @api {get} /csgoapi/v1.0/control/update - * @apiVersion 1.0 - * @apiName Update - * @apiGroup Control - - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 501 Internal Server Error - * { "error": "Update could not be started." } - */ -app.get('/csgoapi/v1.0/control/update', ensureAuthenticated, (req, res) => { - if (!nodejsapiState.serverRunning && nodejsapiState.operationPending == 'none') { - controlEmitter.emit('exec', 'update', 'start'); - let updateSuccess = false; - logger.verbose('Updating Server.'); - let updateProcess = pty.spawn(cfg.updateCommand, cfg.updateArguments); - - updateProcess.on('data', (data) => { - logger.debug(data); - if (data.indexOf('Checking for available updates') != -1) { - controlEmitter.emit('progress', 'Checking Steam client updates', 0); - } else if (data.indexOf('Verifying installation') != -1) { - controlEmitter.emit('progress', 'Verifying client installation', 0); - } else if (data.indexOf('Logging in user') != -1) { - controlEmitter.emit('progress', 'Logging in steam user', 0); - } else if (data.indexOf('Logged in OK') != -1) { - controlEmitter.emit('progress', 'Login OK', 100); - } else if(data.indexOf('Update state (0x') != -1) { - let rex = /Update state \(0x\d+\) (.+), progress: (\d{1,3})\.\d{2}/; - let matches = rex.exec(data); - controlEmitter.emit('progress', matches[1], matches[2]); - } else if (data.indexOf('Downloaaction update (') != -1) { - let rex = /\[(.+)] Downloaaction update/; - let matches = rex.exec(data); - controlEmitter.emit('progress', 'Updating Steam client', matches[1].slice(0, -1)); - } else if (data.indexOf('Success!') != -1) { - controlEmitter.emit('progress', 'Update successful!', 100); - logger.verbose('Update succeeded'); - updateSuccess = true; - controlEmitter.emit('exec', 'update', 'end'); - } - }); - - if (updateProcess) { - if (cfg.webSockets) { - res.json(`{ "success": true }`); - updateProcess.once('close', (code) => { - if (!updateSuccess) { - logger.warn('Update exited without success.'); - controlEmitter.emit('progress', 'Update failed!', 100); - controlEmitter.emit('exec', 'update', 'end'); - } - }); - } else { - updateProcess.once('close', (code) => { - if (updateSuccess) { - res.json({ "success": true }); - } else { - logger.warn('Update exited without success.'); - res.status(501).json({ "error": "Update was not successful" }); - } - controlEmitter.emit('exec', 'update', 'end'); - }); - } - } else { - logger.error('Update could not be started.'); - res.status(501).json({ "error": "Update could not be started." }); - controlEmitter.emit('exec', 'update', 'end'); - } - } else if (nodejsapiState.serverRunning) { - logger.warn('Update triggered, while server running.'); - res.status(503).json({ "error": "Server is running - stop before updating" }); - } else if (nodejsapiState.operationPending != 'none') { - logger.warn(`Update triggered, while ${nodejsapiState.operationPending} pending`); - res.status(503).json({ "error": `Another Operation is Pending: ${nodejsapiState.operationPending}` }); - } -}); - -//change map -/** - * @apiDescription Change Map - * - * @api {get} /csgoapi/v1.0/control/changemap - * @apiVersion 1.0 - * @apiName changemap - * @apiGroup Control - * - * @apiParam {string} mapname filename of the map without extension (.bsp) - * @apiParamExample {string} Map-example - * cs_italy - * - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - * @apiError {string} error - * @apiErrorExample {json} - * HTTP/1.1 501 Internal Server Error - * { "error": "RCON error: Unable to write to socket" } - */ -app.get('/csgoapi/v1.0/control/changemap', ensureAuthenticated, (req, res) => { - var args = req.query; - if (nodejsapiState.operationPending == 'none') { - controlEmitter.emit('exec', 'mapchange', 'start'); - // only try to change map, if it exists on the server. - if (serverInfo.mapsAvail.includes(args.map)) { - executeRcon(`map ${args.map}`).then((answer) => { - if (!cfg.webSockets) { - // If the mapchange completed, send success and cancel timeout. - let sendCompleted = (operation, action) => { - if (operation == 'mapchange' && action == 'end') { - res.json({ "success": true }); - clearTimeout(mapchangeTimeout); - } - }; - controlEmitter.once('exec', sendCompleted); - - // Failure of a mapchange is unfortunately not logged by the server, - // so we use a timeout after 30 sec. - let mapchangeTimeout = setTimeout( () => { - res.status(501).json({ "error": "Mapchange failed - timeout" }); - controlEmitter.emit('exec', 'mapchange', 'fail'); - }, 30000); - } else { - res.json({ "success": true }); - // If the mapchange is successful, cancel the timeout. - let removeTimeout = (operation, action) => { - if (operation == 'mapchange' && action == 'end') { - clearTimeout(mapchangeTimeout); - } - }; - controlEmitter.once('exec', removeTimeout); - - // Failure of a mapchange is unfortunately not logged by the server, - // so we use a timeout after 30 sec. - let mapchangeTimeout = setTimeout( () => { - controlEmitter.emit('exec', 'mapchange', 'fail'); - }, 30000); - } - }).catch((err) => { - res.status(501).json({ "error": `RCON error: ${err.toString()}`}); - controlEmitter.emit('exec', 'mapchange', 'fail'); - }); - } else { - res.status(501).json({ "error": `Map ${args.map} not available` }); - controlEmitter.emit('exec', 'mapchange', 'fail'); - } - } else { - logger.warn(`Mapchange triggered, while ${nodejsapiState.operationPending} pending.`); - res.status(503).json({ "error": `Another Operation is Pending: ${nodejsapiState.operationPending}` }); - } -}); - -/** - * @apiDescription Reload availbale maps from server. - * - * @api {get} /csgoapi/v1.0/control/reloadmaplist - * @apiVersion 1.0 - * @apiName reloadmaplist - * @apiGroup Control - - * @apiSuccess {boolean} success - * @apiSuccessExample {json} - * HTTP/1.1 200 OK - * { "success": true } - */ -app.get('/csgoapi/v1.0/control/reloadmaplist', ensureAuthenticated, (req, res) => { - reloadMaplist().then( (answer) => { - res.json(answer); - }); -}); - -/** - * @apiDescription Process rcon requests - * - * @api /csgoapi/v1.0/rcon - * @apiVersion 1.0 - * @apiName Rcon - * @apiGroup Rcon - * - * @apiParam {string} message RCON Command to execute. - * @apiParamExample {string} - * 'mp_limitteams 0' - * - * @apiSuccess {string} answer Servers answer string to RCON request - * @apiSuccessExample {string} - * "mp_maxrounds" = "30" ( def. "0" ) min. 0.000000 game notify replicated - max number of rounds to play before server changes maps - * L 11/06/2020 - 19:05:14: rcon from "127.0.0.1:54598": command "mp_maxrounds" - * @apiError {string} errortext - * @apiErrorExample {string} - * 'Error, check server logs for details.' - */ -app.get('/csgoapi/v1.0/rcon', ensureAuthenticated, (req, res) => { - var message = req.query.message; - res.set('Content-Type', 'text/plain'); - executeRcon(message).then((answer) => { - res.send(answer); - }).catch( (err) => { - res.status(501).send('Error, check server logs for details.'); - logger.error(err); - }); -}); -//------------------------ END V1.0 ----------------------------// if (cfg.useHttps) { var server = http.createServer(httpsCredentials, app); @@ -1395,16 +308,16 @@ if (cfg.webSockets) { * @param {string} action (start, end, fail) */ var sendControlNotification = (operation, action) => { - ws.send(`{ "type": "commandstatus", "payload": { "operation": "${operation}", "state": "${action}" } }`); - // For backward compatibility, this is still sent, will be deleted with version 1.0 - if (operation == 'mapchange' && action != 'start') { - ws.send(`{ "type": "mapchange", "payload": { "success": ${(action == 'end')} } }`); + ws.send(`{ "type": "commandstatus", "payload": { "operation": "${operation}", "state": "${action}" } }`); + // For backward compatibility, this is still sent, will be deleted with version 1.0 + if (operation == 'mapchange' && action != 'start') { + ws.send(`{ "type": "mapchange", "payload": { "success": ${(action == 'end')} } }`); + } } - } - /** - * Listens for execution notification of control operations. - * @listens controlEmitter#exec - */ + /** + * Listens for execution notification of control operations. + * @listens controlEmitter#exec + */ controlEmitter.on('exec', sendControlNotification); /** @@ -1413,12 +326,12 @@ if (cfg.webSockets) { * @param {int} progress - Integer representing the percentage of the action that is completed. */ var reportProgress = (action, progress) => { - ws.send(`{ "type": "progress", "payload": { "step": "${action}", "progress": ${progress} } }`); - } - /** - * Listens for progress reporst from update process and sends them to the client. - * @listens controlEmitter#progress - */ + ws.send(`{ "type": "progress", "payload": { "step": "${action}", "progress": ${progress} } }`); + } + /** + * Listens for progress reporst from update process and sends them to the client. + * @listens controlEmitter#progress + */ controlEmitter.on('progress', reportProgress); /** @@ -1442,7 +355,7 @@ if (cfg.webSockets) { logger.verbose(localIP); } - if(cfg.useHttps) { + if (cfg.useHttps) { const ws = new webSocket(`wss://${host}:${wssServer.address().port}`); } else { const ws = new webSocket(`ws://${host}:${wssServer.address().port}`); @@ -1471,13 +384,13 @@ var receiver = new logReceiver.LogReceiver(logOptions); receiver.on('data', (data) => { if (data.isValid) { // Start authentication, when not authenticated. - if ((data.message.indexOf('Log file started') != -1) && !nodejsapiState.authenticated) { + if ((data.message.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. - authenticate().then((data) => { + sf.authenticate().then((data) => { logger.verbose(`authentication ${data.authenticated}`); }).catch((data) => { logger.verbose(`authentication ${data.authenticated}`); @@ -1491,10 +404,10 @@ receiver.on('data', (data) => { let rex = /Started map \"(\S+)\"/g; let matches = rex.exec(data.message); let mapstring = matches[1]; - mapstring = cutMapName(mapstring); + mapstring = sf.cutMapName(mapstring); serverInfo.map = mapstring; // since 'started map' is also reported on server-start, only emit on mapchange. - if (nodejsapiState.operationPending == 'mapchange') { + if (serverInfo.serverState.operationPending == 'mapchange') { controlEmitter.emit('exec', 'mapchange', 'end'); } logger.verbose(`Started map: ${mapstring}`); @@ -1546,7 +459,7 @@ receiver.on('data', (data) => { let rex = /"(.+)<\d+><(STEAM_\d+:\d+:\d+)>/g; let matches = rex.exec(data.message); if (data.message.indexOf('entered the game') != -1) { - serverInfo.addPlayer( {'name': matches[1], 'steamID': matches[2]} ); + serverInfo.addPlayer({ 'name': matches[1], 'steamID': matches[2] }); } else if (data.message.search(/disconnected \(reason/) != -1) { serverInfo.removePlayer(matches[2]); } else if (data.message.indexOf('switched from team') != -1) { @@ -1573,7 +486,7 @@ receiver.on('invalid', (invalidMessage) => { * Query the server for mp_maxrounds.and store them in serverInfo */ function queryMaxRounds() { - executeRcon('mp_maxrounds').then((answer) => { + sf.executeRcon('mp_maxrounds').then((answer) => { // "mp_maxrounds" = "30" ( def. "0" ) min. 0.000000 game notify replicated // - max number of rounds to play before server changes maps let rex = /\"mp_maxrounds\" = \"(\d+)\"/g; @@ -1582,38 +495,4 @@ function queryMaxRounds() { }).catch((err) => { logger.error('Error getting Maxrounds: ' + err); }); -} - -/** - * 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. - * @returns {string} mapstring - The mapname without workshop path or .bsp - */ -function cutMapName(mapstring) { - if (mapstring.search('workshop') != -1) { - re = /(\w+)/g; - matches = mapstring.match(re); - mapstring = matches[2]; - } - if (mapstring.search(".bsp") != -1) { - mapstring = mapstring.substr(0, mapstring.length - 4); - } - return mapstring; -} +} \ No newline at end of file From 3882eb750385c6e90423eaeb1589ebab6bbe4f8a Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Mon, 11 Apr 2022 13:10:02 +0200 Subject: [PATCH 3/8] Merge branch 'dev' of https://github.com/taraman17/nodejs-csgo-api into dev --- .gitignore | 0 OfficialMaps.json | 0 config.js | 0 example/gameserver.css | 0 example/gameserver.htm | 0 example/js/gameserver.js | 0 example/js/jquery-3.5.1.min.js | 0 example/js/onload.js | 0 example/maplist.txt | 0 example/notauth.htm | 0 example/pic/ak47.gif | Bin example/pic/playicon.png | Bin example/pic/power-off.png | Bin example/pic/power-on.png | Bin modules/apiV10.js | 0 modules/configClass.js | 0 modules/controlEmitter.js | 0 modules/logger.js | 0 modules/serverInfo.js | 0 modules/sharedFunctions.js | 0 serverControl.js | 0 21 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 OfficialMaps.json mode change 100755 => 100644 config.js mode change 100755 => 100644 example/gameserver.css mode change 100755 => 100644 example/gameserver.htm mode change 100755 => 100644 example/js/gameserver.js mode change 100755 => 100644 example/js/jquery-3.5.1.min.js mode change 100755 => 100644 example/js/onload.js mode change 100755 => 100644 example/maplist.txt mode change 100755 => 100644 example/notauth.htm mode change 100755 => 100644 example/pic/ak47.gif mode change 100755 => 100644 example/pic/playicon.png mode change 100755 => 100644 example/pic/power-off.png mode change 100755 => 100644 example/pic/power-on.png mode change 100755 => 100644 modules/apiV10.js mode change 100755 => 100644 modules/configClass.js mode change 100755 => 100644 modules/controlEmitter.js mode change 100755 => 100644 modules/logger.js mode change 100755 => 100644 modules/serverInfo.js mode change 100755 => 100644 modules/sharedFunctions.js mode change 100755 => 100644 serverControl.js diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/OfficialMaps.json b/OfficialMaps.json old mode 100755 new mode 100644 diff --git a/config.js b/config.js old mode 100755 new mode 100644 diff --git a/example/gameserver.css b/example/gameserver.css old mode 100755 new mode 100644 diff --git a/example/gameserver.htm b/example/gameserver.htm old mode 100755 new mode 100644 diff --git a/example/js/gameserver.js b/example/js/gameserver.js old mode 100755 new mode 100644 diff --git a/example/js/jquery-3.5.1.min.js b/example/js/jquery-3.5.1.min.js old mode 100755 new mode 100644 diff --git a/example/js/onload.js b/example/js/onload.js old mode 100755 new mode 100644 diff --git a/example/maplist.txt b/example/maplist.txt old mode 100755 new mode 100644 diff --git a/example/notauth.htm b/example/notauth.htm old mode 100755 new mode 100644 diff --git a/example/pic/ak47.gif b/example/pic/ak47.gif old mode 100755 new mode 100644 diff --git a/example/pic/playicon.png b/example/pic/playicon.png old mode 100755 new mode 100644 diff --git a/example/pic/power-off.png b/example/pic/power-off.png old mode 100755 new mode 100644 diff --git a/example/pic/power-on.png b/example/pic/power-on.png old mode 100755 new mode 100644 diff --git a/modules/apiV10.js b/modules/apiV10.js old mode 100755 new mode 100644 diff --git a/modules/configClass.js b/modules/configClass.js old mode 100755 new mode 100644 diff --git a/modules/controlEmitter.js b/modules/controlEmitter.js old mode 100755 new mode 100644 diff --git a/modules/logger.js b/modules/logger.js old mode 100755 new mode 100644 diff --git a/modules/serverInfo.js b/modules/serverInfo.js old mode 100755 new mode 100644 diff --git a/modules/sharedFunctions.js b/modules/sharedFunctions.js old mode 100755 new mode 100644 diff --git a/serverControl.js b/serverControl.js old mode 100755 new mode 100644 From a3e79670e1a90223fceec0ceec6b315abf447aa3 Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Fri, 22 Apr 2022 21:05:51 +0200 Subject: [PATCH 4/8] Add http basic authentication as option with a single configurable user. --- config.js | 4 + example/gameserver.htm | 240 ++++++++++++++++----------------- example/js/jquery-3.5.1.min.js | 4 +- example/maplist.txt | 14 +- modules/configClass.js | 7 + package.json | 2 +- serverControl.js | 38 +++++- 7 files changed, 172 insertions(+), 137 deletions(-) diff --git a/config.js b/config.js index 86a1e61..03288bd 100644 --- a/config.js +++ b/config.js @@ -16,6 +16,10 @@ "redirectPage": "/loginStatus", // Time in minutes, after which a new login is needed. "loginValidity": 300, + // Use digest authentication [true/false] + "httpAuth": false, + // User for digest Authentication. Password needs to be hashed with TBD + "httpUser": { "username": "", "password": "" }, // Port, the webserver for API calls listens on. "apiPort": 8090, // Set to true if you use Websockets for status updates. diff --git a/example/gameserver.htm b/example/gameserver.htm index 3f8c6eb..155b720 100644 --- a/example/gameserver.htm +++ b/example/gameserver.htm @@ -1,121 +1,121 @@ - - - - CS:GO Gameserver - - - - - - -
- -
- - -
-
- - - - - - - -
- Starten mit:   - -
-
-
- -
-
-
-
-
current Map:
-
Rounds: 30 / Left: 30
-
Score: CT: 0 / T: 0
-
-
-

Players:

-
-
-

Counter Terrorists

-
    -
-
-
-

Terrorists

-
    -
-
-
-

Unassigned

-
    -
-
-
-

Spectators

-
    -
-
-
-
-
-
- - -
- -
-
- - - - - - + + + + CS:GO Gameserver + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+ Starten mit:   + +
+
+
+ +
+
+
+
+
current Map:
+
Rounds: 30 / Left: 30
+
Score: CT: 0 / T: 0
+
+
+

Players:

+
+
+

Counter Terrorists

+
    +
+
+
+

Terrorists

+
    +
+
+
+

Unassigned

+
    +
+
+
+

Spectators

+
    +
+
+
+
+
+
+ + +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/example/js/jquery-3.5.1.min.js b/example/js/jquery-3.5.1.min.js index b061403..c5e96cc 100644 --- a/example/js/jquery-3.5.1.min.js +++ b/example/js/jquery-3.5.1.min.js @@ -1,2 +1,2 @@ -/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 { * Creates an express server to handle the API requests */ const app = express(); +var apiV10 = require('./modules/apiV10.js'); const limit = rateLimit({ max: 50, // max requests windowMs: 60 * 1000, // 1 Minute @@ -178,7 +180,7 @@ function ensureAuthenticated(req, res, next) { return res.status(401).send('User not in Admin list.'); } } - logger.info({ + logger.warn({ 'user': 'unknown', 'message': `Unauthorized Access from ${req.ip}.` }); @@ -221,7 +223,7 @@ app.get('/csgoapi/login/return', * @apiName Logout * @apiGroup Auth * - * @apiSuccess (302) Redirect to confiured page. + * @apiSuccess (302) Redirect to configured page. */ app.get('/csgoapi/logout', (req, res) => { logger.http({ @@ -252,21 +254,43 @@ app.get('/csgoapi/loginStatus', (req, res) => { res.json({ "login": false }); } }); -//------------------------ END Steam authentication ----------------------------// -//------------------------ Routes with steam auth ----------------------------// -var apiV10 = require('./modules/apiV10.js'); app.use('/csgoapi/v1.0/', ensureAuthenticated, apiV10); -//------------------------ END Routes with steam auth ----------------------------// +//------------------------ END Steam authentication ----------------------------// +//------------------------ Basic authentication ----------------------------// +if (cfg.httpAuth) { + passport.use(new BasicStrategy({ qop: 'auth', passReqToCallback: true }, + (req, username, password, done) => { + if (username == cfg.httpUser.username) { + if (password == cfg.httpUser.password) { + return done(null, cfg.httpUser.username); + } else { + logger.warn({ + 'user': username, + 'message': `Unauthorized http Access - wrong Password - from ${req.ip}.` + }); + return done(null, false); + } + } else { + logger.warn({ + 'user': username, + 'message': `Unauthorized http Access - unknown user - from ${req.ip}.` + }); + return done(null, false); + } + } + )); + app.use('/csgoapi/http/v1.0/', passport.authenticate('basic', { session: false }), apiV10); +} +//--------------------- END Basic authentication --------------------------// if (cfg.useHttps) { var server = http.createServer(httpsCredentials, app); } else { var server = http.createServer(app); } - server.listen(cfg.apiPort); /*----------------- WebSockets Code -------------------*/ From 76c2cca5f1bd1723a4d61313508f1f2ebc416ecc Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Thu, 5 May 2022 18:48:53 +0200 Subject: [PATCH 5/8] fix reporting of failed update --- example/js/onload.js | 16 +++++++++------- modules/apiV10.js | 6 +++++- serverControl.js | 30 ++++++++++++++---------------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/example/js/onload.js b/example/js/onload.js index d8c4a07..5d4e8d2 100644 --- a/example/js/onload.js +++ b/example/js/onload.js @@ -1,6 +1,6 @@ // what to do after document is loaded. var socket = null; -$( document ).ready(() => { +$(document).ready(() => { let startSocket = () => { try { socket = new WebSocket(`wss://${host}:8091`); @@ -28,7 +28,7 @@ $( document ).ready(() => { $('.playerDiv ul').empty(); $('.playerDiv').hide(0); if (serverInfo.players.length > 0) { - for (let i=0; i < serverInfo.players.length; i++) { + 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); @@ -38,7 +38,7 @@ $( document ).ready(() => { if (serverInfo.mapsDetails) { let maplist = serverInfo.mapsDetails; $("#mapSelector").empty(); - maplist.forEach( (map) => { + maplist.forEach((map) => { if ('content' in document.createElement('template')) { var mapDiv = document.querySelector('#maptemplate'); mapDiv.content.querySelector('.mapname').textContent = map.name; @@ -59,16 +59,18 @@ $( document ).ready(() => { $('.container-popup').css('display', 'flex'); } else if (data.payload.state == 'end' && data.payload.operation != 'start') { $('#popupText').html(`${data.payload.operation} success!`); - setTimeout( () => { + setTimeout(() => { $('.container-popup').css('display', 'none'); setupPage(); }, 1500); } else if (data.payload.state == 'fail') { $('#popupText').html(`${data.payload.operation} failed!`); - setTimeout( () => { + setTimeout(() => { $('.container-popup').css('display', 'none'); - window.location.href = './notauth.htm'; - }, 1500); + if (data.payload.operation != 'update') { + window.location.href = './notauth.htm'; + } + }, 3000); } } else if (data.type == "progress") { $('#popupText').html(`${data.payload.step}: ${data.payload.progress}%`); diff --git a/modules/apiV10.js b/modules/apiV10.js index fc0ddb9..d10f212 100644 --- a/modules/apiV10.js +++ b/modules/apiV10.js @@ -438,6 +438,10 @@ router.get('/control/update', (req, res) => { controlEmitter.emit('progress', 'Verifying client installation', 0); } else if (data.indexOf('Logging in user') != -1) { controlEmitter.emit('progress', 'Logging in steam user', 0); + } else if (data.indexOf('FAILED') != -1) { + let rex = /FAILED \((.+)\)/; + let matches = rex.exec(data); + controlEmitter.emit('progress', `Login Failed: ${matches[1]}`, 0); } else if (data.indexOf('Logged in OK') != -1) { controlEmitter.emit('progress', 'Login OK', 100); } else if (data.indexOf('Update state (0x') != -1) { @@ -463,7 +467,7 @@ router.get('/control/update', (req, res) => { if (!updateSuccess) { logger.warn('Update exited without success.'); controlEmitter.emit('progress', 'Update failed!', 100); - controlEmitter.emit('exec', 'update', 'end'); + controlEmitter.emit('exec', 'update', 'fail'); } }); } else { diff --git a/serverControl.js b/serverControl.js index 3e7a96f..b803608 100644 --- a/serverControl.js +++ b/serverControl.js @@ -332,16 +332,13 @@ if (cfg.webSockets) { * @param {string} action (start, end, fail) */ var sendControlNotification = (operation, action) => { - ws.send(`{ "type": "commandstatus", "payload": { "operation": "${operation}", "state": "${action}" } }`); - // For backward compatibility, this is still sent, will be deleted with version 1.0 - if (operation == 'mapchange' && action != 'start') { - ws.send(`{ "type": "mapchange", "payload": { "success": ${(action == 'end')} } }`); - } - } - /** - * Listens for execution notification of control operations. - * @listens controlEmitter#exec - */ + ws.send(`{ "type": "commandstatus", "payload": { "operation": "${operation}", "state": "${action}" } }`); + } + + /** + * Listens for execution notification of control operations. + * @listens controlEmitter#exec + */ controlEmitter.on('exec', sendControlNotification); /** @@ -350,12 +347,13 @@ if (cfg.webSockets) { * @param {int} progress - Integer representing the percentage of the action that is completed. */ var reportProgress = (action, progress) => { - ws.send(`{ "type": "progress", "payload": { "step": "${action}", "progress": ${progress} } }`); - } - /** - * Listens for progress reporst from update process and sends them to the client. - * @listens controlEmitter#progress - */ + ws.send(`{ "type": "progress", "payload": { "step": "${action}", "progress": ${progress} } }`); + } + + /** + * Listens for progress reporst from update process and sends them to the client. + * @listens controlEmitter#progress + */ controlEmitter.on('progress', reportProgress); /** From 998c725d78775ebb7e518923fe02f7e9f92e5b9b Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Thu, 5 May 2022 20:01:21 +0200 Subject: [PATCH 6/8] add logging for successful http api calls --- serverControl.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/serverControl.js b/serverControl.js index b803608..8ece80d 100644 --- a/serverControl.js +++ b/serverControl.js @@ -264,6 +264,10 @@ if (cfg.httpAuth) { (req, username, password, done) => { if (username == cfg.httpUser.username) { if (password == cfg.httpUser.password) { + logger.http({ + "user": username, + "message": `${req.method}:${req.url}` + }); return done(null, cfg.httpUser.username); } else { logger.warn({ From bdd157a88603f6a79b8b567d777657b1f38fb7b7 Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Thu, 5 May 2022 20:16:12 +0200 Subject: [PATCH 7/8] Update README.MD for http authentication --- README.MD | 20 ++++++++++++++++---- config.js | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.MD b/README.MD index ea87cf9..8af15fe 100644 --- a/README.MD +++ b/README.MD @@ -19,6 +19,10 @@ download the script files to and install the dependencies for nodejs ```console npm install --save rcon-srcds srcds-log-receiver local-ip express express-session express-rate-limit cors passport passport-steam node-pty ws winston winston-daily-rotate-file ``` +If you want to use http authentication, add +```console +npm install --save passport-http +``` ## Configuration The CS:GO Server must be configured to send logs to the local IP (not 127.0.0.1): on port 9871 @@ -147,12 +151,20 @@ UpdateProgress looks as follows: ```javascript { "type": "updateProgress", "payload": { "step": , "progress": } } ``` +## Api calls with http authentication +For stateless API-Calls via command line or other automatic tools the http basic authentication can be activated in the config with +``` +"httpAuth": true, +"httpUser": { "username": "", "password": "" }, +``` +For now, only one user can be specified. -mapchange message (deprecated, do not use anymore): -```javascript -{ "type": "mapchange", "payload": { "success": true/false } +If you enable this option, you should use https to avoid sending of paswords in the clear. + +The interface is: +``` +http://:/csgoapi/http/v1.0/... ``` -false is sent after a 30 sec. timeout when no "Started map" log has been received. ## Example An example of a webinterface is available in the folder "example" diff --git a/config.js b/config.js index 03288bd..19acac4 100644 --- a/config.js +++ b/config.js @@ -16,7 +16,7 @@ "redirectPage": "/loginStatus", // Time in minutes, after which a new login is needed. "loginValidity": 300, - // Use digest authentication [true/false] + // Use http authentication [true/false] "httpAuth": false, // User for digest Authentication. Password needs to be hashed with TBD "httpUser": { "username": "", "password": "" }, From 55751f37549740e55e1133730524d2418d3b6a3b Mon Sep 17 00:00:00 2001 From: Taraman17 Date: Thu, 5 May 2022 20:32:12 +0200 Subject: [PATCH 8/8] little corrections in README.MD --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 8af15fe..b0c6df7 100644 --- a/README.MD +++ b/README.MD @@ -159,7 +159,7 @@ For stateless API-Calls via command line or other automatic tools the http basic ``` For now, only one user can be specified. -If you enable this option, you should use https to avoid sending of paswords in the clear. +**If you enable this option, you should use https to avoid sending of paswords in the clear.** The interface is: ```