Update ssh.ts

This commit is contained in:
Karmaa
2025-07-31 15:20:27 -05:00
committed by GitHub
parent 4b53139c20
commit 38f836b94f

View File

@@ -10,19 +10,13 @@ const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): st
return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${sshIconSymbol}]`)} ${message}`; return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${sshIconSymbol}]`)} ${message}`;
}; };
const logger = { const logger = {
info: (msg: string): void => { info: (msg: string): void => console.log(formatMessage('info', chalk.cyan, msg)),
console.log(formatMessage('info', chalk.cyan, msg)); warn: (msg: string): void => console.warn(formatMessage('warn', chalk.yellow, msg)),
},
warn: (msg: string): void => {
console.warn(formatMessage('warn', chalk.yellow, msg));
},
error: (msg: string, err?: unknown): void => { error: (msg: string, err?: unknown): void => {
console.error(formatMessage('error', chalk.redBright, msg)); console.error(formatMessage('error', chalk.redBright, msg));
if (err) console.error(err); if (err) console.error(err);
}, },
success: (msg: string): void => { success: (msg: string): void => console.log(formatMessage('success', chalk.greenBright, msg)),
console.log(formatMessage('success', chalk.greenBright, msg));
},
debug: (msg: string): void => { debug: (msg: string): void => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.debug(formatMessage('debug', chalk.magenta, msg)); console.debug(formatMessage('debug', chalk.magenta, msg));
@@ -33,6 +27,7 @@ const logger = {
wss.on('connection', (ws: WebSocket) => { wss.on('connection', (ws: WebSocket) => {
let sshConn: Client | null = null; let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null; let sshStream: ClientChannel | null = null;
let keepAliveTimer: NodeJS.Timeout | null = null;
ws.on('close', () => { ws.on('close', () => {
cleanupSSH(); cleanupSSH();
@@ -54,38 +49,21 @@ wss.on('connection', (ws: WebSocket) => {
case 'connectToHost': case 'connectToHost':
handleConnectToHost(data); handleConnectToHost(data);
break; break;
case 'resize': case 'resize':
handleResize(data); handleResize(data);
break; break;
case 'disconnect': case 'disconnect':
cleanupSSH(); cleanupSSH();
break; break;
case 'input': case 'input':
if (sshStream) sshStream.write(data); if (sshStream) sshStream.write(data);
break; break;
default: default:
logger.warn('Unknown message type: ' + type); logger.warn('Unknown message type: ' + type);
} }
}); });
function handleConnectToHost(data: { function handleConnectToHost(data: any) {
cols: number;
rows: number;
hostConfig: {
ip: string;
port: number;
username: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
authType?: string;
};
}) {
const { cols, rows, hostConfig } = data; const { cols, rows, hostConfig } = data;
const { ip, port, username, password, key, keyPassword, keyType, authType } = hostConfig; const { ip, port, username, password, key, keyPassword, keyType, authType } = hostConfig;
@@ -94,13 +72,11 @@ wss.on('connection', (ws: WebSocket) => {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid username provided' })); ws.send(JSON.stringify({ type: 'error', message: 'Invalid username provided' }));
return; return;
} }
if (!ip || typeof ip !== 'string' || ip.trim() === '') { if (!ip || typeof ip !== 'string' || ip.trim() === '') {
logger.error('Invalid IP provided'); logger.error('Invalid IP provided');
ws.send(JSON.stringify({ type: 'error', message: 'Invalid IP provided' })); ws.send(JSON.stringify({ type: 'error', message: 'Invalid IP provided' }));
return; return;
} }
if (!port || typeof port !== 'number' || port <= 0) { if (!port || typeof port !== 'number' || port <= 0) {
logger.error('Invalid port provided'); logger.error('Invalid port provided');
ws.send(JSON.stringify({ type: 'error', message: 'Invalid port provided' })); ws.send(JSON.stringify({ type: 'error', message: 'Invalid port provided' }));
@@ -130,17 +106,12 @@ wss.on('connection', (ws: WebSocket) => {
sshStream = stream; sshStream = stream;
const keepaliveTimer = setInterval(() => {
if (sshStream && sshStream.writable) {
sshStream.write('\x00');
}
}, 30000);
stream.on('data', (chunk: Buffer) => { stream.on('data', (chunk: Buffer) => {
ws.send(JSON.stringify({ type: 'data', data: chunk.toString() })); ws.send(JSON.stringify({ type: 'data', data: chunk.toString() }));
}); });
stream.on('close', () => { stream.on('close', () => {
ws.send(JSON.stringify({ type: 'disconnected', message: 'SSH session closed' }));
cleanupSSH(); cleanupSSH();
}); });
@@ -150,32 +121,37 @@ wss.on('connection', (ws: WebSocket) => {
}); });
ws.send(JSON.stringify({ type: 'connected', message: 'SSH connected' })); ws.send(JSON.stringify({ type: 'connected', message: 'SSH connected' }));
keepAliveTimer = setInterval(() => {
if (sshStream && sshStream.writable) {
sshStream.write(''); // keepalive
}
}, 30000);
}); });
}); });
sshConn.on('error', (err: Error) => { sshConn.on('error', (err: Error) => {
logger.error('SSH connection error: ' + err.message); logger.error('SSH connection error: ' + err.message);
let errorMessage = 'SSH error: ' + err.message; let errorMessage = 'SSH error: ' + err.message;
if (err.message.includes('No matching key exchange algorithm')) { if (err.message.includes('No matching key exchange algorithm')) {
errorMessage = 'SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device.'; errorMessage = 'SSH error: No compatible key exchange algorithm found.';
} else if (err.message.includes('No matching cipher')) { } else if (err.message.includes('No matching cipher')) {
errorMessage = 'SSH error: No compatible cipher found. This may be due to an older SSH server or network device.'; errorMessage = 'SSH error: No compatible cipher found.';
} else if (err.message.includes('No matching MAC')) {
errorMessage = 'SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device.';
} else if (err.message.includes('ENOTFOUND') || err.message.includes('ENOENT')) { } else if (err.message.includes('ENOTFOUND') || err.message.includes('ENOENT')) {
errorMessage = 'SSH error: Could not resolve hostname or connect to server.'; errorMessage = 'SSH error: Could not resolve hostname or connect to server.';
} else if (err.message.includes('ECONNREFUSED')) { } else if (err.message.includes('ECONNREFUSED')) {
errorMessage = 'SSH error: Connection refused. The server may not be running or the port may be incorrect.'; errorMessage = 'SSH error: Connection refused.';
} else if (err.message.includes('ETIMEDOUT')) { } else if (err.message.includes('ETIMEDOUT')) {
errorMessage = 'SSH error: Connection timed out. Check your network connection and server availability.'; errorMessage = 'SSH error: Connection timed out.';
} }
ws.send(JSON.stringify({ type: 'error', message: errorMessage })); ws.send(JSON.stringify({ type: 'error', message: errorMessage }));
cleanupSSH(); cleanupSSH();
}); });
sshConn.on('close', () => { sshConn.on('close', () => {
ws.send(JSON.stringify({ type: 'disconnected', message: 'SSH connection closed' }));
cleanupSSH(); cleanupSSH();
}); });
@@ -186,7 +162,6 @@ wss.on('connection', (ws: WebSocket) => {
keepaliveInterval: 10000, keepaliveInterval: 10000,
keepaliveCountMax: 60, keepaliveCountMax: 60,
readyTimeout: 10000, readyTimeout: 10000,
algorithms: { algorithms: {
kex: [ kex: [
'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha256',
@@ -209,30 +184,18 @@ wss.on('connection', (ws: WebSocket) => {
'aes256-cbc', 'aes256-cbc',
'3des-cbc' '3des-cbc'
], ],
hmac: [ hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1', 'hmac-md5'],
'hmac-sha2-256', compress: ['none', 'zlib@openssh.com', 'zlib']
'hmac-sha2-512',
'hmac-sha1',
'hmac-md5'
],
compress: [
'none',
'zlib@openssh.com',
'zlib'
]
} }
}; };
if (authType === 'key' && key) { if (authType === 'key' && key) {
connectConfig.privateKey = key; connectConfig.privateKey = key;
if (keyPassword) { if (keyPassword) connectConfig.passphrase = keyPassword;
connectConfig.passphrase = keyPassword; if (keyType && keyType !== 'auto') connectConfig.privateKeyType = keyType;
}
if (keyType && keyType !== 'auto') {
connectConfig.privateKeyType = keyType;
}
} else if (authType === 'key') { } else if (authType === 'key') {
logger.error('SSH key authentication requested but no key provided'); logger.error('SSH key auth requested but no key provided');
ws.send(JSON.stringify({ type: 'error', message: 'SSH key authentication requested but no key provided' })); ws.send(JSON.stringify({ type: 'error', message: 'SSH key auth requested but no key provided' }));
return; return;
} else { } else {
connectConfig.password = password; connectConfig.password = password;
@@ -249,6 +212,10 @@ wss.on('connection', (ws: WebSocket) => {
} }
function cleanupSSH() { function cleanupSSH() {
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
keepAliveTimer = null;
}
if (sshStream) { if (sshStream) {
try { try {
sshStream.end(); sshStream.end();
@@ -257,7 +224,6 @@ wss.on('connection', (ws: WebSocket) => {
} }
sshStream = null; sshStream = null;
} }
if (sshConn) { if (sshConn) {
try { try {
sshConn.end(); sshConn.end();