chore: refactor debugging, logging and error handling.
This commit is contained in:
parent
e30c0c1c9b
commit
66ce643dd9
13 changed files with 450 additions and 309 deletions
160
app/app.js
160
app/app.js
|
@ -1,149 +1,75 @@
|
|||
// server
|
||||
// app/app.js
|
||||
|
||||
// const createDebug = require("debug")
|
||||
const http = require("http")
|
||||
const express = require("express")
|
||||
const socketIo = require("socket.io")
|
||||
const path = require("path")
|
||||
const bodyParser = require("body-parser")
|
||||
const session = require("express-session")
|
||||
const sharedsession = require("express-socket.io-session")
|
||||
const config = require("./config")
|
||||
const socketHandler = require("./socket")
|
||||
const sshRoutes = require("./routes")
|
||||
const { getCorsConfig } = require("./config")
|
||||
const { applyMiddleware } = require("./middleware")
|
||||
const { createServer, startServer } = require("./server")
|
||||
const { configureSocketIO } = require("./io")
|
||||
const { handleError, ConfigError } = require("./errors")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
|
||||
// const debug = createDebug("webssh2")
|
||||
const debug = createNamespacedDebug("app")
|
||||
|
||||
/**
|
||||
* Creates and configures the Express application
|
||||
* @returns {express.Application} The Express application instance
|
||||
* @returns {Object} An object containing the app and sessionMiddleware
|
||||
*/
|
||||
function createApp() {
|
||||
const app = express()
|
||||
|
||||
// Resolve the correct path to the webssh2_client module
|
||||
const clientPath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"webssh2_client",
|
||||
"client",
|
||||
"public"
|
||||
)
|
||||
try {
|
||||
// Resolve the correct path to the webssh2_client module
|
||||
const clientPath = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"webssh2_client",
|
||||
"client",
|
||||
"public"
|
||||
)
|
||||
|
||||
// Set up session middleware
|
||||
const sessionMiddleware = session({
|
||||
secret: config.session.secret || "webssh2_secret",
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
name: config.session.name || "webssh2.sid"
|
||||
})
|
||||
app.use(sessionMiddleware)
|
||||
// Apply middleware
|
||||
const { sessionMiddleware } = applyMiddleware(app, config)
|
||||
|
||||
// Handle POST and GET parameters
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(bodyParser.json())
|
||||
// Serve static files from the webssh2_client module with a custom prefix
|
||||
app.use("/ssh/assets", express.static(clientPath))
|
||||
|
||||
// Add cookie-setting middleware
|
||||
app.use((req, res, next) => {
|
||||
if (req.session.sshCredentials) {
|
||||
const cookieData = {
|
||||
host: req.session.sshCredentials.host,
|
||||
port: req.session.sshCredentials.port
|
||||
}
|
||||
res.cookie("basicauth", JSON.stringify(cookieData), {
|
||||
httpOnly: false,
|
||||
path: "/ssh/host/",
|
||||
sameSite: "Strict"
|
||||
}) // ensure httOnly is false for the client to read the cookie
|
||||
}
|
||||
next()
|
||||
})
|
||||
// Use the SSH routes
|
||||
app.use("/ssh", sshRoutes)
|
||||
|
||||
// Serve static files from the webssh2_client module with a custom prefix
|
||||
app.use("/ssh/assets", express.static(clientPath))
|
||||
|
||||
// Use the SSH routes
|
||||
app.use("/ssh", sshRoutes)
|
||||
|
||||
return { app, sessionMiddleware }
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures Socket.IO with the given server
|
||||
* @param {http.Server} server - The HTTP server instance
|
||||
* @param {Function} sessionMiddleware - The session middleware
|
||||
* @returns {import('socket.io').Server} The Socket.IO server instance
|
||||
*/
|
||||
function configureSocketIO(server, sessionMiddleware) {
|
||||
const io = socketIo(server, {
|
||||
serveClient: false,
|
||||
path: "/ssh/socket.io",
|
||||
pingTimeout: 60000, // 1 minute
|
||||
pingInterval: 25000, // 25 seconds
|
||||
cors: getCorsConfig()
|
||||
})
|
||||
|
||||
// Share session with io sockets
|
||||
io.use(
|
||||
sharedsession(sessionMiddleware, {
|
||||
autoSave: true
|
||||
})
|
||||
)
|
||||
|
||||
return io
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures the HTTP server
|
||||
* @param {express.Application} app - The Express application instance
|
||||
* @returns {http.Server} The HTTP server instance
|
||||
*/
|
||||
function createServer(app) {
|
||||
return http.createServer(app)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles server errors
|
||||
* @param {Error} err - The error object
|
||||
*/
|
||||
function handleServerError(err) {
|
||||
console.error("WebSSH2 server.listen ERROR:", err.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Socket.IO event listeners
|
||||
* @param {import('socket.io').Server} io - The Socket.IO server instance
|
||||
*/
|
||||
function setupSocketIOListeners(io) {
|
||||
socketHandler(io, config)
|
||||
return { app: app, sessionMiddleware: sessionMiddleware }
|
||||
} catch (err) {
|
||||
throw new ConfigError(`Failed to configure Express app: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts the server
|
||||
* @returns {Object} An object containing the server, io, and app instances
|
||||
*/
|
||||
function startServer() {
|
||||
const { app, sessionMiddleware } = createApp()
|
||||
const server = createServer(app)
|
||||
const io = configureSocketIO(server, sessionMiddleware)
|
||||
function initializeServer() {
|
||||
try {
|
||||
const { app, sessionMiddleware } = createApp()
|
||||
const server = createServer(app)
|
||||
const io = configureSocketIO(server, sessionMiddleware, config)
|
||||
|
||||
// Set up Socket.IO listeners
|
||||
setupSocketIOListeners(io)
|
||||
// Set up Socket.IO listeners
|
||||
socketHandler(io, config)
|
||||
|
||||
// Start the server
|
||||
server.listen(config.listen.port, config.listen.ip, () => {
|
||||
console.log(
|
||||
`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`
|
||||
)
|
||||
})
|
||||
// Start the server
|
||||
startServer(server, config)
|
||||
|
||||
server.on("error", handleServerError)
|
||||
debug("Server initialized")
|
||||
|
||||
return { server, io, app }
|
||||
return { server: server, io: io, app: app }
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't start the server immediately, export the function instead
|
||||
module.exports = { startServer, config }
|
||||
module.exports = { initializeServer: initializeServer, config: config }
|
||||
|
|
159
app/config.js
159
app/config.js
|
@ -1,49 +1,13 @@
|
|||
// server
|
||||
// app/config.js
|
||||
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const readConfig = require("read-config-ng")
|
||||
const Ajv = require("ajv")
|
||||
const { deepMerge, generateSecureSecret } = require("./utils")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
const { ConfigError, handleError } = require("./errors")
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {Object} listen - Listening configuration
|
||||
* @property {string} listen.ip - IP address to listen on
|
||||
* @property {number} listen.port - Port to listen on
|
||||
* @property {Object} http - HTTP configuration
|
||||
* @property {string[]} http.origins - Allowed origins
|
||||
* @property {Object} user - User configuration
|
||||
* @property {string|null} user.name - Username
|
||||
* @property {string|null} user.password - Password
|
||||
* @property {Object} ssh - SSH configuration
|
||||
* @property {string|null} ssh.host - SSH host
|
||||
* @property {number} ssh.port - SSH port
|
||||
* @property {string} ssh.term - Terminal type
|
||||
* @property {number} ssh.readyTimeout - Ready timeout
|
||||
* @property {number} ssh.keepaliveInterval - Keepalive interval
|
||||
* @property {number} ssh.keepaliveCountMax - Max keepalive count
|
||||
* @property {Object} header - Header configuration
|
||||
* @property {string|null} header.text - Header text
|
||||
* @property {string} header.background - Header background color
|
||||
* @property {Object} options - Options configuration
|
||||
* @property {boolean} options.challengeButton - Challenge button enabled
|
||||
* @property {boolean} options.autoLog - Auto log enabled
|
||||
* @property {boolean} options.allowReauth - Allow reauthentication
|
||||
* @property {boolean} options.allowReconnect - Allow reconnection
|
||||
* @property {boolean} options.allowReplay - Allow replay
|
||||
* @property {Object} algorithms - Encryption algorithms
|
||||
* @property {string[]} algorithms.kex - Key exchange algorithms
|
||||
* @property {string[]} algorithms.cipher - Cipher algorithms
|
||||
* @property {string[]} algorithms.hmac - HMAC algorithms
|
||||
* @property {string[]} algorithms.compress - Compression algorithms
|
||||
*/
|
||||
const debug = createNamespacedDebug("config")
|
||||
|
||||
/**
|
||||
* Default configuration
|
||||
* @type {Config}
|
||||
*/
|
||||
const defaultConfig = {
|
||||
listen: {
|
||||
ip: "0.0.0.0",
|
||||
|
@ -71,9 +35,9 @@ const defaultConfig = {
|
|||
options: {
|
||||
challengeButton: true,
|
||||
autoLog: false,
|
||||
allowReauth: false,
|
||||
allowReconnect: false,
|
||||
allowReplay: false
|
||||
allowReauth: true,
|
||||
allowReconnect: true,
|
||||
allowReplay: true
|
||||
},
|
||||
algorithms: {
|
||||
kex: [
|
||||
|
@ -206,36 +170,15 @@ const configSchema = {
|
|||
required: ["listen", "http", "user", "ssh", "header", "options", "algorithms"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the config file
|
||||
* @returns {string} The path to the config file
|
||||
*/
|
||||
function getConfigPath() {
|
||||
const nodeRoot = path.dirname(require.main.filename)
|
||||
return path.join(nodeRoot, "config.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the config file synchronously
|
||||
* @param {string} configPath - The path to the config file
|
||||
* @returns {Object} The configuration object
|
||||
*/
|
||||
function readConfigFile(configPath) {
|
||||
console.log(`WebSSH2 service reading config from: ${configPath}`)
|
||||
return readConfig.sync(configPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the configuration against the schema
|
||||
* @param {Object} config - The configuration object to validate
|
||||
* @returns {Object} The validated configuration object
|
||||
* @throws {Error} If the configuration is invalid
|
||||
*/
|
||||
function validateConfig(config) {
|
||||
const ajv = new Ajv()
|
||||
const validate = ajv.compile(configSchema)
|
||||
const valid = validate(config)
|
||||
console.log("WebSSH2 service validating config")
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`Config validation error: ${ajv.errorsText(validate.errors)}`
|
||||
|
@ -244,77 +187,85 @@ function validateConfig(config) {
|
|||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message
|
||||
* @param {string} message - The error message
|
||||
* @param {Error} [error] - The error object
|
||||
*/
|
||||
function logError(message, error) {
|
||||
console.error(message)
|
||||
if (error) {
|
||||
console.error(`ERROR:\n\n ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and merges the configuration synchronously
|
||||
* @returns {Object} The merged configuration
|
||||
*/
|
||||
function loadConfig() {
|
||||
const configPath = getConfigPath()
|
||||
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const providedConfig = readConfigFile(configPath)
|
||||
|
||||
// Deep merge the provided config with the default config
|
||||
const providedConfig = readConfig.sync(configPath)
|
||||
const mergedConfig = deepMerge(
|
||||
JSON.parse(JSON.stringify(defaultConfig)),
|
||||
providedConfig
|
||||
)
|
||||
|
||||
// Override the port with the PORT environment variable if it's set
|
||||
if (process.env.PORT) {
|
||||
mergedConfig.listen.port = parseInt(process.env.PORT, 10)
|
||||
console.log(`Using PORT from environment: ${mergedConfig.listen.port}`)
|
||||
debug("Using PORT from environment: %s", mergedConfig.listen.port)
|
||||
}
|
||||
|
||||
const validatedConfig = validateConfig(mergedConfig)
|
||||
console.log("Merged and validated configuration")
|
||||
debug("Merged and validated configuration")
|
||||
return validatedConfig
|
||||
}
|
||||
logError(
|
||||
`\n\nERROR: Missing config.json for webssh. Using default config: ${JSON.stringify(
|
||||
defaultConfig
|
||||
)}\n\n See config.json.sample for details\n\n`
|
||||
)
|
||||
debug("Missing config.json for webssh. Using default config")
|
||||
return defaultConfig
|
||||
} catch (err) {
|
||||
logError(
|
||||
`\n\nERROR: Problem loading config.json for webssh. Using default config: ${JSON.stringify(
|
||||
defaultConfig
|
||||
)}\n\n See config.json.sample for details\n\n`,
|
||||
err
|
||||
const error = new ConfigError(
|
||||
`Problem loading config.json for webssh: ${err.message}`
|
||||
)
|
||||
handleError(error)
|
||||
return defaultConfig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The loaded configuration
|
||||
* @type {Object}
|
||||
* Configuration for the application.
|
||||
*
|
||||
* @returns {Object} config
|
||||
* @property {Object} listen - Configuration for listening IP and port.
|
||||
* @property {string} listen.ip - The IP address to listen on.
|
||||
* @property {number} listen.port - The port number to listen on.
|
||||
* @property {Object} http - Configuration for HTTP settings.
|
||||
* @property {string[]} http.origins - The allowed origins for HTTP requests.
|
||||
* @property {Object} user - Configuration for user settings.
|
||||
* @property {string|null} user.name - The name of the user.
|
||||
* @property {string|null} user.password - The password of the user.
|
||||
* @property {Object} ssh - Configuration for SSH settings.
|
||||
* @property {string|null} ssh.host - The SSH host.
|
||||
* @property {number} ssh.port - The SSH port.
|
||||
* @property {string} ssh.term - The SSH terminal type.
|
||||
* @property {number} ssh.readyTimeout - The SSH ready timeout.
|
||||
* @property {number} ssh.keepaliveInterval - The SSH keepalive interval.
|
||||
* @property {number} ssh.keepaliveCountMax - The SSH keepalive count maximum.
|
||||
* @property {Object} header - Configuration for header settings.
|
||||
* @property {string|null} header.text - The header text.
|
||||
* @property {string} header.background - The header background color.
|
||||
* @property {Object} options - Configuration for options settings.
|
||||
* @property {boolean} options.challengeButton - Whether to show the challenge button.
|
||||
* @property {boolean} options.autoLog - Whether to automatically log.
|
||||
* @property {boolean} options.allowReauth - Whether to allow reauthentication.
|
||||
* @property {boolean} options.allowReconnect - Whether to allow reconnection.
|
||||
* @property {boolean} options.allowReplay - Whether to allow replay.
|
||||
* @property {Object} algorithms - Configuration for algorithms settings.
|
||||
* @property {string[]} algorithms.kex - The key exchange algorithms.
|
||||
* @property {string[]} algorithms.cipher - The cipher algorithms.
|
||||
* @property {string[]} algorithms.hmac - The HMAC algorithms.
|
||||
* @property {string[]} algorithms.compress - The compression algorithms.
|
||||
* @property {Object} session - Configuration for session settings.
|
||||
* @property {string} session.secret - The session secret.
|
||||
* @property {string} session.name - The session name.
|
||||
*/
|
||||
const config = loadConfig()
|
||||
|
||||
module.exports = config
|
||||
/**
|
||||
* Gets the CORS configuration
|
||||
* @returns {Object} The CORS configuration object
|
||||
*/
|
||||
module.exports.getCorsConfig = function getCorsConfig() {
|
||||
function getCorsConfig() {
|
||||
return {
|
||||
origin: config.origin || ["*.*"],
|
||||
origin: config.http.origins,
|
||||
methods: ["GET", "POST"],
|
||||
credentials: true
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the config object with the getCorsConfig function
|
||||
config.getCorsConfig = getCorsConfig
|
||||
|
||||
module.exports = config
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
// server
|
||||
// app/connectionHandler.js
|
||||
|
||||
const createDebug = require("debug")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
|
||||
const debug = createDebug("webssh2:connectionHandler")
|
||||
|
||||
const debug = createNamespacedDebug("connectionHandler")
|
||||
/**
|
||||
* Modify the HTML content by replacing certain placeholders with dynamic values.
|
||||
* @param {string} html - The original HTML content.
|
||||
|
|
69
app/errors.js
Normal file
69
app/errors.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
// server
|
||||
// app/errors.js
|
||||
|
||||
const util = require("util")
|
||||
const { logError, createNamespacedDebug } = require("./logger")
|
||||
|
||||
const debug = createNamespacedDebug("errors")
|
||||
|
||||
/**
|
||||
* Custom error for WebSSH2
|
||||
* @param {string} message - The error message
|
||||
* @param {string} code - The error code
|
||||
*/
|
||||
function WebSSH2Error(message, code) {
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
this.name = this.constructor.name
|
||||
this.message = message
|
||||
this.code = code
|
||||
}
|
||||
|
||||
util.inherits(WebSSH2Error, Error)
|
||||
|
||||
/**
|
||||
* Custom error for configuration issues
|
||||
* @param {string} message - The error message
|
||||
*/
|
||||
function ConfigError(message) {
|
||||
WebSSH2Error.call(this, message, "CONFIG_ERROR")
|
||||
}
|
||||
|
||||
util.inherits(ConfigError, WebSSH2Error)
|
||||
|
||||
/**
|
||||
* Custom error for SSH connection issues
|
||||
* @param {string} message - The error message
|
||||
*/
|
||||
function SSHConnectionError(message) {
|
||||
WebSSH2Error.call(this, message, "SSH_CONNECTION_ERROR")
|
||||
}
|
||||
|
||||
util.inherits(SSHConnectionError, WebSSH2Error)
|
||||
|
||||
/**
|
||||
* Handles an error by logging it and optionally sending a response
|
||||
* @param {Error} err - The error to handle
|
||||
* @param {Object} [res] - The response object (if in an Express route)
|
||||
*/
|
||||
function handleError(err, res) {
|
||||
if (err instanceof WebSSH2Error) {
|
||||
logError(err.message, err)
|
||||
debug(err.message)
|
||||
if (res) {
|
||||
res.status(500).json({ error: err.message, code: err.code })
|
||||
}
|
||||
} else {
|
||||
logError("An unexpected error occurred", err)
|
||||
debug("Unexpected error: %O", err)
|
||||
if (res) {
|
||||
res.status(500).json({ error: "An unexpected error occurred" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WebSSH2Error: WebSSH2Error,
|
||||
ConfigError: ConfigError,
|
||||
SSHConnectionError: SSHConnectionError,
|
||||
handleError: handleError
|
||||
}
|
35
app/io.js
Normal file
35
app/io.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const socketIo = require("socket.io")
|
||||
const sharedsession = require("express-socket.io-session")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
|
||||
const debug = createNamespacedDebug("app")
|
||||
|
||||
/**
|
||||
* Configures Socket.IO with the given server
|
||||
* @param {http.Server} server - The HTTP server instance
|
||||
* @param {Function} sessionMiddleware - The session middleware
|
||||
* @param {Object} config - The configuration object
|
||||
* @returns {import('socket.io').Server} The Socket.IO server instance
|
||||
*/
|
||||
function configureSocketIO(server, sessionMiddleware, config) {
|
||||
const io = socketIo(server, {
|
||||
serveClient: false,
|
||||
path: "/ssh/socket.io",
|
||||
pingTimeout: 60000, // 1 minute
|
||||
pingInterval: 25000, // 25 seconds
|
||||
cors: config.getCorsConfig()
|
||||
})
|
||||
|
||||
// Share session with io sockets
|
||||
io.use(
|
||||
sharedsession(sessionMiddleware, {
|
||||
autoSave: true
|
||||
})
|
||||
)
|
||||
|
||||
debug("Socket.IO configured")
|
||||
|
||||
return io
|
||||
}
|
||||
|
||||
module.exports = { configureSocketIO }
|
30
app/logger.js
Normal file
30
app/logger.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
// server
|
||||
// app/logger.js
|
||||
|
||||
const createDebug = require("debug")
|
||||
|
||||
/**
|
||||
* Creates a debug function for a specific namespace
|
||||
* @param {string} namespace - The debug namespace
|
||||
* @returns {Function} The debug function
|
||||
*/
|
||||
function createNamespacedDebug(namespace) {
|
||||
return createDebug(`webssh2:${namespace}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message
|
||||
* @param {string} message - The error message
|
||||
* @param {Error} [error] - The error object
|
||||
*/
|
||||
function logError(message, error) {
|
||||
console.error(message)
|
||||
if (error) {
|
||||
console.error(`ERROR:\n\n ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createNamespacedDebug: createNamespacedDebug,
|
||||
logError: logError
|
||||
}
|
76
app/middleware.js
Normal file
76
app/middleware.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
// server
|
||||
// app/middleware.js
|
||||
|
||||
const createDebug = require("debug")
|
||||
const session = require("express-session")
|
||||
const bodyParser = require("body-parser")
|
||||
|
||||
const debug = createDebug("webssh2:middleware")
|
||||
|
||||
/**
|
||||
* Creates and configures session middleware
|
||||
* @param {Object} config - The configuration object
|
||||
* @returns {Function} The session middleware
|
||||
*/
|
||||
function createSessionMiddleware(config) {
|
||||
return session({
|
||||
secret: config.session.secret || "webssh2_secret",
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
name: config.session.name || "webssh2.sid"
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates body parser middleware
|
||||
* @returns {Function[]} Array of body parser middleware
|
||||
*/
|
||||
function createBodyParserMiddleware() {
|
||||
return [bodyParser.urlencoded({ extended: true }), bodyParser.json()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates cookie-setting middleware
|
||||
* @returns {Function} The cookie-setting middleware
|
||||
*/
|
||||
function createCookieMiddleware() {
|
||||
return (req, res, next) => {
|
||||
if (req.session.sshCredentials) {
|
||||
const cookieData = {
|
||||
host: req.session.sshCredentials.host,
|
||||
port: req.session.sshCredentials.port
|
||||
}
|
||||
res.cookie("basicauth", JSON.stringify(cookieData), {
|
||||
httpOnly: false,
|
||||
path: "/ssh/host/",
|
||||
sameSite: "Strict"
|
||||
})
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all middleware to the Express app
|
||||
* @param {express.Application} app - The Express application
|
||||
* @param {Object} config - The configuration object
|
||||
* @returns {Object} An object containing the session middleware
|
||||
*/
|
||||
function applyMiddleware(app, config) {
|
||||
const sessionMiddleware = createSessionMiddleware(config)
|
||||
app.use(sessionMiddleware)
|
||||
|
||||
app.use(createBodyParserMiddleware())
|
||||
app.use(createCookieMiddleware())
|
||||
|
||||
debug("Middleware applied")
|
||||
|
||||
return { sessionMiddleware }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyMiddleware,
|
||||
createSessionMiddleware,
|
||||
createBodyParserMiddleware,
|
||||
createCookieMiddleware
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
// server
|
||||
// /app/routes.js
|
||||
const createDebug = require("debug")
|
||||
|
||||
const debug = createDebug("webssh2:routes")
|
||||
const express = require("express")
|
||||
|
||||
const router = express.Router()
|
||||
const basicAuth = require("basic-auth")
|
||||
const maskObject = require("jsmasker")
|
||||
const validator = require("validator")
|
||||
|
@ -15,6 +11,11 @@ const {
|
|||
validateSshTerm
|
||||
} = require("./utils")
|
||||
const handleConnection = require("./connectionHandler")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
const { ConfigError, handleError } = require("./errors")
|
||||
|
||||
const debug = createNamespacedDebug("routes")
|
||||
const router = express.Router()
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
function auth(req, res, next) {
|
||||
|
@ -39,32 +40,36 @@ router.get("/", function(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) {
|
||||
debug(`router.get.host: /ssh/host/${req.params.host} route`)
|
||||
|
||||
const host = getValidatedHost(req.params.host)
|
||||
const port = getValidatedPort(req.query.port)
|
||||
try {
|
||||
const host = getValidatedHost(req.params.host)
|
||||
const port = getValidatedPort(req.query.port)
|
||||
|
||||
// Validate and sanitize sshterm parameter if it exists
|
||||
const sshterm = validateSshTerm(req.query.sshterm)
|
||||
// Validate and sanitize sshterm parameter if it exists
|
||||
const sshterm = validateSshTerm(req.query.sshterm)
|
||||
|
||||
req.session.sshCredentials = req.session.sshCredentials || {}
|
||||
req.session.sshCredentials.host = host
|
||||
req.session.sshCredentials.port = port
|
||||
if (req.query.sshterm) {
|
||||
req.session.sshCredentials.term = sshterm
|
||||
req.session.sshCredentials = req.session.sshCredentials || {}
|
||||
req.session.sshCredentials.host = host
|
||||
req.session.sshCredentials.port = port
|
||||
if (req.query.sshterm) {
|
||||
req.session.sshCredentials.term = sshterm
|
||||
}
|
||||
req.session.usedBasicAuth = true
|
||||
|
||||
// Sanitize and log the sshCredentials object
|
||||
const sanitizedCredentials = maskObject(
|
||||
JSON.parse(JSON.stringify(req.session.sshCredentials))
|
||||
)
|
||||
debug("/ssh/host/ Credentials: ", sanitizedCredentials)
|
||||
|
||||
handleConnection(req, res, { host: host })
|
||||
} catch (err) {
|
||||
const error = new ConfigError(`Invalid configuration: ${err.message}`)
|
||||
handleError(error, res)
|
||||
}
|
||||
req.session.usedBasicAuth = true
|
||||
|
||||
// Sanitize and log the sshCredentials object
|
||||
const sanitizedCredentials = maskObject(
|
||||
JSON.parse(JSON.stringify(req.session.sshCredentials))
|
||||
)
|
||||
debug("/ssh/host/ Credentials: ", sanitizedCredentials)
|
||||
|
||||
handleConnection(req, res, { host: host })
|
||||
})
|
||||
|
||||
// Clear credentials route
|
||||
|
|
37
app/server.js
Normal file
37
app/server.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
const http = require("http")
|
||||
// const { createNamespacedDebug } = require("./logger")
|
||||
|
||||
// const debug = createNamespacedDebug("server")
|
||||
/**
|
||||
* Creates and configures the HTTP server
|
||||
* @param {express.Application} app - The Express application instance
|
||||
* @returns {http.Server} The HTTP server instance
|
||||
*/
|
||||
function createServer(app) {
|
||||
return http.createServer(app)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles server errors
|
||||
* @param {Error} err - The error object
|
||||
*/
|
||||
function handleServerError(err) {
|
||||
console.error("WebSSH2 server.listen ERROR:", err.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server
|
||||
* @param {http.Server} server - The server instance
|
||||
* @param {Object} config - The configuration object
|
||||
*/
|
||||
function startServer(server, config) {
|
||||
server.listen(config.listen.port, config.listen.ip, () => {
|
||||
console.log(
|
||||
`startServer: listening on ${config.listen.ip}:${config.listen.port}`
|
||||
)
|
||||
})
|
||||
|
||||
server.on("error", handleServerError)
|
||||
}
|
||||
|
||||
module.exports = { createServer, startServer }
|
110
app/socket.js
110
app/socket.js
|
@ -1,20 +1,16 @@
|
|||
// server
|
||||
// app/socket.js
|
||||
|
||||
const createDebug = require("debug")
|
||||
const maskObject = require("jsmasker")
|
||||
const validator = require("validator")
|
||||
const SSHConnection = require("./ssh")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
const { SSHConnectionError, handleError } = require("./errors")
|
||||
|
||||
const debug = createDebug("webssh2:socket")
|
||||
const debug = createNamespacedDebug("socket")
|
||||
const { validateSshTerm, isValidCredentials } = require("./utils")
|
||||
|
||||
class WebSSH2Socket {
|
||||
/**
|
||||
* Creates an instance of WebSSH2Socket.
|
||||
* @param {SocketIO.Socket} socket - The socket instance.
|
||||
* @param {Object} config - The configuration object.
|
||||
*/
|
||||
constructor(socket, config) {
|
||||
this.socket = socket
|
||||
this.config = config
|
||||
|
@ -32,9 +28,6 @@ class WebSSH2Socket {
|
|||
this.initializeSocketEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the socket event listeners.
|
||||
*/
|
||||
initializeSocketEvents() {
|
||||
debug(`io.on connection: ${this.socket.id}`)
|
||||
|
||||
|
@ -53,15 +46,26 @@ class WebSSH2Socket {
|
|||
this.socket.emit("authentication", { action: "request_auth" })
|
||||
}
|
||||
|
||||
this.socket.on("authenticate", creds => this.handleAuthenticate(creds))
|
||||
this.socket.on("terminal", data => this.handleTerminal(data))
|
||||
this.socket.on("disconnect", reason => this.handleConnectionClose(reason))
|
||||
this.socket.on(
|
||||
"authenticate",
|
||||
function(creds) {
|
||||
this.handleAuthenticate(creds)
|
||||
}.bind(this)
|
||||
)
|
||||
this.socket.on(
|
||||
"terminal",
|
||||
function(data) {
|
||||
this.handleTerminal(data)
|
||||
}.bind(this)
|
||||
)
|
||||
this.socket.on(
|
||||
"disconnect",
|
||||
function(reason) {
|
||||
this.handleConnectionClose(reason)
|
||||
}.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authentication process.
|
||||
* @param {Object} creds - The credentials for authentication.
|
||||
*/
|
||||
handleAuthenticate(creds) {
|
||||
debug(`handleAuthenticate: ${this.socket.id}, %O`, maskObject(creds))
|
||||
|
||||
|
@ -71,7 +75,7 @@ class WebSSH2Socket {
|
|||
: this.config.ssh.term
|
||||
this.initializeConnection(creds)
|
||||
} else {
|
||||
console.warn(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
|
||||
debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
|
||||
this.socket.emit("authentication", {
|
||||
success: false,
|
||||
message: "Invalid credentials format"
|
||||
|
@ -79,10 +83,6 @@ class WebSSH2Socket {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the SSH connection.
|
||||
* @param {Object} creds - The credentials for the SSH connection.
|
||||
*/
|
||||
initializeConnection(creds) {
|
||||
debug(
|
||||
`initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
|
||||
|
@ -91,40 +91,47 @@ class WebSSH2Socket {
|
|||
|
||||
this.ssh
|
||||
.connect(creds)
|
||||
.then(() => {
|
||||
this.sessionState = Object.assign({}, this.sessionState, {
|
||||
authenticated: true,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
host: creds.host,
|
||||
port: creds.port
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
this.sessionState = Object.assign({}, this.sessionState, {
|
||||
authenticated: true,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
host: creds.host,
|
||||
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
|
||||
}
|
||||
this.socket.emit("permissions", permissions)
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
if (this.config.header && this.config.header.text !== null) {
|
||||
this.updateElement("header", this.config.header.text)
|
||||
}
|
||||
|
||||
this.socket.emit("getTerminal", true)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(
|
||||
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
||||
)
|
||||
this.handleError("SSH CONNECTION ERROR", err)
|
||||
})
|
||||
this.socket.emit("getTerminal", true)
|
||||
}.bind(this)
|
||||
)
|
||||
.catch(
|
||||
function(err) {
|
||||
debug(
|
||||
`initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
||||
)
|
||||
handleError(
|
||||
new SSHConnectionError(`SSH CONNECTION ERROR: ${err.message}`)
|
||||
)
|
||||
this.socket.emit("ssherror", `SSH CONNECTION ERROR: ${err.message}`)
|
||||
}.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,6 +239,7 @@ class WebSSH2Socket {
|
|||
*/
|
||||
handleError(context, err) {
|
||||
const errorMessage = err ? `: ${err.message}` : ""
|
||||
handleError(new SSHConnectionError(`SSH ${context}${errorMessage}`))
|
||||
this.socket.emit("ssherror", `SSH ${context}${errorMessage}`)
|
||||
this.handleConnectionClose()
|
||||
}
|
||||
|
|
14
app/ssh.js
14
app/ssh.js
|
@ -1,11 +1,12 @@
|
|||
// server
|
||||
// app/ssh.js
|
||||
|
||||
const createDebug = require("debug")
|
||||
const SSH = require("ssh2").Client
|
||||
const maskObject = require("jsmasker")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
const { SSHConnectionError, handleError } = require("./errors")
|
||||
|
||||
const debug = createDebug("webssh2:ssh")
|
||||
const debug = createNamespacedDebug("ssh")
|
||||
|
||||
function SSHConnection(config) {
|
||||
this.config = config
|
||||
|
@ -32,8 +33,11 @@ SSHConnection.prototype.connect = function(creds) {
|
|||
})
|
||||
|
||||
self.conn.on("error", function(err) {
|
||||
console.error(`connect: error:${err.message}`)
|
||||
reject(err)
|
||||
const error = new SSHConnectionError(
|
||||
`SSH Connection error: ${err.message}`
|
||||
)
|
||||
handleError(error)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
self.conn.connect(sshConfig)
|
||||
|
@ -53,7 +57,7 @@ SSHConnection.prototype.getSSHConfig = function(creds) {
|
|||
creds.keepaliveInterval || this.config.ssh.keepaliveInterval,
|
||||
keepaliveCountMax:
|
||||
creds.keepaliveCountMax || this.config.ssh.keepaliveCountMax,
|
||||
debug: createDebug("ssh")
|
||||
debug: createNamespacedDebug("ssh2")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// server
|
||||
// /app/utils.js
|
||||
const validator = require("validator")
|
||||
const createDebug = require("debug")
|
||||
const crypto = require("crypto")
|
||||
const { createNamespacedDebug } = require("./logger")
|
||||
|
||||
const debug = createDebug("webssh2:utils")
|
||||
const debug = createNamespacedDebug("utils")
|
||||
|
||||
/**
|
||||
* Deep merges two objects
|
||||
|
|
9
index.js
9
index.js
|
@ -1,4 +1,5 @@
|
|||
"use strict"
|
||||
#!/usr/bin/env node
|
||||
|
||||
// server
|
||||
// index.js
|
||||
/**
|
||||
|
@ -8,13 +9,13 @@
|
|||
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
|
||||
*/
|
||||
|
||||
const { startServer, config } = require("./app/app")
|
||||
const { initializeServer } = require("./app/app")
|
||||
|
||||
/**
|
||||
* Main function to start the application
|
||||
*/
|
||||
function main() {
|
||||
startServer()
|
||||
initializeServer()
|
||||
}
|
||||
|
||||
// Run the application
|
||||
|
@ -22,5 +23,5 @@ main()
|
|||
|
||||
// For testing purposes, export the functions
|
||||
module.exports = {
|
||||
startServer
|
||||
initializeServer
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue