/** * SSH IPC Handlers * 处理 SSH 连接、Shell、会话、端口转发相关 IPC * 安全增强:规范化错误处理,不暴露敏感信息 */ import { ipcMain, IpcMainInvokeEvent } from 'electron' import type { SSHService } from '../services/ssh-service' import type { ZmodemService } from '../services/zmodem-service' import { createLogger } from '../utils/logger' const logger = createLogger('SSH_IPC') // 生产环境日志:使用结构化日志,不输出敏感信息 const DEBUG = process.env.NODE_ENV === 'development' function debug(...args: unknown[]) { if (DEBUG) logger.debug('[SSH_IPC]', ...args) } function logHeader(msg: string) { logger.info(`[${msg}]`) } function logInfo(label: string, value: unknown) { logger.info(`${label}: ${value}`) } function logSuccess(msg: string) { logger.info(`[OK] ${msg}`) } function logError(msg: string) { logger.error(msg) } // IPC channel names export const SSH_CHANNELS = { CONNECT: 'ssh:connect', DISCONNECT: 'ssh:disconnect', EXEC: 'ssh:exec', CREATE_SHELL: 'ssh:createShell', SHELL_WRITE: 'ssh:shellWrite', SHELL_RESIZE: 'ssh:shellResize', FORWARD_LOCAL: 'ssh:forwardLocal', FORWARD_REMOTE: 'ssh:forwardRemote', FORWARD_DYNAMIC: 'ssh:forwardDynamic', SHELL_DATA: 'ssh:shellData', } as const export interface SSHIPCOptions { sshService: SSHService zmodemService: ZmodemService mainWindowGetter: () => Electron.BrowserWindow | null } export function registerSSHIPCHandlers(options: SSHIPCOptions): void { const { sshService, zmodemService, mainWindowGetter: _mainWindowGetter } = options // SSH connect ipcMain.handle(SSH_CHANNELS.CONNECT, async (_event: IpcMainInvokeEvent, config) => { try { const connection = await sshService.connect(config) return { success: true, connectionId: connection.id } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { success: false, error: message } } }) // SSH disconnect ipcMain.handle(SSH_CHANNELS.DISCONNECT, async (_event: IpcMainInvokeEvent, connectionId: string) => { try { await sshService.disconnect(connectionId) return { success: true } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { success: false, error: message } } }) // SSH exec command ipcMain.handle(SSH_CHANNELS.EXEC, async (_event: IpcMainInvokeEvent, connectionId: string, command: string) => { try { const result = await sshService.exec(connectionId, command) return { success: true, result } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { success: false, error: message } } }) // SSH create shell ipcMain.handle(SSH_CHANNELS.CREATE_SHELL, async (event: IpcMainInvokeEvent, connectionId: string, options?: { cols?: number; rows?: number }, shellId?: string) => { debug('CREATE_SHELL connectionId:', connectionId, 'preferredId:', shellId) try { const preferredShellId = shellId logger.info('[SSH_IPC] Creating shell for connection:', connectionId, 'preferredId:', preferredShellId) debug('>>> Calling sshService.createShell...') const shell = await sshService.createShell(connectionId, options || {}, (shellId, data) => { // 检查 sender 是否已被销毁 if (event.sender.isDestroyed()) { debug('SHELL DATA CALLBACK - SKIPPED (sender destroyed)') return } debug('SHELL DATA CALLBACK shellId:', shellId, 'data length:', data.length) try { debug('>>> Sending SHELL_DATA to renderer...') event.sender.send(SSH_CHANNELS.SHELL_DATA, connectionId, shellId, data) debug('>>> SHELL_DATA sent successfully') } catch (err) { logger.error('>>> FAILED to send SHELL_DATA:', err) } zmodemService.feedShellData(shellId, data) }, preferredShellId) logger.info('[SSH_IPC] Shell created:', shell.id) debug('>>> Shell created successfully:', shell.id) return { success: true, shellId: shell.id } } catch (error) { logger.error('[SSH_IPC] Shell creation failed:', error) const message = error instanceof Error ? error.message : String(error) debug('>>> Shell creation FAILED:', message) return { success: false, error: message } } }) // SSH shell write ipcMain.handle(SSH_CHANNELS.SHELL_WRITE, async (_event: IpcMainInvokeEvent, shellId: string, data: string) => { try { sshService.shellWrite(shellId, data) return { success: true } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { success: false, error: message } } }) // SSH shell resize ipcMain.handle(SSH_CHANNELS.SHELL_RESIZE, async (_event: IpcMainInvokeEvent, shellId: string, cols: number, rows: number) => { try { sshService.shellResize(shellId, cols, rows) return { success: true } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { success: false, error: message } } }) // SSH local port forward ipcMain.handle(SSH_CHANNELS.FORWARD_LOCAL, async ( _event: IpcMainInvokeEvent, connectionId: string, localPort: number, remoteHost: string, remotePort: number ) => { logHeader('FORWARD_LOCAL') logInfo('connectionId', connectionId) logInfo('localPort', localPort) logInfo('remoteHost', remoteHost) logInfo('remotePort', remotePort) try { const result = await sshService.forwardLocal(connectionId, localPort, remoteHost, remotePort) logSuccess(`Local forward bound to port ${result.boundPort}`) return { success: true, boundPort: result.boundPort } } catch (error) { const message = error instanceof Error ? error.message : String(error) logError(`Local forward failed: ${message}`) return { success: false, error: message } } }) // SSH remote port forward ipcMain.handle(SSH_CHANNELS.FORWARD_REMOTE, async ( _event: IpcMainInvokeEvent, connectionId: string, remotePort: number, localHost: string, localPort: number ) => { logHeader('FORWARD_REMOTE') logInfo('connectionId', connectionId) logInfo('remotePort', remotePort) logInfo('localHost', localHost) logInfo('localPort', localPort) try { await sshService.forwardRemote(connectionId, remotePort, localHost, localPort) logSuccess('Remote forward established') return { success: true } } catch (error) { const message = error instanceof Error ? error.message : String(error) logError(`Remote forward failed: ${message}`) return { success: false, error: message } } }) // SSH dynamic port forward (SOCKS5) ipcMain.handle(SSH_CHANNELS.FORWARD_DYNAMIC, async ( _event: IpcMainInvokeEvent, connectionId: string, localPort: number ) => { logHeader('FORWARD_DYNAMIC (SOCKS5)') logInfo('connectionId', connectionId) logInfo('localPort', localPort) try { const result = await sshService.forwardDynamic(connectionId, localPort) logSuccess(`SOCKS5 proxy started on port ${result.boundPort}`) return { success: true, boundPort: result.boundPort } } catch (error) { const message = error instanceof Error ? error.message : String(error) logError(`Dynamic forward failed: ${message}`) return { success: false, error: message } } }) }