fix: SSH host key verification — send verifyId, track pending clients, guard stale callbacks
Frontend sends verifyId with host-key-accept so backend can correlate the verification response. Backend tracks pre-ready connections in pendingClients map, destroys on error/disconnect, and guards against calling verify() after the connection has already timed out or errored (_destruct crash fix). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63315f94c4
commit
8d4ee04285
@ -17,6 +17,7 @@ export interface SshSession {
|
||||
export class SshConnectionService {
|
||||
private readonly logger = new Logger(SshConnectionService.name);
|
||||
private sessions = new Map<string, SshSession>();
|
||||
private pendingClients = new Map<string, Client>(); // sessionId → client (before 'ready')
|
||||
|
||||
constructor(
|
||||
private credentials: CredentialsService,
|
||||
@ -39,9 +40,13 @@ export class SshConnectionService {
|
||||
|
||||
const sessionId = uuid();
|
||||
const client = new Client();
|
||||
this.pendingClients.set(sessionId, client);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
client.on('ready', () => {
|
||||
this.pendingClients.delete(sessionId);
|
||||
client.shell({ term: 'xterm-256color' }, (err, stream) => {
|
||||
if (err) {
|
||||
client.end();
|
||||
@ -85,7 +90,10 @@ export class SshConnectionService {
|
||||
|
||||
client.on('error', (err) => {
|
||||
this.logger.error(`SSH error for host ${hostId}: ${err.message}`);
|
||||
settled = true;
|
||||
this.pendingClients.delete(sessionId);
|
||||
this.disconnect(sessionId);
|
||||
client.destroy();
|
||||
onClose(err.message);
|
||||
reject(err);
|
||||
});
|
||||
@ -95,9 +103,7 @@ export class SshConnectionService {
|
||||
port: host.port,
|
||||
username: cred?.username || 'root',
|
||||
debug: (msg: string) => {
|
||||
if (msg.includes('auth') || msg.includes('Auth') || msg.includes('key') || msg.includes('Key')) {
|
||||
this.logger.log(`[SSH-DEBUG] ${msg}`);
|
||||
}
|
||||
this.logger.log(`[SSH-DEBUG] ${msg}`);
|
||||
},
|
||||
hostVerifier: (key: Buffer, verify: (accept: boolean) => void) => {
|
||||
const fingerprint = createHash('sha256').update(key).digest('base64');
|
||||
@ -119,6 +125,7 @@ export class SshConnectionService {
|
||||
|
||||
// No stored fingerprint — first connection, ask the user via WebSocket
|
||||
onHostKeyVerify(fp, true).then((accepted) => {
|
||||
if (settled) return; // connection already timed out / errored — don't call back into ssh2
|
||||
if (accepted) {
|
||||
// Persist fingerprint so future connections auto-accept
|
||||
this.prisma.host.update({
|
||||
@ -163,6 +170,13 @@ export class SshConnectionService {
|
||||
}
|
||||
|
||||
disconnect(sessionId: string) {
|
||||
// Kill pending (not yet 'ready') connections
|
||||
const pending = this.pendingClients.get(sessionId);
|
||||
if (pending) {
|
||||
pending.destroy();
|
||||
this.pendingClients.delete(sessionId);
|
||||
}
|
||||
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.stream?.close();
|
||||
|
||||
@ -105,7 +105,7 @@ export function useTerminal() {
|
||||
break
|
||||
case 'host-key-verify':
|
||||
// Auto-accept for now — full UX in polish phase
|
||||
ws!.send(JSON.stringify({ type: 'host-key-accept' }))
|
||||
ws!.send(JSON.stringify({ type: 'host-key-accept', verifyId: msg.verifyId }))
|
||||
break
|
||||
case 'error':
|
||||
term.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user