chore: Add ajv dependency and refactor server startup code
This commit is contained in:
parent
50f1769fd5
commit
1ecf19c5df
6 changed files with 726 additions and 210 deletions
75
app/app.js
75
app/app.js
|
@ -6,30 +6,85 @@ const socketIo = require('socket.io')
|
||||||
const config = require('./config')
|
const config = require('./config')
|
||||||
const socketHandler = require('./socket')
|
const socketHandler = require('./socket')
|
||||||
|
|
||||||
const server = http.createServer()
|
/**
|
||||||
|
* Creates and configures the HTTP server
|
||||||
|
* @returns {http.Server} The HTTP server instance
|
||||||
|
*/
|
||||||
|
function createServer() {
|
||||||
|
return http.createServer()
|
||||||
|
}
|
||||||
|
|
||||||
const io = socketIo(server, {
|
/**
|
||||||
path: '/ssh/socket.io',
|
* Configures Socket.IO with the given server
|
||||||
cors: {
|
* @param {http.Server} server - The HTTP server instance
|
||||||
origin: config.origin || ["*.*"],
|
* @returns {import('socket.io').Server} The Socket.IO server instance
|
||||||
|
*/
|
||||||
|
function configureSocketIO(server) {
|
||||||
|
return socketIo(server, {
|
||||||
|
path: '/ssh/socket.io',
|
||||||
|
cors: getCorsConfig()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the CORS configuration
|
||||||
|
* @returns {Object} The CORS configuration object
|
||||||
|
*/
|
||||||
|
function getCorsConfig() {
|
||||||
|
return {
|
||||||
|
origin: config.origin || ['*.*'],
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST'],
|
||||||
credentials: true
|
credentials: true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
/**
|
||||||
|
* Sets up Socket.IO event listeners
|
||||||
|
* @param {import('socket.io').Server} io - The Socket.IO server instance
|
||||||
|
*/
|
||||||
|
function setupSocketIOListeners(io) {
|
||||||
|
socketHandler(io, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a new Socket.IO connection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function handleConnection(socket) {
|
||||||
|
logNewConnection(socket)
|
||||||
|
setupDisconnectListener(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs information about a new connection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function logNewConnection(socket) {
|
||||||
console.log(
|
console.log(
|
||||||
'New connection:',
|
'New connection:',
|
||||||
socket.id,
|
socket.id,
|
||||||
'Transport:',
|
'Transport:',
|
||||||
socket.conn.transport.name
|
socket.conn.transport.name
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
socketHandler(io, socket)
|
/**
|
||||||
|
* Sets up the disconnect listener for a socket
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function setupDisconnectListener(socket) {
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
console.log('Client disconnected:', socket.id, reason)
|
console.log('Client disconnected:', socket.id, reason)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Create and configure the server
|
||||||
|
const server = createServer()
|
||||||
|
const io = configureSocketIO(server)
|
||||||
|
|
||||||
|
// Set up Socket.IO listeners
|
||||||
|
setupSocketIOListeners(io)
|
||||||
|
|
||||||
|
// Log the config object to verify its contents
|
||||||
|
|
||||||
module.exports = { server, config, io }
|
module.exports = { server, config, io }
|
265
app/config.js
265
app/config.js
|
@ -1,11 +1,55 @@
|
||||||
// config.js
|
'use strict'
|
||||||
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const nodeRoot = path.dirname(require.main.filename)
|
const readConfig = require('read-config-ng')
|
||||||
const configPath = path.join(nodeRoot, 'config.json')
|
const Ajv = require('ajv')
|
||||||
|
|
||||||
// Default configuration
|
/**
|
||||||
let config = {
|
* @typedef {Object} Config
|
||||||
|
* @property {Object} listen - Listening configuration
|
||||||
|
* @property {string} listen.ip - IP address to listen on
|
||||||
|
* @property {number} listen.port - Port to listen on
|
||||||
|
* @property {Object} http - HTTP configuration
|
||||||
|
* @property {string[]} http.origins - Allowed origins
|
||||||
|
* @property {Object} user - User configuration
|
||||||
|
* @property {string|null} user.name - Username
|
||||||
|
* @property {string|null} user.password - Password
|
||||||
|
* @property {Object} ssh - SSH configuration
|
||||||
|
* @property {string|null} ssh.host - SSH host
|
||||||
|
* @property {number} ssh.port - SSH port
|
||||||
|
* @property {string} ssh.term - Terminal type
|
||||||
|
* @property {number} ssh.readyTimeout - Ready timeout
|
||||||
|
* @property {number} ssh.keepaliveInterval - Keepalive interval
|
||||||
|
* @property {number} ssh.keepaliveCountMax - Max keepalive count
|
||||||
|
* @property {Object} terminal - Terminal configuration
|
||||||
|
* @property {boolean} terminal.cursorBlink - Whether cursor blinks
|
||||||
|
* @property {number} terminal.scrollback - Scrollback limit
|
||||||
|
* @property {number} terminal.tabStopWidth - Tab stop width
|
||||||
|
* @property {string} terminal.bellStyle - Bell style
|
||||||
|
* @property {Object} header - Header configuration
|
||||||
|
* @property {string|null} header.text - Header text
|
||||||
|
* @property {string} header.background - Header background color
|
||||||
|
* @property {Object} options - Options configuration
|
||||||
|
* @property {boolean} options.challengeButton - Challenge button enabled
|
||||||
|
* @property {boolean} options.allowreauth - Allow reauthentication
|
||||||
|
* @property {Object} algorithms - Encryption algorithms
|
||||||
|
* @property {string[]} algorithms.kex - Key exchange algorithms
|
||||||
|
* @property {string[]} algorithms.cipher - Cipher algorithms
|
||||||
|
* @property {string[]} algorithms.hmac - HMAC algorithms
|
||||||
|
* @property {string[]} algorithms.compress - Compression algorithms
|
||||||
|
* @property {Object} serverlog - Server log configuration
|
||||||
|
* @property {boolean} serverlog.client - Client logging enabled
|
||||||
|
* @property {boolean} serverlog.server - Server logging enabled
|
||||||
|
* @property {boolean} accesslog - Access logging enabled
|
||||||
|
* @property {boolean} verify - Verification enabled
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration
|
||||||
|
* @type {Config}
|
||||||
|
*/
|
||||||
|
const defaultConfig = {
|
||||||
listen: {
|
listen: {
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: 2222
|
port: 2222
|
||||||
|
@ -37,7 +81,8 @@ let config = {
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
challengeButton: true,
|
challengeButton: true,
|
||||||
allowreauth: true
|
allowreauth: false,
|
||||||
|
allowReplay: false
|
||||||
},
|
},
|
||||||
algorithms: {
|
algorithms: {
|
||||||
kex: [
|
kex: [
|
||||||
|
@ -68,24 +113,196 @@ let config = {
|
||||||
verify: false
|
verify: false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
/**
|
||||||
if (fs.existsSync(configPath)) {
|
* Schema for validating the config
|
||||||
console.log('WebSSH2 service reading config from: ' + configPath)
|
*/
|
||||||
config = require('read-config-ng')(configPath)
|
const configSchema = {
|
||||||
} else {
|
type: 'object',
|
||||||
console.error(
|
properties: {
|
||||||
'\n\nERROR: Missing config.json for webssh. Current config: ' +
|
listen: {
|
||||||
JSON.stringify(config)
|
type: 'object',
|
||||||
)
|
properties: {
|
||||||
console.error('\n See config.json.sample for details\n\n')
|
ip: { type: 'string', format: 'ipv4' },
|
||||||
}
|
port: { type: 'integer', minimum: 1, maximum: 65535 }
|
||||||
} catch (err) {
|
},
|
||||||
console.error(
|
required: ['ip', 'port']
|
||||||
'\n\nERROR: Missing config.json for webssh. Current config: ' +
|
},
|
||||||
JSON.stringify(config)
|
http: {
|
||||||
)
|
type: 'object',
|
||||||
console.error('\n See config.json.sample for details\n\n')
|
properties: {
|
||||||
console.error('ERROR:\n\n ' + err)
|
origins: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['origins']
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: ['string', 'null'] },
|
||||||
|
password: { type: ['string', 'null'] }
|
||||||
|
},
|
||||||
|
required: ['name', 'password']
|
||||||
|
},
|
||||||
|
ssh: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
host: { type: ['string', 'null'] },
|
||||||
|
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
||||||
|
term: { type: 'string' },
|
||||||
|
readyTimeout: { type: 'integer' },
|
||||||
|
keepaliveInterval: { type: 'integer' },
|
||||||
|
keepaliveCountMax: { type: 'integer' }
|
||||||
|
},
|
||||||
|
required: ['host', 'port', 'term', 'readyTimeout', 'keepaliveInterval', 'keepaliveCountMax']
|
||||||
|
},
|
||||||
|
terminal: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
cursorBlink: { type: 'boolean' },
|
||||||
|
scrollback: { type: 'integer' },
|
||||||
|
tabStopWidth: { type: 'integer' },
|
||||||
|
bellStyle: { type: 'string' }
|
||||||
|
},
|
||||||
|
required: ['cursorBlink', 'scrollback', 'tabStopWidth', 'bellStyle']
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
text: { type: ['string', 'null'] },
|
||||||
|
background: { type: 'string' }
|
||||||
|
},
|
||||||
|
required: ['text', 'background']
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
challengeButton: { type: 'boolean' },
|
||||||
|
allowreauth: { type: 'boolean' },
|
||||||
|
allowReplay: { type: 'boolean' }
|
||||||
|
},
|
||||||
|
required: ['challengeButton', 'allowreauth', 'allowReplay']
|
||||||
|
},
|
||||||
|
algorithms: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
kex: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
},
|
||||||
|
cipher: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
},
|
||||||
|
hmac: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['kex', 'cipher', 'hmac', 'compress']
|
||||||
|
},
|
||||||
|
serverlog: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
client: { type: 'boolean' },
|
||||||
|
server: { type: 'boolean' }
|
||||||
|
},
|
||||||
|
required: ['client', 'server']
|
||||||
|
},
|
||||||
|
accesslog: { type: 'boolean' },
|
||||||
|
verify: { type: 'boolean' }
|
||||||
|
},
|
||||||
|
required: ['listen', 'http', 'user', 'ssh', 'terminal', 'header', 'options', 'algorithms', 'serverlog', 'accesslog', 'verify']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path to the config file
|
||||||
|
* @returns {string} The path to the config file
|
||||||
|
*/
|
||||||
|
function getConfigPath() {
|
||||||
|
const nodeRoot = path.dirname(require.main.filename)
|
||||||
|
return path.join(nodeRoot, 'config.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the config file
|
||||||
|
* @param {string} configPath - The path to the config file
|
||||||
|
* @returns {Config} The configuration object
|
||||||
|
*/
|
||||||
|
function readConfigFile(configPath) {
|
||||||
|
console.log('WebSSH2 service reading config from: ' + configPath)
|
||||||
|
return readConfig(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the configuration against the schema
|
||||||
|
* @param {Object} config - The configuration object to validate
|
||||||
|
* @returns {Object} The validated configuration object
|
||||||
|
* @throws {Error} If the configuration is invalid
|
||||||
|
*/
|
||||||
|
function validateConfig(config) {
|
||||||
|
const ajv = new Ajv()
|
||||||
|
const validate = ajv.compile(configSchema)
|
||||||
|
const valid = validate(config)
|
||||||
|
console.log('WebSSH2 service validating config')
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error('Config validation error: ' + ajv.errorsText(validate.errors))
|
||||||
|
}
|
||||||
|
console.log("config: ", JSON.stringify(config, null, 2))
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs an error message
|
||||||
|
* @param {string} message - The error message
|
||||||
|
* @param {Error} [error] - The error object
|
||||||
|
*/
|
||||||
|
function logError(message, error) {
|
||||||
|
console.error(message)
|
||||||
|
if (error) {
|
||||||
|
console.error('ERROR:\n\n ' + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the configuration
|
||||||
|
* @returns {Config} The loaded configuration
|
||||||
|
*/
|
||||||
|
function loadConfig() {
|
||||||
|
const configPath = getConfigPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const config = readConfigFile(configPath)
|
||||||
|
return validateConfig(config)
|
||||||
|
} else {
|
||||||
|
logError(
|
||||||
|
'\n\nERROR: Missing config.json for webssh. Current config: ' +
|
||||||
|
JSON.stringify(defaultConfig) +
|
||||||
|
'\n\n See config.json.sample for details\n\n'
|
||||||
|
)
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError(
|
||||||
|
'\n\nERROR: Missing config.json for webssh. Current config: ' +
|
||||||
|
JSON.stringify(defaultConfig) +
|
||||||
|
'\n\n See config.json.sample for details\n\n',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The loaded configuration
|
||||||
|
* @type {Config}
|
||||||
|
*/
|
||||||
|
const config = loadConfig()
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
|
448
app/socket.js
448
app/socket.js
|
@ -5,142 +5,316 @@ const debug = require('debug')
|
||||||
const debugWebSSH2 = require('debug')('WebSSH2')
|
const debugWebSSH2 = require('debug')('WebSSH2')
|
||||||
const SSH = require('ssh2').Client
|
const SSH = require('ssh2').Client
|
||||||
|
|
||||||
module.exports = function (io) {
|
let conn = null;
|
||||||
io.on('connection', (socket) => {
|
let stream = null;
|
||||||
let conn = null
|
|
||||||
let stream = null
|
|
||||||
console.log(`SOCKET CONNECT: ${socket.id}`)
|
|
||||||
|
|
||||||
// Remove existing listeners to prevent duplicates
|
/**
|
||||||
socket.removeAllListeners('authenticate')
|
* Handles WebSocket connections for SSH
|
||||||
socket.removeAllListeners('data')
|
* @param {import('socket.io').Server} io - The Socket.IO server instance
|
||||||
socket.removeAllListeners('resize')
|
* @param {Object} config - The configuration object
|
||||||
socket.removeAllListeners('disconnect')
|
*/
|
||||||
|
module.exports = function (io, config) {
|
||||||
// Authenticate user
|
io.on('connection', (socket) => handleConnection(socket, config))
|
||||||
socket.on('authenticate', (credentials) => {
|
}
|
||||||
console.log(`SOCKET AUTHENTICATE: ${socket.id}`)
|
|
||||||
if (isValidCredentials(credentials)) {
|
/**
|
||||||
console.log(`SOCKET AUTHENTICATE SUCCESS: ${socket.id}`)
|
* Handles a new WebSocket connection
|
||||||
initializeConnection(socket, credentials)
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
} else {
|
* @param {Object} config - The configuration object
|
||||||
console.log(`SOCKET AUTHENTICATE FAILED: ${socket.id}`)
|
*/
|
||||||
socket.emit('auth_result', {
|
function handleConnection(socket, config) {
|
||||||
success: false,
|
let isConnectionClosed = false;
|
||||||
message: 'Invalid credentials'
|
|
||||||
})
|
console.log(`SOCKET CONNECT: ${socket.id}`);
|
||||||
}
|
|
||||||
})
|
removeExistingListeners(socket)
|
||||||
|
setupInitialSocketListeners(socket, config)
|
||||||
socket.on('disconnect', (reason) => {
|
|
||||||
debugWebSSH2(`SOCKET DISCONNECT: ${socket.id}, Reason: ${reason}`)
|
/**
|
||||||
if (conn) {
|
* Removes existing listeners to prevent duplicates
|
||||||
conn.end()
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
}
|
*/
|
||||||
// Clean up listeners
|
function removeExistingListeners(socket) {
|
||||||
socket.removeAllListeners()
|
['authenticate', 'data', 'resize', 'disconnect', 'control'].forEach(event => {
|
||||||
})
|
socket.removeAllListeners(event)
|
||||||
|
})
|
||||||
socket.on('data', (data) => {
|
}
|
||||||
if (stream) {
|
|
||||||
stream.write(data)
|
/**
|
||||||
}
|
* Sets up initial socket event listeners
|
||||||
})
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {Object} config - The configuration object
|
||||||
socket.on('resize', (data) => {
|
*/
|
||||||
if (stream) {
|
function setupInitialSocketListeners(socket, config) {
|
||||||
stream.setWindow(data.rows, data.cols)
|
socket.on('authenticate', creds => handleAuthentication(socket, creds, config))
|
||||||
}
|
socket.on('disconnect', reason => handleDisconnect(socket, reason))
|
||||||
})
|
}
|
||||||
|
|
||||||
function initializeConnection (socket, credentials) {
|
/**
|
||||||
if (conn) {
|
* Handles authentication attempts
|
||||||
// If there's an existing connection, end it before creating a new one
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
conn.end()
|
* @param {Credentials} creds - The credentials for authentication
|
||||||
}
|
* @param {Object} config - The configuration object
|
||||||
|
*/
|
||||||
conn = new SSH()
|
function handleAuthentication(socket, creds, config) {
|
||||||
|
console.log(`SOCKET AUTHENTICATE: ${socket.id}`)
|
||||||
conn.on('ready', () => {
|
if (isValidCredentials(creds)) {
|
||||||
console.log(
|
console.log(`SOCKET AUTHENTICATE SUCCESS: ${socket.id}`)
|
||||||
`WebSSH2 Login: user=${credentials.username} from=${socket.handshake.address} host=${credentials.host} port=${credentials.port} sessionID=${socket.id}`
|
initializeConnection(socket, creds, config)
|
||||||
)
|
} else {
|
||||||
|
console.log(`SOCKET AUTHENTICATE FAILED: ${socket.id}`)
|
||||||
socket.emit('auth_result', { success: true })
|
socket.emit('auth_result', { success: false, message: 'Invalid credentials' })
|
||||||
socket.emit('allowreauth', true)
|
}
|
||||||
socket.emit('allowreplay', true)
|
}
|
||||||
socket.emit('title', `ssh://${credentials.host}`)
|
|
||||||
socket.emit('status', 'SSH CONNECTION ESTABLISHED')
|
/**
|
||||||
socket.emit('statusBackground', 'green')
|
* Initializes an SSH connection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
conn.shell(
|
* @param {Credentials} creds - The user credentials
|
||||||
{
|
* @param {Object} config - The configuration object
|
||||||
term: credentials.term,
|
*/
|
||||||
cols: credentials.cols,
|
function initializeConnection(socket, creds, config) {
|
||||||
rows: credentials.rows
|
if (conn) {
|
||||||
},
|
conn.end()
|
||||||
(err, str) => {
|
}
|
||||||
if (err) {
|
|
||||||
return SSHerror('EXEC ERROR', err)
|
conn = new SSH()
|
||||||
}
|
|
||||||
stream = str
|
conn.on('ready', () => {
|
||||||
|
console.log(`SSH CONNECTION READY: ${socket.id}`)
|
||||||
stream.on('data', (data) => {
|
socket.emit('auth_result', { success: true })
|
||||||
socket.emit('data', data.toString('utf-8'))
|
socket.emit('allowreplay', config.options.allowReplay || false)
|
||||||
})
|
socket.emit('allowreauth', config.options.allowreauth || false)
|
||||||
|
setupSSHListeners(socket, creds)
|
||||||
stream.on('close', (code, signal) => {
|
initializeShell(socket, creds)
|
||||||
SSHerror('STREAM CLOSE', {
|
})
|
||||||
message:
|
|
||||||
code || signal
|
conn.on('error', err => {
|
||||||
? `CODE: ${code} SIGNAL: ${signal}`
|
console.log(`SSH CONNECTION ERROR: ${socket.id}`, err)
|
||||||
: undefined
|
handleError(socket, 'SSH CONNECTION ERROR', err)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
conn.connect(getSSHConfig(creds, config))
|
||||||
stream.stderr.on('data', (data) => {
|
}
|
||||||
console.log('STDERR: ' + data)
|
|
||||||
})
|
/**
|
||||||
}
|
* Sets up SSH-specific event listeners
|
||||||
)
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
})
|
* @param {Credentials} creds - The user credentials
|
||||||
|
*/
|
||||||
conn.on('banner', (data) => {
|
function setupSSHListeners(socket, creds) {
|
||||||
socket.emit('data', data.replace(/\r?\n/g, '\r\n'))
|
conn.on('banner', data => handleBanner(socket, data))
|
||||||
})
|
conn.on('end', () => handleSSHEnd(socket))
|
||||||
|
conn.on('close', () => handleSSHClose(socket))
|
||||||
conn.on('end', () => SSHerror('CONN END BY HOST'))
|
|
||||||
conn.on('close', () => SSHerror('CONN CLOSE'))
|
socket.on('data', data => handleData(socket, stream, data))
|
||||||
conn.on('error', (err) => SSHerror('CONN ERROR', err))
|
socket.on('resize', data => handleResize(stream, data))
|
||||||
|
socket.on('control', controlData => handleControl(socket, stream, creds, controlData, config))
|
||||||
conn.connect({
|
}
|
||||||
host: credentials.host,
|
|
||||||
port: credentials.port,
|
/**
|
||||||
username: credentials.username,
|
* Initializes the SSH shell
|
||||||
password: credentials.password,
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
tryKeyboard: true,
|
* @param {Credentials} creds - The user credentials
|
||||||
algorithms: credentials.algorithms,
|
*/
|
||||||
readyTimeout: credentials.readyTimeout,
|
function initializeShell(socket, creds) {
|
||||||
keepaliveInterval: credentials.keepaliveInterval,
|
conn.shell(
|
||||||
keepaliveCountMax: credentials.keepaliveCountMax,
|
{
|
||||||
debug: debug('ssh2')
|
term: creds.term,
|
||||||
})
|
cols: creds.cols,
|
||||||
}
|
rows: creds.rows
|
||||||
|
},
|
||||||
function SSHerror (myFunc, err) {
|
(err, str) => {
|
||||||
const errorMessage = err ? `: ${err.message}` : ''
|
if (err) {
|
||||||
console.log(`WebSSH2 error: ${myFunc}${errorMessage}`)
|
return handleError(socket, 'EXEC ERROR', err)
|
||||||
socket.emit('ssherror', `SSH ${myFunc}${errorMessage}`)
|
}
|
||||||
if (conn) {
|
stream = str
|
||||||
conn.end()
|
|
||||||
}
|
stream.on('data', data => socket.emit('data', data.toString('utf-8')))
|
||||||
// Don't disconnect the socket here, let the client handle reconnection if necessary
|
stream.on('close', (code, signal) => {
|
||||||
// socket.disconnect(true);
|
handleError(socket, 'STREAM CLOSE', {
|
||||||
}
|
message: code || signal ? `CODE: ${code} SIGNAL: ${signal}` : undefined
|
||||||
|
})
|
||||||
function isValidCredentials (credentials) {
|
})
|
||||||
// Implement your credential validation logic here
|
stream.stderr.on('data', data => console.log('STDERR: ' + data))
|
||||||
return credentials && credentials.username && credentials.password
|
}
|
||||||
}
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the 'banner' event of the SSH connection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {string} data - The banner data
|
||||||
|
*/
|
||||||
|
function handleBanner(socket, data) {
|
||||||
|
socket.emit('data', data.replace(/\r?\n/g, '\r\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the SSH connection end event
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function handleSSHEnd(socket) {
|
||||||
|
console.log(`SSH CONNECTION ENDED: ${socket.id}`)
|
||||||
|
handleConnectionClose(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the SSH connection close event
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function handleSSHClose(socket) {
|
||||||
|
console.log(`SSH CONNECTION CLOSED: ${socket.id}`)
|
||||||
|
handleConnectionClose(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the closure of the SSH connection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function handleConnectionClose(socket) {
|
||||||
|
isConnectionClosed = true
|
||||||
|
if (stream) {
|
||||||
|
stream.end()
|
||||||
|
stream = null
|
||||||
|
}
|
||||||
|
if (conn) {
|
||||||
|
conn.end()
|
||||||
|
conn = null
|
||||||
|
}
|
||||||
|
socket.emit('connection_closed')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles socket disconnection
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {string} reason - The reason for disconnection
|
||||||
|
*/
|
||||||
|
function handleDisconnect(socket, reason) {
|
||||||
|
console.log(`SOCKET DISCONNECT: ${socket.id}, Reason: ${reason}`)
|
||||||
|
handleConnectionClose(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming data from the client
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {import('ssh2').Channel} stream - The SSH stream
|
||||||
|
* @param {string} data - The incoming data
|
||||||
|
*/
|
||||||
|
function handleData(socket, stream, data) {
|
||||||
|
if (stream && !isConnectionClosed) {
|
||||||
|
try {
|
||||||
|
stream.write(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error writing to stream:', error.message)
|
||||||
|
handleConnectionClose(socket)
|
||||||
|
}
|
||||||
|
} else if (isConnectionClosed) {
|
||||||
|
console.log('Attempted to write to closed connection')
|
||||||
|
socket.emit('connection_closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles terminal resize events
|
||||||
|
* @param {import('ssh2').Channel} stream - The SSH stream
|
||||||
|
* @param {Object} data - The resize data
|
||||||
|
* @param {number} data.rows - The number of rows
|
||||||
|
* @param {number} data.cols - The number of columns
|
||||||
|
*/
|
||||||
|
function handleResize(stream, data) {
|
||||||
|
if (stream) {
|
||||||
|
stream.setWindow(data.rows, data.cols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles control commands from the client
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {import('ssh2').Channel} stream - The SSH stream
|
||||||
|
* @param {Credentials} credentials - The user credentials
|
||||||
|
* @param {string} controlData - The control command
|
||||||
|
* @param {Object} config - The configuration object
|
||||||
|
*/
|
||||||
|
function handleControl(socket, stream, credentials, controlData, config) {
|
||||||
|
console.log(`Received control data: ${controlData}`);
|
||||||
|
|
||||||
|
if (controlData === 'replayCredentials' && stream && credentials) {
|
||||||
|
replayCredentials(socket, stream, credentials, config);
|
||||||
|
} else if (controlData === 'reauth' && config.options.allowreauth) {
|
||||||
|
handleReauth(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replays the user credentials to the SSH stream
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {import('ssh2').Channel} stream - The SSH stream
|
||||||
|
* @param {Credentials} credentials - The user credentials
|
||||||
|
* @param {Object} config - The configuration object
|
||||||
|
*/
|
||||||
|
function replayCredentials(socket, stream, credentials, config) {
|
||||||
|
let allowReplay = config.options.allowReplay || false;
|
||||||
|
|
||||||
|
if (allowReplay) {
|
||||||
|
console.log(`Replaying credentials for ${socket.id}`);
|
||||||
|
stream.write(credentials.password + '\n');
|
||||||
|
} else {
|
||||||
|
console.log(`Credential replay not allowed for ${socket.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles reauthentication request
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
*/
|
||||||
|
function handleReauth(socket) {
|
||||||
|
console.log(`Reauthentication requested for ${socket.id}`);
|
||||||
|
handleConnectionClose(socket);
|
||||||
|
socket.emit('reauth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles SSH errors
|
||||||
|
* @param {import('socket.io').Socket} socket - The Socket.IO socket
|
||||||
|
* @param {string} context - The context where the error occurred
|
||||||
|
* @param {Error} [err] - The error object
|
||||||
|
*/
|
||||||
|
function handleError(socket, context, err) {
|
||||||
|
const errorMessage = err ? `: ${err.message}` : ''
|
||||||
|
console.log(`WebSSH2 error: ${context}${errorMessage}`)
|
||||||
|
socket.emit('ssherror', `SSH ${context}${errorMessage}`)
|
||||||
|
handleConnectionClose(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the provided credentials
|
||||||
|
* @param {Credentials} credentials - The credentials to validate
|
||||||
|
* @returns {boolean} Whether the credentials are valid
|
||||||
|
*/
|
||||||
|
function isValidCredentials(credentials) {
|
||||||
|
// Implement your credential validation logic here
|
||||||
|
return credentials && credentials.username && credentials.password
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the SSH configuration object
|
||||||
|
* @param {Credentials} credentials - The user credentials
|
||||||
|
* @param {Object} config - The configuration object
|
||||||
|
* @returns {import('ssh2').ConnectConfig} The SSH configuration object
|
||||||
|
*/
|
||||||
|
function getSSHConfig(credentials, config) {
|
||||||
|
return {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
tryKeyboard: true,
|
||||||
|
algorithms: credentials.algorithms,
|
||||||
|
readyTimeout: credentials.readyTimeout,
|
||||||
|
keepaliveInterval: credentials.keepaliveInterval,
|
||||||
|
keepaliveCountMax: credentials.keepaliveCountMax,
|
||||||
|
debug: debug('ssh2')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
53
index.js
53
index.js
|
@ -1,20 +1,51 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
/* jshint esversion: 6, asi: true, node: true */
|
|
||||||
/*
|
/**
|
||||||
* index.js
|
* index.js
|
||||||
*
|
*
|
||||||
* WebSSH2 - Web to SSH2 gateway
|
* WebSSH2 - Web to SSH2 gateway
|
||||||
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
|
* Bill Church - https://github.com/billchurch/WebSSH2 - May 2017
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { server, config } = require('./app/app')
|
const { server, config } = require('./app/app')
|
||||||
|
|
||||||
server.listen(config.listen.port, config.listen.ip, () => {
|
/**
|
||||||
console.log(
|
* Starts the server
|
||||||
`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`
|
* @param {Object} config - The server configuration
|
||||||
)
|
* @param {string} config.listen.ip - The IP address to listen on
|
||||||
})
|
* @param {number} config.listen.port - The port to listen on
|
||||||
|
* @param {import('http').Server} server - The HTTP server instance
|
||||||
|
*/
|
||||||
|
function startServer(config, server) {
|
||||||
|
server.listen(config.listen.port, config.listen.ip, () => {
|
||||||
|
console.log(
|
||||||
|
`WebSSH2 service listening on ${config.listen.ip}:${config.listen.port}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
server.on('error', function (err) {
|
server.on('error', handleServerError)
|
||||||
console.log('WebSSH2 server.listen ERROR: ' + err.code)
|
}
|
||||||
})
|
|
||||||
|
/**
|
||||||
|
* Handles server errors
|
||||||
|
* @param {Error} err - The error object
|
||||||
|
*/
|
||||||
|
function handleServerError(err) {
|
||||||
|
console.error('WebSSH2 server.listen ERROR:', err.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to start the application
|
||||||
|
*/
|
||||||
|
function main() {
|
||||||
|
startServer(config, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
main()
|
||||||
|
|
||||||
|
// For testing purposes, export the functions
|
||||||
|
module.exports = {
|
||||||
|
startServer,
|
||||||
|
handleServerError
|
||||||
|
}
|
92
package-lock.json
generated
92
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "webssh2-server",
|
"name": "webssh2-server",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -31,15 +31,12 @@
|
||||||
"integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA=="
|
"integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA=="
|
||||||
},
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.12.6",
|
"version": "4.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"co": "^4.6.0",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"json-stable-stringify": "^1.0.1"
|
||||||
"json-schema-traverse": "^0.4.1",
|
|
||||||
"uri-js": "^4.2.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
|
@ -446,7 +443,6 @@
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-define-property": "^1.0.0",
|
"es-define-property": "^1.0.0",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
@ -580,6 +576,11 @@
|
||||||
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
|
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"co": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="
|
||||||
|
},
|
||||||
"collection-visit": {
|
"collection-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||||
|
@ -789,7 +790,6 @@
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-define-property": "^1.0.0",
|
"es-define-property": "^1.0.0",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
@ -1015,7 +1015,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"get-intrinsic": "^1.2.4"
|
"get-intrinsic": "^1.2.4"
|
||||||
}
|
}
|
||||||
|
@ -1023,8 +1022,7 @@
|
||||||
"es-errors": {
|
"es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"es-object-atoms": {
|
"es-object-atoms": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -1109,6 +1107,18 @@
|
||||||
"text-table": "^0.2.0"
|
"text-table": "^0.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
@ -1675,8 +1685,7 @@
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"function.prototype.name": {
|
"function.prototype.name": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
|
@ -1706,7 +1715,6 @@
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
|
@ -1808,7 +1816,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"get-intrinsic": "^1.1.3"
|
"get-intrinsic": "^1.1.3"
|
||||||
}
|
}
|
||||||
|
@ -1890,7 +1897,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-define-property": "^1.0.0"
|
"es-define-property": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -1898,14 +1904,12 @@
|
||||||
"has-proto": {
|
"has-proto": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"has-symbols": {
|
"has-symbols": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"has-tostringtag": {
|
"has-tostringtag": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -1952,7 +1956,6 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
|
@ -2405,6 +2408,24 @@
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"json-stable-stringify": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
|
||||||
|
"requires": {
|
||||||
|
"call-bind": "^1.0.5",
|
||||||
|
"isarray": "^2.0.5",
|
||||||
|
"jsonify": "^0.0.1",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"json-stable-stringify-without-jsonify": {
|
"json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
|
@ -2416,6 +2437,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||||
},
|
},
|
||||||
|
"jsonify": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="
|
||||||
|
},
|
||||||
"jsx-ast-utils": {
|
"jsx-ast-utils": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
|
||||||
|
@ -2781,8 +2807,7 @@
|
||||||
"object-keys": {
|
"object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"object-visit": {
|
"object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
@ -3419,7 +3444,6 @@
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"define-data-property": "^1.1.4",
|
"define-data-property": "^1.1.4",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
@ -3988,6 +4012,20 @@
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"slice-ansi": "1.0.0",
|
"slice-ansi": "1.0.0",
|
||||||
"string-width": "^2.1.1"
|
"string-width": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"term-size": {
|
"term-size": {
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"url": "https://github.com/billchurch/WebSSH2/issues"
|
"url": "https://github.com/billchurch/WebSSH2/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "^4.11.8",
|
||||||
"debug": "~4.1.0",
|
"debug": "~4.1.0",
|
||||||
"read-config-ng": "~3.0.7",
|
"read-config-ng": "~3.0.7",
|
||||||
"socket.io": "~2.2.0",
|
"socket.io": "~2.2.0",
|
||||||
|
|
Loading…
Reference in a new issue