diff --git a/app/connectionHandler.js b/app/connectionHandler.js index 378eef6..2ca5cdf 100644 --- a/app/connectionHandler.js +++ b/app/connectionHandler.js @@ -5,25 +5,9 @@ const fs = require("fs") const path = require("path") const { createNamespacedDebug } = require("./logger") const { HTTP, MESSAGES, DEFAULTS } = require("./constants") +const { modifyHtml } = require("./utils") const debug = createNamespacedDebug("connectionHandler") -/** - * Modify the HTML content by replacing certain placeholders with dynamic values. - * @param {string} html - The original HTML content. - * @param {Object} config - The configuration object to inject into the HTML. - * @returns {string} - The modified HTML content. - */ -function modifyHtml(html, config) { - const modifiedHtml = html.replace( - /(src|href)="(?!http|\/\/)/g, - '$1="/ssh/assets/' - ) - - return modifiedHtml.replace( - "window.webssh2Config = null;", - `window.webssh2Config = ${JSON.stringify(config)};` - ) -} /** * Handle reading the file and processing the response. diff --git a/app/constants.js b/app/constants.js index dd546ea..725790f 100644 --- a/app/constants.js +++ b/app/constants.js @@ -1,7 +1,6 @@ // server // app/constants.js -const crypto = require("crypto") const path = require("path") /** @@ -14,7 +13,9 @@ const MESSAGES = { 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" + CLIENT_FILE_ERROR: "Error loading client file", + FAILED_SESSION_SAVE: "Failed to save session", + CONFIG_VALIDATION_ERROR: "Config validation error" } /** @@ -38,20 +39,6 @@ const DEFAULTS = { CLIENT_FILE: "client.htm" } -/** - * Socket events - */ -const SOCKET_EVENTS = { - CONNECTION: "connection", - DISCONNECT: "disconnect", - AUTHENTICATE: "authenticate", - AUTHENTICATION: "authentication", - TERMINAL: "terminal", - DATA: "data", - RESIZE: "resize", - CONTROL: "control" -} - /** * HTTP Related */ @@ -66,12 +53,11 @@ const HTTP = { PATH: "/ssh/host/", SAMESITE: "Strict", SESSION_SID: "webssh2_sid", - CREDS_CLEARED: "Credentials cleared." + CREDS_CLEARED: "Credentials cleared.", } module.exports = { MESSAGES, DEFAULTS, - SOCKET_EVENTS, HTTP } diff --git a/app/routes.js b/app/routes.js index 37e7c4a..6b8e69d 100644 --- a/app/routes.js +++ b/app/routes.js @@ -3,11 +3,11 @@ const express = require("express") const basicAuth = require("basic-auth") -const maskObject = require("jsmasker") const validator = require("validator") const { getValidatedHost, getValidatedPort, + maskSensitiveData, validateSshTerm } = require("./utils") const handleConnection = require("./connectionHandler") @@ -61,7 +61,7 @@ router.get("/host/:host", auth, function(req, res) { req.session.usedBasicAuth = true // Sanitize and log the sshCredentials object - const sanitizedCredentials = maskObject( + const sanitizedCredentials = maskSensitiveData( JSON.parse(JSON.stringify(req.session.sshCredentials)) ) debug("/ssh/host/ Credentials: ", sanitizedCredentials) diff --git a/app/server.js b/app/server.js index 709a000..f885fe1 100644 --- a/app/server.js +++ b/app/server.js @@ -16,7 +16,7 @@ function createServer(app) { * @param {Error} err - The error object */ function handleServerError(err) { - console.error("WebSSH2 server.listen ERROR:", err.code) + console.error("HTTP Server ERROR: %O", err) } /** diff --git a/app/socket.js b/app/socket.js index 060459e..27c8263 100644 --- a/app/socket.js +++ b/app/socket.js @@ -1,14 +1,18 @@ // server // app/socket.js -const maskObject = require("jsmasker") const validator = require("validator") const SSHConnection = require("./ssh") const { createNamespacedDebug } = require("./logger") const { SSHConnectionError, handleError } = require("./errors") const debug = createNamespacedDebug("socket") -const { validateSshTerm, isValidCredentials } = require("./utils") +const { + isValidCredentials, + maskSensitiveData, + validateSshTerm +} = require("./utils") +const { MESSAGES } = require("./constants") class WebSSH2Socket { constructor(socket, config) { @@ -38,7 +42,7 @@ class WebSSH2Socket { const creds = this.socket.handshake.session.sshCredentials debug( `handleConnection: ${this.socket.id}, Host: ${creds.host}: HTTP Basic Credentials Exist, creds: %O`, - maskObject(creds) + maskSensitiveData(creds) ) this.handleAuthenticate(creds) } else if (!this.sessionState.authenticated) { @@ -67,7 +71,7 @@ class WebSSH2Socket { } handleAuthenticate(creds) { - debug(`handleAuthenticate: ${this.socket.id}, %O`, maskObject(creds)) + debug(`handleAuthenticate: ${this.socket.id}, %O`, maskSensitiveData(creds)) if (isValidCredentials(creds)) { this.sessionState.term = validateSshTerm(creds.term) @@ -86,7 +90,7 @@ class WebSSH2Socket { initializeConnection(creds) { debug( `initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`, - maskObject(creds) + maskSensitiveData(creds) ) this.ssh @@ -278,7 +282,10 @@ class WebSSH2Socket { this.socket.handshake.session.save(err => { if (err) - console.error(`Failed to save session for ${this.socket.id}:`, err) + console.error( + `clearSessionCredentials: ${MESSAGES.FAILED_SESSION_SAVE} ${this.socket.id}:`, + err + ) }) } } diff --git a/app/ssh.js b/app/ssh.js index d743226..fe9098e 100644 --- a/app/ssh.js +++ b/app/ssh.js @@ -2,9 +2,9 @@ // app/ssh.js const SSH = require("ssh2").Client -const maskObject = require("jsmasker") const { createNamespacedDebug } = require("./logger") const { SSHConnectionError, handleError } = require("./errors") +const { maskSensitiveData } = require("./utils") const debug = createNamespacedDebug("ssh") @@ -17,7 +17,7 @@ function SSHConnection(config) { SSHConnection.prototype.connect = function(creds) { const self = this return new Promise(function(resolve, reject) { - debug("connect: %O", maskObject(creds)) + debug("connect: %O", maskSensitiveData(creds)) if (self.conn) { self.conn.end() @@ -51,12 +51,10 @@ SSHConnection.prototype.getSSHConfig = function(creds) { username: creds.username, password: creds.password, tryKeyboard: true, - algorithms: creds.algorithms || this.config.ssh.algorithms, - readyTimeout: creds.readyTimeout || this.config.ssh.readyTimeout, - keepaliveInterval: - creds.keepaliveInterval || this.config.ssh.keepaliveInterval, - keepaliveCountMax: - creds.keepaliveCountMax || this.config.ssh.keepaliveCountMax, + algorithms: this.config.ssh.algorithms, + readyTimeout: this.config.ssh.readyTimeout, + keepaliveInterval: this.config.ssh.keepaliveInterval, + keepaliveCountMax: this.config.ssh.keepaliveCountMax, debug: createNamespacedDebug("ssh2") } } diff --git a/app/utils.js b/app/utils.js index 32a43f6..4c559a2 100644 --- a/app/utils.js +++ b/app/utils.js @@ -2,8 +2,11 @@ // /app/utils.js const validator = require("validator") const crypto = require("crypto") +const Ajv = require("ajv") +const maskObject = require("jsmasker") const { createNamespacedDebug } = require("./logger") -const { DEFAULTS } = require("./constants") +const { DEFAULTS, MESSAGES } = require("./constants") +const { configSchema } = require("./config") const debug = createNamespacedDebug("utils") @@ -127,11 +130,71 @@ function validateSshTerm(term) { return validatedSshTerm ? term : null } +/** + * Validates the given configuration object. + * + * @param {Object} config - The configuration object to validate. + * @throws {Error} If the configuration object fails validation. + * @returns {Object} The validated configuration object. + */ +function validateConfig(config) { + const ajv = new Ajv() + const validate = ajv.compile(configSchema) + const valid = validate(config) + if (!valid) { + throw new Error( + `${MESSAGES.CONFIG_VALIDATION_ERROR}: ${ajv.errorsText(validate.errors)}` + ) + } + return config +} + +/** + * Modify the HTML content by replacing certain placeholders with dynamic values. + * @param {string} html - The original HTML content. + * @param {Object} config - The configuration object to inject into the HTML. + * @returns {string} - The modified HTML content. + */ +function modifyHtml(html, config) { + debug("modifyHtml") + const modifiedHtml = html.replace( + /(src|href)="(?!http|\/\/)/g, + '$1="/ssh/assets/' + ) + + return modifiedHtml.replace( + "window.webssh2Config = null;", + `window.webssh2Config = ${JSON.stringify(config)};` + ) +} + +/** + * Masks sensitive information in an object + * @param {Object} obj - The object to mask + * @param {Object} [options] - Optional configuration for masking + * @returns {Object} The masked object + */ +function maskSensitiveData(obj, options) { + const defaultOptions = { + // Add any default masking options here + // For example: + // password: true, + // token: true + } + + const maskingOptions = Object.assign({}, defaultOptions, options || {}) + + return maskObject(obj, maskingOptions) +} + module.exports = { deepMerge, generateSecureSecret, getValidatedHost, getValidatedPort, isValidCredentials, + maskSensitiveData, + modifyHtml, + validateConfig, validateSshTerm }