From befc5ee49d763b3cf4ab539b764cda589e40bd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Mon, 21 Oct 2024 14:32:11 +0000 Subject: [PATCH] fix(startup): Better detection of invalid config file structure and content Fixes #925 --- .gitignore | 1 + .vscode/launch.json | 2 +- src/butler-sos.js | 20 +---- src/config/production_template.yaml | 4 +- src/globals.js | 33 +++++++ src/lib/config-file-schema.js | 2 +- src/lib/config-file-verify.js | 133 +++++++++++++--------------- 7 files changed, 103 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index ae89cac3..ff528eb9 100755 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ jspm_packages # Optional REPL history .node_repl_history +build.cjs diff --git a/.vscode/launch.json b/.vscode/launch.json index 2c832a60..c0251804 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "request": "launch", "name": "Launch Program", "program": "${workspaceFolder}/src/butler-sos.js", - "runtimeVersion": "20", + "runtimeVersion": "18", "cwd": "${workspaceFolder}/src", "env": { "NODE_CONFIG_DIR": "${workspaceFolder}/src/config", diff --git a/src/butler-sos.js b/src/butler-sos.js index c335df22..bbb60f32 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -22,7 +22,6 @@ import { udpInitUserActivityServer } from './lib/udp_handlers_user_activity.js'; import { udpInitLogEventServer } from './lib/udp_handlers_log_events.js'; import { setupAnonUsageReportTimer } from './lib/telemetry.js'; import { setupPromClient } from './lib/prom-client.js'; -import { verifyConfigFile } from './lib/config-file-verify.js'; import { setupConfigVisServer } from './lib/config-visualise.js'; import { setupUdpEventsStorage } from './lib/udp-event.js'; @@ -60,25 +59,8 @@ async function mainScript() { const globals = await settingsObj.init(); globals.logger.verbose(`START: Globals init done: ${globals.initialised}`); - // Verify that the config file has the correct format - // Only do this if the command line option no-config-file-verify is NOT set - let configFileVerify = false; - if (globals.options.skipConfigVerification) { - globals.logger.warn('MAIN: Skipping config file verification'); - } else { - configFileVerify = await verifyConfigFile(); - } - - // If config file verification failed, the previous function would have returned false. - // In that case, we should exit the script. - if (!configFileVerify) { - globals.logger.error('MAIN: Config file verification failed. Exiting.'); - process.exit(1); - } - // Ensure that initialisation of globals is complete - // Sleep 5 seconds otherwise to llow globals to be initialised - + // Sleep 5 seconds otherwise to allow globals to be initialised function sleepLocal(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 3ed9bc04..8bfdaa6c 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -3,7 +3,7 @@ Butler-SOS: # All configuration items are mandatory, unless otherwise noted. # Logging configuration - logLevel: info # Log level. Possible log levels are silly, debug, verbose, info, warn, error + logLevel: info # Log level. Possible log levels are silly, debug, verbose, info, warn, error. Case sensitive. fileLogging: true # true/false to enable/disable logging to disk file logDirectory: log # Subdirectory where log files are stored anonTelemetry: true # Can Butler SOS send anonymous data about what computer it is running on? @@ -81,7 +81,7 @@ Butler-SOS: qlikSenseEvents: # Shared settings for user and log events (see below) influxdb: enable: false # Should summary (counter) of user/log events, and rejected events be stored in InfluxDB? - writeFrequency: 20000 # How often (milliseconds) should rejected event count be written to InfluxDB? + writeFrequency: 20000 # How often (milliseconds) should event counts be written to InfluxDB? eventCount: # Track how many events are received from Sense. # Some events are valid, some are not. Of the valid events, some are rejected by Butler SOS # based on the configuration in this file. diff --git a/src/globals.js b/src/globals.js index 83db1e63..31df1b82 100755 --- a/src/globals.js +++ b/src/globals.js @@ -16,6 +16,7 @@ import { fileURLToPath } from 'url'; import { getServerTags } from './lib/servertags.js'; import { UdpEvents } from './lib/udp-event.js'; +import { verifyConfigFileSchema, verifyAppConfig } from './lib/config-file-verify.js'; let instance = null; @@ -134,6 +135,7 @@ class Settings { process.exit(1); } } else { + // No config file specified on command line. // Get value of env variable NODE_ENV const env = process.env.NODE_ENV; @@ -143,8 +145,39 @@ class Settings { this.configFile = upath.resolve(dirname, `./config/${env}.yaml`); } + // Full path to config file in this.configFile + // Verify schema of config file + // Only do this if the command line option no-config-file-verify is NOT set + if (this.options.skipConfigVerification) { + console.warn('MAIN: Skipping config file verification'); + } else { + let configFileVerify = await verifyConfigFileSchema(this.configFile); + + // If config file verification failed, the previous function would have returned false. + // In that case, we should exit the script. + if (!configFileVerify) { + console.error('MAIN: Config file verification failed. Exiting.'); + process.exit(1); + } + } + + // Load config file this.config = (await import('config')).default; + // Verify application specific settings and relationships between settings + if (this.options.skipConfigVerification) { + console.warn('MAIN: Skipping application specific config verification'); + } else { + let appConfigVerify = await verifyAppConfig(this.config); + + // If application specific config verification failed, the previous function would have returned false. + // In that case, we should exit the script. + if (!appConfigVerify) { + console.error('MAIN: Application specific config verification failed. Exiting.'); + process.exit(1); + } + } + this.execPath = this.isPkg ? upath.dirname(process.execPath) : process.cwd(); // Are we running as standalone app or not? diff --git a/src/lib/config-file-schema.js b/src/lib/config-file-schema.js index 2ab15573..626a00f9 100755 --- a/src/lib/config-file-schema.js +++ b/src/lib/config-file-schema.js @@ -7,7 +7,7 @@ export const confifgFileSchema = { logLevel: { type: 'string', enum: ['error', 'warn', 'info', 'verbose', 'debug', 'silly'], - transform: ['trim', 'toLowerCase'], + transform: ['trim'], }, fileLogging: { type: 'boolean' }, logDirectory: { type: 'string' }, diff --git a/src/lib/config-file-verify.js b/src/lib/config-file-verify.js index ad2b9469..f84df94e 100755 --- a/src/lib/config-file-verify.js +++ b/src/lib/config-file-verify.js @@ -2,12 +2,11 @@ import { load } from 'js-yaml'; import fs from 'fs/promises'; import { default as Ajv } from 'ajv'; -import globals from '../globals.js'; import { confifgFileSchema } from './config-file-schema.js'; // Function to verify that the config file has the correct format // Use yaml-validator to validate the config file -export async function verifyConfigFile() { +export async function verifyConfigFileSchema(configFile) { try { const ajv = new Ajv({ strict: true, @@ -27,8 +26,8 @@ export async function verifyConfigFile() { // Add formats to ajv instance ajvFormats.default(ajv); - // Load the YAML schema file, identified by globals.configFile, from file - const fileContent = await fs.readFile(globals.configFile, 'utf8'); + // Load the YAML schema file, identified by configFile, from file + const fileContent = await fs.readFile(configFile, 'utf8'); // Parse the YAML file let parsedFileContent; @@ -52,87 +51,83 @@ export async function verifyConfigFile() { // - message: The error message for (const error of validate.errors) { - globals.logger.error( - `VERIFY CONFIG FILE: ${error.instancePath} : ${error.message}` - ); + console.error(`VERIFY CONFIG FILE ERROR: ${error.instancePath} : ${error.message}`); } process.exit(1); } - // ------------------------------ - // Verify values of specific config entries - - // If InfluxDB is enabled, check if the version is valid - // Valid values: 1 and 2 - if (globals.config.get('Butler-SOS.influxdbConfig.enable') === true) { - const influxdbVersion = globals.config.get('Butler-SOS.influxdbConfig.version'); - if (influxdbVersion !== 1 && influxdbVersion !== 2) { - globals.logger.error( - `VERIFY CONFIG FILE: Butler-SOS.influxdbConfig.enable (=InfluxDB version) ${influxdbVersion} is invalid. Exiting.` - ); - process.exit(1); - } - } + console.info( + `VERIFY CONFIG FILE: Your config file at ${configFile} is correctly formatted, good work!` + ); - // Verify that server tags are correctly defined - // In the config file section `Butler-SOS.serversToMonitor.serverTagsDefinition` it's possible to define zero or more tags that can be set for each server that is to be monitored. - // When Butler SOS is started, do the following checks: - // 1. All tags present in `Butler-SOS.serversToMonitor.serverTagsDefinition` must be set for each server in `SOS.serversToMonitor.servers[]` - // 2. The tags specified for each server in `SOS.serversToMonitor.servers[].serverTags` must be present in `Butler-SOS.serversToMonitor.serverTagsDefinition` - // If either of the conditions above is false, an error should be logged and Butler SOS should not start. - try { - // Loop over all defined server tags - const serverTagsDefinition = globals.config.get( - 'Butler-SOS.serversToMonitor.serverTagsDefinition' + return true; + } catch (err) { + console.error(`VERIFY CONFIG FILE: ${err}`); + + return false; + } +} + +// Function to do verification of app specific settings and relationships between settings +export async function verifyAppConfig(cfg) { + // Verify values of specific config entries + + // If InfluxDB is enabled, check if the version is valid + // Valid values: 1 and 2 + if (cfg.get('Butler-SOS.influxdbConfig.enable') === true) { + const influxdbVersion = cfg.get('Butler-SOS.influxdbConfig.version'); + if (influxdbVersion !== 1 && influxdbVersion !== 2) { + console.error( + `VERIFY CONFIG FILE ERROR: Butler-SOS.influxdbConfig.enable (=InfluxDB version) ${influxdbVersion} is invalid. Exiting.` ); - for (const tag of serverTagsDefinition) { - // Check that all servers have this tag - const servers = globals.config.get('Butler-SOS.serversToMonitor.servers'); - for (const server of servers) { - // Check if server.serverTags.tag is defined - if (server?.serverTags === null || !server?.serverTags[tag]) { - globals.logger.error( - `VERIFY CONFIG FILE: Server tag "${tag}" is not defined for server "${server.serverName}". Exiting.` - ); - process.exit(1); - } else { - globals.logger.verbose( - `VERIFY CONFIG FILE: Server tag "${tag}" is defined for server "${server.serverName}".` - ); - } - } - } + return false; + } + } - // Now ensure that the tags defined for each server are valid and that there are no extra tags there - const servers = globals.config.get('Butler-SOS.serversToMonitor.servers'); + // Verify that server tags are correctly defined + // In the config file section `Butler-SOS.serversToMonitor.serverTagsDefinition` it's possible to define zero or more tags that can be set for each server that is to be monitored. + // When Butler SOS is started, do the following checks: + // 1. All tags present in `Butler-SOS.serversToMonitor.serverTagsDefinition` must be set for each server in `SOS.serversToMonitor.servers[]` + // 2. The tags specified for each server in `SOS.serversToMonitor.servers[].serverTags` must be present in `Butler-SOS.serversToMonitor.serverTagsDefinition` + // If either of the conditions above is false, an error should be logged and Butler SOS should not start. + try { + // Loop over all defined server tags + const serverTagsDefinition = cfg.get('Butler-SOS.serversToMonitor.serverTagsDefinition'); + for (const tag of serverTagsDefinition) { + // Check that all servers have this tag + const servers = cfg.get('Butler-SOS.serversToMonitor.servers'); for (const server of servers) { - for (const tag in server.serverTags) { - if (!serverTagsDefinition.includes(tag)) { - globals.logger.error( - `VERIFY CONFIG FILE: Server tag "${tag}" for server "${server.serverName}" is not defined in Butler-SOS.serversToMonitor.serverTagsDefinition. Exiting.` - ); - process.exit(1); - } else { - globals.logger.verbose( - `VERIFY CONFIG FILE: Server tag "${tag}" is defined in Butler-SOS.serversToMonitor.serverTagsDefinition.` - ); - } + // Check if server.serverTags.tag is defined + if (server?.serverTags === null || !server?.serverTags[tag]) { + console.error( + `VERIFY CONFIG FILE: Server tag "${tag}" is not defined for server "${server.serverName}". Exiting.` + ); + return false; + } else { + // The tag is defined for this server } } - } catch (err) { - globals.logger.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`); - process.exit(1); } - globals.logger.info( - `VERIFY CONFIG FILE: Your config file at ${globals.configFile} is valid, good work!` - ); + // Now ensure that the tags defined for each server are valid and that there are no extra tags there + const servers = cfg.get('Butler-SOS.serversToMonitor.servers'); + for (const server of servers) { + for (const tag in server.serverTags) { + if (!serverTagsDefinition.includes(tag)) { + console.error( + `VERIFY CONFIG FILE: Server tag "${tag}" for server "${server.serverName}" is not defined in Butler-SOS.serversToMonitor.serverTagsDefinition. Exiting.` + ); + return false; + } else { + // The tag is defined in Butler-SOS.serversToMonitor.serverTagsDefinition + } + } + } return true; } catch (err) { - globals.logger.error(`VERIFY CONFIG FILE: ${err}`); - + console.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`); return false; } }