// server // /app/utils.js const validator = require("validator") const Ajv = require("ajv") const maskObject = require("jsmasker") const { createNamespacedDebug } = require("./logger") const { DEFAULTS, MESSAGES } = require("./constants") const configSchema = require("./configSchema") const debug = createNamespacedDebug("utils") /** * 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) { const output = Object.assign({}, target) // Avoid mutating target directly Object.keys(source).forEach(key => { if (Object.hasOwnProperty.call(source, key)) { if ( source[key] instanceof Object && !Array.isArray(source[key]) && source[key] !== null ) { output[key] = deepMerge(output[key] || {}, source[key]) } else { output[key] = source[key] } } }) return output } /** * Determines if a given host is an IP address or a hostname. * If it's a hostname, it escapes it for safety. * * @param {string} host - The host string to validate and escape. * @returns {string} - The original IP or escaped hostname. */ function getValidatedHost(host) { let validatedHost if (validator.isIP(host)) { validatedHost = host } else { validatedHost = validator.escape(host) } return validatedHost } /** * Validates and sanitizes a port value. * If no port is provided, defaults to port 22. * If a port is provided, checks if it is a valid port number (1-65535). * If the port is invalid, defaults to port 22. * * @param {string} [portInput] - The port string to validate and parse. * @returns {number} - The validated port number. */ function getValidatedPort(portInput) { const defaultPort = DEFAULTS.SSH_PORT const port = defaultPort debug("getValidatedPort: input: %O", portInput) if (portInput) { if (validator.isInt(portInput, { min: 1, max: 65535 })) { return parseInt(portInput, 10) } } debug( "getValidatedPort: port not specified or is invalid, setting port to: %O", port ) return port } /** * Checks if the provided credentials object is valid. * Valid credentials must have: * - username (string) * - host (string) * - port (number) * AND either: * - password (string) OR * - privateKey (string) with optional passphrase (string) * * @param {Object} creds - The credentials object. * @returns {boolean} - Returns true if the credentials are valid, otherwise false. */ function isValidCredentials(creds) { const hasRequiredFields = !!( creds && typeof creds.username === "string" && typeof creds.host === "string" && typeof creds.port === "number" ) if (!hasRequiredFields) { return false } // Must have either password or privateKey const hasPassword = typeof creds.password === "string" const hasPrivateKey = typeof creds.privateKey === "string" // Passphrase is optional but must be string if provided const hasValidPassphrase = !creds.passphrase || typeof creds.passphrase === "string" return (hasPassword || hasPrivateKey) && hasValidPassphrase } /** * Validates and sanitizes the SSH terminal name using validator functions. * Allows alphanumeric characters, hyphens, and periods. * Returns null if the terminal name is invalid or not provided. * * @param {string} [term] - The terminal name to validate. * @returns {string|null} - The sanitized terminal name if valid, null otherwise. */ function validateSshTerm(term) { debug(`validateSshTerm: %O`, term) if (!term) { return null } const validatedSshTerm = validator.isLength(term, { min: 1, max: 30 }) && validator.matches(term, /^[a-zA-Z0-9.-]+$/) return validatedSshTerm ? term : null } /** * Validates the given configuration object. * * @param {Object} config - The configuration object to validate. * @throws {Error} If the configuration object fails validation. * @returns {Object} The validated configuration object. */ function validateConfig(config) { const ajv = new Ajv() const validate = ajv.compile(configSchema) const valid = validate(config) if (!valid) { throw new Error( `${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}` ) } return config } /** * Modify the HTML content by replacing certain placeholders with dynamic values. * @param {string} html - The original HTML content. * @param {Object} config - The configuration object to inject into the HTML. * @returns {string} - The modified HTML content. */ function modifyHtml(html, config) { debug("modifyHtml") const modifiedHtml = html.replace( /(src|href)="(?!http|\/\/)/g, '$1="/ssh/assets/' ) return modifiedHtml.replace( "window.webssh2Config = null;", `window.webssh2Config = ${JSON.stringify(config)};` ) } /** * Masks sensitive information in an object * @param {Object} obj - The object to mask * @param {Object} [options] - Optional configuration for masking * @param {string[]} [options.properties=['password', 'key', 'secret', 'token']] - The properties to be masked * @param {number} [options.maskLength=8] - The length of the generated mask * @param {number} [options.minLength=5] - The minimum length of the generated mask * @param {number} [options.maxLength=15] - The maximum length of the generated mask * @param {string} [options.maskChar='*'] - The character used for masking * @param {boolean} [options.fullMask=false] - Whether to use a full mask for all properties * @returns {Object} The masked object */ function maskSensitiveData(obj, options) { const defaultOptions = { properties: [ "password", "privateKey", "passphrase", "key", "secret", "token" ] } debug("maskSensitiveData") const maskingOptions = Object.assign({}, defaultOptions, options || {}) const maskedObject = maskObject(obj, maskingOptions) return maskedObject } /** * Validates and sanitizes environment variable key names * @param {string} key - The environment variable key to validate * @returns {boolean} - Whether the key is valid */ function isValidEnvKey(key) { // Only allow uppercase letters, numbers, and underscore return /^[A-Z][A-Z0-9_]*$/.test(key) } /** * Validates and sanitizes environment variable values * @param {string} value - The environment variable value to validate * @returns {boolean} - Whether the value is valid */ function isValidEnvValue(value) { // Disallow special characters that could be used for command injection return !/[;&|`$]/.test(value) } /** * Parses and validates environment variables from URL query string * @param {string} envString - The environment string from URL query * @returns {Object|null} - Object containing validated env vars or null if invalid */ function parseEnvVars(envString) { if (!envString) return null const envVars = {} const pairs = envString.split(",") for (let i = 0; i < pairs.length; i += 1) { const pair = pairs[i].split(":") // eslint-disable-next-line no-continue if (pair.length !== 2) continue const key = pair[0].trim() const value = pair[1].trim() if (isValidEnvKey(key) && isValidEnvValue(value)) { envVars[key] = value } else { debug(`parseEnvVars: Invalid env var pair: ${key}:${value}`) } } return Object.keys(envVars).length > 0 ? envVars : null } module.exports = { deepMerge, getValidatedHost, getValidatedPort, isValidCredentials, isValidEnvKey, isValidEnvValue, maskSensitiveData, modifyHtml, parseEnvVars, validateConfig, validateSshTerm }