chore: linting
This commit is contained in:
parent
dbcfc30cd0
commit
da21c89f20
7 changed files with 207 additions and 240 deletions
39
app/app.js
39
app/app.js
|
@ -1,9 +1,7 @@
|
||||||
// server
|
// server
|
||||||
// app/app.js
|
// app/app.js
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const createDebug = require("debug")
|
// const createDebug = require("debug")
|
||||||
const debug = createDebug("webssh2")
|
|
||||||
const http = require("http")
|
const http = require("http")
|
||||||
const express = require("express")
|
const express = require("express")
|
||||||
const socketIo = require("socket.io")
|
const socketIo = require("socket.io")
|
||||||
|
@ -14,6 +12,9 @@ const sharedsession = require("express-socket.io-session")
|
||||||
const config = require("./config")
|
const config = require("./config")
|
||||||
const socketHandler = require("./socket")
|
const socketHandler = require("./socket")
|
||||||
const sshRoutes = require("./routes")
|
const sshRoutes = require("./routes")
|
||||||
|
const { getCorsConfig } = require("./config")
|
||||||
|
|
||||||
|
// const debug = createDebug("webssh2")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and configures the Express application
|
* Creates and configures the Express application
|
||||||
|
@ -54,8 +55,8 @@ function createApp() {
|
||||||
}
|
}
|
||||||
res.cookie("basicauth", JSON.stringify(cookieData), {
|
res.cookie("basicauth", JSON.stringify(cookieData), {
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
path: '/ssh/host/',
|
path: "/ssh/host/",
|
||||||
sameSite: 'Strict'
|
sameSite: "Strict"
|
||||||
}) // ensure httOnly is false for the client to read the cookie
|
}) // ensure httOnly is false for the client to read the cookie
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
|
@ -104,18 +105,6 @@ function createServer(app) {
|
||||||
return http.createServer(app)
|
return http.createServer(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the CORS configuration
|
|
||||||
* @returns {Object} The CORS configuration object
|
|
||||||
*/
|
|
||||||
function getCorsConfig() {
|
|
||||||
return {
|
|
||||||
origin: config.origin || ["*.*"],
|
|
||||||
methods: ["GET", "POST"],
|
|
||||||
credentials: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles server errors
|
* Handles server errors
|
||||||
* @param {Error} err - The error object
|
* @param {Error} err - The error object
|
||||||
|
@ -124,6 +113,14 @@ function handleServerError(err) {
|
||||||
console.error("WebSSH2 server.listen ERROR:", err.code)
|
console.error("WebSSH2 server.listen ERROR:", err.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up Socket.IO event listeners
|
||||||
|
* @param {import('socket.io').Server} io - The Socket.IO server instance
|
||||||
|
*/
|
||||||
|
function setupSocketIOListeners(io) {
|
||||||
|
socketHandler(io, config)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and starts the server
|
* Initializes and starts the server
|
||||||
* @returns {Object} An object containing the server, io, and app instances
|
* @returns {Object} An object containing the server, io, and app instances
|
||||||
|
@ -148,13 +145,5 @@ function startServer() {
|
||||||
return { server, io, app }
|
return { server, io, app }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up Socket.IO event listeners
|
|
||||||
* @param {import('socket.io').Server} io - The Socket.IO server instance
|
|
||||||
*/
|
|
||||||
function setupSocketIOListeners(io) {
|
|
||||||
socketHandler(io, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't start the server immediately, export the function instead
|
// Don't start the server immediately, export the function instead
|
||||||
module.exports = { startServer, config }
|
module.exports = { startServer, config }
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
// server
|
// server
|
||||||
// app/config.js
|
// app/config.js
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const readConfig = require("read-config-ng")
|
const readConfig = require("read-config-ng")
|
||||||
const Ajv = require("ajv")
|
const Ajv = require("ajv")
|
||||||
const crypto = require("crypto")
|
const { deepMerge, generateSecureSecret } = require("./utils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Config
|
* @typedef {Object} Config
|
||||||
|
@ -204,15 +203,7 @@ const configSchema = {
|
||||||
required: ["secret", "name"]
|
required: ["secret", "name"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: [
|
required: ["listen", "http", "user", "ssh", "header", "options", "algorithms"]
|
||||||
"listen",
|
|
||||||
"http",
|
|
||||||
"user",
|
|
||||||
"ssh",
|
|
||||||
"header",
|
|
||||||
"options",
|
|
||||||
"algorithms"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -230,7 +221,7 @@ function getConfigPath() {
|
||||||
* @returns {Object} The configuration object
|
* @returns {Object} The configuration object
|
||||||
*/
|
*/
|
||||||
function readConfigFile(configPath) {
|
function readConfigFile(configPath) {
|
||||||
console.log("WebSSH2 service reading config from: " + configPath)
|
console.log(`WebSSH2 service reading config from: ${configPath}`)
|
||||||
return readConfig.sync(configPath)
|
return readConfig.sync(configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +238,7 @@ function validateConfig(config) {
|
||||||
console.log("WebSSH2 service validating config")
|
console.log("WebSSH2 service validating config")
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Config validation error: " + ajv.errorsText(validate.errors)
|
`Config validation error: ${ajv.errorsText(validate.errors)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
@ -261,7 +252,7 @@ function validateConfig(config) {
|
||||||
function logError(message, error) {
|
function logError(message, error) {
|
||||||
console.error(message)
|
console.error(message)
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("ERROR:\n\n " + error)
|
console.error(`ERROR:\n\n ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,54 +280,26 @@ function loadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedConfig = validateConfig(mergedConfig)
|
const validatedConfig = validateConfig(mergedConfig)
|
||||||
console.log('Merged and validated configuration')
|
console.log("Merged and validated configuration")
|
||||||
return validatedConfig
|
return validatedConfig
|
||||||
} else {
|
}
|
||||||
logError(
|
logError(
|
||||||
'\n\nERROR: Missing config.json for webssh. Using default config: ' +
|
`\n\nERROR: Missing config.json for webssh. Using default config: ${JSON.stringify(
|
||||||
JSON.stringify(defaultConfig) +
|
defaultConfig
|
||||||
'\n\n See config.json.sample for details\n\n'
|
)}\n\n See config.json.sample for details\n\n`
|
||||||
)
|
)
|
||||||
return defaultConfig
|
return defaultConfig
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(
|
logError(
|
||||||
'\n\nERROR: Problem loading config.json for webssh. Using default config: ' +
|
`\n\nERROR: Problem loading config.json for webssh. Using default config: ${JSON.stringify(
|
||||||
JSON.stringify(defaultConfig) +
|
defaultConfig
|
||||||
'\n\n See config.json.sample for details\n\n',
|
)}\n\n See config.json.sample for details\n\n`,
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
return defaultConfig
|
return defaultConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* The loaded configuration
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
|
@ -344,3 +307,14 @@ function deepMerge(target, source) {
|
||||||
const config = loadConfig()
|
const config = loadConfig()
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
/**
|
||||||
|
* Gets the CORS configuration
|
||||||
|
* @returns {Object} The CORS configuration object
|
||||||
|
*/
|
||||||
|
module.exports.getCorsConfig = function getCorsConfig() {
|
||||||
|
return {
|
||||||
|
origin: config.origin || ["*.*"],
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,59 +1,55 @@
|
||||||
// server
|
// server
|
||||||
// app/connectionHandler.js
|
// app/connectionHandler.js
|
||||||
const createDebug = require('debug')
|
const createDebug = require("debug")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const extend = require('util')._extend
|
|
||||||
const debug = createDebug('webssh2:connectionHandler')
|
|
||||||
|
|
||||||
function handleConnection(req, res, urlParams) {
|
const debug = createDebug("webssh2:connectionHandler")
|
||||||
debug('Handling connection')
|
|
||||||
urlParams = urlParams || {}
|
function handleConnection(req, res) {
|
||||||
|
debug("Handling connection")
|
||||||
|
|
||||||
const clientPath = path.resolve(
|
const clientPath = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
"..",
|
||||||
'node_modules',
|
"node_modules",
|
||||||
'webssh2_client',
|
"webssh2_client",
|
||||||
'client',
|
"client",
|
||||||
'public'
|
"public"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tempConfig = {
|
const tempConfig = {
|
||||||
socket: {
|
socket: {
|
||||||
url: req.protocol + '://' + req.get('host'),
|
url: `${req.protocol}://${req.get("host")}`,
|
||||||
path: '/ssh/socket.io'
|
path: "/ssh/socket.io"
|
||||||
},
|
},
|
||||||
autoConnect: false // Default to false
|
autoConnect: false // Default to false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the current route is /host/:host
|
// Check if the current route is /host/:host
|
||||||
debug('handleConnection req.path:', req.path)
|
debug("handleConnection req.path:", req.path)
|
||||||
if (req.path.startsWith('/host/')) {
|
if (req.path.startsWith("/host/")) {
|
||||||
tempConfig.autoConnect = true
|
tempConfig.autoConnect = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.readFile(
|
fs.readFile(path.join(clientPath, "client.htm"), "utf8", function(err, data) {
|
||||||
path.join(clientPath, 'client.htm'),
|
|
||||||
'utf8',
|
|
||||||
function (err, data) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(500).send('Error loading client file')
|
return res.status(500).send("Error loading client file")
|
||||||
}
|
}
|
||||||
|
|
||||||
var modifiedHtml = data.replace(
|
let modifiedHtml = data.replace(
|
||||||
/(src|href)="(?!http|\/\/)/g,
|
/(src|href)="(?!http|\/\/)/g,
|
||||||
'$1="/ssh/assets/'
|
'$1="/ssh/assets/'
|
||||||
)
|
)
|
||||||
|
|
||||||
modifiedHtml = modifiedHtml.replace(
|
modifiedHtml = modifiedHtml.replace(
|
||||||
'window.webssh2Config = null;',
|
"window.webssh2Config = null;",
|
||||||
'window.webssh2Config = ' + JSON.stringify(tempConfig) + ';'
|
`window.webssh2Config = ${JSON.stringify(tempConfig)};`
|
||||||
)
|
)
|
||||||
|
|
||||||
res.send(modifiedHtml)
|
res.send(modifiedHtml)
|
||||||
}
|
// Explicitly return to satisfy the linter
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = handleConnection
|
module.exports = handleConnection
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
// server
|
// server
|
||||||
// /app/routes.js
|
// /app/routes.js
|
||||||
const createDebug = require("debug")
|
const createDebug = require("debug")
|
||||||
|
|
||||||
const debug = createDebug("webssh2:routes")
|
const debug = createDebug("webssh2:routes")
|
||||||
const express = require("express")
|
const express = require("express")
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const handleConnection = require("./connectionHandler")
|
|
||||||
const basicAuth = require("basic-auth")
|
const basicAuth = require("basic-auth")
|
||||||
const { validateSshTerm } = require("./utils")
|
const maskObject = require("jsmasker")
|
||||||
const maskObject = require('jsmasker');
|
|
||||||
const validator = require("validator")
|
const validator = require("validator")
|
||||||
|
const { validateSshTerm } = require("./utils")
|
||||||
|
const handleConnection = require("./connectionHandler")
|
||||||
|
|
||||||
function auth(req, res, next) {
|
function auth(req, res, next) {
|
||||||
debug("auth: Basic Auth")
|
debug("auth: Basic Auth")
|
||||||
var credentials = basicAuth(req)
|
const credentials = basicAuth(req)
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
res.setHeader("WWW-Authenticate", 'Basic realm="WebSSH2"')
|
res.setHeader("WWW-Authenticate", 'Basic realm="WebSSH2"')
|
||||||
return res.status(401).send("Authentication required.")
|
return res.status(401).send("Authentication required.")
|
||||||
|
@ -27,13 +29,13 @@ function auth(req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scenario 1: No auth required, uses websocket authentication instead
|
// Scenario 1: No auth required, uses websocket authentication instead
|
||||||
router.get("/", function (req, res) {
|
router.get("/", function(req, res) {
|
||||||
debug("Accessed / route")
|
debug("Accessed / route")
|
||||||
handleConnection(req, res)
|
handleConnection(req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scenario 2: Auth required, uses HTTP Basic Auth
|
// Scenario 2: Auth required, uses HTTP Basic Auth
|
||||||
router.get("/host/:host", auth, function (req, res) {
|
router.get("/host/:host", auth, function(req, res) {
|
||||||
debug(`Accessed /ssh/host/${req.params.host} route`)
|
debug(`Accessed /ssh/host/${req.params.host} route`)
|
||||||
|
|
||||||
// Validate and sanitize host parameter
|
// Validate and sanitize host parameter
|
||||||
|
@ -73,12 +75,12 @@ router.get("/host/:host", auth, function (req, res) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear credentials route
|
// Clear credentials route
|
||||||
router.get("/clear-credentials", function (req, res) {
|
router.get("/clear-credentials", function(req, res) {
|
||||||
req.session.sshCredentials = null
|
req.session.sshCredentials = null
|
||||||
res.status(200).send("Credentials cleared.")
|
res.status(200).send("Credentials cleared.")
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/force-reconnect", function (req, res) {
|
router.get("/force-reconnect", function(req, res) {
|
||||||
req.session.sshCredentials = null
|
req.session.sshCredentials = null
|
||||||
res.status(401).send("Authentication required.")
|
res.status(401).send("Authentication required.")
|
||||||
})
|
})
|
||||||
|
|
176
app/socket.js
176
app/socket.js
|
@ -1,19 +1,19 @@
|
||||||
// server
|
// server
|
||||||
// app/socket.js
|
// app/socket.js
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const createDebug = require("debug")
|
const createDebug = require("debug")
|
||||||
|
|
||||||
const debug = createDebug("webssh2:socket")
|
const debug = createDebug("webssh2:socket")
|
||||||
|
const maskObject = require("jsmasker")
|
||||||
|
const validator = require("validator")
|
||||||
const SSHConnection = require("./ssh")
|
const SSHConnection = require("./ssh")
|
||||||
const { validateSshTerm } = require("./utils")
|
const { validateSshTerm } = require("./utils")
|
||||||
const maskObject = require('jsmasker');
|
|
||||||
const validator = require("validator")
|
|
||||||
|
|
||||||
module.exports = function (io, config) {
|
module.exports = function(io, config) {
|
||||||
io.on("connection", function (socket) {
|
io.on("connection", function(socket) {
|
||||||
debug("io.on connection: " + socket.id)
|
debug(`io.on connection: ${socket.id}`)
|
||||||
var ssh = new SSHConnection(config)
|
const ssh = new SSHConnection(config)
|
||||||
var sessionState = {
|
const sessionState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
|
@ -31,7 +31,7 @@ module.exports = function (io, config) {
|
||||||
* @param {Object} config - The configuration object.
|
* @param {Object} config - The configuration object.
|
||||||
*/
|
*/
|
||||||
function handleAuthenticate(creds) {
|
function handleAuthenticate(creds) {
|
||||||
debug("handleAuthenticate: " + socket.id + ", %O", maskObject(creds))
|
debug(`handleAuthenticate: ${socket.id}, %O`, maskObject(creds))
|
||||||
|
|
||||||
if (isValidCredentials(creds)) {
|
if (isValidCredentials(creds)) {
|
||||||
sessionState.term = validateSshTerm(creds.term)
|
sessionState.term = validateSshTerm(creds.term)
|
||||||
|
@ -39,9 +39,7 @@ module.exports = function (io, config) {
|
||||||
: config.ssh.term
|
: config.ssh.term
|
||||||
initializeConnection(creds)
|
initializeConnection(creds)
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(`handleAuthenticate: ${socket.id}, CREDENTIALS INVALID`)
|
||||||
"handleAuthenticate: " + socket.id + ", CREDENTIALS INVALID"
|
|
||||||
)
|
|
||||||
socket.emit("authentication", {
|
socket.emit("authentication", {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Invalid credentials format"
|
message: "Invalid credentials format"
|
||||||
|
@ -61,17 +59,13 @@ module.exports = function (io, config) {
|
||||||
*/
|
*/
|
||||||
function initializeConnection(creds) {
|
function initializeConnection(creds) {
|
||||||
debug(
|
debug(
|
||||||
"initializeConnection: " +
|
`initializeConnection: ${socket.id}, INITIALIZING SSH CONNECTION: Host: ${creds.host}, creds: %O`,
|
||||||
socket.id +
|
|
||||||
", INITIALIZING SSH CONNECTION: Host: " +
|
|
||||||
creds.host +
|
|
||||||
", creds: %O",
|
|
||||||
maskObject(creds)
|
maskObject(creds)
|
||||||
)
|
)
|
||||||
|
|
||||||
ssh
|
ssh
|
||||||
.connect(creds)
|
.connect(creds)
|
||||||
.then(function () {
|
.then(function() {
|
||||||
sessionState.authenticated = true
|
sessionState.authenticated = true
|
||||||
sessionState.username = creds.username
|
sessionState.username = creds.username
|
||||||
sessionState.password = creds.password
|
sessionState.password = creds.password
|
||||||
|
@ -79,65 +73,51 @@ module.exports = function (io, config) {
|
||||||
sessionState.port = creds.port
|
sessionState.port = creds.port
|
||||||
|
|
||||||
debug(
|
debug(
|
||||||
"initializeConnection: " +
|
`initializeConnection: ${socket.id} conn.on ready: Host: ${creds.host}`
|
||||||
socket.id +
|
|
||||||
" conn.on ready: Host: " +
|
|
||||||
creds.host
|
|
||||||
)
|
)
|
||||||
console.log(
|
console.log(
|
||||||
"initializeConnection: " +
|
`initializeConnection: ${socket.id} conn.on ready: ${creds.username}@${creds.host}:${creds.port} successfully connected`
|
||||||
socket.id +
|
|
||||||
" conn.on ready: " +
|
|
||||||
creds.username +
|
|
||||||
"@" +
|
|
||||||
creds.host +
|
|
||||||
":" +
|
|
||||||
creds.port +
|
|
||||||
" successfully connected"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var auth_result = { action: "auth_result", success: true }
|
const auth_result = { action: "auth_result", success: true }
|
||||||
debug(
|
debug(
|
||||||
"initializeConnection: " +
|
`initializeConnection: ${
|
||||||
socket.id +
|
socket.id
|
||||||
" conn.on ready: emitting authentication: " +
|
} conn.on ready: emitting authentication: ${JSON.stringify(
|
||||||
JSON.stringify(auth_result)
|
auth_result
|
||||||
|
)}`
|
||||||
)
|
)
|
||||||
socket.emit("authentication", auth_result)
|
socket.emit("authentication", auth_result)
|
||||||
|
|
||||||
// Emit consolidated permissions
|
// Emit consolidated permissions
|
||||||
var permissions = {
|
const permissions = {
|
||||||
autoLog: config.options.autoLog || false,
|
autoLog: config.options.autoLog || false,
|
||||||
allowReplay: config.options.allowReplay || false,
|
allowReplay: config.options.allowReplay || false,
|
||||||
allowReconnect: config.options.allowReconnect || false,
|
allowReconnect: config.options.allowReconnect || false,
|
||||||
allowReauth: config.options.allowReauth || false
|
allowReauth: config.options.allowReauth || false
|
||||||
}
|
}
|
||||||
debug(
|
debug(
|
||||||
"initializeConnection: " +
|
`initializeConnection: ${
|
||||||
socket.id +
|
socket.id
|
||||||
" conn.on ready: emitting permissions: " +
|
} conn.on ready: emitting permissions: ${JSON.stringify(
|
||||||
JSON.stringify(permissions)
|
permissions
|
||||||
|
)}`
|
||||||
)
|
)
|
||||||
socket.emit("permissions", permissions)
|
socket.emit("permissions", permissions)
|
||||||
|
|
||||||
updateElement("footer", "ssh://" + creds.host + ":" + creds.port)
|
updateElement("footer", `ssh://${creds.host}:${creds.port}`)
|
||||||
|
|
||||||
if (config.header && config.header.text !== null) {
|
if (config.header && config.header.text !== null) {
|
||||||
debug("initializeConnection header: " + config.header)
|
debug(`initializeConnection header: ${config.header}`)
|
||||||
updateElement("header", config.header.text)
|
updateElement("header", config.header.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request terminal information from client
|
// Request terminal information from client
|
||||||
socket.emit("getTerminal", true)
|
socket.emit("getTerminal", true)
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function(err) {
|
||||||
console.error(
|
console.error(
|
||||||
"initializeConnection: SSH CONNECTION ERROR: " +
|
`initializeConnection: SSH CONNECTION ERROR: ${socket.id}, Host: ${creds.host}, Error: ${err.message}`
|
||||||
socket.id +
|
|
||||||
", Host: " +
|
|
||||||
creds.host +
|
|
||||||
", Error: " +
|
|
||||||
err.message
|
|
||||||
)
|
)
|
||||||
if (err.level === "client-authentication") {
|
if (err.level === "client-authentication") {
|
||||||
socket.emit("authentication", {
|
socket.emit("authentication", {
|
||||||
|
@ -161,24 +141,24 @@ module.exports = function (io, config) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function handleTerminal(data) {
|
function handleTerminal(data) {
|
||||||
debug("handleTerminal: Received terminal data: " + JSON.stringify(data))
|
debug(`handleTerminal: Received terminal data: ${JSON.stringify(data)}`)
|
||||||
var term = data.term
|
const { term } = data
|
||||||
var rows = data.rows
|
const { rows } = data
|
||||||
var cols = data.cols
|
const { cols } = data
|
||||||
|
|
||||||
if (term && validateSshTerm(term)) {
|
if (term && validateSshTerm(term)) {
|
||||||
sessionState.term = term
|
sessionState.term = term
|
||||||
debug("handleTerminal: Set term to " + sessionState.term)
|
debug(`handleTerminal: Set term to ${sessionState.term}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rows && validator.isInt(rows.toString())) {
|
if (rows && validator.isInt(rows.toString())) {
|
||||||
sessionState.rows = parseInt(rows, 10)
|
sessionState.rows = parseInt(rows, 10)
|
||||||
debug("handleTerminal: Set rows to " + sessionState.rows)
|
debug(`handleTerminal: Set rows to ${sessionState.rows}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cols && validator.isInt(cols.toString())) {
|
if (cols && validator.isInt(cols.toString())) {
|
||||||
sessionState.cols = parseInt(cols, 10)
|
sessionState.cols = parseInt(cols, 10)
|
||||||
debug("handleTerminal: Set cols to " + sessionState.cols)
|
debug(`handleTerminal: Set cols to ${sessionState.cols}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we have terminal information, we can create the shell
|
// Now that we have terminal information, we can create the shell
|
||||||
|
@ -199,35 +179,35 @@ module.exports = function (io, config) {
|
||||||
cols: sessionState.cols,
|
cols: sessionState.cols,
|
||||||
rows: sessionState.rows
|
rows: sessionState.rows
|
||||||
})
|
})
|
||||||
.then(function (stream) {
|
.then(function(stream) {
|
||||||
stream.on("data", function (data) {
|
stream.on("data", function(data) {
|
||||||
socket.emit("data", data.toString("utf-8"))
|
socket.emit("data", data.toString("utf-8"))
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.stderr.on("data", function (data) {
|
stream.stderr.on("data", function(data) {
|
||||||
debug("STDERR: " + data)
|
debug(`STDERR: ${data}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.on("close", function (code, signal) {
|
stream.on("close", function(code, signal) {
|
||||||
debug("handleStreamClose: " + socket.id)
|
debug(`handleStreamClose: ${socket.id}`)
|
||||||
handleConnectionClose()
|
handleConnectionClose()
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("data", function (data) {
|
socket.on("data", function(data) {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
stream.write(data)
|
stream.write(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("control", function (controlData) {
|
socket.on("control", function(controlData) {
|
||||||
handleControl(controlData)
|
handleControl(controlData)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("resize", function (data) {
|
socket.on("resize", function(data) {
|
||||||
handleResize(data)
|
handleResize(data)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function(err) {
|
||||||
handleError("SHELL ERROR", err)
|
handleError("SHELL ERROR", err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -238,8 +218,8 @@ module.exports = function (io, config) {
|
||||||
* @param {Object} data - The resize data containing the number of rows and columns.
|
* @param {Object} data - The resize data containing the number of rows and columns.
|
||||||
*/
|
*/
|
||||||
function handleResize(data) {
|
function handleResize(data) {
|
||||||
var rows = data.rows
|
const { rows } = data
|
||||||
var cols = data.cols
|
const { cols } = data
|
||||||
|
|
||||||
if (ssh.stream) {
|
if (ssh.stream) {
|
||||||
if (rows && validator.isInt(rows.toString())) {
|
if (rows && validator.isInt(rows.toString())) {
|
||||||
|
@ -248,9 +228,7 @@ module.exports = function (io, config) {
|
||||||
if (cols && validator.isInt(cols.toString())) {
|
if (cols && validator.isInt(cols.toString())) {
|
||||||
sessionState.cols = parseInt(cols, 10)
|
sessionState.cols = parseInt(cols, 10)
|
||||||
}
|
}
|
||||||
debug(
|
debug(`Resizing terminal to ${sessionState.rows}x${sessionState.cols}`)
|
||||||
"Resizing terminal to " + sessionState.rows + "x" + sessionState.cols
|
|
||||||
)
|
|
||||||
ssh.resizeTerminal(sessionState.rows, sessionState.cols)
|
ssh.resizeTerminal(sessionState.rows, sessionState.cols)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,7 +240,7 @@ module.exports = function (io, config) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function handleControl(controlData) {
|
function handleControl(controlData) {
|
||||||
debug("handleControl: Received control data: " + controlData)
|
debug(`handleControl: Received control data: ${controlData}`)
|
||||||
if (
|
if (
|
||||||
validator.isIn(controlData, ["replayCredentials", "reauth"]) &&
|
validator.isIn(controlData, ["replayCredentials", "reauth"]) &&
|
||||||
ssh.stream
|
ssh.stream
|
||||||
|
@ -274,7 +252,7 @@ module.exports = function (io, config) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"handleControl: Invalid control command received: " + controlData
|
`handleControl: Invalid control command received: ${controlData}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,15 +263,15 @@ module.exports = function (io, config) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function replayCredentials() {
|
function replayCredentials() {
|
||||||
var password = sessionState.password
|
const { password } = sessionState
|
||||||
var allowReplay = config.options.allowReplay || false
|
const allowReplay = config.options.allowReplay || false
|
||||||
|
|
||||||
if (allowReplay && ssh.stream) {
|
if (allowReplay && ssh.stream) {
|
||||||
debug(`replayCredentials: ${socket.id} Replaying credentials for `)
|
debug(`replayCredentials: ${socket.id} Replaying credentials for `)
|
||||||
ssh.stream.write(password + "\n")
|
ssh.stream.write(`${password}\n`)
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
"replayCredentials: Credential replay not allowed for " + socket.id
|
`replayCredentials: Credential replay not allowed for ${socket.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,7 +280,7 @@ module.exports = function (io, config) {
|
||||||
* Handles reauthentication for the socket.
|
* Handles reauthentication for the socket.
|
||||||
*/
|
*/
|
||||||
function handleReauth() {
|
function handleReauth() {
|
||||||
debug("handleReauth: Reauthentication requested for " + socket.id)
|
debug(`handleReauth: Reauthentication requested for ${socket.id}`)
|
||||||
if (config.options.allowReauth) {
|
if (config.options.allowReauth) {
|
||||||
clearSessionCredentials()
|
clearSessionCredentials()
|
||||||
debug(`handleReauth: Reauthenticating ${socket.id}`)
|
debug(`handleReauth: Reauthenticating ${socket.id}`)
|
||||||
|
@ -322,9 +300,9 @@ module.exports = function (io, config) {
|
||||||
* @param {Error} err - The error object.
|
* @param {Error} err - The error object.
|
||||||
*/
|
*/
|
||||||
function handleError(context, err) {
|
function handleError(context, err) {
|
||||||
var errorMessage = err ? ": " + err.message : ""
|
const errorMessage = err ? `: ${err.message}` : ""
|
||||||
debug("WebSSH2 error: " + context + errorMessage)
|
debug(`WebSSH2 error: ${context}${errorMessage}`)
|
||||||
socket.emit("ssherror", "SSH " + context + errorMessage)
|
socket.emit("ssherror", `SSH ${context}${errorMessage}`)
|
||||||
handleConnectionClose()
|
handleConnectionClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,14 +314,7 @@ module.exports = function (io, config) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function updateElement(element, value) {
|
function updateElement(element, value) {
|
||||||
debug(
|
debug(`updateElement: ${socket.id}, Element: ${element}, Value: ${value}`)
|
||||||
"updateElement: " +
|
|
||||||
socket.id +
|
|
||||||
", Element: " +
|
|
||||||
element +
|
|
||||||
", Value: " +
|
|
||||||
value
|
|
||||||
)
|
|
||||||
socket.emit("updateUI", { element: element, value: value })
|
socket.emit("updateUI", { element: element, value: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,8 +324,8 @@ module.exports = function (io, config) {
|
||||||
* @param {string} reason - The reason for the closure.
|
* @param {string} reason - The reason for the closure.
|
||||||
*/
|
*/
|
||||||
function handleConnectionClose(reason) {
|
function handleConnectionClose(reason) {
|
||||||
debug("handleDisconnect: " + socket.id + ", Reason: " + reason)
|
debug(`handleDisconnect: ${socket.id}, Reason: ${reason}`)
|
||||||
debug("handleConnectionClose: " + socket.id)
|
debug(`handleConnectionClose: ${socket.id}`)
|
||||||
if (ssh) {
|
if (ssh) {
|
||||||
ssh.end()
|
ssh.end()
|
||||||
}
|
}
|
||||||
|
@ -366,7 +337,7 @@ module.exports = function (io, config) {
|
||||||
*/
|
*/
|
||||||
function clearSessionCredentials() {
|
function clearSessionCredentials() {
|
||||||
debug(
|
debug(
|
||||||
"clearSessionCredentials: Clearing session credentials for " + socket.id
|
`clearSessionCredentials: Clearing session credentials for ${socket.id}`
|
||||||
)
|
)
|
||||||
if (socket.handshake.session.sshCredentials) {
|
if (socket.handshake.session.sshCredentials) {
|
||||||
socket.handshake.session.sshCredentials.username = null
|
socket.handshake.session.sshCredentials.username = null
|
||||||
|
@ -376,28 +347,27 @@ module.exports = function (io, config) {
|
||||||
sessionState.authenticated = false
|
sessionState.authenticated = false
|
||||||
sessionState.username = null
|
sessionState.username = null
|
||||||
sessionState.password = null
|
sessionState.password = null
|
||||||
socket.handshake.session.save(function (err) {
|
socket.handshake.session.save(function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Failed to save session for " + socket.id + ":", err)
|
console.error(`Failed to save session for ${socket.id}:`, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for HTTP Basic Auth credentials
|
// Check for HTTP Basic Auth credentials
|
||||||
if (socket.handshake.session.usedBasicAuth && socket.handshake.session.sshCredentials) {
|
if (
|
||||||
|
socket.handshake.session.usedBasicAuth &&
|
||||||
|
socket.handshake.session.sshCredentials
|
||||||
|
) {
|
||||||
// if (socket.handshake.session.sshCredentials) {
|
// if (socket.handshake.session.sshCredentials) {
|
||||||
var creds = socket.handshake.session.sshCredentials
|
const creds = socket.handshake.session.sshCredentials
|
||||||
debug(
|
debug(
|
||||||
"handleConnection: " +
|
`handleConnection: ${socket.id}, Host: ${creds.host}: HTTP Basic Credentials Exist, creds: %O`,
|
||||||
socket.id +
|
|
||||||
", Host: " +
|
|
||||||
creds.host +
|
|
||||||
": HTTP Basic Credentials Exist, creds: %O",
|
|
||||||
maskObject(creds)
|
maskObject(creds)
|
||||||
)
|
)
|
||||||
handleAuthenticate(creds)
|
handleAuthenticate(creds)
|
||||||
} else if (!sessionState.authenticated) {
|
} else if (!sessionState.authenticated) {
|
||||||
debug("handleConnection: " + socket.id + ", emitting request_auth")
|
debug(`handleConnection: ${socket.id}, emitting request_auth`)
|
||||||
socket.emit("authentication", { action: "request_auth" })
|
socket.emit("authentication", { action: "request_auth" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
app/ssh.js
22
app/ssh.js
|
@ -1,11 +1,11 @@
|
||||||
// server
|
// server
|
||||||
// app/ssh.js
|
// app/ssh.js
|
||||||
"use strict"
|
|
||||||
|
|
||||||
const createDebug = require("debug")
|
const createDebug = require("debug")
|
||||||
const debug = createDebug("webssh2:ssh")
|
|
||||||
const SSH = require("ssh2").Client
|
const SSH = require("ssh2").Client
|
||||||
const maskObject = require('jsmasker');
|
const maskObject = require("jsmasker")
|
||||||
|
|
||||||
|
const debug = createDebug("webssh2:ssh")
|
||||||
|
|
||||||
function SSHConnection(config) {
|
function SSHConnection(config) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
@ -14,7 +14,7 @@ function SSHConnection(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
SSHConnection.prototype.connect = function(creds) {
|
SSHConnection.prototype.connect = function(creds) {
|
||||||
var self = this
|
const self = this
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
debug("connect: %O", maskObject(creds))
|
debug("connect: %O", maskObject(creds))
|
||||||
|
|
||||||
|
@ -24,15 +24,15 @@ SSHConnection.prototype.connect = function(creds) {
|
||||||
|
|
||||||
self.conn = new SSH()
|
self.conn = new SSH()
|
||||||
|
|
||||||
var sshConfig = self.getSSHConfig(creds)
|
const sshConfig = self.getSSHConfig(creds)
|
||||||
|
|
||||||
self.conn.on("ready", function() {
|
self.conn.on("ready", function() {
|
||||||
debug("connect: ready: " + creds.host)
|
debug(`connect: ready: ${creds.host}`)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.conn.on("error", function(err) {
|
self.conn.on("error", function(err) {
|
||||||
console.error("connect: error:" + err.message)
|
console.error(`connect: error:${err.message}`)
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -49,14 +49,16 @@ SSHConnection.prototype.getSSHConfig = function(creds) {
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
algorithms: creds.algorithms || this.config.ssh.algorithms,
|
algorithms: creds.algorithms || this.config.ssh.algorithms,
|
||||||
readyTimeout: creds.readyTimeout || this.config.ssh.readyTimeout,
|
readyTimeout: creds.readyTimeout || this.config.ssh.readyTimeout,
|
||||||
keepaliveInterval: creds.keepaliveInterval || this.config.ssh.keepaliveInterval,
|
keepaliveInterval:
|
||||||
keepaliveCountMax: creds.keepaliveCountMax || this.config.ssh.keepaliveCountMax,
|
creds.keepaliveInterval || this.config.ssh.keepaliveInterval,
|
||||||
|
keepaliveCountMax:
|
||||||
|
creds.keepaliveCountMax || this.config.ssh.keepaliveCountMax,
|
||||||
debug: createDebug("ssh")
|
debug: createDebug("ssh")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SSHConnection.prototype.shell = function(options) {
|
SSHConnection.prototype.shell = function(options) {
|
||||||
var self = this
|
const self = this
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
self.conn.shell(options, function(err, stream) {
|
self.conn.shell(options, function(err, stream) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
34
app/utils.js
34
app/utils.js
|
@ -2,6 +2,8 @@
|
||||||
// /app/utils.js
|
// /app/utils.js
|
||||||
const validator = require("validator")
|
const validator = require("validator")
|
||||||
const createDebug = require("debug")
|
const createDebug = require("debug")
|
||||||
|
const crypto = require("crypto")
|
||||||
|
|
||||||
const debug = createDebug("webssh2:utils")
|
const debug = createDebug("webssh2:utils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,3 +25,35 @@ function validateSshTerm(term) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.validateSshTerm = validateSshTerm
|
exports.validateSshTerm = validateSshTerm
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const output = Object.assign({}, target) // Avoid mutating target directly
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
if (Object.hasOwnProperty.call(source, key)) {
|
||||||
|
if (
|
||||||
|
source[key] instanceof Object &&
|
||||||
|
!Array.isArray(source[key]) &&
|
||||||
|
source[key] !== null
|
||||||
|
) {
|
||||||
|
output[key] = deepMerge(output[key] || {}, source[key])
|
||||||
|
} else {
|
||||||
|
output[key] = source[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
exports.deepMerge = deepMerge
|
||||||
|
/**
|
||||||
|
* Generates a secure random session secret
|
||||||
|
* @returns {string} A random 32-byte hex string
|
||||||
|
*/
|
||||||
|
function generateSecureSecret() {
|
||||||
|
return crypto.randomBytes(32).toString("hex")
|
||||||
|
}
|
||||||
|
exports.generateSecureSecret = generateSecureSecret
|
||||||
|
|
Loading…
Reference in a new issue