chore: refactor socket.js

This commit is contained in:
Bill Church 2024-08-21 12:56:13 +00:00
parent edceab346d
commit 76b2f787fd
No known key found for this signature in database

View file

@ -2,18 +2,24 @@
// app/socket.js // app/socket.js
const createDebug = require("debug") const createDebug = require("debug")
const debug = createDebug("webssh2:socket")
const maskObject = require("jsmasker") const maskObject = require("jsmasker")
const validator = require("validator") const validator = require("validator")
const SSHConnection = require("./ssh") const SSHConnection = require("./ssh")
const debug = createDebug("webssh2:socket")
const { validateSshTerm, isValidCredentials } = require("./utils") const { validateSshTerm, isValidCredentials } = require("./utils")
module.exports = function(io, config) { class WebSSH2Socket {
io.on("connection", function(socket) { /**
debug(`io.on connection: ${socket.id}`) * Creates an instance of WebSSH2Socket.
const ssh = new SSHConnection(config) * @param {SocketIO.Socket} socket - The socket instance.
const sessionState = { * @param {Object} config - The configuration object.
*/
constructor(socket, config) {
this.socket = socket
this.config = config
this.ssh = new SSHConnection(config)
this.sessionState = {
authenticated: false, authenticated: false,
username: null, username: null,
password: null, password: null,
@ -23,24 +29,50 @@ module.exports = function(io, config) {
cols: null, cols: null,
rows: null rows: null
} }
this.initializeSocketEvents()
}
/** /**
* Handles socket connections and SSH authentication for the webssh2 application. * Initializes the socket event listeners.
*
* @param {SocketIO.Server} io - The Socket.IO server instance.
* @param {Object} config - The configuration object.
*/ */
function handleAuthenticate(creds) { initializeSocketEvents() {
debug(`handleAuthenticate: ${socket.id}, %O`, maskObject(creds)) debug(`io.on connection: ${this.socket.id}`)
if (
this.socket.handshake.session.usedBasicAuth &&
this.socket.handshake.session.sshCredentials
) {
const creds = this.socket.handshake.session.sshCredentials
debug(
`handleConnection: ${this.socket.id}, Host: ${creds.host}: HTTP Basic Credentials Exist, creds: %O`,
maskObject(creds)
)
this.handleAuthenticate(creds)
} else if (!this.sessionState.authenticated) {
debug(`handleConnection: ${this.socket.id}, emitting request_auth`)
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))
}
/**
* Handles the authentication process.
* @param {Object} creds - The credentials for authentication.
*/
handleAuthenticate(creds) {
debug(`handleAuthenticate: ${this.socket.id}, %O`, maskObject(creds))
if (isValidCredentials(creds)) { if (isValidCredentials(creds)) {
sessionState.term = validateSshTerm(creds.term) this.sessionState.term = validateSshTerm(creds.term)
? creds.term ? creds.term
: config.ssh.term : this.config.ssh.term
initializeConnection(creds) this.initializeConnection(creds)
} else { } else {
console.warn(`handleAuthenticate: ${socket.id}, CREDENTIALS INVALID`) console.warn(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`)
socket.emit("authentication", { this.socket.emit("authentication", {
success: false, success: false,
message: "Invalid credentials format" message: "Invalid credentials format"
}) })
@ -48,207 +80,124 @@ module.exports = function(io, config) {
} }
/** /**
* Initializes an SSH connection using the provided credentials. * Initializes the SSH connection.
* * @param {Object} creds - The credentials for the SSH connection.
* @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) { initializeConnection(creds) {
debug( debug(
`initializeConnection: ${socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`, `initializeConnection: ${this.socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
maskObject(creds) maskObject(creds)
) )
ssh this.ssh
.connect(creds) .connect(creds)
.then(function() { .then(() => {
sessionState.authenticated = true this.sessionState = Object.assign({}, this.sessionState, {
sessionState.username = creds.username authenticated: true,
sessionState.password = creds.password username: creds.username,
sessionState.host = creds.host password: creds.password,
sessionState.port = creds.port host: creds.host,
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`
)
const authResult = { action: "auth_result", success: true } const authResult = { action: "auth_result", success: true }
debug( this.socket.emit("authentication", authResult)
`initializeConnection: ${
socket.id
} conn.on ready: emitting authentication: ${JSON.stringify(
authResult
)}`
)
socket.emit("authentication", authResult)
// Emit consolidated permissions
const permissions = { const permissions = {
autoLog: config.options.autoLog || false, autoLog: this.config.options.autoLog || false,
allowReplay: config.options.allowReplay || false, allowReplay: this.config.options.allowReplay || false,
allowReconnect: config.options.allowReconnect || false, allowReconnect: this.config.options.allowReconnect || false,
allowReauth: config.options.allowReauth || false allowReauth: this.config.options.allowReauth || false
} }
debug( this.socket.emit("permissions", permissions)
`initializeConnection: ${
socket.id
} conn.on ready: emitting permissions: ${JSON.stringify(
permissions
)}`
)
socket.emit("permissions", permissions)
updateElement("footer", `ssh://${creds.host}:${creds.port}`) this.updateElement("footer", `ssh://${creds.host}:${creds.port}`)
if (config.header && config.header.text !== null) { if (this.config.header && this.config.header.text !== null) {
debug(`initializeConnection header: ${config.header}`) this.updateElement("header", this.config.header.text)
updateElement("header", config.header.text)
} }
// Request terminal information from client this.socket.emit("getTerminal", true)
socket.emit("getTerminal", true)
}) })
.catch(function(err) { .catch(err => {
console.error( console.error(
`initializeConnection: SSH CONNECTION ERROR: ${socket.id}, Host: ${creds.host}, Error: ${err.message}` `initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}`
) )
if (err.level === "client-authentication") { this.handleError("SSH CONNECTION ERROR", err)
socket.emit("authentication", {
action: "auth_result",
success: false,
message: "Authentication failed"
})
} else {
handleError("SSH CONNECTION ERROR", err)
}
}) })
} }
/** /**
* Handles the terminal data. * Handles terminal data.
*
* @param {Object} data - 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) { handleTerminal(data) {
debug(`handleTerminal: Received terminal data: ${JSON.stringify(data)}`) const { term, rows, cols } = data
const { term } = data if (term && validateSshTerm(term)) this.sessionState.term = term
const { rows } = data if (rows && validator.isInt(rows.toString()))
const { cols } = data this.sessionState.rows = parseInt(rows, 10)
if (cols && validator.isInt(cols.toString()))
this.sessionState.cols = parseInt(cols, 10)
if (term && validateSshTerm(term)) { this.createShell()
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. * Creates a new SSH shell session.
*
* @function createShell
* @memberof module:socket
* @returns {void}
*/ */
function createShell() { createShell() {
ssh this.ssh
.shell({ .shell({
term: sessionState.term, term: this.sessionState.term,
cols: sessionState.cols, cols: this.sessionState.cols,
rows: sessionState.rows rows: this.sessionState.rows
})
.then(function(stream) {
stream.on("data", function(data) {
socket.emit("data", data.toString("utf-8"))
}) })
.then(stream => {
stream.on("data", data =>
this.socket.emit("data", data.toString("utf-8"))
)
stream.stderr.on("data", data => debug(`STDERR: ${data}`))
stream.on("close", (code, signal) =>
this.handleConnectionClose(code, signal)
)
stream.stderr.on("data", function(data) { this.socket.on("data", data => stream.write(data))
debug(`STDERR: ${data}`) this.socket.on("control", controlData =>
}) this.handleControl(controlData)
)
stream.on("close", function(code, signal) { this.socket.on("resize", data => this.handleResize(data))
debug(`handleStreamClose: ${socket.id}: ${code}, ${signal}`)
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)
}) })
.catch(err => this.handleError("SHELL ERROR", err))
} }
/** /**
* Handles the resize event of the terminal. * Handles the resize event for the terminal.
* * @param {Object} data - The resize data.
* @param {Object} data - The resize data containing the number of rows and columns.
*/ */
function handleResize(data) { handleResize(data) {
const { rows } = data const { rows, cols } = data
const { cols } = data if (this.ssh.stream) {
if (rows && validator.isInt(rows.toString()))
if (ssh.stream) { this.sessionState.rows = parseInt(rows, 10)
if (rows && validator.isInt(rows.toString())) { if (cols && validator.isInt(cols.toString()))
sessionState.rows = parseInt(rows, 10) this.sessionState.cols = parseInt(cols, 10)
} this.ssh.resizeTerminal(this.sessionState.rows, this.sessionState.cols)
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. * Handles control commands.
* * @param {string} controlData - The control command received.
* @param {string} controlData - The control data received.
* @returns {void}
*/ */
function handleControl(controlData) { handleControl(controlData) {
debug(`handleControl: Received control data: ${controlData}`)
if ( if (
validator.isIn(controlData, ["replayCredentials", "reauth"]) && validator.isIn(controlData, ["replayCredentials", "reauth"]) &&
ssh.stream this.ssh.stream
) { ) {
if (controlData === "replayCredentials") { if (controlData === "replayCredentials") {
replayCredentials() this.replayCredentials()
} else if (controlData === "reauth") { } else if (controlData === "reauth") {
handleReauth() this.handleReauth()
} }
} else { } else {
console.warn( console.warn(
@ -258,126 +207,74 @@ module.exports = function(io, config) {
} }
/** /**
* Replays the stored credentials for the current session. * Replays stored credentials.
*
* @returns {void}
*/ */
function replayCredentials() { replayCredentials() {
const { password } = sessionState if (this.config.options.allowReplay && this.ssh.stream) {
const allowReplay = config.options.allowReplay || false this.ssh.stream.write(`${this.sessionState.password}\n`)
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. * Handles reauthentication.
*/ */
function handleReauth() { handleReauth() {
debug(`handleReauth: Reauthentication requested for ${socket.id}`) if (this.config.options.allowReauth) {
if (config.options.allowReauth) { this.clearSessionCredentials()
clearSessionCredentials() this.socket.emit("authentication", { action: "reauth" })
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. * Handles errors.
* * @param {string} context - The error context.
* @param {string} context - The context in which the error occurred.
* @param {Error} err - The error object. * @param {Error} err - The error object.
*/ */
function handleError(context, err) { handleError(context, err) {
const errorMessage = err ? `: ${err.message}` : "" const errorMessage = err ? `: ${err.message}` : ""
debug(`WebSSH2 error: ${context}${errorMessage}`) this.socket.emit("ssherror", `SSH ${context}${errorMessage}`)
socket.emit("ssherror", `SSH ${context}${errorMessage}`) this.handleConnectionClose()
handleConnectionClose()
} }
/** /**
* Updates the specified element with the given value. * Updates a UI element on the client side.
*
* @param {string} element - The element to update. * @param {string} element - The element to update.
* @param {any} value - The value to set for the element. * @param {any} value - The new value for the element.
* @returns {void}
*/ */
function updateElement(element, value) { updateElement(element, value) {
debug(`updateElement: ${socket.id}, Element: ${element}, Value: ${value}`) this.socket.emit("updateUI", { element, value })
socket.emit("updateUI", { element: element, value: value })
} }
/** /**
* Handles the closure of a connection. * Handles the closure of the connection.
*
* @param {string} reason - The reason for the closure. * @param {string} reason - The reason for the closure.
*/ */
function handleConnectionClose(reason) { handleConnectionClose(reason) {
debug(`handleDisconnect: ${socket.id}, Reason: ${reason}`) if (this.ssh) this.ssh.end()
debug(`handleConnectionClose: ${socket.id}`) debug(`handleConnectionClose: ${this.socket.id}, Reason: ${reason}`)
if (ssh) { this.socket.disconnect(true)
ssh.end()
}
socket.disconnect(true)
} }
/** /**
* Clears the session credentials for the current socket. * Clears session credentials.
*/ */
function clearSessionCredentials() { clearSessionCredentials() {
debug( if (this.socket.handshake.session.sshCredentials) {
`clearSessionCredentials: Clearing session credentials for ${socket.id}` this.socket.handshake.session.sshCredentials.username = null
) this.socket.handshake.session.sshCredentials.password = null
const { session } = socket.handshake
if (session.sshCredentials) {
session.sshCredentials.username = null
session.sshCredentials.password = null
} }
this.socket.handshake.session.usedBasicAuth = false
this.sessionState.authenticated = false
this.sessionState.username = null
this.sessionState.password = null
session.usedBasicAuth = false this.socket.handshake.session.save(err => {
sessionState.authenticated = false if (err)
sessionState.username = null console.error(`Failed to save session for ${this.socket.id}:`, err)
sessionState.password = null
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) {
const 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) module.exports = function(io, config) {
socket.on("terminal", handleTerminal) io.on("connection", socket => new WebSSH2Socket(socket, config))
socket.on("disconnect", handleConnectionClose)
})
} }