chore: update dependencies and migrate to ES modules #383

This commit is contained in:
Bill Church 2024-12-14 00:44:39 +00:00
parent 626e581921
commit 56a6ce1d8d
No known key found for this signature in database
23 changed files with 355 additions and 322 deletions

View file

@ -1 +0,0 @@
**/*{.,-}min.js

View file

@ -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"]
}
}
}

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ ssl/*
bigip/* bigip/*
config.json # config.json
# Logs # Logs
logs logs

View file

@ -1,18 +1,19 @@
// server // server
// app/app.js // app/app.js
const express = require("express") import express from 'express'
const config = require("./config") import config from './config.js'
const socketHandler = require("./socket") import socketHandler from './socket.js'
const sshRoutes = require("./routes")(config) import { createRoutes } from './routes.js'
const { applyMiddleware } = require("./middleware") import { applyMiddleware } from './middleware.js'
const { createServer, startServer } = require("./server") import { createServer, startServer } from './server.js'
const { configureSocketIO } = require("./io") import { configureSocketIO } from './io.js'
const { handleError, ConfigError } = require("./errors") import { handleError, ConfigError } from './errors.js'
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from './logger.js'
const { DEFAULTS, MESSAGES } = require("./constants") import { DEFAULTS, MESSAGES } from './constants.js'
const debug = createNamespacedDebug("app") const debug = createNamespacedDebug("app")
const sshRoutes = createRoutes(config)
/** /**
* Creates and configures the Express application * Creates and configures the Express application
@ -67,4 +68,4 @@ function initializeServer() {
} }
} }
module.exports = { initializeServer: initializeServer, config: config } export { initializeServer, config }

View file

@ -1,14 +1,14 @@
// server // server
// app/config.js // app/config.js
const path = require("path") import path from 'path'
const fs = require("fs") import fs from 'fs'
const readConfig = require("read-config-ng") import readConfig from 'read-config-ng'
const { deepMerge, validateConfig } = require("./utils") import { deepMerge, validateConfig } from './utils.js'
const { generateSecureSecret } = require("./crypto-utils") import { generateSecureSecret } from './crypto-utils.js'
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from './logger.js'
const { ConfigError, handleError } = require("./errors") import { ConfigError, handleError } from './errors.js'
const { DEFAULTS } = require("./constants") import { DEFAULTS } from './constants.js'
const debug = createNamespacedDebug("config") 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() { function getConfigPath() {
const nodeRoot = path.dirname(require.main.filename) return path.join(__dirname, "..", "config.json")
return path.join(nodeRoot, "config.json")
} }
function loadConfig() { 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 config.getCorsConfig = getCorsConfig
module.exports = config export default config

View file

@ -106,4 +106,5 @@ const configSchema = {
}, },
required: ["listen", "http", "user", "ssh", "header", "options"] required: ["listen", "http", "user", "ssh", "header", "options"]
} }
module.exports = configSchema
export default configSchema

View file

@ -1,12 +1,16 @@
// server // server
// app/connectionHandler.js // app/connectionHandler.js
const fs = require("fs") import { fileURLToPath } from 'url'
const path = require("path") import { dirname } from 'path'
const { createNamespacedDebug } = require("./logger") import fs from "fs"
const { HTTP, MESSAGES, DEFAULTS } = require("./constants") import path from "path"
const { modifyHtml } = require("./utils") 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") const debug = createNamespacedDebug("connectionHandler")
/** /**
@ -58,4 +62,4 @@ function handleConnection(req, res) {
handleFileRead(filePath, tempConfig, res) handleFileRead(filePath, tempConfig, res)
} }
module.exports = handleConnection export default handleConnection

View file

@ -1,12 +1,17 @@
// server // server
// app/constants.js // 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 * Error messages
*/ */
const MESSAGES = { export const MESSAGES = {
INVALID_CREDENTIALS: "Invalid credentials format", INVALID_CREDENTIALS: "Invalid credentials format",
SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR", SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR",
SHELL_ERROR: "SHELL ERROR", SHELL_ERROR: "SHELL ERROR",
@ -21,12 +26,12 @@ const MESSAGES = {
/** /**
* Default values * Default values
*/ */
const DEFAULTS = { export const DEFAULTS = {
SSH_PORT: 22, SSH_PORT: 22,
LISTEN_PORT: 2222, LISTEN_PORT: 2222,
SSH_TERM: "xterm-color", SSH_TERM: "xterm-color",
IO_PING_TIMEOUT: 60000, // 1 minute IO_PING_TIMEOUT: 60000,
IO_PING_INTERVAL: 25000, // 25 seconds IO_PING_INTERVAL: 25000,
IO_PATH: "/ssh/socket.io", IO_PATH: "/ssh/socket.io",
WEBSSH2_CLIENT_PATH: path.resolve( WEBSSH2_CLIENT_PATH: path.resolve(
__dirname, __dirname,
@ -39,11 +44,10 @@ const DEFAULTS = {
CLIENT_FILE: "client.htm", CLIENT_FILE: "client.htm",
MAX_AUTH_ATTEMPTS: 2 MAX_AUTH_ATTEMPTS: 2
} }
/** /**
* HTTP Related * HTTP Related
*/ */
const HTTP = { export const HTTP = {
OK: 200, OK: 200,
UNAUTHORIZED: 401, UNAUTHORIZED: 401,
INTERNAL_SERVER_ERROR: 500, INTERNAL_SERVER_ERROR: 500,
@ -56,9 +60,3 @@ const HTTP = {
SESSION_SID: "webssh2_sid", SESSION_SID: "webssh2_sid",
CREDS_CLEARED: "Credentials cleared." CREDS_CLEARED: "Credentials cleared."
} }
module.exports = {
MESSAGES,
DEFAULTS,
HTTP
}

View file

@ -1,16 +1,11 @@
// server // server
// app/crypto-utils.js // app/crypto-utils.js
const crypto = require("crypto") import crypto from "crypto"
/** /**
* Generates a secure random session secret * Generates a secure random session secret
* @returns {string} A random 32-byte hex string * @returns {string} A random 32-byte hex string
*/ */
function generateSecureSecret() { export function generateSecureSecret() {
return crypto.randomBytes(32).toString("hex") return crypto.randomBytes(32).toString("hex")
} }
module.exports = {
generateSecureSecret
}

View file

@ -1,9 +1,8 @@
// server // server
// app/errors.js // app/errors.js
const util = require("util") import { logError, createNamespacedDebug } from './logger.js'
const { logError, createNamespacedDebug } = require("./logger") import { HTTP, MESSAGES } from './constants.js'
const { HTTP, MESSAGES } = require("./constants")
const debug = createNamespacedDebug("errors") const debug = createNamespacedDebug("errors")
@ -12,35 +11,34 @@ const debug = createNamespacedDebug("errors")
* @param {string} message - The error message * @param {string} message - The error message
* @param {string} code - The error code * @param {string} code - The error code
*/ */
function WebSSH2Error(message, code) { class WebSSH2Error extends Error {
Error.captureStackTrace(this, this.constructor) constructor(message, code) {
this.name = this.constructor.name super(message)
this.message = message this.name = this.constructor.name
this.code = code this.code = code
}
} }
util.inherits(WebSSH2Error, Error)
/** /**
* Custom error for configuration issues * Custom error for configuration issues
* @param {string} message - The error message * @param {string} message - The error message
*/ */
function ConfigError(message) { class ConfigError extends WebSSH2Error {
WebSSH2Error.call(this, message, MESSAGES.CONFIG_ERROR) constructor(message) {
super(message, MESSAGES.CONFIG_ERROR)
}
} }
util.inherits(ConfigError, WebSSH2Error)
/** /**
* Custom error for SSH connection issues * Custom error for SSH connection issues
* @param {string} message - The error message * @param {string} message - The error message
*/ */
function SSHConnectionError(message) { class SSHConnectionError extends WebSSH2Error {
WebSSH2Error.call(this, message, MESSAGES.SSH_CONNECTION_ERROR) constructor(message) {
super(message, MESSAGES.SSH_CONNECTION_ERROR)
}
} }
util.inherits(SSHConnectionError, WebSSH2Error)
/** /**
* Handles an error by logging it and optionally sending a response * Handles an error by logging it and optionally sending a response
* @param {Error} err - The error to handle * @param {Error} err - The error to handle
@ -66,9 +64,4 @@ function handleError(err, res) {
} }
} }
module.exports = { export { WebSSH2Error, ConfigError, SSHConnectionError, handleError }
WebSSH2Error: WebSSH2Error,
ConfigError: ConfigError,
SSHConnectionError: SSHConnectionError,
handleError: handleError
}

View file

@ -1,7 +1,7 @@
const socketIo = require("socket.io") import socketIo from "socket.io"
const sharedsession = require("express-socket.io-session") import sharedsession from "express-socket.io-session"
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from "./logger.js"
const { DEFAULTS } = require("./constants") import { DEFAULTS } from "./constants.js"
const debug = createNamespacedDebug("app") const debug = createNamespacedDebug("app")
@ -12,7 +12,7 @@ const debug = createNamespacedDebug("app")
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
* @returns {import('socket.io').Server} The Socket.IO server instance * @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, { const io = socketIo(server, {
serveClient: false, serveClient: false,
path: DEFAULTS.IO_PATH, path: DEFAULTS.IO_PATH,
@ -32,5 +32,3 @@ function configureSocketIO(server, sessionMiddleware, config) {
return io return io
} }
module.exports = { configureSocketIO }

View file

@ -1,14 +1,14 @@
// server // server
// app/logger.js // app/logger.js
const createDebug = require("debug") import createDebug from 'debug'
/** /**
* Creates a debug function for a specific namespace * Creates a debug function for a specific namespace
* @param {string} namespace - The debug namespace * @param {string} namespace - The debug namespace
* @returns {Function} The debug function * @returns {Function} The debug function
*/ */
function createNamespacedDebug(namespace) { export function createNamespacedDebug(namespace) {
return createDebug(`webssh2:${namespace}`) return createDebug(`webssh2:${namespace}`)
} }
@ -17,14 +17,9 @@ function createNamespacedDebug(namespace) {
* @param {string} message - The error message * @param {string} message - The error message
* @param {Error} [error] - The error object * @param {Error} [error] - The error object
*/ */
function logError(message, error) { export function logError(message, error) {
console.error(message) console.error(message)
if (error) { if (error) {
console.error(`ERROR: ${error}`) console.error(`ERROR: ${error}`)
} }
} }
module.exports = {
createNamespacedDebug: createNamespacedDebug,
logError: logError
}

View file

@ -1,14 +1,20 @@
// server // server
// app/middleware.js // app/middleware.js
const createDebug = require("debug") // Scenario 2: Basic Auth
const session = require("express-session")
const bodyParser = require("body-parser")
import createDebug from "debug"
import session from "express-session"
import bodyParser from 'body-parser'
const { urlencoded, json } = bodyParser
const debug = createDebug("webssh2:middleware") const debug = createDebug("webssh2:middleware")
const basicAuth = require("basic-auth") import basicAuth from "basic-auth"
const validator = require("validator")
const { HTTP } = require("./constants") import validator from 'validator'
import { HTTP } from "./constants.js"
/** /**
* Middleware function that handles HTTP Basic Authentication for the application. * 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 * If the authentication fails, the function will send a 401 Unauthorized response
* with the appropriate WWW-Authenticate header. * with the appropriate WWW-Authenticate header.
*/ */
function createAuthMiddleware(config) { export function createAuthMiddleware(config) {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return (req, res, next) => { return (req, res, next) => {
// Check if username and either password or private key is configured // Check if username and either password or private key is configured
@ -73,7 +79,7 @@ function createAuthMiddleware(config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
* @returns {Function} The session middleware * @returns {Function} The session middleware
*/ */
function createSessionMiddleware(config) { export function createSessionMiddleware(config) {
return session({ return session({
secret: config.session.secret, secret: config.session.secret,
resave: false, resave: false,
@ -86,15 +92,15 @@ function createSessionMiddleware(config) {
* Creates body parser middleware * Creates body parser middleware
* @returns {Function[]} Array of body parser middleware * @returns {Function[]} Array of body parser middleware
*/ */
function createBodyParserMiddleware() { export function createBodyParserMiddleware() {
return [bodyParser.urlencoded({ extended: true }), bodyParser.json()] return [urlencoded({ extended: true }), json()]
} }
/** /**
* Creates cookie-setting middleware * Creates cookie-setting middleware
* @returns {Function} The cookie-setting middleware * @returns {Function} The cookie-setting middleware
*/ */
function createCookieMiddleware() { export function createCookieMiddleware() {
return (req, res, next) => { return (req, res, next) => {
if (req.session.sshCredentials) { if (req.session.sshCredentials) {
const cookieData = { const cookieData = {
@ -117,7 +123,7 @@ function createCookieMiddleware() {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
* @returns {Object} An object containing the session middleware * @returns {Object} An object containing the session middleware
*/ */
function applyMiddleware(app, config) { export function applyMiddleware(app, config) {
const sessionMiddleware = createSessionMiddleware(config) const sessionMiddleware = createSessionMiddleware(config)
app.use(sessionMiddleware) app.use(sessionMiddleware)
@ -128,11 +134,3 @@ function applyMiddleware(app, config) {
return { sessionMiddleware } return { sessionMiddleware }
} }
module.exports = {
applyMiddleware,
createAuthMiddleware,
createSessionMiddleware,
createBodyParserMiddleware,
createCookieMiddleware
}

View file

@ -1,24 +1,17 @@
// server // server
// app/routes.js // app/routes.js
const express = require("express") import express from "express"
import { getValidatedHost, getValidatedPort, maskSensitiveData, validateSshTerm, parseEnvVars } from "./utils.js"
const { import handleConnection from "./connectionHandler.js"
getValidatedHost, import { createNamespacedDebug } from "./logger.js"
getValidatedPort, import { createAuthMiddleware } from "./middleware.js"
maskSensitiveData, import { ConfigError, handleError } from "./errors.js"
validateSshTerm import { HTTP } from "./constants.js"
} = 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")
const debug = createNamespacedDebug("routes") const debug = createNamespacedDebug("routes")
module.exports = function(config) { export function createRoutes(config) {
const router = express.Router() const router = express.Router()
const auth = createAuthMiddleware(config) const auth = createAuthMiddleware(config)

View file

@ -1,4 +1,4 @@
const http = require("http") import http from "http"
// const { createNamespacedDebug } = require("./logger") // const { createNamespacedDebug } = require("./logger")
// const debug = createNamespacedDebug("server") // const debug = createNamespacedDebug("server")
@ -7,7 +7,7 @@ const http = require("http")
* @param {express.Application} app - The Express application instance * @param {express.Application} app - The Express application instance
* @returns {http.Server} The HTTP server instance * @returns {http.Server} The HTTP server instance
*/ */
function createServer(app) { export function createServer(app) {
return http.createServer(app) return http.createServer(app)
} }
@ -24,7 +24,7 @@ function handleServerError(err) {
* @param {http.Server} server - The server instance * @param {http.Server} server - The server instance
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function startServer(server, config) { export function startServer(server, config) {
server.listen(config.listen.port, config.listen.ip, () => { server.listen(config.listen.port, config.listen.ip, () => {
console.log( console.log(
`startServer: listening on ${config.listen.ip}:${config.listen.port}` `startServer: listening on ${config.listen.ip}:${config.listen.port}`
@ -33,5 +33,3 @@ function startServer(server, config) {
server.on("error", handleServerError) server.on("error", handleServerError)
} }
module.exports = { createServer, startServer }

View file

@ -1,19 +1,19 @@
// server // server
// app/socket.js // app/socket.js
const validator = require("validator") import validator from "validator"
const EventEmitter = require("events") import { EventEmitter } from "events"
const SSHConnection = require("./ssh") import SSHConnection from "./ssh.js"
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from "./logger.js"
const { SSHConnectionError, handleError } = require("./errors") import { SSHConnectionError, handleError } from "./errors.js"
const debug = createNamespacedDebug("socket") const debug = createNamespacedDebug("socket")
const { import {
isValidCredentials, isValidCredentials,
maskSensitiveData, maskSensitiveData,
validateSshTerm validateSshTerm
} = require("./utils") } from "./utils.js"
const { MESSAGES } = require("./constants") import { MESSAGES } from "./constants.js"
class WebSSH2Socket extends EventEmitter { class WebSSH2Socket extends EventEmitter {
constructor(socket, config) { 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)) io.on("connection", socket => new WebSSH2Socket(socket, config))
} }

View file

@ -1,12 +1,12 @@
// server // server
// app/ssh.js // app/ssh.js
const SSH = require("ssh2").Client import { Client as SSH } from "ssh2"
const EventEmitter = require("events") import { EventEmitter } from "events"
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from "./logger.js"
const { SSHConnectionError, handleError } = require("./errors") import { SSHConnectionError, handleError } from "./errors.js"
const { maskSensitiveData } = require("./utils") import { maskSensitiveData } from "./utils.js"
const { DEFAULTS } = require("./constants") import { DEFAULTS } from "./constants.js"
const debug = createNamespacedDebug("ssh") const debug = createNamespacedDebug("ssh")
@ -243,4 +243,4 @@ class SSHConnection extends EventEmitter {
} }
} }
module.exports = SSHConnection export default SSHConnection

View file

@ -1,11 +1,11 @@
// server // server
// /app/utils.js // /app/utils.js
const validator = require("validator") import validator from 'validator'
const Ajv = require("ajv") import Ajv from 'ajv'
const maskObject = require("jsmasker") import maskObject from 'jsmasker'
const { createNamespacedDebug } = require("./logger") import { createNamespacedDebug } from './logger.js'
const { DEFAULTS, MESSAGES } = require("./constants") import { DEFAULTS, MESSAGES } from './constants.js'
const configSchema = require("./configSchema") import configSchema from './configSchema.js'
const debug = createNamespacedDebug("utils") const debug = createNamespacedDebug("utils")
@ -15,8 +15,8 @@ const debug = createNamespacedDebug("utils")
* @param {Object} source - The source object to merge from * @param {Object} source - The source object to merge from
* @returns {Object} The merged object * @returns {Object} The merged object
*/ */
function deepMerge(target, source) { export function deepMerge(target, source) {
const output = Object.assign({}, target) // Avoid mutating target directly const output = Object.assign({}, target)
Object.keys(source).forEach(key => { Object.keys(source).forEach(key => {
if (Object.hasOwnProperty.call(source, key)) { if (Object.hasOwnProperty.call(source, key)) {
if ( if (
@ -40,7 +40,7 @@ function deepMerge(target, source) {
* @param {string} host - The host string to validate and escape. * @param {string} host - The host string to validate and escape.
* @returns {string} - The original IP or escaped hostname. * @returns {string} - The original IP or escaped hostname.
*/ */
function getValidatedHost(host) { export function getValidatedHost(host) {
let validatedHost let validatedHost
if (validator.isIP(host)) { if (validator.isIP(host)) {
@ -61,7 +61,7 @@ function getValidatedHost(host) {
* @param {string} [portInput] - The port string to validate and parse. * @param {string} [portInput] - The port string to validate and parse.
* @returns {number} - The validated port number. * @returns {number} - The validated port number.
*/ */
function getValidatedPort(portInput) { export function getValidatedPort(portInput) {
const defaultPort = DEFAULTS.SSH_PORT const defaultPort = DEFAULTS.SSH_PORT
const port = defaultPort const port = defaultPort
debug("getValidatedPort: input: %O", portInput) debug("getValidatedPort: input: %O", portInput)
@ -92,7 +92,7 @@ function getValidatedPort(portInput) {
* @param {Object} creds - The credentials object. * @param {Object} creds - The credentials object.
* @returns {boolean} - Returns true if the credentials are valid, otherwise false. * @returns {boolean} - Returns true if the credentials are valid, otherwise false.
*/ */
function isValidCredentials(creds) { export function isValidCredentials(creds) {
const hasRequiredFields = !!( const hasRequiredFields = !!(
creds && creds &&
typeof creds.username === "string" && typeof creds.username === "string" &&
@ -120,7 +120,7 @@ function isValidCredentials(creds) {
* @param {string} [term] - The terminal name to validate. * @param {string} [term] - The terminal name to validate.
* @returns {string|null} - The sanitized terminal name if valid, null otherwise. * @returns {string|null} - The sanitized terminal name if valid, null otherwise.
*/ */
function validateSshTerm(term) { export function validateSshTerm(term) {
debug(`validateSshTerm: %O`, term) debug(`validateSshTerm: %O`, term)
if (!term) { if (!term) {
@ -141,7 +141,7 @@ function validateSshTerm(term) {
* @throws {Error} If the configuration object fails validation. * @throws {Error} If the configuration object fails validation.
* @returns {Object} The validated configuration object. * @returns {Object} The validated configuration object.
*/ */
function validateConfig(config) { export function validateConfig(config) {
const ajv = new Ajv() const ajv = new Ajv()
const validate = ajv.compile(configSchema) const validate = ajv.compile(configSchema)
const valid = validate(config) const valid = validate(config)
@ -159,7 +159,7 @@ function validateConfig(config) {
* @param {Object} config - The configuration object to inject into the HTML. * @param {Object} config - The configuration object to inject into the HTML.
* @returns {string} - The modified HTML content. * @returns {string} - The modified HTML content.
*/ */
function modifyHtml(html, config) { export function modifyHtml(html, config) {
debug("modifyHtml") debug("modifyHtml")
const modifiedHtml = html.replace( const modifiedHtml = html.replace(
/(src|href)="(?!http|\/\/)/g, /(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 * @param {boolean} [options.fullMask=false] - Whether to use a full mask for all properties
* @returns {Object} The masked object * @returns {Object} The masked object
*/ */
function maskSensitiveData(obj, options) { export function maskSensitiveData(obj, options) {
const defaultOptions = {} const defaultOptions = {}
debug("maskSensitiveData") debug("maskSensitiveData")
@ -199,8 +199,7 @@ function maskSensitiveData(obj, options) {
* @param {string} key - The environment variable key to validate * @param {string} key - The environment variable key to validate
* @returns {boolean} - Whether the key is valid * @returns {boolean} - Whether the key is valid
*/ */
function isValidEnvKey(key) { export function isValidEnvKey(key) {
// Only allow uppercase letters, numbers, and underscore
return /^[A-Z][A-Z0-9_]*$/.test(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 * @param {string} value - The environment variable value to validate
* @returns {boolean} - Whether the value is valid * @returns {boolean} - Whether the value is valid
*/ */
function isValidEnvValue(value) { export function isValidEnvValue(value) {
// Disallow special characters that could be used for command injection // Disallow special characters that could be used for command injection
return !/[;&|`$]/.test(value) return !/[;&|`$]/.test(value)
} }
@ -219,7 +218,7 @@ function isValidEnvValue(value) {
* @param {string} envString - The environment string from URL query * @param {string} envString - The environment string from URL query
* @returns {Object|null} - Object containing validated env vars or null if invalid * @returns {Object|null} - Object containing validated env vars or null if invalid
*/ */
function parseEnvVars(envString) { export function parseEnvVars(envString) {
if (!envString) return null if (!envString) return null
const envVars = {} const envVars = {}
@ -241,17 +240,3 @@ function parseEnvVars(envString) {
return Object.keys(envVars).length > 0 ? envVars : null return Object.keys(envVars).length > 0 ? envVars : null
} }
module.exports = {
deepMerge,
getValidatedHost,
getValidatedPort,
isValidCredentials,
isValidEnvKey,
isValidEnvValue,
maskSensitiveData,
modifyHtml,
parseEnvVars,
validateConfig,
validateSshTerm
}

76
config.json Normal file
View file

@ -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
}
}

87
eslint.config.js Normal file
View file

@ -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'
}
}
];

View file

@ -9,7 +9,7 @@
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017 * 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 * Main function to start the application
@ -21,7 +21,5 @@ function main() {
// Run the application // Run the application
main() main()
// For testing purposes, export the functions // For testing purposes, export the function
module.exports = { export { initializeServer }
initializeServer
}

71
package-lock.json generated
View file

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"ajv": "^4.11.8", "ajv": "^4.11.8",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"body-parser": "^1.15.2", "body-parser": "^1.20.3",
"debug": "^3.2.7", "debug": "^3.2.7",
"express": "^4.14.1", "express": "^4.14.1",
"express-session": "^1.18.0", "express-session": "^1.18.0",
@ -19,8 +19,8 @@
"jsmasker": "^1.4.0", "jsmasker": "^1.4.0",
"read-config-ng": "~3.0.7", "read-config-ng": "~3.0.7",
"socket.io": "~2.2.0", "socket.io": "~2.2.0",
"ssh2": "~0.8.9", "ssh2": "1.16",
"validator": "^12.2.0", "validator": "^13.12.0",
"webssh2_client": "^0.2.27" "webssh2_client": "^0.2.27"
}, },
"bin": { "bin": {
@ -2156,6 +2156,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/builtin-modules": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@ -3004,6 +3013,20 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/crc": {
"version": "3.8.0", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
@ -8199,7 +8222,6 @@
"version": "2.22.0", "version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
@ -10539,27 +10561,20 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/ssh2": { "node_modules/ssh2": {
"version": "0.8.9", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"hasInstallScript": true,
"dependencies": { "dependencies": {
"ssh2-streams": "~0.4.10" "asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
}, },
"engines": { "engines": {
"node": ">=5.2.0" "node": ">=10.16.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"
}, },
"engines": { "optionalDependencies": {
"node": ">=5.2.0" "cpu-features": "~0.0.10",
"nan": "^2.20.0"
} }
}, },
"node_modules/sshpk": { "node_modules/sshpk": {
@ -10905,14 +10920,6 @@
"node": ">= 0.8" "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": { "node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -11598,9 +11605,9 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "12.2.0", "version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==", "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"

View file

@ -33,7 +33,7 @@
"dependencies": { "dependencies": {
"ajv": "^4.11.8", "ajv": "^4.11.8",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"body-parser": "^1.15.2", "body-parser": "^1.20.3",
"debug": "^3.2.7", "debug": "^3.2.7",
"express": "^4.14.1", "express": "^4.14.1",
"express-session": "^1.18.0", "express-session": "^1.18.0",
@ -41,8 +41,8 @@
"jsmasker": "^1.4.0", "jsmasker": "^1.4.0",
"read-config-ng": "~3.0.7", "read-config-ng": "~3.0.7",
"socket.io": "~2.2.0", "socket.io": "~2.2.0",
"ssh2": "~0.8.9", "ssh2": "1.16",
"validator": "^12.2.0", "validator": "^13.12.0",
"webssh2_client": "^0.2.27" "webssh2_client": "^0.2.27"
}, },
"scripts": { "scripts": {
@ -98,5 +98,6 @@
"directories": { "directories": {
"test": "tests" "test": "tests"
}, },
"author": "" "author": "",
"type": "module"
} }