chore: update dependencies and migrate to ES modules #383
This commit is contained in:
parent
626e581921
commit
56a6ce1d8d
23 changed files with 355 additions and 322 deletions
|
@ -1 +0,0 @@
|
|||
**/*{.,-}min.js
|
99
.eslintrc.js
99
.eslintrc.js
|
@ -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
2
.gitignore
vendored
|
@ -5,7 +5,7 @@ ssl/*
|
|||
|
||||
bigip/*
|
||||
|
||||
config.json
|
||||
# config.json
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
23
app/app.js
23
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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -106,4 +106,5 @@ const configSchema = {
|
|||
},
|
||||
required: ["listen", "http", "user", "ssh", "header", "options"]
|
||||
}
|
||||
module.exports = configSchema
|
||||
|
||||
export default configSchema
|
||||
|
|
|
@ -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
|
|
@ -1,12 +1,17 @@
|
|||
// 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",
|
||||
SHELL_ERROR: "SHELL ERROR",
|
||||
|
@ -21,12 +26,12 @@ 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,
|
||||
|
@ -39,11 +44,10 @@ const DEFAULTS = {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
class WebSSH2Error extends Error {
|
||||
constructor(message, code) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
this.message = message
|
||||
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 }
|
||||
|
|
12
app/io.js
12
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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
14
app/ssh.js
14
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
|
51
app/utils.js
51
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
|
||||
}
|
||||
|
|
76
config.json
Normal file
76
config.json
Normal 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
87
eslint.config.js
Normal 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'
|
||||
}
|
||||
}
|
||||
];
|
8
index.js
8
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 }
|
||||
|
|
71
package-lock.json
generated
71
package-lock.json
generated
|
@ -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": ">=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": {
|
||||
"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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue