398 lines
12 KiB
JavaScript
398 lines
12 KiB
JavaScript
// server
|
|
// app/socket.js
|
|
|
|
const createDebug = require("debug")
|
|
|
|
const debug = createDebug("webssh2:socket")
|
|
const maskObject = require("jsmasker")
|
|
const validator = require("validator")
|
|
const SSHConnection = require("./ssh")
|
|
const { validateSshTerm } = require("./utils")
|
|
|
|
module.exports = function(io, config) {
|
|
io.on("connection", function(socket) {
|
|
debug(`io.on connection: ${socket.id}`)
|
|
const ssh = new SSHConnection(config)
|
|
const 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`
|
|
)
|
|
|
|
const 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
|
|
const 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)}`)
|
|
const { term } = data
|
|
const { rows } = data
|
|
const { cols } = data
|
|
|
|
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) {
|
|
const { rows } = data
|
|
const { cols } = data
|
|
|
|
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() {
|
|
const { password } = sessionState
|
|
const 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) {
|
|
const 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) {
|
|
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)
|
|
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"
|
|
)
|
|
}
|