chore: update config, routes, middleware, and other files to es style modules #383
This commit is contained in:
parent
56a6ce1d8d
commit
db891ecb92
14 changed files with 322 additions and 377 deletions
12
app/app.js
12
app/app.js
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.',
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
app/io.js
16
app/io.js
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
161
app/socket.js
161
app/socket.js
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
69
app/ssh.js
69
app/ssh.js
|
@ -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
|
||||||
|
|
58
app/utils.js
58
app/utils.js
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue