chore: formatting

This commit is contained in:
Bill Church 2024-08-13 12:38:00 +00:00
parent 40715023b2
commit aa633aef0b
No known key found for this signature in database
6 changed files with 374 additions and 298 deletions

View file

@ -1,50 +1,57 @@
// server // server
// app/app.js // app/app.js
'use strict' "use strict"
const createDebug = require('debug') const createDebug = require("debug")
const debug = createDebug('webssh2') const debug = createDebug("webssh2")
const http = require('http') const http = require("http")
const express = require('express') const express = require("express")
const socketIo = require('socket.io') const socketIo = require("socket.io")
const path = require('path') const path = require("path")
const bodyParser = require('body-parser') const bodyParser = require("body-parser")
const session = require('express-session') const session = require("express-session")
const sharedsession = require("express-socket.io-session") const sharedsession = require("express-socket.io-session")
const config = require('./config') const config = require("./config")
const socketHandler = require('./socket') const socketHandler = require("./socket")
const sshRoutes = require('./routes') const sshRoutes = require("./routes")
/** /**
* Creates and configures the Express application * Creates and configures the Express application
* @returns {express.Application} The Express application instance * @returns {express.Application} The Express application instance
*/ */
function createApp() { function createApp() {
const app = express(); const app = express()
// Resolve the correct path to the webssh2_client module // Resolve the correct path to the webssh2_client module
const clientPath = path.resolve(__dirname, '..', 'node_modules', 'webssh2_client', 'client', 'public'); const clientPath = path.resolve(
__dirname,
"..",
"node_modules",
"webssh2_client",
"client",
"public"
)
// Set up session middleware // Set up session middleware
const sessionMiddleware = session({ const sessionMiddleware = session({
secret: config.session.secret || 'webssh2_secret', secret: config.session.secret || "webssh2_secret",
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
name: config.session.name || 'webssh2.sid' name: config.session.name || "webssh2.sid"
}); })
app.use(sessionMiddleware); app.use(sessionMiddleware)
// Handle POST and GET parameters // Handle POST and GET parameters
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json()); app.use(bodyParser.json())
// 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, sessionMiddleware }; return { app, sessionMiddleware }
} }
/** /**
@ -67,18 +74,20 @@ function createServer(app) {
function configureSocketIO(server, sessionMiddleware) { function configureSocketIO(server, sessionMiddleware) {
const io = socketIo(server, { const io = socketIo(server, {
serveClient: false, serveClient: false,
path: '/ssh/socket.io', path: "/ssh/socket.io",
pingTimeout: 60000, // 1 minute pingTimeout: 60000, // 1 minute
pingInterval: 25000, // 25 seconds pingInterval: 25000, // 25 seconds
cors: getCorsConfig() cors: getCorsConfig()
}); })
// Share session with io sockets // Share session with io sockets
io.use(sharedsession(sessionMiddleware, { io.use(
sharedsession(sessionMiddleware, {
autoSave: true autoSave: true
})); })
)
return io; return io
} }
/** /**
@ -87,8 +96,8 @@ function configureSocketIO(server, sessionMiddleware) {
*/ */
function getCorsConfig() { function getCorsConfig() {
return { return {
origin: config.origin || ['*.*'], origin: config.origin || ["*.*"],
methods: ['GET', 'POST'], methods: ["GET", "POST"],
credentials: true credentials: true
} }
} }
@ -115,10 +124,12 @@ function startServer() {
// Start the server // Start the server
server.listen(config.listen.port, config.listen.ip, () => { server.listen(config.listen.port, config.listen.ip, () => {
console.log(`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`) console.log(
`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`
)
}) })
server.on('error', handleServerError) server.on("error", handleServerError)
return { server, io, app } return { server, io, app }
} }
@ -128,7 +139,7 @@ function startServer() {
* @param {Error} err - The error object * @param {Error} err - The error object
*/ */
function handleServerError(err) { function handleServerError(err) {
console.error('WebSSH2 server.listen ERROR:', err.code) console.error("WebSSH2 server.listen ERROR:", err.code)
} }
// Don't start the server immediately, export the function instead // Don't start the server immediately, export the function instead

View file

@ -1,12 +1,12 @@
// server // server
// app/config.js // app/config.js
'use strict' "use strict"
const path = require('path') const path = require("path")
const fs = require('fs') const fs = require("fs")
const readConfig = require('read-config-ng') const readConfig = require("read-config-ng")
const Ajv = require('ajv') const Ajv = require("ajv")
const crypto = require('crypto') const crypto = require("crypto")
/** /**
* @typedef {Object} Config * @typedef {Object} Config
@ -54,11 +54,11 @@ const crypto = require('crypto')
*/ */
const defaultConfig = { const defaultConfig = {
listen: { listen: {
ip: '0.0.0.0', ip: "0.0.0.0",
port: 2222 port: 2222
}, },
http: { http: {
origins: ['*:*'] origins: ["*:*"]
}, },
user: { user: {
name: null, name: null,
@ -67,7 +67,7 @@ const defaultConfig = {
ssh: { ssh: {
host: null, host: null,
port: 22, port: 22,
term: 'xterm-color', term: "xterm-color",
readyTimeout: 20000, readyTimeout: 20000,
keepaliveInterval: 120000, keepaliveInterval: 120000,
keepaliveCountMax: 10 keepaliveCountMax: 10
@ -76,11 +76,11 @@ const defaultConfig = {
cursorBlink: true, cursorBlink: true,
scrollback: 10000, scrollback: 10000,
tabStopWidth: 8, tabStopWidth: 8,
bellStyle: 'sound' bellStyle: "sound"
}, },
header: { header: {
text: null, text: null,
background: 'green' background: "green"
}, },
options: { options: {
challengeButton: true, challengeButton: true,
@ -89,28 +89,28 @@ const defaultConfig = {
}, },
algorithms: { algorithms: {
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"
], ],
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"
], ],
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1"],
compress: ['none', 'zlib@openssh.com', 'zlib'] compress: ["none", "zlib@openssh.com", "zlib"]
}, },
session: { session: {
secret: generateSecureSecret(), secret: generateSecureSecret(),
name: 'webssh2.sid' name: "webssh2.sid"
}, },
serverlog: { serverlog: {
client: false, client: false,
@ -124,115 +124,134 @@ const defaultConfig = {
* 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"] }
}, },
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" }
}, },
required: ['host', 'port', 'term', 'readyTimeout', 'keepaliveInterval', 'keepaliveCountMax'] required: [
"host",
"port",
"term",
"readyTimeout",
"keepaliveInterval",
"keepaliveCountMax"
]
}, },
terminal: { terminal: {
type: 'object', type: "object",
properties: { properties: {
cursorBlink: { type: 'boolean' }, cursorBlink: { type: "boolean" },
scrollback: { type: 'integer' }, scrollback: { type: "integer" },
tabStopWidth: { type: 'integer' }, tabStopWidth: { type: "integer" },
bellStyle: { type: 'string' } bellStyle: { type: "string" }
}, },
required: ['cursorBlink', 'scrollback', 'tabStopWidth', 'bellStyle'] required: ["cursorBlink", "scrollback", "tabStopWidth", "bellStyle"]
}, },
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" },
allowReauth: { type: 'boolean' }, allowReauth: { type: "boolean" },
allowReplay: { type: 'boolean' } allowReplay: { type: "boolean" }
}, },
required: ['challengeButton', 'allowReauth', 'allowReplay'] required: ["challengeButton", "allowReauth", "allowReplay"]
}, },
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" }
}, },
compress: { compress: {
type: 'array', type: "array",
items: { type: 'string' } items: { type: "string" }
} }
}, },
required: ['kex', 'cipher', 'hmac', 'compress'] required: ["kex", "cipher", "hmac", "compress"]
}, },
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"]
}, },
serverlog: { serverlog: {
type: 'object', type: "object",
properties: { properties: {
client: { type: 'boolean' }, client: { type: "boolean" },
server: { type: 'boolean' } server: { type: "boolean" }
}, },
required: ['client', 'server'] required: ["client", "server"]
}, },
accesslog: { type: 'boolean' }, accesslog: { type: "boolean" },
verify: { type: 'boolean' } verify: { type: "boolean" }
}, },
required: ['listen', 'http', 'user', 'ssh', 'terminal', 'header', 'options', 'algorithms', 'serverlog', 'accesslog', 'verify'] required: [
"listen",
"http",
"user",
"ssh",
"terminal",
"header",
"options",
"algorithms",
"serverlog",
"accesslog",
"verify"
]
} }
/** /**
@ -241,7 +260,7 @@ const configSchema = {
*/ */
function getConfigPath() { function getConfigPath() {
const nodeRoot = path.dirname(require.main.filename) const nodeRoot = path.dirname(require.main.filename)
return path.join(nodeRoot, 'config.json') return path.join(nodeRoot, "config.json")
} }
/** /**
@ -250,7 +269,7 @@ function getConfigPath() {
* @returns {Config} The configuration object * @returns {Config} The configuration object
*/ */
function readConfigFile(configPath) { function readConfigFile(configPath) {
console.log('WebSSH2 service reading config from: ' + configPath) console.log("WebSSH2 service reading config from: " + configPath)
return readConfig(configPath) return readConfig(configPath)
} }
@ -264,9 +283,11 @@ function validateConfig(config) {
const ajv = new Ajv() const ajv = new Ajv()
const validate = ajv.compile(configSchema) const validate = ajv.compile(configSchema)
const valid = validate(config) const valid = validate(config)
console.log('WebSSH2 service validating config') console.log("WebSSH2 service validating config")
if (!valid) { if (!valid) {
throw new Error('Config validation error: ' + ajv.errorsText(validate.errors)) throw new Error(
"Config validation error: " + ajv.errorsText(validate.errors)
)
} }
return config return config
} }
@ -279,7 +300,7 @@ function validateConfig(config) {
function logError(message, error) { function logError(message, error) {
console.error(message) console.error(message)
if (error) { if (error) {
console.error('ERROR:\n\n ' + error) console.error("ERROR:\n\n " + error)
} }
} }
@ -295,24 +316,27 @@ function loadConfig() {
const providedConfig = readConfigFile(configPath) const providedConfig = readConfigFile(configPath)
// Deep merge the provided config with the default config // Deep merge the provided config with the default config
const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig) const mergedConfig = deepMerge(
JSON.parse(JSON.stringify(defaultConfig)),
providedConfig
)
const validatedConfig = validateConfig(mergedConfig) const validatedConfig = validateConfig(mergedConfig)
console.log('Merged and validated configuration') console.log("Merged and validated configuration")
return validatedConfig return validatedConfig
} else { } else {
logError( logError(
'\n\nERROR: Missing config.json for webssh. Using default config: ' + "\n\nERROR: Missing config.json for webssh. Using default config: " +
JSON.stringify(defaultConfig) + JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n' "\n\n See config.json.sample for details\n\n"
) )
return defaultConfig return defaultConfig
} }
} catch (err) { } catch (err) {
logError( logError(
'\n\nERROR: Problem loading config.json for webssh. Using default config: ' + "\n\nERROR: Problem loading config.json for webssh. Using default config: " +
JSON.stringify(defaultConfig) + JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n', "\n\n See config.json.sample for details\n\n",
err err
) )
return defaultConfig return defaultConfig
@ -324,7 +348,7 @@ function loadConfig() {
* @returns {string} A random 32-byte hex string * @returns {string} A random 32-byte hex string
*/ */
function generateSecureSecret() { function generateSecureSecret() {
return crypto.randomBytes(32).toString('hex') return crypto.randomBytes(32).toString("hex")
} }
/** /**

View file

@ -1,48 +1,65 @@
// server // server
// app/connectionHandler.js // app/connectionHandler.js
var path = require('path'); var path = require("path")
var fs = require('fs'); var fs = require("fs")
var extend = require('util')._extend; var extend = require("util")._extend
function handleConnection(req, res, urlParams) { function handleConnection(req, res, urlParams) {
urlParams = urlParams || {}; urlParams = urlParams || {}
// The path to the client directory of the webssh2 module. // The path to the client directory of the webssh2 module.
var clientPath = path.resolve(__dirname, '..', 'node_modules', 'webssh2_client', 'client', 'public'); var clientPath = path.resolve(
__dirname,
"..",
"node_modules",
"webssh2_client",
"client",
"public"
)
// Combine URL parameters, query parameters, and form data // Combine URL parameters, query parameters, and form data
var connectionParams = extend({}, urlParams); var connectionParams = extend({}, urlParams)
extend(connectionParams, req.query); extend(connectionParams, req.query)
extend(connectionParams, req.body || {}); extend(connectionParams, req.body || {})
// Inject configuration // Inject configuration
var config = { var config = {
socket: { socket: {
url: req.protocol + '://' + req.get('host'), url: req.protocol + "://" + req.get("host"),
path: '/ssh/socket.io' path: "/ssh/socket.io"
}, },
ssh: { ssh: {
host: urlParams.host || '', host: urlParams.host || "",
port: urlParams.port || 22 port: urlParams.port || 22
}, },
autoConnect: !!req.session.sshCredentials autoConnect: !!req.session.sshCredentials
}; }
// Read the client.htm file // Read the client.htm file
fs.readFile(path.join(clientPath, 'client.htm'), 'utf8', function(err, data) { fs.readFile(
path.join(clientPath, "client.htm"),
"utf8",
function (err, data) {
if (err) { if (err) {
return res.status(500).send('Error loading client file'); return res.status(500).send("Error loading client file")
} }
// Replace relative paths with the correct path // Replace relative paths with the correct path
var modifiedHtml = data.replace(/(src|href)="(?!http|\/\/)/g, '$1="/ssh/assets/'); var modifiedHtml = data.replace(
/(src|href)="(?!http|\/\/)/g,
'$1="/ssh/assets/'
)
// Inject the configuration into the HTML // Inject the configuration into the HTML
modifiedHtml = modifiedHtml.replace('window.webssh2Config = null;', 'window.webssh2Config = ' + JSON.stringify(config) + ';'); modifiedHtml = modifiedHtml.replace(
"window.webssh2Config = null;",
"window.webssh2Config = " + JSON.stringify(config) + ";"
)
// Send the modified HTML // Send the modified HTML
res.send(modifiedHtml); res.send(modifiedHtml)
}); }
)
} }
module.exports = handleConnection; module.exports = handleConnection

View file

@ -1,34 +1,34 @@
// server // server
// /app/routes.js // /app/routes.js
const createDebug = require('debug') const createDebug = require("debug")
const debug = createDebug('webssh2:routes') const debug = createDebug("webssh2:routes")
const express = require('express'); const express = require("express")
const router = express.Router(); const router = express.Router()
const handleConnection = require('./connectionHandler'); const handleConnection = require("./connectionHandler")
const basicAuth = require('basic-auth'); const basicAuth = require("basic-auth")
function auth(req, res, next) { function auth(req, res, next) {
debug('Authenticating user with HTTP Basic Auth'); debug("Authenticating user with HTTP Basic Auth")
var credentials = basicAuth(req); var credentials = basicAuth(req)
if (!credentials) { if (!credentials) {
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH2"'); res.setHeader("WWW-Authenticate", 'Basic realm="WebSSH2"')
return res.status(401).send('Authentication required.'); return res.status(401).send("Authentication required.")
} }
// Store credentials in session // Store credentials in session
req.session.sshCredentials = credentials; req.session.sshCredentials = credentials
next(); next()
} }
// Scenario 1: No auth required, uses websocket authentication instead // Scenario 1: No auth required, uses websocket authentication instead
router.get('/', function(req, res) { router.get("/", function (req, res) {
debug('Accessed /ssh route'); debug("Accessed /ssh route")
handleConnection(req, res); handleConnection(req, res)
}); })
// Scenario 2: Auth required, uses HTTP Basic Auth // Scenario 2: Auth required, uses HTTP Basic Auth
router.get('/host/:host', auth, function(req, res) { router.get("/host/:host", auth, function (req, res) {
debug(`Accessed /ssh/host/${req.params.host} route`); debug(`Accessed /ssh/host/${req.params.host} route`)
handleConnection(req, res, { host: req.params.host }); handleConnection(req, res, { host: req.params.host })
}); })
module.exports = router; module.exports = router

View file

@ -1,11 +1,11 @@
// server // server
// app/socket.js // app/socket.js
'use strict' "use strict"
const createDebug = require('debug') const createDebug = require("debug")
const { header } = require('./config') const { header } = require("./config")
const debug = createDebug('webssh2:socket') const debug = createDebug("webssh2:socket")
const SSH = require('ssh2').Client const SSH = require("ssh2").Client
/** /**
* Handles WebSocket connections for SSH * Handles WebSocket connections for SSH
@ -13,7 +13,7 @@ const SSH = require('ssh2').Client
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
module.exports = function (io, config) { module.exports = function (io, config) {
io.on('connection', (socket) => handleConnection(socket, config)) io.on("connection", (socket) => handleConnection(socket, config))
} }
/** /**
@ -22,20 +22,22 @@ module.exports = function (io, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function handleConnection(socket, config) { function handleConnection(socket, config) {
let conn = null; let conn = null
let stream = null; let stream = null
let authenticated = false; let authenticated = false
let isConnectionClosed = false; let isConnectionClosed = false
debug(`CONNECT: ${socket.id}, URL: ${socket.handshake.url}`); debug(`CONNECT: ${socket.id}, URL: ${socket.handshake.url}`)
removeExistingListeners(socket) removeExistingListeners(socket)
setupInitialSocketListeners(socket, config) setupInitialSocketListeners(socket, config)
// Emit an event to the client to request authentication // Emit an event to the client to request authentication
if (!authenticated) { if (!authenticated) {
debug(`Requesting authentication for ${socket.id} and authenticated is ${authenticated}`); debug(
socket.emit('authentication', { action: 'request_auth' }); `Requesting authentication for ${socket.id} and authenticated is ${authenticated}`
)
socket.emit("authentication", { action: "request_auth" })
} }
/** /**
@ -43,9 +45,11 @@ function handleConnection(socket, config) {
* @param {import('socket.io').Socket} socket - The Socket.IO socket * @param {import('socket.io').Socket} socket - The Socket.IO socket
*/ */
function removeExistingListeners(socket) { function removeExistingListeners(socket) {
['authenticate', 'data', 'resize', 'disconnect', 'control'].forEach(event => { ;["authenticate", "data", "resize", "disconnect", "control"].forEach(
(event) => {
socket.removeAllListeners(event) socket.removeAllListeners(event)
}) }
)
} }
/** /**
@ -54,20 +58,24 @@ function handleConnection(socket, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function setupInitialSocketListeners(socket, config) { function setupInitialSocketListeners(socket, config) {
socket.on('error', (error) => console.error(`Socket error for ${socket.id}:`, error)); socket.on("error", (error) =>
socket.on('authenticate', creds => handleAuthentication(socket, creds, config)) console.error(`Socket error for ${socket.id}:`, error)
socket.on('disconnect', (reason) => { )
debug(`Client ${socket.id} disconnected. Reason: ${reason}`); socket.on("authenticate", (creds) =>
debug('Socket state at disconnect:', socket.conn.transport.readyState); handleAuthentication(socket, creds, config)
)
socket.on("disconnect", (reason) => {
debug(`Client ${socket.id} disconnected. Reason: ${reason}`)
debug("Socket state at disconnect:", socket.conn.transport.readyState)
if (conn) { if (conn) {
conn.end(); conn.end()
conn = null; conn = null
} }
if (stream) { if (stream) {
stream.end(); stream.end()
stream = null; stream = null
} }
}); })
} }
/** /**
@ -78,15 +86,17 @@ function handleConnection(socket, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function handleAuthentication(socket, creds, config) { function handleAuthentication(socket, creds, config) {
debug(`AUTHENTICATE: ${socket.id}, Host: ${creds.host}`); debug(`AUTHENTICATE: ${socket.id}, Host: ${creds.host}`)
if (isValidCredentials(creds)) { if (isValidCredentials(creds)) {
debug(`CREDENTIALS VALID: ${socket.id}, Host: ${creds.host}`); debug(`CREDENTIALS VALID: ${socket.id}, Host: ${creds.host}`)
initializeConnection(socket, creds, config); initializeConnection(socket, creds, config)
} else { } else {
debug(`CREDENTIALS INVALID: ${socket.id}, Host: ${creds.host}`); debug(`CREDENTIALS INVALID: ${socket.id}, Host: ${creds.host}`)
socket.emit('authentication', { success: false, message: 'Invalid credentials format' }); socket.emit("authentication", {
success: false,
message: "Invalid credentials format"
})
} }
} }
@ -97,39 +107,48 @@ function handleAuthentication(socket, creds, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function initializeConnection(socket, creds, config) { function initializeConnection(socket, creds, config) {
debug(`INITIALIZING SSH CONNECTION: ${socket.id}, Host: ${creds.host}`); debug(`INITIALIZING SSH CONNECTION: ${socket.id}, Host: ${creds.host}`)
if (conn) { if (conn) {
conn.end() conn.end()
} }
conn = new SSH() conn = new SSH()
conn.on('ready', () => { conn.on("ready", () => {
authenticated = true; authenticated = true
debug(`SSH CONNECTION READY: ${socket.id}, Host: ${creds.host}`); debug(`SSH CONNECTION READY: ${socket.id}, Host: ${creds.host}`)
socket.emit('authentication', { action: 'auth_result', success: true }); socket.emit("authentication", { action: "auth_result", success: true })
// Emit consolidated permissions // Emit consolidated permissions
socket.emit('permissions', { socket.emit("permissions", {
allowReplay: config.options.allowReplay || false, allowReplay: config.options.allowReplay || false,
allowReauth: config.options.allowReauth || false allowReauth: config.options.allowReauth || false
}); })
if (config.header && config.header.text !== null) { if (config.header && config.header.text !== null) {
debug('header:', config.header) debug("header:", config.header)
socket.emit('updateUI', { header: config.header } || { header: { text: '', background: '' } }) socket.emit(
"updateUI",
{ header: config.header } || { header: { text: "", background: "" } }
)
} }
setupSSHListeners(socket, creds) setupSSHListeners(socket, creds)
initializeShell(socket, creds) initializeShell(socket, creds)
}) })
conn.on('error', err => { conn.on("error", (err) => {
console.error(`SSH CONNECTION ERROR: ${socket.id}, Host: ${creds.host}, Error: ${err.message}`); console.error(
if (err.level === 'client-authentication') { `SSH CONNECTION ERROR: ${socket.id}, Host: ${creds.host}, Error: ${err.message}`
socket.emit('authentication', { action: 'auth_result', success: false, message: 'Authentication failed' }) )
if (err.level === "client-authentication") {
socket.emit("authentication", {
action: "auth_result",
success: false,
message: "Authentication failed"
})
} else { } else {
handleError(socket, 'SSH CONNECTION ERROR', err) handleError(socket, "SSH CONNECTION ERROR", err)
} }
}) })
@ -142,13 +161,15 @@ function handleAuthentication(socket, creds, config) {
* @param {Credentials} creds - The user credentials * @param {Credentials} creds - The user credentials
*/ */
function setupSSHListeners(socket, creds) { function setupSSHListeners(socket, creds) {
conn.on('banner', data => handleBanner(socket, data)) conn.on("banner", (data) => handleBanner(socket, data))
conn.on('end', () => handleSSHEnd(socket)) conn.on("end", () => handleSSHEnd(socket))
conn.on('close', () => handleSSHClose(socket)) conn.on("close", () => handleSSHClose(socket))
socket.on('data', data => handleData(socket, stream, data)) socket.on("data", (data) => handleData(socket, stream, data))
socket.on('resize', data => handleResize(stream, data)) socket.on("resize", (data) => handleResize(stream, data))
socket.on('control', controlData => handleControl(socket, stream, creds, controlData, config)) socket.on("control", (controlData) =>
handleControl(socket, stream, creds, controlData, config)
)
} }
/** /**
@ -165,17 +186,18 @@ function handleAuthentication(socket, creds, config) {
}, },
(err, str) => { (err, str) => {
if (err) { if (err) {
return handleError(socket, 'EXEC ERROR', err) return handleError(socket, "EXEC ERROR", err)
} }
stream = str stream = str
stream.on('data', data => socket.emit('data', data.toString('utf-8'))) stream.on("data", (data) => socket.emit("data", data.toString("utf-8")))
stream.on('close', (code, signal) => { stream.on("close", (code, signal) => {
handleError(socket, 'STREAM CLOSE', { handleError(socket, "STREAM CLOSE", {
message: code || signal ? `CODE: ${code} SIGNAL: ${signal}` : undefined message:
code || signal ? `CODE: ${code} SIGNAL: ${signal}` : undefined
}) })
}) })
stream.stderr.on('data', data => debug('STDERR: ' + data)) stream.stderr.on("data", (data) => debug("STDERR: " + data))
} }
) )
} }
@ -186,7 +208,7 @@ function handleAuthentication(socket, creds, config) {
* @param {string} data - The banner data * @param {string} data - The banner data
*/ */
function handleBanner(socket, data) { function handleBanner(socket, data) {
socket.emit('data', data.replace(/\r?\n/g, '\r\n')) socket.emit("data", data.replace(/\r?\n/g, "\r\n"))
} }
/** /**
@ -221,7 +243,7 @@ function handleAuthentication(socket, creds, config) {
conn.end() conn.end()
conn = null conn = null
} }
socket.emit('connection_closed') socket.emit("connection_closed")
} }
/** /**
@ -245,12 +267,12 @@ function handleAuthentication(socket, creds, config) {
try { try {
stream.write(data) stream.write(data)
} catch (error) { } catch (error) {
debug('Error writing to stream:', error.message) debug("Error writing to stream:", error.message)
handleConnectionClose(socket) handleConnectionClose(socket)
} }
} else if (isConnectionClosed) { } else if (isConnectionClosed) {
debug('Attempted to write to closed connection') debug("Attempted to write to closed connection")
socket.emit('connection_closed') socket.emit("connection_closed")
} }
} }
@ -276,12 +298,12 @@ function handleAuthentication(socket, creds, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function handleControl(socket, stream, credentials, controlData, config) { function handleControl(socket, stream, credentials, controlData, config) {
debug(`Received control data: ${controlData}`); debug(`Received control data: ${controlData}`)
if (controlData === 'replayCredentials' && stream && credentials) { if (controlData === "replayCredentials" && stream && credentials) {
replayCredentials(socket, stream, credentials, config); replayCredentials(socket, stream, credentials, config)
} else if (controlData === 'reauth' && config.options.allowReauth) { } else if (controlData === "reauth" && config.options.allowReauth) {
handleReauth(socket); handleReauth(socket)
} }
} }
@ -293,13 +315,13 @@ function handleAuthentication(socket, creds, config) {
* @param {Object} config - The configuration object * @param {Object} config - The configuration object
*/ */
function replayCredentials(socket, stream, credentials, config) { function replayCredentials(socket, stream, credentials, config) {
let allowReplay = config.options.allowReplay || false; let allowReplay = config.options.allowReplay || false
if (allowReplay) { if (allowReplay) {
debug(`Replaying credentials for ${socket.id}`); debug(`Replaying credentials for ${socket.id}`)
stream.write(credentials.password + '\n'); stream.write(credentials.password + "\n")
} else { } else {
debug(`Credential replay not allowed for ${socket.id}`); debug(`Credential replay not allowed for ${socket.id}`)
} }
} }
@ -308,9 +330,9 @@ function handleAuthentication(socket, creds, config) {
* @param {import('socket.io').Socket} socket - The Socket.IO socket * @param {import('socket.io').Socket} socket - The Socket.IO socket
*/ */
function handleReauth(socket) { function handleReauth(socket) {
debug(`Reauthentication requested for ${socket.id}`); debug(`Reauthentication requested for ${socket.id}`)
socket.emit('authentication', { action: 'reauth' }); socket.emit("authentication", { action: "reauth" })
handleConnectionClose(socket); handleConnectionClose(socket)
} }
/** /**
@ -320,9 +342,9 @@ function handleAuthentication(socket, creds, config) {
* @param {Error} [err] - The error object * @param {Error} [err] - The error object
*/ */
function handleError(socket, context, err) { function handleError(socket, context, err) {
const errorMessage = err ? `: ${err.message}` : '' const errorMessage = err ? `: ${err.message}` : ""
debug(`WebSSH2 error: ${context}${errorMessage}`) debug(`WebSSH2 error: ${context}${errorMessage}`)
socket.emit('ssherror', `SSH ${context}${errorMessage}`) socket.emit("ssherror", `SSH ${context}${errorMessage}`)
handleConnectionClose(socket) handleConnectionClose(socket)
} }
@ -333,11 +355,13 @@ function handleAuthentication(socket, creds, config) {
*/ */
function isValidCredentials(credentials) { function isValidCredentials(credentials) {
// Basic format validation // Basic format validation
return credentials && return (
typeof credentials.username === 'string' && credentials &&
typeof credentials.password === 'string' && typeof credentials.username === "string" &&
typeof credentials.host === 'string' && typeof credentials.password === "string" &&
typeof credentials.port === 'number' typeof credentials.host === "string" &&
typeof credentials.port === "number"
)
} }
/** /**
@ -357,7 +381,7 @@ function handleAuthentication(socket, creds, config) {
readyTimeout: credentials.readyTimeout, readyTimeout: credentials.readyTimeout,
keepaliveInterval: credentials.keepaliveInterval, keepaliveInterval: credentials.keepaliveInterval,
keepaliveCountMax: credentials.keepaliveCountMax, keepaliveCountMax: credentials.keepaliveCountMax,
debug: createDebug('webssh2:ssh') debug: createDebug("webssh2:ssh")
} }
} }
} }

View file

@ -1,4 +1,4 @@
'use strict' "use strict"
// server // server
// index.js // index.js
/** /**
@ -8,7 +8,7 @@
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017 * Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
*/ */
const { startServer, config } = require('./app/app') const { startServer, config } = require("./app/app")
/** /**
* Main function to start the application * Main function to start the application