webssh2/app/utils.js

242 lines
7 KiB
JavaScript

// server
// /app/utils.js
import validator from 'validator'
import Ajv from 'ajv'
import maskObject from 'jsmasker'
import { createNamespacedDebug } from './logger.js'
import { DEFAULTS, MESSAGES } from './constants.js'
import configSchema from './configSchema.js'
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
*/
export function deepMerge(target, source) {
const output = Object.assign({}, target)
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.
*/
export 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.
*/
export 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.
*/
export 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.
*/
export 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.
*/
export 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.
*/
export 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
*/
export 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
*/
export function isValidEnvKey(key) {
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
*/
export 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
*/
export 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
}