webssh2/app/utils.js

257 lines
7.2 KiB
JavaScript

// 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/privateKey (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/privateKey
const hasPassword = typeof creds.password === "string"
const hasPrivateKey =
typeof creds.privatekey === "string" || typeof creds.privateKey === "string"
return hasPassword || hasPrivateKey
}
/**
* 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 = {}
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(":")
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
}