// server // app/socket.js "use strict" const createDebug = require("debug") const debug = createDebug("webssh2:socket") const SSHConnection = require("./ssh") const { validateSshTerm } = require("./utils") const maskObject = require('jsmasker'); const validator = require("validator") module.exports = function (io, config) { io.on("connection", function (socket) { debug("io.on connection: " + socket.id) var ssh = new SSHConnection(config) var sessionState = { authenticated: false, username: null, password: null, host: null, port: null, term: null, cols: null, rows: null } /** * Handles socket connections and SSH authentication for the webssh2 application. * * @param {SocketIO.Server} io - The Socket.IO server instance. * @param {Object} config - The configuration object. */ function handleAuthenticate(creds) { debug("handleAuthenticate: " + socket.id + ", %O", maskObject(creds)) if (isValidCredentials(creds)) { sessionState.term = validateSshTerm(creds.term) ? creds.term : config.ssh.term initializeConnection(creds) } else { console.warn( "handleAuthenticate: " + socket.id + ", CREDENTIALS INVALID" ) socket.emit("authentication", { success: false, message: "Invalid credentials format" }) } } /** * Initializes an SSH connection using the provided credentials. * * @param {Object} creds - The credentials required to establish the SSH connection. * @param {string} creds.host - The hostname or IP address of the SSH server. * @param {string} creds.username - The username for SSH authentication. * @param {string} creds.password - The password for SSH authentication. * @param {string} [creds.privateKey] - The private key for SSH authentication (optional). * @param {string} [creds.passphrase] - The passphrase for the private key (optional). */ function initializeConnection(creds) { debug( "initializeConnection: " + socket.id + ", INITIALIZING SSH CONNECTION: Host: " + creds.host + ", creds: %O", maskObject(creds) ) ssh .connect(creds) .then(function () { sessionState.authenticated = true sessionState.username = creds.username sessionState.password = creds.password sessionState.host = creds.host sessionState.port = creds.port debug( "initializeConnection: " + socket.id + " conn.on ready: Host: " + creds.host ) console.log( "initializeConnection: " + socket.id + " conn.on ready: " + creds.username + "@" + creds.host + ":" + creds.port + " successfully connected" ) var auth_result = { action: "auth_result", success: true } debug( "initializeConnection: " + socket.id + " conn.on ready: emitting authentication: " + JSON.stringify(auth_result) ) socket.emit("authentication", auth_result) // Emit consolidated permissions var permissions = { autoLog: config.options.autoLog || false, allowReplay: config.options.allowReplay || false, allowReconnect: config.options.allowReconnect || false, allowReauth: config.options.allowReauth || false } debug( "initializeConnection: " + socket.id + " conn.on ready: emitting permissions: " + JSON.stringify(permissions) ) socket.emit("permissions", permissions) updateElement("footer", "ssh://" + creds.host + ":" + creds.port) if (config.header && config.header.text !== null) { debug("initializeConnection header: " + config.header) updateElement("header", config.header.text) } // Request terminal information from client socket.emit("getTerminal", true) }) .catch(function (err) { console.error( "initializeConnection: SSH CONNECTION ERROR: " + socket.id + ", Host: " + creds.host + ", Error: " + err.message ) if (err.level === "client-authentication") { socket.emit("authentication", { action: "auth_result", success: false, message: "Authentication failed" }) } else { handleError("SSH CONNECTION ERROR", err) } }) } /** * Handles the terminal data. * * @param {Object} data - The terminal data. * @param {string} data.term - The terminal term. * @param {number} data.rows - The number of rows. * @param {number} data.cols - The number of columns. * @returns {void} */ function handleTerminal(data) { debug("handleTerminal: Received terminal data: " + JSON.stringify(data)) var term = data.term var rows = data.rows var cols = data.cols if (term && validateSshTerm(term)) { sessionState.term = term debug("handleTerminal: Set term to " + sessionState.term) } if (rows && validator.isInt(rows.toString())) { sessionState.rows = parseInt(rows, 10) debug("handleTerminal: Set rows to " + sessionState.rows) } if (cols && validator.isInt(cols.toString())) { sessionState.cols = parseInt(cols, 10) debug("handleTerminal: Set cols to " + sessionState.cols) } // Now that we have terminal information, we can create the shell createShell() } /** * Creates a shell using SSH and establishes a bidirectional communication between the shell and the socket. * * @function createShell * @memberof module:socket * @returns {void} */ function createShell() { ssh .shell({ term: sessionState.term, cols: sessionState.cols, rows: sessionState.rows }) .then(function (stream) { stream.on("data", function (data) { socket.emit("data", data.toString("utf-8")) }) stream.stderr.on("data", function (data) { debug("STDERR: " + data) }) stream.on("close", function (code, signal) { debug("handleStreamClose: " + socket.id) handleConnectionClose() }) socket.on("data", function (data) { if (stream) { stream.write(data) } }) socket.on("control", function (controlData) { handleControl(controlData) }) socket.on("resize", function (data) { handleResize(data) }) }) .catch(function (err) { handleError("SHELL ERROR", err) }) } /** * Handles the resize event of the terminal. * * @param {Object} data - The resize data containing the number of rows and columns. */ function handleResize(data) { var rows = data.rows var cols = data.cols if (ssh.stream) { if (rows && validator.isInt(rows.toString())) { sessionState.rows = parseInt(rows, 10) } if (cols && validator.isInt(cols.toString())) { sessionState.cols = parseInt(cols, 10) } debug( "Resizing terminal to " + sessionState.rows + "x" + sessionState.cols ) ssh.resizeTerminal(sessionState.rows, sessionState.cols) } } /** * Handles control data received from the client. * * @param {string} controlData - The control data received. * @returns {void} */ function handleControl(controlData) { debug("handleControl: Received control data: " + controlData) if ( validator.isIn(controlData, ["replayCredentials", "reauth"]) && ssh.stream ) { if (controlData === "replayCredentials") { replayCredentials() } else if (controlData === "reauth") { handleReauth() } } else { console.warn( "handleControl: Invalid control command received: " + controlData ) } } /** * Replays the stored credentials for the current session. * * @returns {void} */ function replayCredentials() { var password = sessionState.password var allowReplay = config.options.allowReplay || false if (allowReplay && ssh.stream) { debug(`replayCredentials: ${socket.id} Replaying credentials for `) ssh.stream.write(password + "\n") } else { console.warn( "replayCredentials: Credential replay not allowed for " + socket.id ) } } /** * Handles reauthentication for the socket. */ function handleReauth() { debug("handleReauth: Reauthentication requested for " + socket.id) if (config.options.allowReauth) { clearSessionCredentials() debug(`handleReauth: Reauthenticating ${socket.id}`) socket.emit("authentication", { action: "reauth" }) // handleConnectionClose() } else { console.warn( `handleReauth: Reauthentication not allowed for ${socket.id}` ) } } /** * Handles errors in the WebSSH2 application. * * @param {string} context - The context in which the error occurred. * @param {Error} err - The error object. */ function handleError(context, err) { var errorMessage = err ? ": " + err.message : "" debug("WebSSH2 error: " + context + errorMessage) socket.emit("ssherror", "SSH " + context + errorMessage) handleConnectionClose() } /** * Updates the specified element with the given value. * * @param {string} element - The element to update. * @param {any} value - The value to set for the element. * @returns {void} */ function updateElement(element, value) { debug( "updateElement: " + socket.id + ", Element: " + element + ", Value: " + value ) socket.emit("updateUI", { element: element, value: value }) } /** * Handles the closure of a connection. * * @param {string} reason - The reason for the closure. */ function handleConnectionClose(reason) { debug("handleDisconnect: " + socket.id + ", Reason: " + reason) debug("handleConnectionClose: " + socket.id) if (ssh) { ssh.end() } socket.disconnect(true) } /** * Clears the session credentials for the current socket. */ function clearSessionCredentials() { debug( "clearSessionCredentials: Clearing session credentials for " + socket.id ) if (socket.handshake.session.sshCredentials) { socket.handshake.session.sshCredentials.username = null socket.handshake.session.sshCredentials.password = null } socket.handshake.session.usedBasicAuth = false sessionState.authenticated = false sessionState.username = null sessionState.password = null socket.handshake.session.save(function (err) { if (err) { console.error("Failed to save session for " + socket.id + ":", err) } }) } // Check for HTTP Basic Auth credentials if (socket.handshake.session.usedBasicAuth && socket.handshake.session.sshCredentials) { // if (socket.handshake.session.sshCredentials) { var creds = socket.handshake.session.sshCredentials debug( "handleConnection: " + socket.id + ", Host: " + creds.host + ": HTTP Basic Credentials Exist, creds: %O", maskObject(creds) ) handleAuthenticate(creds) } else if (!sessionState.authenticated) { debug("handleConnection: " + socket.id + ", emitting request_auth") socket.emit("authentication", { action: "request_auth" }) } socket.on("authenticate", handleAuthenticate) socket.on("terminal", handleTerminal) socket.on("disconnect", handleConnectionClose) }) } /** * Checks if the provided credentials object is valid. * * @param {Object} creds - The credentials object. * @param {string} creds.username - The username. * @param {string} creds.password - The password. * @param {string} creds.host - The host. * @param {number} creds.port - The port. * @returns {boolean} - Returns true if the credentials are valid, otherwise false. */ function isValidCredentials(creds) { return ( creds && typeof creds.username === "string" && typeof creds.password === "string" && typeof creds.host === "string" && typeof creds.port === "number" ) }