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 { DEFAULTS, MESSAGES } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("app")
|
||||
const debug = createNamespacedDebug('app')
|
||||
const sshRoutes = createRoutes(config)
|
||||
|
||||
/**
|
||||
|
@ -30,16 +30,14 @@ function createApp() {
|
|||
const { sessionMiddleware } = applyMiddleware(app, config)
|
||||
|
||||
// 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
|
||||
app.use("/ssh", sshRoutes)
|
||||
app.use('/ssh', sshRoutes)
|
||||
|
||||
return { app: app, sessionMiddleware: sessionMiddleware }
|
||||
} catch (err) {
|
||||
throw new ConfigError(
|
||||
`${MESSAGES.EXPRESS_APP_CONFIG_ERROR}: ${err.message}`
|
||||
)
|
||||
throw new ConfigError(`${MESSAGES.EXPRESS_APP_CONFIG_ERROR}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +57,7 @@ function initializeServer() {
|
|||
// Start the server
|
||||
startServer(server, config)
|
||||
|
||||
debug("Server initialized")
|
||||
debug('Server initialized')
|
||||
|
||||
return { server: server, io: io, app: app }
|
||||
} catch (err) {
|
||||
|
|
|
@ -10,19 +10,19 @@ import { createNamespacedDebug } from './logger.js'
|
|||
import { ConfigError, handleError } from './errors.js'
|
||||
import { DEFAULTS } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("config")
|
||||
const debug = createNamespacedDebug('config')
|
||||
|
||||
const defaultConfig = {
|
||||
listen: {
|
||||
ip: "0.0.0.0",
|
||||
port: DEFAULTS.LISTEN_PORT
|
||||
ip: '0.0.0.0',
|
||||
port: DEFAULTS.LISTEN_PORT,
|
||||
},
|
||||
http: {
|
||||
origins: ["*:*"]
|
||||
origins: ['*:*'],
|
||||
},
|
||||
user: {
|
||||
name: null,
|
||||
password: null
|
||||
password: null,
|
||||
},
|
||||
ssh: {
|
||||
host: null,
|
||||
|
@ -35,47 +35,47 @@ const defaultConfig = {
|
|||
disableInteractiveAuth: false,
|
||||
algorithms: {
|
||||
cipher: [
|
||||
"aes128-ctr",
|
||||
"aes192-ctr",
|
||||
"aes256-ctr",
|
||||
"aes128-gcm",
|
||||
"aes128-gcm@openssh.com",
|
||||
"aes256-gcm",
|
||||
"aes256-gcm@openssh.com",
|
||||
"aes256-cbc"
|
||||
'aes128-ctr',
|
||||
'aes192-ctr',
|
||||
'aes256-ctr',
|
||||
'aes128-gcm',
|
||||
'aes128-gcm@openssh.com',
|
||||
'aes256-gcm',
|
||||
'aes256-gcm@openssh.com',
|
||||
'aes256-cbc',
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
|
||||
compress: ['none', 'zlib@openssh.com', 'zlib'],
|
||||
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
|
||||
kex: [
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group14-sha1"
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
],
|
||||
serverHostKey: [
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ssh-rsa"
|
||||
]
|
||||
}
|
||||
'ecdsa-sha2-nistp256',
|
||||
'ecdsa-sha2-nistp384',
|
||||
'ecdsa-sha2-nistp521',
|
||||
'ssh-rsa',
|
||||
],
|
||||
},
|
||||
},
|
||||
header: {
|
||||
text: null,
|
||||
background: "green"
|
||||
background: 'green',
|
||||
},
|
||||
options: {
|
||||
challengeButton: true,
|
||||
autoLog: false,
|
||||
allowReauth: true,
|
||||
allowReconnect: true,
|
||||
allowReplay: true
|
||||
allowReplay: true,
|
||||
},
|
||||
session: {
|
||||
secret: process.env.WEBSSH_SESSION_SECRET || generateSecureSecret(),
|
||||
name: "webssh2.sid"
|
||||
}
|
||||
name: 'webssh2.sid',
|
||||
},
|
||||
}
|
||||
|
||||
import { fileURLToPath } from 'url'
|
||||
|
@ -85,7 +85,7 @@ const __filename = fileURLToPath(import.meta.url)
|
|||
const __dirname = dirname(__filename)
|
||||
|
||||
function getConfigPath() {
|
||||
return path.join(__dirname, "..", "config.json")
|
||||
return path.join(__dirname, '..', 'config.json')
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
|
@ -94,26 +94,21 @@ function loadConfig() {
|
|||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const providedConfig = readConfig.sync(configPath)
|
||||
const mergedConfig = deepMerge(
|
||||
JSON.parse(JSON.stringify(defaultConfig)),
|
||||
providedConfig
|
||||
)
|
||||
const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig)
|
||||
|
||||
if (process.env.PORT) {
|
||||
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)
|
||||
debug("Merged and validated configuration")
|
||||
debug('Merged and validated configuration')
|
||||
return validatedConfig
|
||||
}
|
||||
debug("Missing config.json for webssh. Using default config")
|
||||
debug('Missing config.json for webssh. Using default config')
|
||||
return defaultConfig
|
||||
} catch (err) {
|
||||
const error = new ConfigError(
|
||||
`Problem loading config.json for webssh: ${err.message}`
|
||||
)
|
||||
const error = new ConfigError(`Problem loading config.json for webssh: ${err.message}`)
|
||||
handleError(error)
|
||||
return defaultConfig
|
||||
}
|
||||
|
@ -165,8 +160,8 @@ const config = loadConfig()
|
|||
function getCorsConfig() {
|
||||
return {
|
||||
origin: config.http.origins,
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,109 +2,102 @@
|
|||
* Schema for validating the config
|
||||
*/
|
||||
const configSchema = {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
listen: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
ip: { type: "string", format: "ipv4" },
|
||||
port: { type: "integer", minimum: 1, maximum: 65535 }
|
||||
ip: { type: 'string', format: 'ipv4' },
|
||||
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
||||
},
|
||||
required: ["ip", "port"]
|
||||
required: ['ip', 'port'],
|
||||
},
|
||||
http: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
origins: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
}
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
required: ["origins"]
|
||||
},
|
||||
required: ['origins'],
|
||||
},
|
||||
user: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: ["string", "null"] },
|
||||
password: { type: ["string", "null"] },
|
||||
privateKey: { type: ["string", "null"] }
|
||||
name: { type: ['string', 'null'] },
|
||||
password: { type: ['string', 'null'] },
|
||||
privateKey: { type: ['string', 'null'] },
|
||||
},
|
||||
required: ["name", "password"]
|
||||
required: ['name', 'password'],
|
||||
},
|
||||
ssh: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: { type: ["string", "null"] },
|
||||
port: { type: "integer", minimum: 1, maximum: 65535 },
|
||||
term: { type: "string" },
|
||||
readyTimeout: { type: "integer" },
|
||||
keepaliveInterval: { type: "integer" },
|
||||
keepaliveCountMax: { type: "integer" },
|
||||
host: { type: ['string', 'null'] },
|
||||
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
||||
term: { type: 'string' },
|
||||
readyTimeout: { type: 'integer' },
|
||||
keepaliveInterval: { type: 'integer' },
|
||||
keepaliveCountMax: { type: 'integer' },
|
||||
algorithms: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
kex: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
cipher: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
hmac: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
serverHostKey: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
compress: {
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
}
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
required: ["kex", "cipher", "hmac", "serverHostKey", "compress"]
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"host",
|
||||
"port",
|
||||
"term",
|
||||
"readyTimeout",
|
||||
"keepaliveInterval",
|
||||
"keepaliveCountMax"
|
||||
]
|
||||
required: ['kex', 'cipher', 'hmac', 'serverHostKey', 'compress'],
|
||||
},
|
||||
},
|
||||
required: ['host', 'port', 'term', 'readyTimeout', 'keepaliveInterval', 'keepaliveCountMax'],
|
||||
},
|
||||
header: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: ["string", "null"] },
|
||||
background: { type: "string" }
|
||||
text: { type: ['string', 'null'] },
|
||||
background: { type: 'string' },
|
||||
},
|
||||
required: ["text", "background"]
|
||||
required: ['text', 'background'],
|
||||
},
|
||||
options: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
challengeButton: { type: "boolean" },
|
||||
autoLog: { type: "boolean" },
|
||||
allowReauth: { type: "boolean" },
|
||||
allowReconnect: { type: "boolean" },
|
||||
allowReplay: { type: "boolean" }
|
||||
challengeButton: { type: 'boolean' },
|
||||
autoLog: { type: 'boolean' },
|
||||
allowReauth: { type: 'boolean' },
|
||||
allowReconnect: { type: 'boolean' },
|
||||
allowReplay: { type: 'boolean' },
|
||||
},
|
||||
required: ["challengeButton", "allowReauth", "allowReplay"]
|
||||
required: ['challengeButton', 'allowReauth', 'allowReplay'],
|
||||
},
|
||||
session: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
secret: { type: "string" },
|
||||
name: { type: "string" }
|
||||
secret: { 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
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { createNamespacedDebug } from "./logger.js"
|
||||
import { HTTP, MESSAGES, DEFAULTS } from "./constants.js"
|
||||
import { modifyHtml } from "./utils.js"
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { createNamespacedDebug } from './logger.js'
|
||||
import { HTTP, MESSAGES, DEFAULTS } from './constants.js'
|
||||
import { modifyHtml } from './utils.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const debug = createNamespacedDebug("connectionHandler")
|
||||
const debug = createNamespacedDebug('connectionHandler')
|
||||
|
||||
/**
|
||||
* Handle reading the file and processing the response.
|
||||
|
@ -20,12 +20,9 @@ const debug = createNamespacedDebug("connectionHandler")
|
|||
* @param {Object} res - The Express response object.
|
||||
*/
|
||||
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) {
|
||||
return res
|
||||
.status(HTTP.INTERNAL_SERVER_ERROR)
|
||||
.send(MESSAGES.CLIENT_FILE_ERROR)
|
||||
return res.status(HTTP.INTERNAL_SERVER_ERROR).send(MESSAGES.CLIENT_FILE_ERROR)
|
||||
}
|
||||
|
||||
const modifiedHtml = modifyHtml(data, config)
|
||||
|
@ -39,23 +36,23 @@ function handleFileRead(filePath, config, res) {
|
|||
* @param {Object} res - The Express response object.
|
||||
*/
|
||||
function handleConnection(req, res) {
|
||||
debug("Handling connection req.path:", req.path)
|
||||
debug('Handling connection req.path:', req.path)
|
||||
|
||||
const clientPath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"webssh2_client",
|
||||
"client",
|
||||
"public"
|
||||
'..',
|
||||
'node_modules',
|
||||
'webssh2_client',
|
||||
'client',
|
||||
'public'
|
||||
)
|
||||
|
||||
const tempConfig = {
|
||||
socket: {
|
||||
url: `${req.protocol}://${req.get("host")}`,
|
||||
path: "/ssh/socket.io"
|
||||
url: `${req.protocol}://${req.get('host')}`,
|
||||
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)
|
||||
|
|
|
@ -12,15 +12,15 @@ const __dirname = dirname(__filename)
|
|||
* Error messages
|
||||
*/
|
||||
export const MESSAGES = {
|
||||
INVALID_CREDENTIALS: "Invalid credentials format",
|
||||
SSH_CONNECTION_ERROR: "SSH CONNECTION ERROR",
|
||||
SHELL_ERROR: "SHELL ERROR",
|
||||
CONFIG_ERROR: "CONFIG_ERROR",
|
||||
UNEXPECTED_ERROR: "An unexpected error occurred",
|
||||
EXPRESS_APP_CONFIG_ERROR: "Failed to configure Express app",
|
||||
CLIENT_FILE_ERROR: "Error loading client file",
|
||||
FAILED_SESSION_SAVE: "Failed to save session",
|
||||
CONFIG_VALIDATION_ERROR: "Config validation error"
|
||||
INVALID_CREDENTIALS: 'Invalid credentials format',
|
||||
SSH_CONNECTION_ERROR: 'SSH CONNECTION ERROR',
|
||||
SHELL_ERROR: 'SHELL ERROR',
|
||||
CONFIG_ERROR: 'CONFIG_ERROR',
|
||||
UNEXPECTED_ERROR: 'An unexpected error occurred',
|
||||
EXPRESS_APP_CONFIG_ERROR: 'Failed to configure Express app',
|
||||
CLIENT_FILE_ERROR: 'Error loading client file',
|
||||
FAILED_SESSION_SAVE: 'Failed to save session',
|
||||
CONFIG_VALIDATION_ERROR: 'Config validation error',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,20 +29,20 @@ export const MESSAGES = {
|
|||
export const DEFAULTS = {
|
||||
SSH_PORT: 22,
|
||||
LISTEN_PORT: 2222,
|
||||
SSH_TERM: "xterm-color",
|
||||
SSH_TERM: 'xterm-color',
|
||||
IO_PING_TIMEOUT: 60000,
|
||||
IO_PING_INTERVAL: 25000,
|
||||
IO_PATH: "/ssh/socket.io",
|
||||
IO_PATH: '/ssh/socket.io',
|
||||
WEBSSH2_CLIENT_PATH: path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"webssh2_client",
|
||||
"client",
|
||||
"public"
|
||||
'..',
|
||||
'node_modules',
|
||||
'webssh2_client',
|
||||
'client',
|
||||
'public'
|
||||
),
|
||||
CLIENT_FILE: "client.htm",
|
||||
MAX_AUTH_ATTEMPTS: 2
|
||||
CLIENT_FILE: 'client.htm',
|
||||
MAX_AUTH_ATTEMPTS: 2,
|
||||
}
|
||||
/**
|
||||
* HTTP Related
|
||||
|
@ -51,12 +51,12 @@ export const HTTP = {
|
|||
OK: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
AUTHENTICATE: "WWW-Authenticate",
|
||||
AUTHENTICATE: 'WWW-Authenticate',
|
||||
REALM: 'Basic realm="WebSSH2"',
|
||||
AUTH_REQUIRED: "Authentication required.",
|
||||
COOKIE: "basicauth",
|
||||
PATH: "/ssh/host/",
|
||||
SAMESITE: "Strict",
|
||||
SESSION_SID: "webssh2_sid",
|
||||
CREDS_CLEARED: "Credentials cleared."
|
||||
AUTH_REQUIRED: 'Authentication required.',
|
||||
COOKIE: 'basicauth',
|
||||
PATH: '/ssh/host/',
|
||||
SAMESITE: 'Strict',
|
||||
SESSION_SID: 'webssh2_sid',
|
||||
CREDS_CLEARED: 'Credentials cleared.',
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// server
|
||||
// app/crypto-utils.js
|
||||
|
||||
import crypto from "crypto"
|
||||
import crypto from 'crypto'
|
||||
/**
|
||||
* Generates a secure random session secret
|
||||
* @returns {string} A random 32-byte hex string
|
||||
*/
|
||||
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 { HTTP, MESSAGES } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("errors")
|
||||
const debug = createNamespacedDebug('errors')
|
||||
|
||||
/**
|
||||
* Custom error for WebSSH2
|
||||
|
@ -49,17 +49,13 @@ function handleError(err, res) {
|
|||
logError(err.message, err)
|
||||
debug(err.message)
|
||||
if (res) {
|
||||
res
|
||||
.status(HTTP.INTERNAL_SERVER_ERROR)
|
||||
.json({ error: err.message, code: err.code })
|
||||
res.status(HTTP.INTERNAL_SERVER_ERROR).json({ error: err.message, code: err.code })
|
||||
}
|
||||
} else {
|
||||
logError(MESSAGES.UNEXPECTED_ERROR, err)
|
||||
debug(`handleError: ${MESSAGES.UNEXPECTED_ERROR}: %O`, err)
|
||||
if (res) {
|
||||
res
|
||||
.status(HTTP.INTERNAL_SERVER_ERROR)
|
||||
.json({ error: MESSAGES.UNEXPECTED_ERROR })
|
||||
res.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 sharedsession from "express-socket.io-session"
|
||||
import { createNamespacedDebug } from "./logger.js"
|
||||
import { DEFAULTS } from "./constants.js"
|
||||
import socketIo from 'socket.io'
|
||||
import sharedsession from 'express-socket.io-session'
|
||||
import { createNamespacedDebug } from './logger.js'
|
||||
import { DEFAULTS } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("app")
|
||||
const debug = createNamespacedDebug('app')
|
||||
|
||||
/**
|
||||
* Configures Socket.IO with the given server
|
||||
|
@ -18,17 +18,17 @@ export function configureSocketIO(server, sessionMiddleware, config) {
|
|||
path: DEFAULTS.IO_PATH,
|
||||
pingTimeout: DEFAULTS.IO_PING_TIMEOUT,
|
||||
pingInterval: DEFAULTS.IO_PING_INTERVAL,
|
||||
cors: config.getCorsConfig()
|
||||
cors: config.getCorsConfig(),
|
||||
})
|
||||
|
||||
// Share session with io sockets
|
||||
io.use(
|
||||
sharedsession(sessionMiddleware, {
|
||||
autoSave: true
|
||||
autoSave: true,
|
||||
})
|
||||
)
|
||||
|
||||
debug("IO configured")
|
||||
debug('IO configured')
|
||||
|
||||
return io
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
// server
|
||||
// app/middleware.js
|
||||
|
||||
// Scenario 2: Basic Auth
|
||||
// Scenario 2: Basic Auth
|
||||
|
||||
import createDebug from "debug"
|
||||
import session from "express-session"
|
||||
import createDebug from 'debug'
|
||||
import session from 'express-session'
|
||||
import bodyParser from 'body-parser'
|
||||
|
||||
const { urlencoded, json } = bodyParser
|
||||
const debug = createDebug("webssh2:middleware")
|
||||
import basicAuth from "basic-auth"
|
||||
const debug = createDebug('webssh2:middleware')
|
||||
import basicAuth from 'basic-auth'
|
||||
|
||||
import validator from 'validator'
|
||||
|
||||
import { HTTP } from "./constants.js"
|
||||
|
||||
import { HTTP } from './constants.js'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function createAuthMiddleware(config) {
|
||||
// eslint-disable-next-line consistent-return
|
||||
return (req, res, next) => {
|
||||
// Check if username and either password or private key is configured
|
||||
if (config.user.name && (config.user.password || config.user.privateKey)) {
|
||||
req.session.sshCredentials = {
|
||||
username: config.user.name
|
||||
username: config.user.name,
|
||||
}
|
||||
|
||||
// Add credentials based on what's available
|
||||
|
@ -57,7 +55,7 @@ export function createAuthMiddleware(config) {
|
|||
// Scenario 2: Basic Auth
|
||||
|
||||
// If no configured credentials, fall back to Basic Auth
|
||||
debug("auth: Basic Auth")
|
||||
debug('auth: Basic Auth')
|
||||
const credentials = basicAuth(req)
|
||||
if (!credentials) {
|
||||
res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM)
|
||||
|
@ -67,7 +65,7 @@ export function createAuthMiddleware(config) {
|
|||
// Validate and sanitize credentials
|
||||
req.session.sshCredentials = {
|
||||
username: validator.escape(credentials.name),
|
||||
password: credentials.pass
|
||||
password: credentials.pass,
|
||||
}
|
||||
req.session.usedBasicAuth = true
|
||||
next()
|
||||
|
@ -84,7 +82,7 @@ export function createSessionMiddleware(config) {
|
|||
secret: config.session.secret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
name: config.session.name
|
||||
name: config.session.name,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -105,12 +103,12 @@ export function createCookieMiddleware() {
|
|||
if (req.session.sshCredentials) {
|
||||
const cookieData = {
|
||||
host: req.session.sshCredentials.host,
|
||||
port: req.session.sshCredentials.port
|
||||
port: req.session.sshCredentials.port,
|
||||
}
|
||||
res.cookie(HTTP.COOKIE, JSON.stringify(cookieData), {
|
||||
httpOnly: false,
|
||||
path: HTTP.PATH,
|
||||
sameSite: HTTP.SAMESITE
|
||||
sameSite: HTTP.SAMESITE,
|
||||
})
|
||||
}
|
||||
next()
|
||||
|
@ -130,7 +128,7 @@ export function applyMiddleware(app, config) {
|
|||
app.use(createBodyParserMiddleware())
|
||||
app.use(createCookieMiddleware())
|
||||
|
||||
debug("applyMiddleware")
|
||||
debug('applyMiddleware')
|
||||
|
||||
return { sessionMiddleware }
|
||||
}
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
// server
|
||||
// app/routes.js
|
||||
|
||||
import express from "express"
|
||||
import { getValidatedHost, getValidatedPort, maskSensitiveData, validateSshTerm, parseEnvVars } from "./utils.js"
|
||||
import handleConnection from "./connectionHandler.js"
|
||||
import { createNamespacedDebug } from "./logger.js"
|
||||
import { createAuthMiddleware } from "./middleware.js"
|
||||
import { ConfigError, handleError } from "./errors.js"
|
||||
import { HTTP } from "./constants.js"
|
||||
import express from 'express'
|
||||
import {
|
||||
getValidatedHost,
|
||||
getValidatedPort,
|
||||
maskSensitiveData,
|
||||
validateSshTerm,
|
||||
parseEnvVars,
|
||||
} from './utils.js'
|
||||
import handleConnection from './connectionHandler.js'
|
||||
import { createNamespacedDebug } from './logger.js'
|
||||
import { createAuthMiddleware } from './middleware.js'
|
||||
import { ConfigError, handleError } from './errors.js'
|
||||
import { HTTP } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("routes")
|
||||
const debug = createNamespacedDebug('routes')
|
||||
|
||||
export function createRoutes(config) {
|
||||
const router = express.Router()
|
||||
const auth = createAuthMiddleware(config)
|
||||
|
||||
// Scenario 1: No auth required, uses websocket authentication instead
|
||||
router.get("/", (req, res) => {
|
||||
debug("router.get./: Accessed / route")
|
||||
router.get('/', (req, res) => {
|
||||
debug('router.get./: Accessed / route')
|
||||
handleConnection(req, res)
|
||||
})
|
||||
|
||||
|
@ -35,19 +41,17 @@ export function createRoutes(config) {
|
|||
* @param {Object} req - The Express request 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`)
|
||||
const envVars = parseEnvVars(req.query.env)
|
||||
if (envVars) {
|
||||
req.session.envVars = envVars
|
||||
debug("routes: Parsed environment variables: %O", envVars)
|
||||
debug('routes: Parsed environment variables: %O', envVars)
|
||||
}
|
||||
|
||||
try {
|
||||
if (!config.ssh.host) {
|
||||
throw new ConfigError(
|
||||
"Host parameter required when default host not configured"
|
||||
)
|
||||
throw new ConfigError('Host parameter required when default host not configured')
|
||||
}
|
||||
|
||||
const { host } = config.ssh
|
||||
|
@ -65,7 +69,7 @@ export function createRoutes(config) {
|
|||
const sanitizedCredentials = maskSensitiveData(
|
||||
JSON.parse(JSON.stringify(req.session.sshCredentials))
|
||||
)
|
||||
debug("/ssh/host/ Credentials: ", sanitizedCredentials)
|
||||
debug('/ssh/host/ Credentials: ', sanitizedCredentials)
|
||||
|
||||
handleConnection(req, res, { host: host })
|
||||
} catch (err) {
|
||||
|
@ -75,12 +79,12 @@ export function createRoutes(config) {
|
|||
})
|
||||
|
||||
// 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`)
|
||||
const envVars = parseEnvVars(req.query.env)
|
||||
if (envVars) {
|
||||
req.session.envVars = envVars
|
||||
debug("routes: Parsed environment variables: %O", envVars)
|
||||
debug('routes: Parsed environment variables: %O', envVars)
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -102,7 +106,7 @@ export function createRoutes(config) {
|
|||
const sanitizedCredentials = maskSensitiveData(
|
||||
JSON.parse(JSON.stringify(req.session.sshCredentials))
|
||||
)
|
||||
debug("/ssh/host/ Credentials: ", sanitizedCredentials)
|
||||
debug('/ssh/host/ Credentials: ', sanitizedCredentials)
|
||||
|
||||
handleConnection(req, res, { host: host })
|
||||
} catch (err) {
|
||||
|
@ -112,12 +116,12 @@ export function createRoutes(config) {
|
|||
})
|
||||
|
||||
// Clear credentials route
|
||||
router.get("/clear-credentials", (req, res) => {
|
||||
router.get('/clear-credentials', (req, res) => {
|
||||
req.session.sshCredentials = null
|
||||
res.status(HTTP.OK).send(HTTP.CREDENTIALS_CLEARED)
|
||||
})
|
||||
|
||||
router.get("/force-reconnect", (req, res) => {
|
||||
router.get('/force-reconnect', (req, res) => {
|
||||
req.session.sshCredentials = null
|
||||
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 debug = createNamespacedDebug("server")
|
||||
|
@ -16,7 +16,7 @@ export function createServer(app) {
|
|||
* @param {Error} err - The error object
|
||||
*/
|
||||
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) {
|
||||
server.listen(config.listen.port, config.listen.ip, () => {
|
||||
console.log(
|
||||
`startServer: listening on ${config.listen.ip}:${config.listen.port}`
|
||||
)
|
||||
console.log(`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
|
||||
// app/socket.js
|
||||
|
||||
import validator from "validator"
|
||||
import { EventEmitter } from "events"
|
||||
import SSHConnection from "./ssh.js"
|
||||
import { createNamespacedDebug } from "./logger.js"
|
||||
import { SSHConnectionError, handleError } from "./errors.js"
|
||||
import validator from 'validator'
|
||||
import { EventEmitter } from 'events'
|
||||
import SSHConnection from './ssh.js'
|
||||
import { createNamespacedDebug } from './logger.js'
|
||||
import { SSHConnectionError, handleError } from './errors.js'
|
||||
|
||||
const debug = createNamespacedDebug("socket")
|
||||
import {
|
||||
isValidCredentials,
|
||||
maskSensitiveData,
|
||||
validateSshTerm
|
||||
} from "./utils.js"
|
||||
import { MESSAGES } from "./constants.js"
|
||||
const debug = createNamespacedDebug('socket')
|
||||
import { isValidCredentials, maskSensitiveData, validateSshTerm } from './utils.js'
|
||||
import { MESSAGES } from './constants.js'
|
||||
|
||||
class WebSSH2Socket extends EventEmitter {
|
||||
constructor(socket, config) {
|
||||
|
@ -31,7 +27,7 @@ class WebSSH2Socket extends EventEmitter {
|
|||
port: null,
|
||||
term: null,
|
||||
cols: null,
|
||||
rows: null
|
||||
rows: null,
|
||||
}
|
||||
|
||||
this.initializeSocketEvents()
|
||||
|
@ -54,25 +50,25 @@ class WebSSH2Socket extends EventEmitter {
|
|||
// Check if interactive auth is disabled
|
||||
if (this.config.ssh.disableInteractiveAuth) {
|
||||
debug(`handleConnection: ${this.socket.id}, interactive auth disabled`)
|
||||
this.handleError("Interactive Auth Disabled")
|
||||
this.handleError('Interactive Auth Disabled')
|
||||
return
|
||||
}
|
||||
|
||||
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.socket.on("authenticate", creds => {
|
||||
this.socket.on('authenticate', (creds) => {
|
||||
this.handleAuthenticate(creds)
|
||||
})
|
||||
this.socket.on("terminal", data => {
|
||||
this.socket.on('terminal', (data) => {
|
||||
this.handleTerminal(data)
|
||||
})
|
||||
this.socket.on("disconnect", reason => {
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.handleConnectionClose(reason)
|
||||
})
|
||||
}
|
||||
|
@ -83,27 +79,24 @@ class WebSSH2Socket extends EventEmitter {
|
|||
|
||||
// Send the keyboard-interactive request to the client
|
||||
this.socket.emit(
|
||||
"authentication",
|
||||
'authentication',
|
||||
Object.assign(
|
||||
{
|
||||
action: "keyboard-interactive"
|
||||
action: 'keyboard-interactive',
|
||||
},
|
||||
data
|
||||
)
|
||||
)
|
||||
|
||||
// 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, {
|
||||
properties: ["responses"]
|
||||
properties: ['responses'],
|
||||
})
|
||||
debug(
|
||||
"handleKeyboardInteractive: Client response masked %O",
|
||||
maskedclientResponse
|
||||
)
|
||||
if (clientResponse.action === "keyboard-interactive") {
|
||||
debug('handleKeyboardInteractive: Client response masked %O', maskedclientResponse)
|
||||
if (clientResponse.action === 'keyboard-interactive') {
|
||||
// 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)) {
|
||||
// Set term if provided, otherwise use config default
|
||||
this.sessionState.term = validateSshTerm(creds.term)
|
||||
? creds.term
|
||||
: this.config.ssh.term
|
||||
this.sessionState.term = validateSshTerm(creds.term) ? creds.term : this.config.ssh.term
|
||||
|
||||
this.initializeConnection(creds)
|
||||
} else {
|
||||
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
|
||||
this.socket.emit("authentication", {
|
||||
this.socket.emit('authentication', {
|
||||
success: false,
|
||||
message: "Invalid credentials format"
|
||||
message: 'Invalid credentials format',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -148,40 +139,38 @@ class WebSSH2Socket extends EventEmitter {
|
|||
privateKey: creds.privateKey,
|
||||
passphrase: creds.passphrase,
|
||||
host: creds.host,
|
||||
port: creds.port
|
||||
port: creds.port,
|
||||
})
|
||||
|
||||
const authResult = { action: "auth_result", success: true }
|
||||
this.socket.emit("authentication", authResult)
|
||||
const authResult = { action: 'auth_result', success: true }
|
||||
this.socket.emit('authentication', authResult)
|
||||
|
||||
const permissions = {
|
||||
autoLog: this.config.options.autoLog || false,
|
||||
allowReplay: this.config.options.allowReplay || 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) {
|
||||
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) => {
|
||||
debug(
|
||||
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
||||
)
|
||||
const errorMessage =
|
||||
err instanceof SSHConnectionError
|
||||
? err.message
|
||||
: "SSH connection failed"
|
||||
this.socket.emit("authentication", {
|
||||
action: "auth_result",
|
||||
err instanceof SSHConnectionError ? err.message : 'SSH connection failed'
|
||||
this.socket.emit('authentication', {
|
||||
action: 'auth_result',
|
||||
success: false,
|
||||
message: errorMessage
|
||||
message: errorMessage,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -192,11 +181,15 @@ class WebSSH2Socket extends EventEmitter {
|
|||
*/
|
||||
handleTerminal(data) {
|
||||
const { term, rows, cols } = data
|
||||
if (term && validateSshTerm(term)) this.sessionState.term = term
|
||||
if (rows && validator.isInt(rows.toString()))
|
||||
if (term && validateSshTerm(term)) {
|
||||
this.sessionState.term = term
|
||||
}
|
||||
if (rows && validator.isInt(rows.toString())) {
|
||||
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.createShell()
|
||||
}
|
||||
|
@ -213,47 +206,49 @@ class WebSSH2Socket extends EventEmitter {
|
|||
{
|
||||
term: this.sessionState.term,
|
||||
cols: this.sessionState.cols,
|
||||
rows: this.sessionState.rows
|
||||
rows: this.sessionState.rows,
|
||||
},
|
||||
envVars
|
||||
)
|
||||
.then((stream) => {
|
||||
stream.on("data", (data) => {
|
||||
this.socket.emit("data", data.toString("utf-8"))
|
||||
stream.on('data', (data) => {
|
||||
this.socket.emit('data', data.toString('utf-8'))
|
||||
})
|
||||
// stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec
|
||||
stream.on("close", (code, signal) => {
|
||||
debug("close: SSH Stream closed")
|
||||
stream.on('close', (code, signal) => {
|
||||
debug('close: SSH Stream closed')
|
||||
this.handleConnectionClose(code, signal)
|
||||
})
|
||||
|
||||
stream.on("end", () => {
|
||||
debug("end: SSH Stream ended")
|
||||
stream.on('end', () => {
|
||||
debug('end: SSH Stream ended')
|
||||
})
|
||||
|
||||
stream.on("error", err => {
|
||||
debug("error: SSH Stream error %O", err)
|
||||
stream.on('error', (err) => {
|
||||
debug('error: SSH Stream error %O', err)
|
||||
})
|
||||
|
||||
this.socket.on("data", data => {
|
||||
this.socket.on('data', (data) => {
|
||||
stream.write(data)
|
||||
})
|
||||
this.socket.on("control", controlData => {
|
||||
this.socket.on('control', (controlData) => {
|
||||
this.handleControl(controlData)
|
||||
})
|
||||
this.socket.on("resize", data => {
|
||||
this.socket.on('resize', (data) => {
|
||||
this.handleResize(data)
|
||||
})
|
||||
})
|
||||
.catch(err => this.handleError("createShell: ERROR", err))
|
||||
.catch((err) => this.handleError('createShell: ERROR', err))
|
||||
}
|
||||
|
||||
handleResize(data) {
|
||||
const { rows, cols } = data
|
||||
if (rows && validator.isInt(rows.toString()))
|
||||
if (rows && validator.isInt(rows.toString())) {
|
||||
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.ssh.resizeTerminal(this.sessionState.rows, this.sessionState.cols)
|
||||
}
|
||||
|
||||
|
@ -262,19 +257,14 @@ class WebSSH2Socket extends EventEmitter {
|
|||
* @param {string} controlData - The control command received.
|
||||
*/
|
||||
handleControl(controlData) {
|
||||
if (
|
||||
validator.isIn(controlData, ["replayCredentials", "reauth"]) &&
|
||||
this.ssh.stream
|
||||
) {
|
||||
if (controlData === "replayCredentials") {
|
||||
if (validator.isIn(controlData, ['replayCredentials', 'reauth']) && this.ssh.stream) {
|
||||
if (controlData === 'replayCredentials') {
|
||||
this.replayCredentials()
|
||||
} else if (controlData === "reauth") {
|
||||
} else if (controlData === 'reauth') {
|
||||
this.handleReauth()
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`handleControl: Invalid control command received: ${controlData}`
|
||||
)
|
||||
console.warn(`handleControl: Invalid control command received: ${controlData}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,7 +283,7 @@ class WebSSH2Socket extends EventEmitter {
|
|||
handleReauth() {
|
||||
if (this.config.options.allowReauth) {
|
||||
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.
|
||||
*/
|
||||
handleError(context, err) {
|
||||
const errorMessage = err ? `: ${err.message}` : ""
|
||||
const errorMessage = err ? `: ${err.message}` : ''
|
||||
handleError(new SSHConnectionError(`SSH ${context}${errorMessage}`))
|
||||
this.socket.emit("ssherror", `SSH ${context}${errorMessage}`)
|
||||
this.socket.emit('ssherror', `SSH ${context}${errorMessage}`)
|
||||
this.handleConnectionClose()
|
||||
}
|
||||
|
||||
|
@ -315,7 +305,7 @@ class WebSSH2Socket extends EventEmitter {
|
|||
* @param {any} value - The new value for the element.
|
||||
*/
|
||||
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) {
|
||||
this.ssh.end()
|
||||
debug(
|
||||
`handleConnectionClose: ${this.socket.id}, Code: ${code}, Signal: ${signal}`
|
||||
)
|
||||
debug(`handleConnectionClose: ${this.socket.id}, Code: ${code}, Signal: ${signal}`)
|
||||
this.socket.disconnect(true)
|
||||
}
|
||||
|
||||
|
@ -343,16 +331,17 @@ class WebSSH2Socket extends EventEmitter {
|
|||
this.sessionState.username = null
|
||||
this.sessionState.password = null
|
||||
|
||||
this.socket.handshake.session.save(err => {
|
||||
if (err)
|
||||
this.socket.handshake.session.save((err) => {
|
||||
if (err) {
|
||||
console.error(
|
||||
`clearSessionCredentials: ${MESSAGES.FAILED_SESSION_SAVE} ${this.socket.id}:`,
|
||||
err
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function(io, config) {
|
||||
io.on("connection", socket => new WebSSH2Socket(socket, config))
|
||||
export default function (io, config) {
|
||||
io.on('connection', (socket) => new WebSSH2Socket(socket, config))
|
||||
}
|
||||
|
|
67
app/ssh.js
67
app/ssh.js
|
@ -1,14 +1,14 @@
|
|||
// server
|
||||
// app/ssh.js
|
||||
|
||||
import { Client as SSH } from "ssh2"
|
||||
import { EventEmitter } from "events"
|
||||
import { createNamespacedDebug } from "./logger.js"
|
||||
import { SSHConnectionError, handleError } from "./errors.js"
|
||||
import { maskSensitiveData } from "./utils.js"
|
||||
import { DEFAULTS } from "./constants.js"
|
||||
import { Client as SSH } from 'ssh2'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createNamespacedDebug } from './logger.js'
|
||||
import { SSHConnectionError, handleError } from './errors.js'
|
||||
import { maskSensitiveData } from './utils.js'
|
||||
import { DEFAULTS } from './constants.js'
|
||||
|
||||
const debug = createNamespacedDebug("ssh")
|
||||
const debug = createNamespacedDebug('ssh')
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
connect(creds) {
|
||||
debug("connect: %O", maskSensitiveData(creds))
|
||||
debug('connect: %O', maskSensitiveData(creds))
|
||||
this.creds = creds
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.conn) {
|
||||
|
@ -53,7 +53,7 @@ class SSHConnection extends EventEmitter {
|
|||
|
||||
// First try with key authentication if available
|
||||
const sshConfig = this.getSSHConfig(creds, true)
|
||||
debug("Initial connection config: %O", maskSensitiveData(sshConfig))
|
||||
debug('Initial connection config: %O', maskSensitiveData(sshConfig))
|
||||
|
||||
this.setupConnectionHandlers(resolve, reject)
|
||||
|
||||
|
@ -71,24 +71,22 @@ class SSHConnection extends EventEmitter {
|
|||
* @param {Function} reject - Promise reject function
|
||||
*/
|
||||
setupConnectionHandlers(resolve, reject) {
|
||||
this.conn.on("ready", () => {
|
||||
this.conn.on('ready', () => {
|
||||
debug(`connect: ready: ${this.creds.host}`)
|
||||
resolve(this.conn)
|
||||
})
|
||||
|
||||
this.conn.on("error", (err) => {
|
||||
this.conn.on('error', (err) => {
|
||||
debug(`connect: error: ${err.message}`)
|
||||
|
||||
// Check if this is an authentication error and we haven't exceeded max attempts
|
||||
if (this.authAttempts < DEFAULTS.MAX_AUTH_ATTEMPTS) {
|
||||
this.authAttempts += 1
|
||||
debug(
|
||||
`Authentication attempt ${this.authAttempts} failed, trying password authentication`
|
||||
)
|
||||
debug(`Authentication attempt ${this.authAttempts} failed, trying password authentication`)
|
||||
|
||||
// Only try password auth if we have a password
|
||||
if (this.creds.password) {
|
||||
debug("Retrying with password authentication")
|
||||
debug('Retrying with password authentication')
|
||||
|
||||
// Disconnect current connection
|
||||
if (this.conn) {
|
||||
|
@ -102,14 +100,14 @@ class SSHConnection extends EventEmitter {
|
|||
this.setupConnectionHandlers(resolve, reject)
|
||||
this.conn.connect(passwordConfig)
|
||||
} else {
|
||||
debug("No password available, requesting password from client")
|
||||
this.emit("password-prompt", {
|
||||
debug('No password available, requesting password from client')
|
||||
this.emit('password-prompt', {
|
||||
host: this.creds.host,
|
||||
username: this.creds.username
|
||||
username: this.creds.username,
|
||||
})
|
||||
|
||||
// Listen for password response one time
|
||||
this.once("password-response", (password) => {
|
||||
this.once('password-response', (password) => {
|
||||
this.creds.password = password
|
||||
const newConfig = this.getSSHConfig(this.creds, false)
|
||||
this.setupConnectionHandlers(resolve, reject)
|
||||
|
@ -118,26 +116,15 @@ class SSHConnection extends EventEmitter {
|
|||
}
|
||||
} else {
|
||||
// We've exhausted all authentication attempts
|
||||
const error = new SSHConnectionError(
|
||||
"All authentication methods failed"
|
||||
)
|
||||
const error = new SSHConnectionError('All authentication methods failed')
|
||||
handleError(error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
this.conn.on(
|
||||
"keyboard-interactive",
|
||||
(name, instructions, lang, prompts, finish) => {
|
||||
this.handleKeyboardInteractive(
|
||||
name,
|
||||
instructions,
|
||||
lang,
|
||||
prompts,
|
||||
finish
|
||||
)
|
||||
}
|
||||
)
|
||||
this.conn.on('keyboard-interactive', (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,
|
||||
keepaliveInterval: this.config.ssh.keepaliveInterval,
|
||||
keepaliveCountMax: this.config.ssh.keepaliveCountMax,
|
||||
debug: createNamespacedDebug("ssh2")
|
||||
debug: createNamespacedDebug('ssh2'),
|
||||
}
|
||||
|
||||
// Try private key first if available and useKey is true
|
||||
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
|
||||
if (!this.validatePrivateKey(privateKey)) {
|
||||
throw new SSHConnectionError("Invalid private key format")
|
||||
throw new SSHConnectionError('Invalid private key format')
|
||||
}
|
||||
config.privateKey = privateKey
|
||||
} else if (creds.password) {
|
||||
debug("Using password authentication")
|
||||
debug('Using password authentication')
|
||||
config.password = creds.password
|
||||
}
|
||||
|
||||
|
@ -183,7 +170,7 @@ class SSHConnection extends EventEmitter {
|
|||
*/
|
||||
shell(options, envVars) {
|
||||
const shellOptions = Object.assign({}, options, {
|
||||
env: this.getEnvironment(envVars)
|
||||
env: this.getEnvironment(envVars),
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -230,7 +217,7 @@ class SSHConnection extends EventEmitter {
|
|||
*/
|
||||
getEnvironment(envVars) {
|
||||
const env = {
|
||||
TERM: this.config.ssh.term
|
||||
TERM: this.config.ssh.term,
|
||||
}
|
||||
|
||||
if (envVars) {
|
||||
|
|
58
app/utils.js
58
app/utils.js
|
@ -7,7 +7,7 @@ import { createNamespacedDebug } from './logger.js'
|
|||
import { DEFAULTS, MESSAGES } from './constants.js'
|
||||
import configSchema from './configSchema.js'
|
||||
|
||||
const debug = createNamespacedDebug("utils")
|
||||
const debug = createNamespacedDebug('utils')
|
||||
|
||||
/**
|
||||
* Deep merges two objects
|
||||
|
@ -17,13 +17,9 @@ const debug = createNamespacedDebug("utils")
|
|||
*/
|
||||
export function deepMerge(target, source) {
|
||||
const output = Object.assign({}, target)
|
||||
Object.keys(source).forEach(key => {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (Object.hasOwnProperty.call(source, key)) {
|
||||
if (
|
||||
source[key] instanceof Object &&
|
||||
!Array.isArray(source[key]) &&
|
||||
source[key] !== null
|
||||
) {
|
||||
if (source[key] instanceof Object && !Array.isArray(source[key]) && source[key] !== null) {
|
||||
output[key] = deepMerge(output[key] || {}, source[key])
|
||||
} else {
|
||||
output[key] = source[key]
|
||||
|
@ -64,17 +60,14 @@ export function getValidatedHost(host) {
|
|||
export function getValidatedPort(portInput) {
|
||||
const defaultPort = DEFAULTS.SSH_PORT
|
||||
const port = defaultPort
|
||||
debug("getValidatedPort: input: %O", portInput)
|
||||
debug('getValidatedPort: input: %O', portInput)
|
||||
|
||||
if (portInput) {
|
||||
if (validator.isInt(portInput, { min: 1, max: 65535 })) {
|
||||
return parseInt(portInput, 10)
|
||||
}
|
||||
}
|
||||
debug(
|
||||
"getValidatedPort: port not specified or is invalid, setting port to: %O",
|
||||
port
|
||||
)
|
||||
debug('getValidatedPort: port not specified or is invalid, setting port to: %O', port)
|
||||
|
||||
return port
|
||||
}
|
||||
|
@ -95,9 +88,9 @@ export function getValidatedPort(portInput) {
|
|||
export function isValidCredentials(creds) {
|
||||
const hasRequiredFields = !!(
|
||||
creds &&
|
||||
typeof creds.username === "string" &&
|
||||
typeof creds.host === "string" &&
|
||||
typeof creds.port === "number"
|
||||
typeof creds.username === 'string' &&
|
||||
typeof creds.host === 'string' &&
|
||||
typeof creds.port === 'number'
|
||||
)
|
||||
|
||||
if (!hasRequiredFields) {
|
||||
|
@ -105,9 +98,8 @@ export function isValidCredentials(creds) {
|
|||
}
|
||||
|
||||
// Must have either password or privateKey/privateKey
|
||||
const hasPassword = typeof creds.password === "string"
|
||||
const hasPrivateKey =
|
||||
typeof creds.privateKey === "string" || typeof creds.privateKey === "string"
|
||||
const hasPassword = typeof creds.password === 'string'
|
||||
const hasPrivateKey = typeof creds.privateKey === 'string' || typeof creds.privateKey === 'string'
|
||||
|
||||
return hasPassword || hasPrivateKey
|
||||
}
|
||||
|
@ -128,8 +120,7 @@ export function validateSshTerm(term) {
|
|||
}
|
||||
|
||||
const validatedSshTerm =
|
||||
validator.isLength(term, { min: 1, max: 30 }) &&
|
||||
validator.matches(term, /^[a-zA-Z0-9.-]+$/)
|
||||
validator.isLength(term, { min: 1, max: 30 }) && validator.matches(term, /^[a-zA-Z0-9.-]+$/)
|
||||
|
||||
return validatedSshTerm ? term : null
|
||||
}
|
||||
|
@ -146,9 +137,7 @@ export function validateConfig(config) {
|
|||
const validate = ajv.compile(configSchema)
|
||||
const valid = validate(config)
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}`
|
||||
)
|
||||
throw new Error(`${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}`)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
@ -160,14 +149,11 @@ export function validateConfig(config) {
|
|||
* @returns {string} - The modified HTML content.
|
||||
*/
|
||||
export function modifyHtml(html, config) {
|
||||
debug("modifyHtml")
|
||||
const modifiedHtml = html.replace(
|
||||
/(src|href)="(?!http|\/\/)/g,
|
||||
'$1="/ssh/assets/'
|
||||
)
|
||||
debug('modifyHtml')
|
||||
const modifiedHtml = html.replace(/(src|href)="(?!http|\/\/)/g, '$1="/ssh/assets/')
|
||||
|
||||
return modifiedHtml.replace(
|
||||
"window.webssh2Config = null;",
|
||||
'window.webssh2Config = null;',
|
||||
`window.webssh2Config = ${JSON.stringify(config)};`
|
||||
)
|
||||
}
|
||||
|
@ -186,7 +172,7 @@ export function modifyHtml(html, config) {
|
|||
*/
|
||||
export function maskSensitiveData(obj, options) {
|
||||
const defaultOptions = {}
|
||||
debug("maskSensitiveData")
|
||||
debug('maskSensitiveData')
|
||||
|
||||
const maskingOptions = Object.assign({}, defaultOptions, options || {})
|
||||
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
|
||||
*/
|
||||
export function parseEnvVars(envString) {
|
||||
if (!envString) return null
|
||||
if (!envString) {
|
||||
return null
|
||||
}
|
||||
|
||||
const envVars = {}
|
||||
const pairs = envString.split(",")
|
||||
const pairs = envString.split(',')
|
||||
|
||||
for (let i = 0; i < pairs.length; i += 1) {
|
||||
const pair = pairs[i].split(":")
|
||||
if (pair.length !== 2) continue
|
||||
const pair = pairs[i].split(':')
|
||||
if (pair.length !== 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = pair[0].trim()
|
||||
const value = pair[1].trim()
|
||||
|
|
Loading…
Reference in a new issue