diff --git a/app/app.js b/app/app.js index 46637e2..ea1e0c7 100644 --- a/app/app.js +++ b/app/app.js @@ -7,6 +7,8 @@ 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') @@ -16,10 +18,19 @@ const sshRoutes = require('./routes') * @returns {express.Application} The Express application instance */ function createApp() { - var app = express(); + const app = express(); // Resolve the correct path to the webssh2_client module - var 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 + const sessionMiddleware = session({ + secret: config.session.secret || 'webssh2_secret', + resave: false, + saveUninitialized: true, + name: config.session.name || 'webssh2.sid' + }); + app.use(sessionMiddleware); // Handle POST and GET parameters app.use(bodyParser.urlencoded({ extended: true })); @@ -31,7 +42,7 @@ function createApp() { // Use the SSH routes app.use('/ssh', sshRoutes); - return app; + return { app, sessionMiddleware }; } /** @@ -46,13 +57,21 @@ function createServer(app) { /** * 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) { - return socketIo(server, { +function configureSocketIO(server, sessionMiddleware) { + const io = socketIo(server, { path: '/ssh/socket.io', cors: getCorsConfig() - }) + }); + + // Share session with io sockets + io.use(sharedsession(sessionMiddleware, { + autoSave: true + })); + + return io; } /** @@ -80,9 +99,9 @@ function setupSocketIOListeners(io) { * @returns {Object} An object containing the server, io, and app instances */ function startServer() { - const app = createApp() + const { app, sessionMiddleware } = createApp() const server = createServer(app) - const io = configureSocketIO(server) + const io = configureSocketIO(server, sessionMiddleware) // Set up Socket.IO listeners setupSocketIOListeners(io) diff --git a/app/config.js b/app/config.js index d981834..092249f 100644 --- a/app/config.js +++ b/app/config.js @@ -6,6 +6,7 @@ const path = require('path') const fs = require('fs') const readConfig = require('read-config-ng') const Ajv = require('ajv') +const crypto = require('crypto') /** * @typedef {Object} Config @@ -107,6 +108,10 @@ const defaultConfig = { hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], compress: ['none', 'zlib@openssh.com', 'zlib'] }, + session: { + secret: generateSecureSecret(), + name: 'webssh2.sid' + }, serverlog: { client: false, server: false @@ -208,6 +213,14 @@ const configSchema = { }, required: ['kex', 'cipher', 'hmac', 'compress'] }, + session: { + type: 'object', + properties: { + secret: { type: 'string' }, + name: { type: 'string' } + }, + required: ['secret', 'name'] + }, serverlog: { type: 'object', properties: { @@ -271,19 +284,25 @@ function logError(message, error) { } /** - * Loads the configuration - * @returns {Config} The loaded configuration + * Loads and merges the configuration + * @returns {Config} The merged configuration */ function loadConfig() { const configPath = getConfigPath() try { if (fs.existsSync(configPath)) { - const config = readConfigFile(configPath) - return validateConfig(config) + const providedConfig = readConfigFile(configPath) + + // Deep merge the provided config with the default config + const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig) + + const validatedConfig = validateConfig(mergedConfig) + console.log('Merged and validated configuration') + return validatedConfig } else { logError( - '\n\nERROR: Missing config.json for webssh. Current config: ' + + '\n\nERROR: Missing config.json for webssh. Using default config: ' + JSON.stringify(defaultConfig) + '\n\n See config.json.sample for details\n\n' ) @@ -291,7 +310,7 @@ function loadConfig() { } } catch (err) { logError( - '\n\nERROR: Missing config.json for webssh. Current config: ' + + '\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 @@ -300,6 +319,32 @@ function loadConfig() { } } +/** + * Generates a secure random session secret + * @returns {string} A random 32-byte hex string + */ +function generateSecureSecret() { + return crypto.randomBytes(32).toString('hex') +} + +/** + * Deep merges two objects + * @param {Object} target - The target object to merge into + * @param {Object} source - The source object to merge from + * @returns {Object} The merged object + */ +function deepMerge(target, source) { + for (const key in source) { + if (source.hasOwnProperty(key)) { + if (source[key] instanceof Object && !Array.isArray(source[key])) { + target[key] = deepMerge(target[key] || {}, source[key]) + } else { + target[key] = source[key] + } + } + } + return target +} /** * The loaded configuration * @type {Config} diff --git a/app/connectionHandler.js b/app/connectionHandler.js index 849d9c9..7e2349d 100644 --- a/app/connectionHandler.js +++ b/app/connectionHandler.js @@ -17,18 +17,16 @@ function handleConnection(req, res, urlParams) { // Inject configuration var config = { - socket: { - url: req.protocol + '://' + req.get('host'), - path: '/ssh/socket.io' - }, - ssh: { - host: connectionParams.host || '', - port: connectionParams.port || 22, - username: connectionParams.username || '', - password: connectionParams.password || '' - }, - autoConnect: !!(connectionParams.host && connectionParams.username && connectionParams.password) - }; + socket: { + url: req.protocol + '://' + req.get('host'), + path: '/ssh/socket.io' + }, + ssh: { + host: urlParams.host || '', + port: urlParams.port || 22 + }, + autoConnect: !!req.session.sshCredentials + }; // Read the client.htm file fs.readFile(path.join(clientPath, 'client.htm'), 'utf8', function(err, data) { diff --git a/app/routes.js b/app/routes.js index 717079e..f49def5 100644 --- a/app/routes.js +++ b/app/routes.js @@ -1,9 +1,9 @@ // server // /app/routes.js -var express = require('express'); -var router = express.Router(); -var handleConnection = require('./connectionHandler'); -var basicAuth = require('basic-auth'); +const express = require('express'); +const router = express.Router(); +const handleConnection = require('./connectionHandler'); +const basicAuth = require('basic-auth'); function auth(req, res, next) { var credentials = basicAuth(req); @@ -11,7 +11,8 @@ function auth(req, res, next) { res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH2"'); return res.status(401).send('Authentication required.'); } - req.sshCredentials = credentials; + // Store credentials in session + req.session.sshCredentials = credentials; next(); } @@ -22,11 +23,7 @@ router.get('/', function(req, res) { // Scenario 2: Auth required router.get('/host/:host', auth, function(req, res) { - handleConnection(req, res, { - host: req.params.host, - username: req.sshCredentials.name, - password: req.sshCredentials.pass - }); + handleConnection(req, res, { host: req.params.host }); }); module.exports = router; \ No newline at end of file diff --git a/app/socket.js b/app/socket.js index ccd7fe9..368150b 100644 --- a/app/socket.js +++ b/app/socket.js @@ -59,6 +59,13 @@ function handleConnection(socket, config) { */ function handleAuthentication(socket, creds, config) { console.log(`SOCKET AUTHENTICATE: ${socket.id}`) + const sessionCreds = socket.handshake.session.sshCredentials; + + if (sessionCreds) { + creds.username = sessionCreds.name; + creds.password = sessionCreds.pass; + } + if (isValidCredentials(creds)) { console.log(`SOCKET CREDENTIALS VALID: ${socket.id}`) initializeConnection(socket, creds, config) diff --git a/package-lock.json b/package-lock.json index 145c5fb..8224a92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -325,6 +325,11 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "integrity": "sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "base64id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", @@ -466,6 +471,15 @@ } } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -748,6 +762,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==" }, + "cookie-parser": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", + "integrity": "sha512-YN/8nzPcK5o6Op4MIzAd4H4qUal5+3UaMhVIeaafFYL0pKvBQA/9Yhzo7ZwvBpjdGshsiTAb1+FC37M6RdPDFg==", + "requires": { + "cookie": "0.1.3", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", + "integrity": "sha512-mWkFhcL+HVG1KjeCjEBVJJ7s4sAGMLiBDFSDs4bzzvgLZt7rW8BhP6XV/8b1+pNvx/skd3yYxPuaF3Z6LlQzyw==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -765,6 +795,14 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1617,6 +1655,81 @@ } } }, + "express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "requires": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "express-socket.io-session": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/express-socket.io-session/-/express-socket.io-session-1.3.5.tgz", + "integrity": "sha512-ila9jN7Pu9OuNIDzkuW+ZChR2Y0TzyyFITT7xiOWCjuGCDUWioD382zqxI7HOaa8kIhfs3wTLOZMU9h6buuOFw==", + "requires": { + "cookie-parser": "~1.3.3", + "crc": "^3.3.0", + "debug": "~2.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -2182,6 +2295,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3079,6 +3197,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3400,6 +3523,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4546,6 +4674,14 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 3eb0c88..9182951 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "body-parser": "^1.15.2", "debug": "~4.1.0", "express": "^4.14.1", + "express-session": "^1.18.0", + "express-socket.io-session": "^1.3.5", "fs": "0.0.1-security", "read-config-ng": "~3.0.7", "socket.io": "~2.2.0",