// server // app/config.js 'use strict' const path = require('path') const fs = require('fs') const readConfig = require('read-config-ng') const Ajv = require('ajv') const crypto = require('crypto') /** * @typedef {Object} Config * @property {Object} listen - Listening configuration * @property {string} listen.ip - IP address to listen on * @property {number} listen.port - Port to listen on * @property {Object} http - HTTP configuration * @property {string[]} http.origins - Allowed origins * @property {Object} user - User configuration * @property {string|null} user.name - Username * @property {string|null} user.password - Password * @property {Object} ssh - SSH configuration * @property {string|null} ssh.host - SSH host * @property {number} ssh.port - SSH port * @property {string} ssh.term - Terminal type * @property {number} ssh.readyTimeout - Ready timeout * @property {number} ssh.keepaliveInterval - Keepalive interval * @property {number} ssh.keepaliveCountMax - Max keepalive count * @property {Object} terminal - Terminal configuration * @property {boolean} terminal.cursorBlink - Whether cursor blinks * @property {number} terminal.scrollback - Scrollback limit * @property {number} terminal.tabStopWidth - Tab stop width * @property {string} terminal.bellStyle - Bell style * @property {Object} header - Header configuration * @property {string|null} header.text - Header text * @property {string} header.background - Header background color * @property {Object} options - Options configuration * @property {boolean} options.challengeButton - Challenge button enabled * @property {boolean} options.allowReauth - Allow reauthentication * @property {Object} algorithms - Encryption algorithms * @property {string[]} algorithms.kex - Key exchange algorithms * @property {string[]} algorithms.cipher - Cipher algorithms * @property {string[]} algorithms.hmac - HMAC algorithms * @property {string[]} algorithms.compress - Compression algorithms * @property {Object} serverlog - Server log configuration * @property {boolean} serverlog.client - Client logging enabled * @property {boolean} serverlog.server - Server logging enabled * @property {boolean} accesslog - Access logging enabled * @property {boolean} verify - Verification enabled */ /** * Default configuration * @type {Config} */ const defaultConfig = { listen: { ip: '0.0.0.0', port: 2222 }, http: { origins: ['*:*'] }, user: { name: null, password: null }, ssh: { host: null, port: 22, term: 'xterm-color', readyTimeout: 20000, keepaliveInterval: 120000, keepaliveCountMax: 10 }, terminal: { cursorBlink: true, scrollback: 10000, tabStopWidth: 8, bellStyle: 'sound' }, header: { text: null, background: 'green' }, options: { challengeButton: true, allowReauth: false, allowReplay: false }, algorithms: { kex: [ 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha1' ], cipher: [ 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-gcm', 'aes128-gcm@openssh.com', 'aes256-gcm', 'aes256-gcm@openssh.com', 'aes256-cbc' ], hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], compress: ['none', 'zlib@openssh.com', 'zlib'] }, session: { secret: generateSecureSecret(), name: 'webssh2.sid' }, serverlog: { client: false, server: false }, accesslog: false, verify: false } /** * Schema for validating the config */ const configSchema = { type: 'object', properties: { listen: { type: 'object', properties: { ip: { type: 'string', format: 'ipv4' }, port: { type: 'integer', minimum: 1, maximum: 65535 } }, required: ['ip', 'port'] }, http: { type: 'object', properties: { origins: { type: 'array', items: { type: 'string' } } }, required: ['origins'] }, user: { type: 'object', properties: { name: { type: ['string', 'null'] }, password: { type: ['string', 'null'] } }, required: ['name', 'password'] }, ssh: { type: 'object', properties: { host: { type: ['string', 'null'] }, port: { type: 'integer', minimum: 1, maximum: 65535 }, term: { type: 'string' }, readyTimeout: { type: 'integer' }, keepaliveInterval: { type: 'integer' }, keepaliveCountMax: { type: 'integer' } }, required: ['host', 'port', 'term', 'readyTimeout', 'keepaliveInterval', 'keepaliveCountMax'] }, terminal: { type: 'object', properties: { cursorBlink: { type: 'boolean' }, scrollback: { type: 'integer' }, tabStopWidth: { type: 'integer' }, bellStyle: { type: 'string' } }, required: ['cursorBlink', 'scrollback', 'tabStopWidth', 'bellStyle'] }, header: { type: 'object', properties: { text: { type: ['string', 'null'] }, background: { type: 'string' } }, required: ['text', 'background'] }, options: { type: 'object', properties: { challengeButton: { type: 'boolean' }, allowReauth: { type: 'boolean' }, allowReplay: { type: 'boolean' } }, required: ['challengeButton', 'allowReauth', 'allowReplay'] }, algorithms: { type: 'object', properties: { kex: { type: 'array', items: { type: 'string' } }, cipher: { type: 'array', items: { type: 'string' } }, hmac: { type: 'array', items: { type: 'string' } }, compress: { type: 'array', items: { type: 'string' } } }, required: ['kex', 'cipher', 'hmac', 'compress'] }, session: { type: 'object', properties: { secret: { type: 'string' }, name: { type: 'string' } }, required: ['secret', 'name'] }, serverlog: { type: 'object', properties: { client: { type: 'boolean' }, server: { type: 'boolean' } }, required: ['client', 'server'] }, accesslog: { type: 'boolean' }, verify: { type: 'boolean' } }, required: ['listen', 'http', 'user', 'ssh', 'terminal', 'header', 'options', 'algorithms', 'serverlog', 'accesslog', 'verify'] } /** * Gets the path to the config file * @returns {string} The path to the config file */ function getConfigPath() { const nodeRoot = path.dirname(require.main.filename) return path.join(nodeRoot, 'config.json') } /** * Reads the config file * @param {string} configPath - The path to the config file * @returns {Config} The configuration object */ function readConfigFile(configPath) { console.log('WebSSH2 service reading config from: ' + configPath) return readConfig(configPath) } /** * Validates the configuration against the schema * @param {Object} config - The configuration object to validate * @returns {Object} The validated configuration object * @throws {Error} If the configuration is invalid */ function validateConfig(config) { const ajv = new Ajv() const validate = ajv.compile(configSchema) const valid = validate(config) console.log('WebSSH2 service validating config') if (!valid) { throw new Error('Config validation error: ' + ajv.errorsText(validate.errors)) } return config } /** * Logs an error message * @param {string} message - The error message * @param {Error} [error] - The error object */ function logError(message, error) { console.error(message) if (error) { console.error('ERROR:\n\n ' + error) } } /** * Loads and merges the configuration * @returns {Config} The merged configuration */ function loadConfig() { const configPath = getConfigPath() try { if (fs.existsSync(configPath)) { const providedConfig = readConfigFile(configPath) // Deep merge the provided config with the default config const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig) const validatedConfig = validateConfig(mergedConfig) console.log('Merged and validated configuration') return validatedConfig } else { logError( '\n\nERROR: Missing config.json for webssh. Using default config: ' + JSON.stringify(defaultConfig) + '\n\n See config.json.sample for details\n\n' ) return defaultConfig } } catch (err) { logError( '\n\nERROR: Problem loading config.json for webssh. Using default config: ' + JSON.stringify(defaultConfig) + '\n\n See config.json.sample for details\n\n', err ) return defaultConfig } } /** * Generates a secure random session secret * @returns {string} A random 32-byte hex string */ function generateSecureSecret() { return crypto.randomBytes(32).toString('hex') } /** * Deep merges two objects * @param {Object} target - The target object to merge into * @param {Object} source - The source object to merge from * @returns {Object} The merged object */ function deepMerge(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) { if (source[key] instanceof Object && !Array.isArray(source[key])) { target[key] = deepMerge(target[key] || {}, source[key]) } else { target[key] = source[key] } } } return target } /** * The loaded configuration * @type {Config} */ const config = loadConfig() module.exports = config