chore: update config, routes, middleware, and other files to es style modules #383

This commit is contained in:
Bill Church 2024-12-14 13:47:38 +00:00
parent 56a6ce1d8d
commit db891ecb92
No known key found for this signature in database
14 changed files with 322 additions and 377 deletions

View file

@ -12,7 +12,7 @@ import { handleError, ConfigError } from './errors.js'
import { createNamespacedDebug } from './logger.js' import { createNamespacedDebug } from './logger.js'
import { DEFAULTS, MESSAGES } from './constants.js' import { DEFAULTS, MESSAGES } from './constants.js'
const debug = createNamespacedDebug("app") const debug = createNamespacedDebug('app')
const sshRoutes = createRoutes(config) const sshRoutes = createRoutes(config)
/** /**
@ -30,16 +30,14 @@ function createApp() {
const { sessionMiddleware } = applyMiddleware(app, config) const { sessionMiddleware } = applyMiddleware(app, config)
// Serve static files from the webssh2_client module with a custom prefix // Serve static files from the webssh2_client module with a custom prefix
app.use("/ssh/assets", express.static(clientPath)) app.use('/ssh/assets', express.static(clientPath))
// Use the SSH routes // Use the SSH routes
app.use("/ssh", sshRoutes) app.use('/ssh', sshRoutes)
return { app: app, sessionMiddleware: sessionMiddleware } return { app: app, sessionMiddleware: sessionMiddleware }
} catch (err) { } catch (err) {
throw new ConfigError( throw new ConfigError(`${MESSAGES.EXPRESS_APP_CONFIG_ERROR}: ${err.message}`)
`${MESSAGES.EXPRESS_APP_CONFIG_ERROR}: ${err.message}`
)
} }
} }
@ -59,7 +57,7 @@ function initializeServer() {
// Start the server // Start the server
startServer(server, config) startServer(server, config)
debug("Server initialized") debug('Server initialized')
return { server: server, io: io, app: app } return { server: server, io: io, app: app }
} catch (err) { } catch (err) {

View file

@ -10,19 +10,19 @@ import { createNamespacedDebug } from './logger.js'
import { ConfigError, handleError } from './errors.js' import { ConfigError, handleError } from './errors.js'
import { DEFAULTS } from './constants.js' import { DEFAULTS } from './constants.js'
const debug = createNamespacedDebug("config") const debug = createNamespacedDebug('config')
const defaultConfig = { const defaultConfig = {
listen: { listen: {
ip: "0.0.0.0", ip: '0.0.0.0',
port: DEFAULTS.LISTEN_PORT port: DEFAULTS.LISTEN_PORT,
}, },
http: { http: {
origins: ["*:*"] origins: ['*:*'],
}, },
user: { user: {
name: null, name: null,
password: null password: null,
}, },
ssh: { ssh: {
host: null, host: null,
@ -35,47 +35,47 @@ const defaultConfig = {
disableInteractiveAuth: false, disableInteractiveAuth: false,
algorithms: { algorithms: {
cipher: [ cipher: [
"aes128-ctr", 'aes128-ctr',
"aes192-ctr", 'aes192-ctr',
"aes256-ctr", 'aes256-ctr',
"aes128-gcm", 'aes128-gcm',
"aes128-gcm@openssh.com", 'aes128-gcm@openssh.com',
"aes256-gcm", 'aes256-gcm',
"aes256-gcm@openssh.com", 'aes256-gcm@openssh.com',
"aes256-cbc" 'aes256-cbc',
], ],
compress: ["none", "zlib@openssh.com", "zlib"], compress: ['none', 'zlib@openssh.com', 'zlib'],
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"], hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
kex: [ kex: [
"ecdh-sha2-nistp256", 'ecdh-sha2-nistp256',
"ecdh-sha2-nistp384", 'ecdh-sha2-nistp384',
"ecdh-sha2-nistp521", 'ecdh-sha2-nistp521',
"diffie-hellman-group-exchange-sha256", 'diffie-hellman-group-exchange-sha256',
"diffie-hellman-group14-sha1" 'diffie-hellman-group14-sha1',
], ],
serverHostKey: [ serverHostKey: [
"ecdsa-sha2-nistp256", 'ecdsa-sha2-nistp256',
"ecdsa-sha2-nistp384", 'ecdsa-sha2-nistp384',
"ecdsa-sha2-nistp521", 'ecdsa-sha2-nistp521',
"ssh-rsa" 'ssh-rsa',
] ],
} },
}, },
header: { header: {
text: null, text: null,
background: "green" background: 'green',
}, },
options: { options: {
challengeButton: true, challengeButton: true,
autoLog: false, autoLog: false,
allowReauth: true, allowReauth: true,
allowReconnect: true, allowReconnect: true,
allowReplay: true allowReplay: true,
}, },
session: { session: {
secret: process.env.WEBSSH_SESSION_SECRET || generateSecureSecret(), secret: process.env.WEBSSH_SESSION_SECRET || generateSecureSecret(),
name: "webssh2.sid" name: 'webssh2.sid',
} },
} }
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@ -85,7 +85,7 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
function getConfigPath() { function getConfigPath() {
return path.join(__dirname, "..", "config.json") return path.join(__dirname, '..', 'config.json')
} }
function loadConfig() { function loadConfig() {
@ -94,26 +94,21 @@ function loadConfig() {
try { try {
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
const providedConfig = readConfig.sync(configPath) const providedConfig = readConfig.sync(configPath)
const mergedConfig = deepMerge( const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig)
JSON.parse(JSON.stringify(defaultConfig)),
providedConfig
)
if (process.env.PORT) { if (process.env.PORT) {
mergedConfig.listen.port = parseInt(process.env.PORT, 10) mergedConfig.listen.port = parseInt(process.env.PORT, 10)
debug("Using PORT from environment: %s", mergedConfig.listen.port) debug('Using PORT from environment: %s', mergedConfig.listen.port)
} }
const validatedConfig = validateConfig(mergedConfig) const validatedConfig = validateConfig(mergedConfig)
debug("Merged and validated configuration") debug('Merged and validated configuration')
return validatedConfig return validatedConfig
} }
debug("Missing config.json for webssh. Using default config") debug('Missing config.json for webssh. Using default config')
return defaultConfig return defaultConfig
} catch (err) { } catch (err) {
const error = new ConfigError( const error = new ConfigError(`Problem loading config.json for webssh: ${err.message}`)
`Problem loading config.json for webssh: ${err.message}`
)
handleError(error) handleError(error)
return defaultConfig return defaultConfig
} }
@ -165,8 +160,8 @@ const config = loadConfig()
function getCorsConfig() { function getCorsConfig() {
return { return {
origin: config.http.origins, origin: config.http.origins,
methods: ["GET", "POST"], methods: ['GET', 'POST'],
credentials: true credentials: true,
} }
} }

View file

@ -2,109 +2,102 @@
* Schema for validating the config * Schema for validating the config
*/ */
const configSchema = { const configSchema = {
type: "object", type: 'object',
properties: { properties: {
listen: { listen: {
type: "object", type: 'object',
properties: { properties: {
ip: { type: "string", format: "ipv4" }, ip: { type: 'string', format: 'ipv4' },
port: { type: "integer", minimum: 1, maximum: 65535 } port: { type: 'integer', minimum: 1, maximum: 65535 },
}, },
required: ["ip", "port"] required: ['ip', 'port'],
}, },
http: { http: {
type: "object", type: 'object',
properties: { properties: {
origins: { origins: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
} },
}, },
required: ["origins"] required: ['origins'],
}, },
user: { user: {
type: "object", type: 'object',
properties: { properties: {
name: { type: ["string", "null"] }, name: { type: ['string', 'null'] },
password: { type: ["string", "null"] }, password: { type: ['string', 'null'] },
privateKey: { type: ["string", "null"] } privateKey: { type: ['string', 'null'] },
}, },
required: ["name", "password"] required: ['name', 'password'],
}, },
ssh: { ssh: {
type: "object", type: 'object',
properties: { properties: {
host: { type: ["string", "null"] }, host: { type: ['string', 'null'] },
port: { type: "integer", minimum: 1, maximum: 65535 }, port: { type: 'integer', minimum: 1, maximum: 65535 },
term: { type: "string" }, term: { type: 'string' },
readyTimeout: { type: "integer" }, readyTimeout: { type: 'integer' },
keepaliveInterval: { type: "integer" }, keepaliveInterval: { type: 'integer' },
keepaliveCountMax: { type: "integer" }, keepaliveCountMax: { type: 'integer' },
algorithms: { algorithms: {
type: "object", type: 'object',
properties: { properties: {
kex: { kex: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
}, },
cipher: { cipher: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
}, },
hmac: { hmac: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
}, },
serverHostKey: { serverHostKey: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
}, },
compress: { compress: {
type: "array", type: 'array',
items: { type: "string" } items: { type: 'string' },
} },
}, },
required: ["kex", "cipher", "hmac", "serverHostKey", "compress"] required: ['kex', 'cipher', 'hmac', 'serverHostKey', 'compress'],
} },
}, },
required: [ required: ['host', 'port', 'term', 'readyTimeout', 'keepaliveInterval', 'keepaliveCountMax'],
"host",
"port",
"term",
"readyTimeout",
"keepaliveInterval",
"keepaliveCountMax"
]
}, },
header: { header: {
type: "object", type: 'object',
properties: { properties: {
text: { type: ["string", "null"] }, text: { type: ['string', 'null'] },
background: { type: "string" } background: { type: 'string' },
}, },
required: ["text", "background"] required: ['text', 'background'],
}, },
options: { options: {
type: "object", type: 'object',
properties: { properties: {
challengeButton: { type: "boolean" }, challengeButton: { type: 'boolean' },
autoLog: { type: "boolean" }, autoLog: { type: 'boolean' },
allowReauth: { type: "boolean" }, allowReauth: { type: 'boolean' },
allowReconnect: { type: "boolean" }, allowReconnect: { type: 'boolean' },
allowReplay: { type: "boolean" } allowReplay: { type: 'boolean' },
}, },
required: ["challengeButton", "allowReauth", "allowReplay"] required: ['challengeButton', 'allowReauth', 'allowReplay'],
}, },
session: { session: {
type: "object", type: 'object',
properties: { properties: {
secret: { type: "string" }, secret: { type: 'string' },
name: { type: "string" } name: { type: 'string' },
}, },
required: ["secret", "name"] required: ['secret', 'name'],
} },
}, },
required: ["listen", "http", "user", "ssh", "header", "options"] required: ['listen', 'http', 'user', 'ssh', 'header', 'options'],
} }
export default configSchema export default configSchema

View file

@ -3,15 +3,15 @@
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { dirname } from 'path' import { dirname } from 'path'
import fs from "fs" import fs from 'fs'
import path from "path" import path from 'path'
import { createNamespacedDebug } from "./logger.js" import { createNamespacedDebug } from './logger.js'
import { HTTP, MESSAGES, DEFAULTS } from "./constants.js" import { HTTP, MESSAGES, DEFAULTS } from './constants.js'
import { modifyHtml } from "./utils.js" import { modifyHtml } from './utils.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
const debug = createNamespacedDebug("connectionHandler") const debug = createNamespacedDebug('connectionHandler')
/** /**
* Handle reading the file and processing the response. * Handle reading the file and processing the response.
@ -20,12 +20,9 @@ const debug = createNamespacedDebug("connectionHandler")
* @param {Object} res - The Express response object. * @param {Object} res - The Express response object.
*/ */
function handleFileRead(filePath, config, res) { function handleFileRead(filePath, config, res) {
// eslint-disable-next-line consistent-return fs.readFile(filePath, 'utf8', (err, data) => {
fs.readFile(filePath, "utf8", (err, data) => {
if (err) { if (err) {
return res return res.status(HTTP.INTERNAL_SERVER_ERROR).send(MESSAGES.CLIENT_FILE_ERROR)
.status(HTTP.INTERNAL_SERVER_ERROR)
.send(MESSAGES.CLIENT_FILE_ERROR)
} }
const modifiedHtml = modifyHtml(data, config) const modifiedHtml = modifyHtml(data, config)
@ -39,27 +36,27 @@ function handleFileRead(filePath, config, res) {
* @param {Object} res - The Express response object. * @param {Object} res - The Express response object.
*/ */
function handleConnection(req, res) { function handleConnection(req, res) {
debug("Handling connection req.path:", req.path) debug('Handling connection req.path:', req.path)
const clientPath = path.resolve( const clientPath = path.resolve(
__dirname, __dirname,
"..", '..',
"node_modules", 'node_modules',
"webssh2_client", 'webssh2_client',
"client", 'client',
"public" 'public'
) )
const tempConfig = { const tempConfig = {
socket: { socket: {
url: `${req.protocol}://${req.get("host")}`, url: `${req.protocol}://${req.get('host')}`,
path: "/ssh/socket.io" path: '/ssh/socket.io',
}, },
autoConnect: req.path.startsWith("/host/") // Automatically connect if path starts with /host/ autoConnect: req.path.startsWith('/host/'), // Automatically connect if path starts with /host/
} }
const filePath = path.join(clientPath, DEFAULTS.CLIENT_FILE) const filePath = path.join(clientPath, DEFAULTS.CLIENT_FILE)
handleFileRead(filePath, tempConfig, res) handleFileRead(filePath, tempConfig, res)
} }
export default handleConnection export default handleConnection

View file

@ -12,15 +12,15 @@ const __dirname = dirname(__filename)
* Error messages * Error messages
*/ */
export 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',
CONFIG_ERROR: "CONFIG_ERROR", CONFIG_ERROR: 'CONFIG_ERROR',
UNEXPECTED_ERROR: "An unexpected error occurred", UNEXPECTED_ERROR: 'An unexpected error occurred',
EXPRESS_APP_CONFIG_ERROR: "Failed to configure Express app", EXPRESS_APP_CONFIG_ERROR: 'Failed to configure Express app',
CLIENT_FILE_ERROR: "Error loading client file", CLIENT_FILE_ERROR: 'Error loading client file',
FAILED_SESSION_SAVE: "Failed to save session", FAILED_SESSION_SAVE: 'Failed to save session',
CONFIG_VALIDATION_ERROR: "Config validation error" CONFIG_VALIDATION_ERROR: 'Config validation error',
} }
/** /**
@ -29,20 +29,20 @@ export const MESSAGES = {
export 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, IO_PING_TIMEOUT: 60000,
IO_PING_INTERVAL: 25000, 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,
"..", '..',
"node_modules", 'node_modules',
"webssh2_client", 'webssh2_client',
"client", 'client',
"public" 'public'
), ),
CLIENT_FILE: "client.htm", CLIENT_FILE: 'client.htm',
MAX_AUTH_ATTEMPTS: 2 MAX_AUTH_ATTEMPTS: 2,
} }
/** /**
* HTTP Related * HTTP Related
@ -51,12 +51,12 @@ export const HTTP = {
OK: 200, OK: 200,
UNAUTHORIZED: 401, UNAUTHORIZED: 401,
INTERNAL_SERVER_ERROR: 500, INTERNAL_SERVER_ERROR: 500,
AUTHENTICATE: "WWW-Authenticate", AUTHENTICATE: 'WWW-Authenticate',
REALM: 'Basic realm="WebSSH2"', REALM: 'Basic realm="WebSSH2"',
AUTH_REQUIRED: "Authentication required.", AUTH_REQUIRED: 'Authentication required.',
COOKIE: "basicauth", COOKIE: 'basicauth',
PATH: "/ssh/host/", PATH: '/ssh/host/',
SAMESITE: "Strict", SAMESITE: 'Strict',
SESSION_SID: "webssh2_sid", SESSION_SID: 'webssh2_sid',
CREDS_CLEARED: "Credentials cleared." CREDS_CLEARED: 'Credentials cleared.',
} }

View file

@ -1,11 +1,11 @@
// server // server
// app/crypto-utils.js // app/crypto-utils.js
import crypto from "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
*/ */
export function generateSecureSecret() { export function generateSecureSecret() {
return crypto.randomBytes(32).toString("hex") return crypto.randomBytes(32).toString('hex')
} }

View file

@ -4,7 +4,7 @@
import { logError, createNamespacedDebug } from './logger.js' import { logError, createNamespacedDebug } from './logger.js'
import { HTTP, MESSAGES } from './constants.js' import { HTTP, MESSAGES } from './constants.js'
const debug = createNamespacedDebug("errors") const debug = createNamespacedDebug('errors')
/** /**
* Custom error for WebSSH2 * Custom error for WebSSH2
@ -49,17 +49,13 @@ function handleError(err, res) {
logError(err.message, err) logError(err.message, err)
debug(err.message) debug(err.message)
if (res) { if (res) {
res res.status(HTTP.INTERNAL_SERVER_ERROR).json({ error: err.message, code: err.code })
.status(HTTP.INTERNAL_SERVER_ERROR)
.json({ error: err.message, code: err.code })
} }
} else { } else {
logError(MESSAGES.UNEXPECTED_ERROR, err) logError(MESSAGES.UNEXPECTED_ERROR, err)
debug(`handleError: ${MESSAGES.UNEXPECTED_ERROR}: %O`, err) debug(`handleError: ${MESSAGES.UNEXPECTED_ERROR}: %O`, err)
if (res) { if (res) {
res res.status(HTTP.INTERNAL_SERVER_ERROR).json({ error: MESSAGES.UNEXPECTED_ERROR })
.status(HTTP.INTERNAL_SERVER_ERROR)
.json({ error: MESSAGES.UNEXPECTED_ERROR })
} }
} }
} }

View file

@ -1,9 +1,9 @@
import socketIo from "socket.io" import socketIo from 'socket.io'
import sharedsession from "express-socket.io-session" import sharedsession from 'express-socket.io-session'
import { createNamespacedDebug } from "./logger.js" import { createNamespacedDebug } from './logger.js'
import { DEFAULTS } from "./constants.js" import { DEFAULTS } from './constants.js'
const debug = createNamespacedDebug("app") const debug = createNamespacedDebug('app')
/** /**
* Configures Socket.IO with the given server * Configures Socket.IO with the given server
@ -18,17 +18,17 @@ export function configureSocketIO(server, sessionMiddleware, config) {
path: DEFAULTS.IO_PATH, path: DEFAULTS.IO_PATH,
pingTimeout: DEFAULTS.IO_PING_TIMEOUT, pingTimeout: DEFAULTS.IO_PING_TIMEOUT,
pingInterval: DEFAULTS.IO_PING_INTERVAL, pingInterval: DEFAULTS.IO_PING_INTERVAL,
cors: config.getCorsConfig() cors: config.getCorsConfig(),
}) })
// Share session with io sockets // Share session with io sockets
io.use( io.use(
sharedsession(sessionMiddleware, { sharedsession(sessionMiddleware, {
autoSave: true autoSave: true,
}) })
) )
debug("IO configured") debug('IO configured')
return io return io
} }

View file

@ -1,20 +1,19 @@
// server // server
// app/middleware.js // app/middleware.js
// Scenario 2: Basic Auth // Scenario 2: Basic Auth
import createDebug from "debug" import createDebug from 'debug'
import session from "express-session" import session from 'express-session'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
const { urlencoded, json } = bodyParser const { urlencoded, json } = bodyParser
const debug = createDebug("webssh2:middleware") const debug = createDebug('webssh2:middleware')
import basicAuth from "basic-auth" import basicAuth from 'basic-auth'
import validator from 'validator' import validator from 'validator'
import { HTTP } from "./constants.js" 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.
@ -35,12 +34,11 @@ import { HTTP } from "./constants.js"
* with the appropriate WWW-Authenticate header. * with the appropriate WWW-Authenticate header.
*/ */
export function createAuthMiddleware(config) { export function createAuthMiddleware(config) {
// 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
if (config.user.name && (config.user.password || config.user.privateKey)) { if (config.user.name && (config.user.password || config.user.privateKey)) {
req.session.sshCredentials = { req.session.sshCredentials = {
username: config.user.name username: config.user.name,
} }
// Add credentials based on what's available // Add credentials based on what's available
@ -57,7 +55,7 @@ export function createAuthMiddleware(config) {
// Scenario 2: Basic Auth // Scenario 2: Basic Auth
// If no configured credentials, fall back to Basic Auth // If no configured credentials, fall back to Basic Auth
debug("auth: Basic Auth") debug('auth: Basic Auth')
const credentials = basicAuth(req) const credentials = basicAuth(req)
if (!credentials) { if (!credentials) {
res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM) res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM)
@ -67,7 +65,7 @@ export function createAuthMiddleware(config) {
// Validate and sanitize credentials // Validate and sanitize credentials
req.session.sshCredentials = { req.session.sshCredentials = {
username: validator.escape(credentials.name), username: validator.escape(credentials.name),
password: credentials.pass password: credentials.pass,
} }
req.session.usedBasicAuth = true req.session.usedBasicAuth = true
next() next()
@ -84,7 +82,7 @@ export function createSessionMiddleware(config) {
secret: config.session.secret, secret: config.session.secret,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
name: config.session.name name: config.session.name,
}) })
} }
@ -105,12 +103,12 @@ export function createCookieMiddleware() {
if (req.session.sshCredentials) { if (req.session.sshCredentials) {
const cookieData = { const cookieData = {
host: req.session.sshCredentials.host, host: req.session.sshCredentials.host,
port: req.session.sshCredentials.port port: req.session.sshCredentials.port,
} }
res.cookie(HTTP.COOKIE, JSON.stringify(cookieData), { res.cookie(HTTP.COOKIE, JSON.stringify(cookieData), {
httpOnly: false, httpOnly: false,
path: HTTP.PATH, path: HTTP.PATH,
sameSite: HTTP.SAMESITE sameSite: HTTP.SAMESITE,
}) })
} }
next() next()
@ -130,7 +128,7 @@ export function applyMiddleware(app, config) {
app.use(createBodyParserMiddleware()) app.use(createBodyParserMiddleware())
app.use(createCookieMiddleware()) app.use(createCookieMiddleware())
debug("applyMiddleware") debug('applyMiddleware')
return { sessionMiddleware } return { sessionMiddleware }
} }

View file

@ -1,23 +1,29 @@
// server // server
// app/routes.js // app/routes.js
import express from "express" import express from 'express'
import { getValidatedHost, getValidatedPort, maskSensitiveData, validateSshTerm, parseEnvVars } from "./utils.js" import {
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" 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") const debug = createNamespacedDebug('routes')
export function createRoutes(config) { export function createRoutes(config) {
const router = express.Router() const router = express.Router()
const auth = createAuthMiddleware(config) const auth = createAuthMiddleware(config)
// Scenario 1: No auth required, uses websocket authentication instead // Scenario 1: No auth required, uses websocket authentication instead
router.get("/", (req, res) => { router.get('/', (req, res) => {
debug("router.get./: Accessed / route") debug('router.get./: Accessed / route')
handleConnection(req, res) handleConnection(req, res)
}) })
@ -35,19 +41,17 @@ export function createRoutes(config) {
* @param {Object} req - The Express request object * @param {Object} req - The Express request object
* @param {Object} res - The Express response object * @param {Object} res - The Express response object
*/ */
router.get("/host/", auth, (req, res) => { router.get('/host/', auth, (req, res) => {
debug(`router.get.host: /ssh/host/ route`) debug(`router.get.host: /ssh/host/ route`)
const envVars = parseEnvVars(req.query.env) const envVars = parseEnvVars(req.query.env)
if (envVars) { if (envVars) {
req.session.envVars = envVars req.session.envVars = envVars
debug("routes: Parsed environment variables: %O", envVars) debug('routes: Parsed environment variables: %O', envVars)
} }
try { try {
if (!config.ssh.host) { if (!config.ssh.host) {
throw new ConfigError( throw new ConfigError('Host parameter required when default host not configured')
"Host parameter required when default host not configured"
)
} }
const { host } = config.ssh const { host } = config.ssh
@ -65,7 +69,7 @@ export function createRoutes(config) {
const sanitizedCredentials = maskSensitiveData( const sanitizedCredentials = maskSensitiveData(
JSON.parse(JSON.stringify(req.session.sshCredentials)) JSON.parse(JSON.stringify(req.session.sshCredentials))
) )
debug("/ssh/host/ Credentials: ", sanitizedCredentials) debug('/ssh/host/ Credentials: ', sanitizedCredentials)
handleConnection(req, res, { host: host }) handleConnection(req, res, { host: host })
} catch (err) { } catch (err) {
@ -75,12 +79,12 @@ export function createRoutes(config) {
}) })
// Scenario 2: Auth required, uses HTTP Basic Auth // Scenario 2: Auth required, uses HTTP Basic Auth
router.get("/host/:host?", auth, (req, res) => { router.get('/host/:host?', auth, (req, res) => {
debug(`router.get.host: /ssh/host/${req.params.host} route`) debug(`router.get.host: /ssh/host/${req.params.host} route`)
const envVars = parseEnvVars(req.query.env) const envVars = parseEnvVars(req.query.env)
if (envVars) { if (envVars) {
req.session.envVars = envVars req.session.envVars = envVars
debug("routes: Parsed environment variables: %O", envVars) debug('routes: Parsed environment variables: %O', envVars)
} }
try { try {
@ -102,7 +106,7 @@ export function createRoutes(config) {
const sanitizedCredentials = maskSensitiveData( const sanitizedCredentials = maskSensitiveData(
JSON.parse(JSON.stringify(req.session.sshCredentials)) JSON.parse(JSON.stringify(req.session.sshCredentials))
) )
debug("/ssh/host/ Credentials: ", sanitizedCredentials) debug('/ssh/host/ Credentials: ', sanitizedCredentials)
handleConnection(req, res, { host: host }) handleConnection(req, res, { host: host })
} catch (err) { } catch (err) {
@ -112,12 +116,12 @@ export function createRoutes(config) {
}) })
// Clear credentials route // Clear credentials route
router.get("/clear-credentials", (req, res) => { router.get('/clear-credentials', (req, res) => {
req.session.sshCredentials = null req.session.sshCredentials = null
res.status(HTTP.OK).send(HTTP.CREDENTIALS_CLEARED) res.status(HTTP.OK).send(HTTP.CREDENTIALS_CLEARED)
}) })
router.get("/force-reconnect", (req, res) => { router.get('/force-reconnect', (req, res) => {
req.session.sshCredentials = null req.session.sshCredentials = null
res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED) res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED)
}) })

View file

@ -1,4 +1,4 @@
import http from "http" import http from 'http'
// const { createNamespacedDebug } = require("./logger") // const { createNamespacedDebug } = require("./logger")
// const debug = createNamespacedDebug("server") // const debug = createNamespacedDebug("server")
@ -16,7 +16,7 @@ export function createServer(app) {
* @param {Error} err - The error object * @param {Error} err - The error object
*/ */
function handleServerError(err) { function handleServerError(err) {
console.error("HTTP Server ERROR: %O", err) console.error('HTTP Server ERROR: %O', err)
} }
/** /**
@ -26,10 +26,8 @@ function handleServerError(err) {
*/ */
export 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}`
)
}) })
server.on("error", handleServerError) server.on('error', handleServerError)
} }

View file

@ -1,19 +1,15 @@
// server // server
// app/socket.js // app/socket.js
import validator from "validator" import validator from 'validator'
import { EventEmitter } from "events" import { EventEmitter } from 'events'
import SSHConnection from "./ssh.js" import SSHConnection from './ssh.js'
import { createNamespacedDebug } from "./logger.js" import { createNamespacedDebug } from './logger.js'
import { SSHConnectionError, handleError } from "./errors.js" import { SSHConnectionError, handleError } from './errors.js'
const debug = createNamespacedDebug("socket") const debug = createNamespacedDebug('socket')
import { import { isValidCredentials, maskSensitiveData, validateSshTerm } from './utils.js'
isValidCredentials, import { MESSAGES } from './constants.js'
maskSensitiveData,
validateSshTerm
} from "./utils.js"
import { MESSAGES } from "./constants.js"
class WebSSH2Socket extends EventEmitter { class WebSSH2Socket extends EventEmitter {
constructor(socket, config) { constructor(socket, config) {
@ -31,7 +27,7 @@ class WebSSH2Socket extends EventEmitter {
port: null, port: null,
term: null, term: null,
cols: null, cols: null,
rows: null rows: null,
} }
this.initializeSocketEvents() this.initializeSocketEvents()
@ -54,25 +50,25 @@ class WebSSH2Socket extends EventEmitter {
// Check if interactive auth is disabled // Check if interactive auth is disabled
if (this.config.ssh.disableInteractiveAuth) { if (this.config.ssh.disableInteractiveAuth) {
debug(`handleConnection: ${this.socket.id}, interactive auth disabled`) debug(`handleConnection: ${this.socket.id}, interactive auth disabled`)
this.handleError("Interactive Auth Disabled") this.handleError('Interactive Auth Disabled')
return return
} }
debug(`handleConnection: ${this.socket.id}, emitting request_auth`) debug(`handleConnection: ${this.socket.id}, emitting request_auth`)
this.socket.emit("authentication", { action: "request_auth" }) this.socket.emit('authentication', { action: 'request_auth' })
} }
this.ssh.on("keyboard-interactive", data => { this.ssh.on('keyboard-interactive', (data) => {
this.handleKeyboardInteractive(data) this.handleKeyboardInteractive(data)
}) })
this.socket.on("authenticate", creds => { this.socket.on('authenticate', (creds) => {
this.handleAuthenticate(creds) this.handleAuthenticate(creds)
}) })
this.socket.on("terminal", data => { this.socket.on('terminal', (data) => {
this.handleTerminal(data) this.handleTerminal(data)
}) })
this.socket.on("disconnect", reason => { this.socket.on('disconnect', (reason) => {
this.handleConnectionClose(reason) this.handleConnectionClose(reason)
}) })
} }
@ -83,27 +79,24 @@ class WebSSH2Socket extends EventEmitter {
// Send the keyboard-interactive request to the client // Send the keyboard-interactive request to the client
this.socket.emit( this.socket.emit(
"authentication", 'authentication',
Object.assign( Object.assign(
{ {
action: "keyboard-interactive" action: 'keyboard-interactive',
}, },
data data
) )
) )
// Set up a one-time listener for the client's response // Set up a one-time listener for the client's response
this.socket.once("authentication", clientResponse => { this.socket.once('authentication', (clientResponse) => {
const maskedclientResponse = maskSensitiveData(clientResponse, { const maskedclientResponse = maskSensitiveData(clientResponse, {
properties: ["responses"] properties: ['responses'],
}) })
debug( debug('handleKeyboardInteractive: Client response masked %O', maskedclientResponse)
"handleKeyboardInteractive: Client response masked %O", if (clientResponse.action === 'keyboard-interactive') {
maskedclientResponse
)
if (clientResponse.action === "keyboard-interactive") {
// Forward the client's response to the SSH connection // Forward the client's response to the SSH connection
self.ssh.emit("keyboard-interactive-response", clientResponse.responses) self.ssh.emit('keyboard-interactive-response', clientResponse.responses)
} }
}) })
} }
@ -113,16 +106,14 @@ class WebSSH2Socket extends EventEmitter {
if (isValidCredentials(creds)) { if (isValidCredentials(creds)) {
// Set term if provided, otherwise use config default // Set term if provided, otherwise use config default
this.sessionState.term = validateSshTerm(creds.term) this.sessionState.term = validateSshTerm(creds.term) ? creds.term : this.config.ssh.term
? creds.term
: this.config.ssh.term
this.initializeConnection(creds) this.initializeConnection(creds)
} else { } else {
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`) debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
this.socket.emit("authentication", { this.socket.emit('authentication', {
success: false, success: false,
message: "Invalid credentials format" message: 'Invalid credentials format',
}) })
} }
} }
@ -148,40 +139,38 @@ class WebSSH2Socket extends EventEmitter {
privateKey: creds.privateKey, privateKey: creds.privateKey,
passphrase: creds.passphrase, passphrase: creds.passphrase,
host: creds.host, host: creds.host,
port: creds.port port: creds.port,
}) })
const authResult = { action: "auth_result", success: true } const authResult = { action: 'auth_result', success: true }
this.socket.emit("authentication", authResult) this.socket.emit('authentication', authResult)
const permissions = { const permissions = {
autoLog: this.config.options.autoLog || false, autoLog: this.config.options.autoLog || false,
allowReplay: this.config.options.allowReplay || false, allowReplay: this.config.options.allowReplay || false,
allowReconnect: this.config.options.allowReconnect || false, allowReconnect: this.config.options.allowReconnect || false,
allowReauth: this.config.options.allowReauth || false allowReauth: this.config.options.allowReauth || false,
} }
this.socket.emit("permissions", permissions) this.socket.emit('permissions', permissions)
this.updateElement("footer", `ssh://${creds.host}:${creds.port}`) this.updateElement('footer', `ssh://${creds.host}:${creds.port}`)
if (this.config.header && this.config.header.text !== null) { if (this.config.header && this.config.header.text !== null) {
this.updateElement("header", this.config.header.text) this.updateElement('header', this.config.header.text)
} }
this.socket.emit("getTerminal", true) this.socket.emit('getTerminal', true)
}) })
.catch((err) => { .catch((err) => {
debug( debug(
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}` `initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
) )
const errorMessage = const errorMessage =
err instanceof SSHConnectionError err instanceof SSHConnectionError ? err.message : 'SSH connection failed'
? err.message this.socket.emit('authentication', {
: "SSH connection failed" action: 'auth_result',
this.socket.emit("authentication", {
action: "auth_result",
success: false, success: false,
message: errorMessage message: errorMessage,
}) })
}) })
} }
@ -192,11 +181,15 @@ class WebSSH2Socket extends EventEmitter {
*/ */
handleTerminal(data) { handleTerminal(data) {
const { term, rows, cols } = data const { term, rows, cols } = data
if (term && validateSshTerm(term)) this.sessionState.term = term if (term && validateSshTerm(term)) {
if (rows && validator.isInt(rows.toString())) this.sessionState.term = term
}
if (rows && validator.isInt(rows.toString())) {
this.sessionState.rows = parseInt(rows, 10) this.sessionState.rows = parseInt(rows, 10)
if (cols && validator.isInt(cols.toString())) }
if (cols && validator.isInt(cols.toString())) {
this.sessionState.cols = parseInt(cols, 10) this.sessionState.cols = parseInt(cols, 10)
}
this.createShell() this.createShell()
} }
@ -213,47 +206,49 @@ class WebSSH2Socket extends EventEmitter {
{ {
term: this.sessionState.term, term: this.sessionState.term,
cols: this.sessionState.cols, cols: this.sessionState.cols,
rows: this.sessionState.rows rows: this.sessionState.rows,
}, },
envVars envVars
) )
.then((stream) => { .then((stream) => {
stream.on("data", (data) => { stream.on('data', (data) => {
this.socket.emit("data", data.toString("utf-8")) this.socket.emit('data', data.toString('utf-8'))
}) })
// stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec // stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec
stream.on("close", (code, signal) => { stream.on('close', (code, signal) => {
debug("close: SSH Stream closed") debug('close: SSH Stream closed')
this.handleConnectionClose(code, signal) this.handleConnectionClose(code, signal)
}) })
stream.on("end", () => { stream.on('end', () => {
debug("end: SSH Stream ended") debug('end: SSH Stream ended')
}) })
stream.on("error", err => { stream.on('error', (err) => {
debug("error: SSH Stream error %O", err) debug('error: SSH Stream error %O', err)
}) })
this.socket.on("data", data => { this.socket.on('data', (data) => {
stream.write(data) stream.write(data)
}) })
this.socket.on("control", controlData => { this.socket.on('control', (controlData) => {
this.handleControl(controlData) this.handleControl(controlData)
}) })
this.socket.on("resize", data => { this.socket.on('resize', (data) => {
this.handleResize(data) this.handleResize(data)
}) })
}) })
.catch(err => this.handleError("createShell: ERROR", err)) .catch((err) => this.handleError('createShell: ERROR', err))
} }
handleResize(data) { handleResize(data) {
const { rows, cols } = data const { rows, cols } = data
if (rows && validator.isInt(rows.toString())) if (rows && validator.isInt(rows.toString())) {
this.sessionState.rows = parseInt(rows, 10) this.sessionState.rows = parseInt(rows, 10)
if (cols && validator.isInt(cols.toString())) }
if (cols && validator.isInt(cols.toString())) {
this.sessionState.cols = parseInt(cols, 10) this.sessionState.cols = parseInt(cols, 10)
}
this.ssh.resizeTerminal(this.sessionState.rows, this.sessionState.cols) this.ssh.resizeTerminal(this.sessionState.rows, this.sessionState.cols)
} }
@ -262,19 +257,14 @@ class WebSSH2Socket extends EventEmitter {
* @param {string} controlData - The control command received. * @param {string} controlData - The control command received.
*/ */
handleControl(controlData) { handleControl(controlData) {
if ( if (validator.isIn(controlData, ['replayCredentials', 'reauth']) && this.ssh.stream) {
validator.isIn(controlData, ["replayCredentials", "reauth"]) && if (controlData === 'replayCredentials') {
this.ssh.stream
) {
if (controlData === "replayCredentials") {
this.replayCredentials() this.replayCredentials()
} else if (controlData === "reauth") { } else if (controlData === 'reauth') {
this.handleReauth() this.handleReauth()
} }
} else { } else {
console.warn( console.warn(`handleControl: Invalid control command received: ${controlData}`)
`handleControl: Invalid control command received: ${controlData}`
)
} }
} }
@ -293,7 +283,7 @@ class WebSSH2Socket extends EventEmitter {
handleReauth() { handleReauth() {
if (this.config.options.allowReauth) { if (this.config.options.allowReauth) {
this.clearSessionCredentials() this.clearSessionCredentials()
this.socket.emit("authentication", { action: "reauth" }) this.socket.emit('authentication', { action: 'reauth' })
} }
} }
@ -303,9 +293,9 @@ class WebSSH2Socket extends EventEmitter {
* @param {Error} err - The error object. * @param {Error} err - The error object.
*/ */
handleError(context, err) { handleError(context, err) {
const errorMessage = err ? `: ${err.message}` : "" const errorMessage = err ? `: ${err.message}` : ''
handleError(new SSHConnectionError(`SSH ${context}${errorMessage}`)) handleError(new SSHConnectionError(`SSH ${context}${errorMessage}`))
this.socket.emit("ssherror", `SSH ${context}${errorMessage}`) this.socket.emit('ssherror', `SSH ${context}${errorMessage}`)
this.handleConnectionClose() this.handleConnectionClose()
} }
@ -315,7 +305,7 @@ class WebSSH2Socket extends EventEmitter {
* @param {any} value - The new value for the element. * @param {any} value - The new value for the element.
*/ */
updateElement(element, value) { updateElement(element, value) {
this.socket.emit("updateUI", { element, value }) this.socket.emit('updateUI', { element, value })
} }
/** /**
@ -324,9 +314,7 @@ class WebSSH2Socket extends EventEmitter {
*/ */
handleConnectionClose(code, signal) { handleConnectionClose(code, signal) {
this.ssh.end() this.ssh.end()
debug( debug(`handleConnectionClose: ${this.socket.id}, Code: ${code}, Signal: ${signal}`)
`handleConnectionClose: ${this.socket.id}, Code: ${code}, Signal: ${signal}`
)
this.socket.disconnect(true) this.socket.disconnect(true)
} }
@ -343,16 +331,17 @@ class WebSSH2Socket extends EventEmitter {
this.sessionState.username = null this.sessionState.username = null
this.sessionState.password = null this.sessionState.password = null
this.socket.handshake.session.save(err => { this.socket.handshake.session.save((err) => {
if (err) if (err) {
console.error( console.error(
`clearSessionCredentials: ${MESSAGES.FAILED_SESSION_SAVE} ${this.socket.id}:`, `clearSessionCredentials: ${MESSAGES.FAILED_SESSION_SAVE} ${this.socket.id}:`,
err err
) )
}
}) })
} }
} }
export default 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,14 +1,14 @@
// server // server
// app/ssh.js // app/ssh.js
import { Client as SSH } from "ssh2" import { Client as SSH } from 'ssh2'
import { EventEmitter } from "events" import { EventEmitter } from 'events'
import { createNamespacedDebug } from "./logger.js" import { createNamespacedDebug } from './logger.js'
import { SSHConnectionError, handleError } from "./errors.js" import { SSHConnectionError, handleError } from './errors.js'
import { maskSensitiveData } from "./utils.js" import { maskSensitiveData } from './utils.js'
import { DEFAULTS } from "./constants.js" import { DEFAULTS } from './constants.js'
const debug = createNamespacedDebug("ssh") const debug = createNamespacedDebug('ssh')
/** /**
* SSHConnection class handles SSH connections and operations. * SSHConnection class handles SSH connections and operations.
@ -41,7 +41,7 @@ class SSHConnection extends EventEmitter {
* @returns {Promise<Object>} - A promise that resolves with the SSH connection * @returns {Promise<Object>} - A promise that resolves with the SSH connection
*/ */
connect(creds) { connect(creds) {
debug("connect: %O", maskSensitiveData(creds)) debug('connect: %O', maskSensitiveData(creds))
this.creds = creds this.creds = creds
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.conn) { if (this.conn) {
@ -53,7 +53,7 @@ class SSHConnection extends EventEmitter {
// First try with key authentication if available // First try with key authentication if available
const sshConfig = this.getSSHConfig(creds, true) const sshConfig = this.getSSHConfig(creds, true)
debug("Initial connection config: %O", maskSensitiveData(sshConfig)) debug('Initial connection config: %O', maskSensitiveData(sshConfig))
this.setupConnectionHandlers(resolve, reject) this.setupConnectionHandlers(resolve, reject)
@ -71,24 +71,22 @@ class SSHConnection extends EventEmitter {
* @param {Function} reject - Promise reject function * @param {Function} reject - Promise reject function
*/ */
setupConnectionHandlers(resolve, reject) { setupConnectionHandlers(resolve, reject) {
this.conn.on("ready", () => { this.conn.on('ready', () => {
debug(`connect: ready: ${this.creds.host}`) debug(`connect: ready: ${this.creds.host}`)
resolve(this.conn) resolve(this.conn)
}) })
this.conn.on("error", (err) => { this.conn.on('error', (err) => {
debug(`connect: error: ${err.message}`) debug(`connect: error: ${err.message}`)
// Check if this is an authentication error and we haven't exceeded max attempts // Check if this is an authentication error and we haven't exceeded max attempts
if (this.authAttempts < DEFAULTS.MAX_AUTH_ATTEMPTS) { if (this.authAttempts < DEFAULTS.MAX_AUTH_ATTEMPTS) {
this.authAttempts += 1 this.authAttempts += 1
debug( debug(`Authentication attempt ${this.authAttempts} failed, trying password authentication`)
`Authentication attempt ${this.authAttempts} failed, trying password authentication`
)
// Only try password auth if we have a password // Only try password auth if we have a password
if (this.creds.password) { if (this.creds.password) {
debug("Retrying with password authentication") debug('Retrying with password authentication')
// Disconnect current connection // Disconnect current connection
if (this.conn) { if (this.conn) {
@ -102,14 +100,14 @@ class SSHConnection extends EventEmitter {
this.setupConnectionHandlers(resolve, reject) this.setupConnectionHandlers(resolve, reject)
this.conn.connect(passwordConfig) this.conn.connect(passwordConfig)
} else { } else {
debug("No password available, requesting password from client") debug('No password available, requesting password from client')
this.emit("password-prompt", { this.emit('password-prompt', {
host: this.creds.host, host: this.creds.host,
username: this.creds.username username: this.creds.username,
}) })
// Listen for password response one time // Listen for password response one time
this.once("password-response", (password) => { this.once('password-response', (password) => {
this.creds.password = password this.creds.password = password
const newConfig = this.getSSHConfig(this.creds, false) const newConfig = this.getSSHConfig(this.creds, false)
this.setupConnectionHandlers(resolve, reject) this.setupConnectionHandlers(resolve, reject)
@ -118,26 +116,15 @@ class SSHConnection extends EventEmitter {
} }
} else { } else {
// We've exhausted all authentication attempts // We've exhausted all authentication attempts
const error = new SSHConnectionError( const error = new SSHConnectionError('All authentication methods failed')
"All authentication methods failed"
)
handleError(error) handleError(error)
reject(error) reject(error)
} }
}) })
this.conn.on( this.conn.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => {
"keyboard-interactive", this.handleKeyboardInteractive(name, instructions, lang, prompts, finish)
(name, instructions, lang, prompts, finish) => { })
this.handleKeyboardInteractive(
name,
instructions,
lang,
prompts,
finish
)
}
)
} }
/** /**
@ -156,19 +143,19 @@ class SSHConnection extends EventEmitter {
readyTimeout: this.config.ssh.readyTimeout, readyTimeout: this.config.ssh.readyTimeout,
keepaliveInterval: this.config.ssh.keepaliveInterval, keepaliveInterval: this.config.ssh.keepaliveInterval,
keepaliveCountMax: this.config.ssh.keepaliveCountMax, keepaliveCountMax: this.config.ssh.keepaliveCountMax,
debug: createNamespacedDebug("ssh2") debug: createNamespacedDebug('ssh2'),
} }
// Try private key first if available and useKey is true // Try private key first if available and useKey is true
if (useKey && (creds.privateKey || this.config.user.privateKey)) { if (useKey && (creds.privateKey || this.config.user.privateKey)) {
debug("Using private key authentication") debug('Using private key authentication')
const privateKey = creds.privateKey || this.config.user.privateKey const privateKey = creds.privateKey || this.config.user.privateKey
if (!this.validatePrivateKey(privateKey)) { if (!this.validatePrivateKey(privateKey)) {
throw new SSHConnectionError("Invalid private key format") throw new SSHConnectionError('Invalid private key format')
} }
config.privateKey = privateKey config.privateKey = privateKey
} else if (creds.password) { } else if (creds.password) {
debug("Using password authentication") debug('Using password authentication')
config.password = creds.password config.password = creds.password
} }
@ -183,7 +170,7 @@ class SSHConnection extends EventEmitter {
*/ */
shell(options, envVars) { shell(options, envVars) {
const shellOptions = Object.assign({}, options, { const shellOptions = Object.assign({}, options, {
env: this.getEnvironment(envVars) env: this.getEnvironment(envVars),
}) })
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -230,7 +217,7 @@ class SSHConnection extends EventEmitter {
*/ */
getEnvironment(envVars) { getEnvironment(envVars) {
const env = { const env = {
TERM: this.config.ssh.term TERM: this.config.ssh.term,
} }
if (envVars) { if (envVars) {
@ -243,4 +230,4 @@ class SSHConnection extends EventEmitter {
} }
} }
export default SSHConnection export default SSHConnection

View file

@ -7,7 +7,7 @@ import { createNamespacedDebug } from './logger.js'
import { DEFAULTS, MESSAGES } from './constants.js' import { DEFAULTS, MESSAGES } from './constants.js'
import configSchema from './configSchema.js' import configSchema from './configSchema.js'
const debug = createNamespacedDebug("utils") const debug = createNamespacedDebug('utils')
/** /**
* Deep merges two objects * Deep merges two objects
@ -17,13 +17,9 @@ const debug = createNamespacedDebug("utils")
*/ */
export function deepMerge(target, source) { export function deepMerge(target, source) {
const output = Object.assign({}, target) 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 (source[key] instanceof Object && !Array.isArray(source[key]) && source[key] !== null) {
source[key] instanceof Object &&
!Array.isArray(source[key]) &&
source[key] !== null
) {
output[key] = deepMerge(output[key] || {}, source[key]) output[key] = deepMerge(output[key] || {}, source[key])
} else { } else {
output[key] = source[key] output[key] = source[key]
@ -64,17 +60,14 @@ export function getValidatedHost(host) {
export 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)
if (portInput) { if (portInput) {
if (validator.isInt(portInput, { min: 1, max: 65535 })) { if (validator.isInt(portInput, { min: 1, max: 65535 })) {
return parseInt(portInput, 10) return parseInt(portInput, 10)
} }
} }
debug( debug('getValidatedPort: port not specified or is invalid, setting port to: %O', port)
"getValidatedPort: port not specified or is invalid, setting port to: %O",
port
)
return port return port
} }
@ -95,9 +88,9 @@ export function getValidatedPort(portInput) {
export function isValidCredentials(creds) { export function isValidCredentials(creds) {
const hasRequiredFields = !!( const hasRequiredFields = !!(
creds && creds &&
typeof creds.username === "string" && typeof creds.username === 'string' &&
typeof creds.host === "string" && typeof creds.host === 'string' &&
typeof creds.port === "number" typeof creds.port === 'number'
) )
if (!hasRequiredFields) { if (!hasRequiredFields) {
@ -105,9 +98,8 @@ export function isValidCredentials(creds) {
} }
// Must have either password or privateKey/privateKey // Must have either password or privateKey/privateKey
const hasPassword = typeof creds.password === "string" const hasPassword = typeof creds.password === 'string'
const hasPrivateKey = const hasPrivateKey = typeof creds.privateKey === 'string' || typeof creds.privateKey === 'string'
typeof creds.privateKey === "string" || typeof creds.privateKey === "string"
return hasPassword || hasPrivateKey return hasPassword || hasPrivateKey
} }
@ -128,8 +120,7 @@ export function validateSshTerm(term) {
} }
const validatedSshTerm = const validatedSshTerm =
validator.isLength(term, { min: 1, max: 30 }) && validator.isLength(term, { min: 1, max: 30 }) && validator.matches(term, /^[a-zA-Z0-9.-]+$/)
validator.matches(term, /^[a-zA-Z0-9.-]+$/)
return validatedSshTerm ? term : null return validatedSshTerm ? term : null
} }
@ -146,9 +137,7 @@ export function validateConfig(config) {
const validate = ajv.compile(configSchema) const validate = ajv.compile(configSchema)
const valid = validate(config) const valid = validate(config)
if (!valid) { if (!valid) {
throw new Error( throw new Error(`${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}`)
`${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}`
)
} }
return config return config
} }
@ -160,14 +149,11 @@ export function validateConfig(config) {
* @returns {string} - The modified HTML content. * @returns {string} - The modified HTML content.
*/ */
export 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, '$1="/ssh/assets/')
/(src|href)="(?!http|\/\/)/g,
'$1="/ssh/assets/'
)
return modifiedHtml.replace( return modifiedHtml.replace(
"window.webssh2Config = null;", 'window.webssh2Config = null;',
`window.webssh2Config = ${JSON.stringify(config)};` `window.webssh2Config = ${JSON.stringify(config)};`
) )
} }
@ -186,7 +172,7 @@ export function modifyHtml(html, config) {
*/ */
export function maskSensitiveData(obj, options) { export function maskSensitiveData(obj, options) {
const defaultOptions = {} const defaultOptions = {}
debug("maskSensitiveData") debug('maskSensitiveData')
const maskingOptions = Object.assign({}, defaultOptions, options || {}) const maskingOptions = Object.assign({}, defaultOptions, options || {})
const maskedObject = maskObject(obj, maskingOptions) const maskedObject = maskObject(obj, maskingOptions)
@ -219,14 +205,18 @@ export function isValidEnvValue(value) {
* @returns {Object|null} - Object containing validated env vars or null if invalid * @returns {Object|null} - Object containing validated env vars or null if invalid
*/ */
export function parseEnvVars(envString) { export function parseEnvVars(envString) {
if (!envString) return null if (!envString) {
return null
}
const envVars = {} const envVars = {}
const pairs = envString.split(",") const pairs = envString.split(',')
for (let i = 0; i < pairs.length; i += 1) { for (let i = 0; i < pairs.length; i += 1) {
const pair = pairs[i].split(":") const pair = pairs[i].split(':')
if (pair.length !== 2) continue if (pair.length !== 2) {
continue
}
const key = pair[0].trim() const key = pair[0].trim()
const value = pair[1].trim() const value = pair[1].trim()