From 56a6ce1d8dbfb0ca9ace58ce1c918976a17995af Mon Sep 17 00:00:00 2001 From: Bill Church Date: Sat, 14 Dec 2024 00:44:39 +0000 Subject: [PATCH] chore: update dependencies and migrate to ES modules #383 --- .eslintignore | 1 - .eslintrc.js | 99 ---------------------------------------- .gitignore | 2 +- app/app.js | 23 +++++----- app/config.js | 29 +++++++----- app/configSchema.js | 3 +- app/connectionHandler.js | 16 ++++--- app/constants.js | 28 ++++++------ app/crypto-utils.js | 9 +--- app/errors.js | 41 +++++++---------- app/io.js | 12 ++--- app/logger.js | 11 ++--- app/middleware.js | 38 ++++++++------- app/routes.js | 23 ++++------ app/server.js | 8 ++-- app/socket.js | 18 ++++---- app/ssh.js | 14 +++--- app/utils.js | 51 ++++++++------------- config.json | 76 ++++++++++++++++++++++++++++++ eslint.config.js | 87 +++++++++++++++++++++++++++++++++++ index.js | 8 ++-- package-lock.json | 71 +++++++++++++++------------- package.json | 9 ++-- 23 files changed, 355 insertions(+), 322 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.js create mode 100644 config.json create mode 100644 eslint.config.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 96212a35..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -**/*{.,-}min.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index be72c4b8..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,99 +0,0 @@ -// ESLint configuration for Node.js 22.12.0 LTS -export default { - "env": { - "es2024": true, // Enables ES2024 globals and syntax - "node": true, // Enables Node.js global variables and Node.js scoping - "jest": true // Keep jest environment for legacy tests during migration - }, - "extends": [ - "eslint:recommended", - "plugin:node/recommended", - "plugin:security/recommended", - "plugin:prettier/recommended" - ], - "plugins": [ - "node", - "security", - "prettier" - ], - "parserOptions": { - "ecmaVersion": 2024, - "sourceType": "module", // Enable ES modules - "ecmaFeatures": { - "impliedStrict": true // Enable strict mode automatically - } - }, - "rules": { - // Modern JavaScript - "no-var": "error", - "prefer-const": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "error", - "template-curly-spacing": ["error", "never"], - - // ES Modules - "node/exports-style": ["error", "exports"], - "node/file-extension-in-import": ["error", "always"], - "node/prefer-global/buffer": ["error", "always"], - "node/prefer-global/console": ["error", "always"], - "node/prefer-global/process": ["error", "always"], - "node/prefer-global/url-search-params": ["error", "always"], - "node/prefer-global/url": ["error", "always"], - "node/prefer-promises/dns": "error", - "node/prefer-promises/fs": "error", - - // Async patterns - "no-promise-executor-return": "error", - "require-atomic-updates": "error", - "max-nested-callbacks": ["error", 3], - - // Security - "security/detect-buffer-noassert": "error", - "security/detect-child-process": "warn", - "security/detect-disable-mustache-escape": "error", - "security/detect-eval-with-expression": "error", - "security/detect-new-buffer": "error", - "security/detect-no-csrf-before-method-override": "error", - "security/detect-non-literal-fs-filename": "warn", - "security/detect-non-literal-regexp": "warn", - "security/detect-non-literal-require": "warn", - "security/detect-object-injection": "warn", - "security/detect-possible-timing-attacks": "warn", - "security/detect-pseudoRandomBytes": "warn", - - // Best practices - "no-console": ["warn", { "allow": ["warn", "error", "info", "debug"] }], - "curly": ["error", "all"], - "eqeqeq": ["error", "always", { "null": "ignore" }], - "no-return-await": "error", - "require-await": "error", - - // Style (with Prettier compatibility) - "prettier/prettier": ["error", { - "singleQuote": true, - "trailingComma": "es5", - "printWidth": 100, - "semi": false - }] - }, - "overrides": [ - { - "files": ["**/*.test.js", "**/*.spec.js"], - "env": { - "jest": true, - "node": true - }, - "rules": { - "node/no-unpublished-require": "off", - "node/no-missing-require": "off" - } - } - ], - "settings": { - "node": { - "version": ">=22.12.0", - "tryExtensions": [".js", ".json", ".node"] - } - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 88731ed2..9a262d89 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ ssl/* bigip/* -config.json +# config.json # Logs logs diff --git a/app/app.js b/app/app.js index 4e0086a8..24177c03 100644 --- a/app/app.js +++ b/app/app.js @@ -1,18 +1,19 @@ // server // app/app.js -const express = require("express") -const config = require("./config") -const socketHandler = require("./socket") -const sshRoutes = require("./routes")(config) -const { applyMiddleware } = require("./middleware") -const { createServer, startServer } = require("./server") -const { configureSocketIO } = require("./io") -const { handleError, ConfigError } = require("./errors") -const { createNamespacedDebug } = require("./logger") -const { DEFAULTS, MESSAGES } = require("./constants") +import express from 'express' +import config from './config.js' +import socketHandler from './socket.js' +import { createRoutes } from './routes.js' +import { applyMiddleware } from './middleware.js' +import { createServer, startServer } from './server.js' +import { configureSocketIO } from './io.js' +import { handleError, ConfigError } from './errors.js' +import { createNamespacedDebug } from './logger.js' +import { DEFAULTS, MESSAGES } from './constants.js' const debug = createNamespacedDebug("app") +const sshRoutes = createRoutes(config) /** * Creates and configures the Express application @@ -67,4 +68,4 @@ function initializeServer() { } } -module.exports = { initializeServer: initializeServer, config: config } +export { initializeServer, config } diff --git a/app/config.js b/app/config.js index 8dcf583c..6e6ea983 100644 --- a/app/config.js +++ b/app/config.js @@ -1,14 +1,14 @@ // server // app/config.js -const path = require("path") -const fs = require("fs") -const readConfig = require("read-config-ng") -const { deepMerge, validateConfig } = require("./utils") -const { generateSecureSecret } = require("./crypto-utils") -const { createNamespacedDebug } = require("./logger") -const { ConfigError, handleError } = require("./errors") -const { DEFAULTS } = require("./constants") +import path from 'path' +import fs from 'fs' +import readConfig from 'read-config-ng' +import { deepMerge, validateConfig } from './utils.js' +import { generateSecureSecret } from './crypto-utils.js' +import { createNamespacedDebug } from './logger.js' +import { ConfigError, handleError } from './errors.js' +import { DEFAULTS } from './constants.js' const debug = createNamespacedDebug("config") @@ -78,9 +78,14 @@ const defaultConfig = { } } +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + function getConfigPath() { - const nodeRoot = path.dirname(require.main.filename) - return path.join(nodeRoot, "config.json") + return path.join(__dirname, "..", "config.json") } function loadConfig() { @@ -165,7 +170,7 @@ function getCorsConfig() { } } -// Extend the config object with the getCorsConfig function +// Add getCorsConfig to the config object config.getCorsConfig = getCorsConfig -module.exports = config +export default config diff --git a/app/configSchema.js b/app/configSchema.js index ec8231a1..2e4e5406 100644 --- a/app/configSchema.js +++ b/app/configSchema.js @@ -106,4 +106,5 @@ const configSchema = { }, required: ["listen", "http", "user", "ssh", "header", "options"] } -module.exports = configSchema + +export default configSchema diff --git a/app/connectionHandler.js b/app/connectionHandler.js index 6d6dd84d..784c1796 100644 --- a/app/connectionHandler.js +++ b/app/connectionHandler.js @@ -1,12 +1,16 @@ // server // app/connectionHandler.js -const fs = require("fs") -const path = require("path") -const { createNamespacedDebug } = require("./logger") -const { HTTP, MESSAGES, DEFAULTS } = require("./constants") -const { modifyHtml } = require("./utils") +import { fileURLToPath } from 'url' +import { dirname } from 'path' +import fs from "fs" +import path from "path" +import { createNamespacedDebug } from "./logger.js" +import { HTTP, MESSAGES, DEFAULTS } from "./constants.js" +import { modifyHtml } from "./utils.js" +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) const debug = createNamespacedDebug("connectionHandler") /** @@ -58,4 +62,4 @@ function handleConnection(req, res) { handleFileRead(filePath, tempConfig, res) } -module.exports = handleConnection +export default handleConnection \ No newline at end of file diff --git a/app/constants.js b/app/constants.js index fbc10e89..57e77e67 100644 --- a/app/constants.js +++ b/app/constants.js @@ -1,14 +1,19 @@ // server // app/constants.js -const path = require("path") +import { fileURLToPath } from 'url' +import { dirname } from 'path' +import path from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) /** * Error messages */ -const MESSAGES = { +export const MESSAGES = { INVALID_CREDENTIALS: "Invalid credentials format", - SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR", + SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR", SHELL_ERROR: "SHELL ERROR", CONFIG_ERROR: "CONFIG_ERROR", UNEXPECTED_ERROR: "An unexpected error occurred", @@ -21,29 +26,28 @@ const MESSAGES = { /** * Default values */ -const DEFAULTS = { +export const DEFAULTS = { SSH_PORT: 22, LISTEN_PORT: 2222, SSH_TERM: "xterm-color", - IO_PING_TIMEOUT: 60000, // 1 minute - IO_PING_INTERVAL: 25000, // 25 seconds + IO_PING_TIMEOUT: 60000, + IO_PING_INTERVAL: 25000, IO_PATH: "/ssh/socket.io", WEBSSH2_CLIENT_PATH: path.resolve( __dirname, "..", "node_modules", "webssh2_client", - "client", + "client", "public" ), CLIENT_FILE: "client.htm", MAX_AUTH_ATTEMPTS: 2 } - /** * HTTP Related */ -const HTTP = { +export const HTTP = { OK: 200, UNAUTHORIZED: 401, INTERNAL_SERVER_ERROR: 500, @@ -56,9 +60,3 @@ const HTTP = { SESSION_SID: "webssh2_sid", CREDS_CLEARED: "Credentials cleared." } - -module.exports = { - MESSAGES, - DEFAULTS, - HTTP -} diff --git a/app/crypto-utils.js b/app/crypto-utils.js index 845fde69..b77e4710 100644 --- a/app/crypto-utils.js +++ b/app/crypto-utils.js @@ -1,16 +1,11 @@ // server // app/crypto-utils.js -const crypto = require("crypto") - +import crypto from "crypto" /** * Generates a secure random session secret * @returns {string} A random 32-byte hex string */ -function generateSecureSecret() { +export function generateSecureSecret() { return crypto.randomBytes(32).toString("hex") } - -module.exports = { - generateSecureSecret -} diff --git a/app/errors.js b/app/errors.js index 0accbf40..626ef48c 100644 --- a/app/errors.js +++ b/app/errors.js @@ -1,9 +1,8 @@ // server // app/errors.js -const util = require("util") -const { logError, createNamespacedDebug } = require("./logger") -const { HTTP, MESSAGES } = require("./constants") +import { logError, createNamespacedDebug } from './logger.js' +import { HTTP, MESSAGES } from './constants.js' const debug = createNamespacedDebug("errors") @@ -12,35 +11,34 @@ const debug = createNamespacedDebug("errors") * @param {string} message - The error message * @param {string} code - The error code */ -function WebSSH2Error(message, code) { - Error.captureStackTrace(this, this.constructor) - this.name = this.constructor.name - this.message = message - this.code = code +class WebSSH2Error extends Error { + constructor(message, code) { + super(message) + this.name = this.constructor.name + this.code = code + } } -util.inherits(WebSSH2Error, Error) - /** * Custom error for configuration issues * @param {string} message - The error message */ -function ConfigError(message) { - WebSSH2Error.call(this, message, MESSAGES.CONFIG_ERROR) +class ConfigError extends WebSSH2Error { + constructor(message) { + super(message, MESSAGES.CONFIG_ERROR) + } } -util.inherits(ConfigError, WebSSH2Error) - /** * Custom error for SSH connection issues * @param {string} message - The error message */ -function SSHConnectionError(message) { - WebSSH2Error.call(this, message, MESSAGES.SSH_CONNECTION_ERROR) +class SSHConnectionError extends WebSSH2Error { + constructor(message) { + super(message, MESSAGES.SSH_CONNECTION_ERROR) + } } -util.inherits(SSHConnectionError, WebSSH2Error) - /** * Handles an error by logging it and optionally sending a response * @param {Error} err - The error to handle @@ -66,9 +64,4 @@ function handleError(err, res) { } } -module.exports = { - WebSSH2Error: WebSSH2Error, - ConfigError: ConfigError, - SSHConnectionError: SSHConnectionError, - handleError: handleError -} +export { WebSSH2Error, ConfigError, SSHConnectionError, handleError } diff --git a/app/io.js b/app/io.js index 58b1eaf0..9e44bd14 100644 --- a/app/io.js +++ b/app/io.js @@ -1,7 +1,7 @@ -const socketIo = require("socket.io") -const sharedsession = require("express-socket.io-session") -const { createNamespacedDebug } = require("./logger") -const { DEFAULTS } = require("./constants") +import socketIo from "socket.io" +import sharedsession from "express-socket.io-session" +import { createNamespacedDebug } from "./logger.js" +import { DEFAULTS } from "./constants.js" const debug = createNamespacedDebug("app") @@ -12,7 +12,7 @@ const debug = createNamespacedDebug("app") * @param {Object} config - The configuration object * @returns {import('socket.io').Server} The Socket.IO server instance */ -function configureSocketIO(server, sessionMiddleware, config) { +export function configureSocketIO(server, sessionMiddleware, config) { const io = socketIo(server, { serveClient: false, path: DEFAULTS.IO_PATH, @@ -32,5 +32,3 @@ function configureSocketIO(server, sessionMiddleware, config) { return io } - -module.exports = { configureSocketIO } diff --git a/app/logger.js b/app/logger.js index 9fe5c89f..5db35356 100644 --- a/app/logger.js +++ b/app/logger.js @@ -1,14 +1,14 @@ // server // app/logger.js -const createDebug = require("debug") +import createDebug from 'debug' /** * Creates a debug function for a specific namespace * @param {string} namespace - The debug namespace * @returns {Function} The debug function */ -function createNamespacedDebug(namespace) { +export function createNamespacedDebug(namespace) { return createDebug(`webssh2:${namespace}`) } @@ -17,14 +17,9 @@ function createNamespacedDebug(namespace) { * @param {string} message - The error message * @param {Error} [error] - The error object */ -function logError(message, error) { +export function logError(message, error) { console.error(message) if (error) { console.error(`ERROR: ${error}`) } } - -module.exports = { - createNamespacedDebug: createNamespacedDebug, - logError: logError -} diff --git a/app/middleware.js b/app/middleware.js index 3f4185f6..f0d0afde 100644 --- a/app/middleware.js +++ b/app/middleware.js @@ -1,14 +1,20 @@ // server // app/middleware.js -const createDebug = require("debug") -const session = require("express-session") -const bodyParser = require("body-parser") + // Scenario 2: Basic Auth + +import createDebug from "debug" +import session from "express-session" +import bodyParser from 'body-parser' +const { urlencoded, json } = bodyParser const debug = createDebug("webssh2:middleware") -const basicAuth = require("basic-auth") -const validator = require("validator") -const { HTTP } = require("./constants") +import basicAuth from "basic-auth" + +import validator from 'validator' + +import { HTTP } from "./constants.js" + /** * Middleware function that handles HTTP Basic Authentication for the application. @@ -28,7 +34,7 @@ const { HTTP } = require("./constants") * If the authentication fails, the function will send a 401 Unauthorized response * with the appropriate WWW-Authenticate header. */ -function createAuthMiddleware(config) { +export function createAuthMiddleware(config) { // eslint-disable-next-line consistent-return return (req, res, next) => { // Check if username and either password or private key is configured @@ -73,7 +79,7 @@ function createAuthMiddleware(config) { * @param {Object} config - The configuration object * @returns {Function} The session middleware */ -function createSessionMiddleware(config) { +export function createSessionMiddleware(config) { return session({ secret: config.session.secret, resave: false, @@ -86,15 +92,15 @@ function createSessionMiddleware(config) { * Creates body parser middleware * @returns {Function[]} Array of body parser middleware */ -function createBodyParserMiddleware() { - return [bodyParser.urlencoded({ extended: true }), bodyParser.json()] +export function createBodyParserMiddleware() { + return [urlencoded({ extended: true }), json()] } /** * Creates cookie-setting middleware * @returns {Function} The cookie-setting middleware */ -function createCookieMiddleware() { +export function createCookieMiddleware() { return (req, res, next) => { if (req.session.sshCredentials) { const cookieData = { @@ -117,7 +123,7 @@ function createCookieMiddleware() { * @param {Object} config - The configuration object * @returns {Object} An object containing the session middleware */ -function applyMiddleware(app, config) { +export function applyMiddleware(app, config) { const sessionMiddleware = createSessionMiddleware(config) app.use(sessionMiddleware) @@ -128,11 +134,3 @@ function applyMiddleware(app, config) { return { sessionMiddleware } } - -module.exports = { - applyMiddleware, - createAuthMiddleware, - createSessionMiddleware, - createBodyParserMiddleware, - createCookieMiddleware -} diff --git a/app/routes.js b/app/routes.js index 468a1da8..6cadcb33 100644 --- a/app/routes.js +++ b/app/routes.js @@ -1,24 +1,17 @@ // server // app/routes.js -const express = require("express") - -const { - getValidatedHost, - getValidatedPort, - maskSensitiveData, - validateSshTerm -} = require("./utils") -const handleConnection = require("./connectionHandler") -const { createNamespacedDebug } = require("./logger") -const { createAuthMiddleware } = require("./middleware") -const { ConfigError, handleError } = require("./errors") -const { HTTP } = require("./constants") -const { parseEnvVars } = require("./utils") +import express from "express" +import { getValidatedHost, getValidatedPort, maskSensitiveData, validateSshTerm, parseEnvVars } from "./utils.js" +import handleConnection from "./connectionHandler.js" +import { createNamespacedDebug } from "./logger.js" +import { createAuthMiddleware } from "./middleware.js" +import { ConfigError, handleError } from "./errors.js" +import { HTTP } from "./constants.js" const debug = createNamespacedDebug("routes") -module.exports = function(config) { +export function createRoutes(config) { const router = express.Router() const auth = createAuthMiddleware(config) diff --git a/app/server.js b/app/server.js index f885fe1f..bc6f2719 100644 --- a/app/server.js +++ b/app/server.js @@ -1,4 +1,4 @@ -const http = require("http") +import http from "http" // const { createNamespacedDebug } = require("./logger") // const debug = createNamespacedDebug("server") @@ -7,7 +7,7 @@ const http = require("http") * @param {express.Application} app - The Express application instance * @returns {http.Server} The HTTP server instance */ -function createServer(app) { +export function createServer(app) { return http.createServer(app) } @@ -24,7 +24,7 @@ function handleServerError(err) { * @param {http.Server} server - The server instance * @param {Object} config - The configuration object */ -function startServer(server, config) { +export function startServer(server, config) { server.listen(config.listen.port, config.listen.ip, () => { console.log( `startServer: listening on ${config.listen.ip}:${config.listen.port}` @@ -33,5 +33,3 @@ function startServer(server, config) { server.on("error", handleServerError) } - -module.exports = { createServer, startServer } diff --git a/app/socket.js b/app/socket.js index 04c5389a..e8135532 100644 --- a/app/socket.js +++ b/app/socket.js @@ -1,19 +1,19 @@ // server // app/socket.js -const validator = require("validator") -const EventEmitter = require("events") -const SSHConnection = require("./ssh") -const { createNamespacedDebug } = require("./logger") -const { SSHConnectionError, handleError } = require("./errors") +import validator from "validator" +import { EventEmitter } from "events" +import SSHConnection from "./ssh.js" +import { createNamespacedDebug } from "./logger.js" +import { SSHConnectionError, handleError } from "./errors.js" const debug = createNamespacedDebug("socket") -const { +import { isValidCredentials, maskSensitiveData, validateSshTerm -} = require("./utils") -const { MESSAGES } = require("./constants") +} from "./utils.js" +import { MESSAGES } from "./constants.js" class WebSSH2Socket extends EventEmitter { constructor(socket, config) { @@ -353,6 +353,6 @@ class WebSSH2Socket extends EventEmitter { } } -module.exports = function(io, config) { +export default function(io, config) { io.on("connection", socket => new WebSSH2Socket(socket, config)) } diff --git a/app/ssh.js b/app/ssh.js index ad0deba0..2b7f05bd 100644 --- a/app/ssh.js +++ b/app/ssh.js @@ -1,12 +1,12 @@ // server // app/ssh.js -const SSH = require("ssh2").Client -const EventEmitter = require("events") -const { createNamespacedDebug } = require("./logger") -const { SSHConnectionError, handleError } = require("./errors") -const { maskSensitiveData } = require("./utils") -const { DEFAULTS } = require("./constants") +import { Client as SSH } from "ssh2" +import { EventEmitter } from "events" +import { createNamespacedDebug } from "./logger.js" +import { SSHConnectionError, handleError } from "./errors.js" +import { maskSensitiveData } from "./utils.js" +import { DEFAULTS } from "./constants.js" const debug = createNamespacedDebug("ssh") @@ -243,4 +243,4 @@ class SSHConnection extends EventEmitter { } } -module.exports = SSHConnection +export default SSHConnection \ No newline at end of file diff --git a/app/utils.js b/app/utils.js index 286b4dbc..2f5976f7 100644 --- a/app/utils.js +++ b/app/utils.js @@ -1,11 +1,11 @@ // 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") +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") @@ -15,8 +15,8 @@ const debug = createNamespacedDebug("utils") * @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 +export function deepMerge(target, source) { + const output = Object.assign({}, target) Object.keys(source).forEach(key => { if (Object.hasOwnProperty.call(source, key)) { if ( @@ -40,7 +40,7 @@ function deepMerge(target, source) { * @param {string} host - The host string to validate and escape. * @returns {string} - The original IP or escaped hostname. */ -function getValidatedHost(host) { +export function getValidatedHost(host) { let validatedHost if (validator.isIP(host)) { @@ -61,7 +61,7 @@ function getValidatedHost(host) { * @param {string} [portInput] - The port string to validate and parse. * @returns {number} - The validated port number. */ -function getValidatedPort(portInput) { +export function getValidatedPort(portInput) { const defaultPort = DEFAULTS.SSH_PORT const port = defaultPort debug("getValidatedPort: input: %O", portInput) @@ -92,7 +92,7 @@ function getValidatedPort(portInput) { * @param {Object} creds - The credentials object. * @returns {boolean} - Returns true if the credentials are valid, otherwise false. */ -function isValidCredentials(creds) { +export function isValidCredentials(creds) { const hasRequiredFields = !!( creds && typeof creds.username === "string" && @@ -120,7 +120,7 @@ function isValidCredentials(creds) { * @param {string} [term] - The terminal name to validate. * @returns {string|null} - The sanitized terminal name if valid, null otherwise. */ -function validateSshTerm(term) { +export function validateSshTerm(term) { debug(`validateSshTerm: %O`, term) if (!term) { @@ -141,7 +141,7 @@ function validateSshTerm(term) { * @throws {Error} If the configuration object fails validation. * @returns {Object} The validated configuration object. */ -function validateConfig(config) { +export function validateConfig(config) { const ajv = new Ajv() const validate = ajv.compile(configSchema) const valid = validate(config) @@ -159,7 +159,7 @@ function validateConfig(config) { * @param {Object} config - The configuration object to inject into the HTML. * @returns {string} - The modified HTML content. */ -function modifyHtml(html, config) { +export function modifyHtml(html, config) { debug("modifyHtml") const modifiedHtml = html.replace( /(src|href)="(?!http|\/\/)/g, @@ -184,7 +184,7 @@ function modifyHtml(html, config) { * @param {boolean} [options.fullMask=false] - Whether to use a full mask for all properties * @returns {Object} The masked object */ -function maskSensitiveData(obj, options) { +export function maskSensitiveData(obj, options) { const defaultOptions = {} debug("maskSensitiveData") @@ -199,8 +199,7 @@ function maskSensitiveData(obj, options) { * @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 +export function isValidEnvKey(key) { return /^[A-Z][A-Z0-9_]*$/.test(key) } @@ -209,7 +208,7 @@ function isValidEnvKey(key) { * @param {string} value - The environment variable value to validate * @returns {boolean} - Whether the value is valid */ -function isValidEnvValue(value) { +export function isValidEnvValue(value) { // Disallow special characters that could be used for command injection return !/[;&|`$]/.test(value) } @@ -219,7 +218,7 @@ function isValidEnvValue(value) { * @param {string} envString - The environment string from URL query * @returns {Object|null} - Object containing validated env vars or null if invalid */ -function parseEnvVars(envString) { +export function parseEnvVars(envString) { if (!envString) return null const envVars = {} @@ -241,17 +240,3 @@ function parseEnvVars(envString) { return Object.keys(envVars).length > 0 ? envVars : null } - -module.exports = { - deepMerge, - getValidatedHost, - getValidatedPort, - isValidCredentials, - isValidEnvKey, - isValidEnvValue, - maskSensitiveData, - modifyHtml, - parseEnvVars, - validateConfig, - validateSshTerm -} diff --git a/config.json b/config.json new file mode 100644 index 00000000..50869683 --- /dev/null +++ b/config.json @@ -0,0 +1,76 @@ +{ + "listen": { + "ip": "0.0.0.0", + "port": 2222 + }, + "http": { + "origins": ["*.*"] + }, + "user": { + "name": null, + "password": null, + "privateKey": null + }, + "session": { + "secret": "secret", + "name": "webssh2" + }, + "ssh": { + "host": null, + "port": 22, + "localAddress": null, + "localPort": null, + "term": "xterm-color", + "readyTimeout": 20000, + "keepaliveInterval": 120000, + "keepaliveCountMax": 10, + "allowedSubnets": [], + "alwaysSendKeyboardInteractivePrompts": false, + "disableInteractiveAuth": false, + "algorithms": { + "cipher": [ + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm", + "aes128-gcm@openssh.com", + "aes256-gcm", + "aes256-gcm@openssh.com", + "aes256-cbc" + ], + "compress": [ + "none", + "zlib@openssh.com", + "zlib" + ], + "hmac": [ + "hmac-sha2-256", + "hmac-sha2-512", + "hmac-sha1" + ], + "kex": [ + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha1" + ], + "serverHostKey": [ + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-ed25519" + ] + } + }, + "header": { + "text": null, + "background": "green" + }, + "options": { + "challengeButton": true, + "autoLog": false, + "allowReauth": true, + "allowReconnect": true, + "allowReplay": true + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..3f4b19b7 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,87 @@ +import eslint from '@eslint/js'; +import nodePlugin from 'eslint-plugin-node'; +import securityPlugin from 'eslint-plugin-security'; +import prettierPlugin from 'eslint-plugin-prettier'; + +export default [ + eslint.configs.recommended, + { + ignores: ['**/*{.,-}min.js'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + }, + globals: { + ...nodePlugin.configs.recommended.globals, + } + }, + plugins: { + node: nodePlugin, + security: securityPlugin, + prettier: prettierPlugin + }, + rules: { + // Modern JavaScript + 'no-var': 'error', + 'prefer-const': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'template-curly-spacing': ['error', 'never'], + + // Node.js specific rules + // 'node/exports-style': ['error', 'exports'], + 'node/file-extension-in-import': ['error', 'always'], + 'node/prefer-global/buffer': ['error', 'always'], + 'node/prefer-global/console': ['error', 'always'], + 'node/prefer-global/process': ['error', 'always'], + 'node/prefer-global/url-search-params': ['error', 'always'], + 'node/prefer-global/url': ['error', 'always'], + 'node/prefer-promises/dns': 'error', + 'node/prefer-promises/fs': 'error', + + // Security rules + 'security/detect-buffer-noassert': 'error', + 'security/detect-child-process': 'warn', + 'security/detect-disable-mustache-escape': 'error', + 'security/detect-eval-with-expression': 'error', + 'security/detect-new-buffer': 'error', + 'security/detect-no-csrf-before-method-override': 'error', + 'security/detect-non-literal-fs-filename': 'warn', + 'security/detect-non-literal-regexp': 'warn', + 'security/detect-non-literal-require': 'warn', + 'security/detect-object-injection': 'warn', + 'security/detect-possible-timing-attacks': 'warn', + 'security/detect-pseudoRandomBytes': 'warn', + + // Best practices and style + 'no-console': ['warn', { allow: ['warn', 'error', 'info', 'debug'] }], + 'curly': ['error', 'all'], + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'no-return-await': 'error', + 'require-await': 'error', + 'prettier/prettier': ['error', { + singleQuote: true, + trailingComma: 'es5', + printWidth: 100, + semi: false + }] + } + }, + { + files: ['**/*.test.js', '**/*.spec.js'], + languageOptions: { + globals: { + jest: true + } + }, + rules: { + 'node/no-unpublished-require': 'off', + 'node/no-missing-require': 'off' + } + } +]; diff --git a/index.js b/index.js index 03483e3f..7f454cea 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ * Bill Church - https://github.com/billchurch/WebSSH2 - May 2017 */ -const { initializeServer } = require("./app/app") +import { initializeServer } from "./app/app.js" /** * Main function to start the application @@ -21,7 +21,5 @@ function main() { // Run the application main() -// For testing purposes, export the functions -module.exports = { - initializeServer -} +// For testing purposes, export the function +export { initializeServer } diff --git a/package-lock.json b/package-lock.json index e189c69a..6141c744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "ajv": "^4.11.8", "basic-auth": "^2.0.1", - "body-parser": "^1.15.2", + "body-parser": "^1.20.3", "debug": "^3.2.7", "express": "^4.14.1", "express-session": "^1.18.0", @@ -19,8 +19,8 @@ "jsmasker": "^1.4.0", "read-config-ng": "~3.0.7", "socket.io": "~2.2.0", - "ssh2": "~0.8.9", - "validator": "^12.2.0", + "ssh2": "1.16", + "validator": "^13.12.0", "webssh2_client": "^0.2.27" }, "bin": { @@ -2156,6 +2156,15 @@ "dev": true, "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -3004,6 +3013,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -8199,7 +8222,6 @@ "version": "2.22.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "dev": true, "license": "MIT", "optional": true }, @@ -10539,27 +10561,20 @@ "license": "BSD-3-Clause" }, "node_modules/ssh2": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", - "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, "dependencies": { - "ssh2-streams": "~0.4.10" + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" }, "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/ssh2-streams": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", - "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", - "dependencies": { - "asn1": "~0.2.0", - "bcrypt-pbkdf": "^1.0.2", - "streamsearch": "~0.1.2" + "node": ">=10.16.0" }, - "engines": { - "node": ">=5.2.0" + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" } }, "node_modules/sshpk": { @@ -10905,14 +10920,6 @@ "node": ">= 0.8" } }, - "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -11598,9 +11605,9 @@ } }, "node_modules/validator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", - "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "license": "MIT", "engines": { "node": ">= 0.10" diff --git a/package.json b/package.json index e273f238..9b88b27b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "ajv": "^4.11.8", "basic-auth": "^2.0.1", - "body-parser": "^1.15.2", + "body-parser": "^1.20.3", "debug": "^3.2.7", "express": "^4.14.1", "express-session": "^1.18.0", @@ -41,8 +41,8 @@ "jsmasker": "^1.4.0", "read-config-ng": "~3.0.7", "socket.io": "~2.2.0", - "ssh2": "~0.8.9", - "validator": "^12.2.0", + "ssh2": "1.16", + "validator": "^13.12.0", "webssh2_client": "^0.2.27" }, "scripts": { @@ -98,5 +98,6 @@ "directories": { "test": "tests" }, - "author": "" + "author": "", + "type": "module" }