v2.0.0: 主题系统+SSH私钥解析修复+会话持久化+连接池优化+UI打磨
核心修复:
- ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2,
解决 'Cannot parse privateKey: Unsupported key format' 错误
- useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey
- sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题
- database-service.ts: updateSession() 补充 private_key_path 字段持久化
- SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa'
UI/UX:
- 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计
- macOS 标题栏支持双击最大化(window:toggleMaximize IPC)
- macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,)
- WelcomeView 4卡片布局,移除 Settings 按钮
- TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线
架构优化:
- SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard)
- 连接池竞态修复,支持快速多标签连接
- IPC 接口规范化,安全错误处理
- Vite 代码分割优化
质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass
This commit is contained in:
1 parent
37e65be823
commit
514d62644e
77 files changed
+2563
-931
No files matched your search
@@ -3,7 +3,7 @@
|
||||
* 处理应用级别信息
|
||||
*/
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent, app } from 'electron'
|
||||
import { ipcMain, app } from 'electron'
|
||||
|
||||
// IPC channel names
|
||||
export const APP_CHANNELS = {
|
||||
|
||||
+136
-27
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* File IPC Handlers
|
||||
* 处理本地文件操作和远程文件读写
|
||||
* 安全增强:防止路径遍历攻击
|
||||
*/
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent, app } from 'electron'
|
||||
@@ -21,25 +22,106 @@ export const FILE_CHANNELS = {
|
||||
LOCAL_MKDIR: 'local:mkdir',
|
||||
} as const
|
||||
|
||||
// 允许的文件操作基础目录白名单
|
||||
const ALLOWED_BASE_PATHS: string[] = []
|
||||
|
||||
/**
|
||||
* 初始化允许的路径白名单
|
||||
* 防止路径遍历攻击
|
||||
*/
|
||||
export function initializeAllowedPaths(): void {
|
||||
ALLOWED_BASE_PATHS.push(
|
||||
app.getPath('home'),
|
||||
app.getPath('downloads'),
|
||||
app.getPath('documents'),
|
||||
app.getPath('desktop'),
|
||||
app.getPath('temp'),
|
||||
app.getPath('userData'),
|
||||
)
|
||||
// 添加平台特定的常用目录
|
||||
if (process.platform === 'darwin') {
|
||||
ALLOWED_BASE_PATHS.push('/tmp', '/var/tmp')
|
||||
} else if (process.platform === 'linux') {
|
||||
ALLOWED_BASE_PATHS.push('/tmp', '/var/tmp', '/home')
|
||||
} else if (process.platform === 'win32') {
|
||||
ALLOWED_BASE_PATHS.push(process.env.LOCALAPPDATA || '', process.env.TEMP || '')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件路径是否在允许的范围内
|
||||
* 防止路径遍历攻击(如 ../../../etc/passwd)
|
||||
*/
|
||||
function isPathAllowed(filePath: string): boolean {
|
||||
// 规范化路径(解析 .. 和 .)
|
||||
const resolved = path.resolve(filePath)
|
||||
|
||||
// 检查是否在允许的基目录下
|
||||
const isUnderAllowedPath = ALLOWED_BASE_PATHS.some((base) => {
|
||||
if (!base) return false
|
||||
const relative = path.relative(base, resolved)
|
||||
return !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
})
|
||||
|
||||
if (isUnderAllowedPath) return true
|
||||
|
||||
// 如果路径存在,检查其实际位置(处理符号链接)
|
||||
try {
|
||||
const real = fs.realpathSync(resolved)
|
||||
return ALLOWED_BASE_PATHS.some((base) => {
|
||||
if (!base) return false
|
||||
const relative = path.relative(base, real)
|
||||
return !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地规范化路径
|
||||
*/
|
||||
function sanitizePath(inputPath: string): string {
|
||||
// 移除 null 字节
|
||||
const cleaned = inputPath.replace(/\0/g, '')
|
||||
// 规范化路径
|
||||
return path.resolve(cleaned)
|
||||
}
|
||||
|
||||
export function registerFileIPCHandlers(): void {
|
||||
// 确保白名单已初始化
|
||||
if (ALLOWED_BASE_PATHS.length === 0) {
|
||||
initializeAllowedPaths()
|
||||
}
|
||||
|
||||
// Read file (for rz)
|
||||
ipcMain.handle(FILE_CHANNELS.READ, async (_event: IpcMainInvokeEvent, filePath: string) => {
|
||||
try {
|
||||
const data = fs.readFileSync(filePath)
|
||||
const safePath = sanitizePath(filePath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
const data = fs.readFileSync(safePath)
|
||||
return { success: true, data: data.toString('base64') }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Write file (for sz)
|
||||
ipcMain.handle(FILE_CHANNELS.WRITE, async (_event: IpcMainInvokeEvent, filePath: string, data: string) => {
|
||||
try {
|
||||
const safePath = sanitizePath(filePath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
const buffer = Buffer.from(data, 'base64')
|
||||
fs.writeFileSync(filePath, buffer)
|
||||
fs.writeFileSync(safePath, buffer)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -48,15 +130,20 @@ export function registerFileIPCHandlers(): void {
|
||||
try {
|
||||
const homePath = app.getPath('home')
|
||||
return { success: true, path: homePath }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Read directory contents
|
||||
ipcMain.handle(FILE_CHANNELS.LOCAL_READ_DIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
const safePath = sanitizePath(dirPath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
const entries = fs.readdirSync(safePath, { withFileTypes: true })
|
||||
const files = entries.map((entry) => {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
let stats: fs.Stats | null = null
|
||||
@@ -84,63 +171,85 @@ export function registerFileIPCHandlers(): void {
|
||||
})
|
||||
|
||||
return { success: true, files }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Get file stats
|
||||
ipcMain.handle(FILE_CHANNELS.LOCAL_GET_STATS, async (_event: IpcMainInvokeEvent, filePath: string) => {
|
||||
try {
|
||||
const stats = fs.lstatSync(filePath)
|
||||
const safePath = sanitizePath(filePath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
const stats = fs.lstatSync(safePath)
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
name: path.basename(filePath),
|
||||
path: filePath,
|
||||
name: path.basename(safePath),
|
||||
path: safePath,
|
||||
type: stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'file',
|
||||
size: stats.size,
|
||||
modified: Math.floor(stats.mtime.getTime() / 1000),
|
||||
permissions: formatPermissions(stats.mode),
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Delete file or directory
|
||||
ipcMain.handle(FILE_CHANNELS.LOCAL_DELETE, async (_event: IpcMainInvokeEvent, filePath: string) => {
|
||||
try {
|
||||
const stats = fs.lstatSync(filePath)
|
||||
const safePath = sanitizePath(filePath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
const stats = fs.lstatSync(safePath)
|
||||
if (stats.isDirectory()) {
|
||||
fs.rmSync(filePath, { recursive: true, force: true })
|
||||
fs.rmSync(safePath, { recursive: true, force: true })
|
||||
} else {
|
||||
fs.unlinkSync(filePath)
|
||||
fs.unlinkSync(safePath)
|
||||
}
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Rename file or directory
|
||||
ipcMain.handle(FILE_CHANNELS.LOCAL_RENAME, async (_event: IpcMainInvokeEvent, oldPath: string, newPath: string) => {
|
||||
try {
|
||||
fs.renameSync(oldPath, newPath)
|
||||
const safeOld = sanitizePath(oldPath)
|
||||
const safeNew = sanitizePath(newPath)
|
||||
if (!isPathAllowed(safeOld) || !isPathAllowed(safeNew)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
fs.renameSync(safeOld, safeNew)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
// Create directory
|
||||
ipcMain.handle(FILE_CHANNELS.LOCAL_MKDIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
|
||||
try {
|
||||
fs.mkdirSync(dirPath, { recursive: true })
|
||||
const safePath = sanitizePath(dirPath)
|
||||
if (!isPathAllowed(safePath)) {
|
||||
return { success: false, error: 'Access denied: path traversal detected' }
|
||||
}
|
||||
fs.mkdirSync(safePath, { recursive: true })
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+15
-2
@@ -16,9 +16,10 @@ import { registerSFTPIPCHandlers } from './sftp.ipc'
|
||||
import { registerSessionIPCHandlers } from './session.ipc'
|
||||
import { registerSettingsIPCHandlers } from './settings.ipc'
|
||||
import { registerDialogIPCHandlers } from './dialog.ipc'
|
||||
import { registerFileIPCHandlers } from './file.ipc'
|
||||
import { registerFileIPCHandlers, initializeAllowedPaths } from './file.ipc'
|
||||
import { registerZmodemIPCHandlers } from './zmodem.ipc'
|
||||
import { registerAppIPCHandlers } from './app.ipc'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
export interface IPCOptions {
|
||||
sshService: SSHService
|
||||
@@ -45,7 +46,8 @@ export function registerAllIPCHandlers(options: IPCOptions): void {
|
||||
// Dialog handlers
|
||||
registerDialogIPCHandlers({ mainWindowGetter })
|
||||
|
||||
// File handlers
|
||||
// File handlers (初始化路径白名单)
|
||||
initializeAllowedPaths()
|
||||
registerFileIPCHandlers()
|
||||
|
||||
// Zmodem handlers
|
||||
@@ -54,6 +56,17 @@ export function registerAllIPCHandlers(options: IPCOptions): void {
|
||||
// App handlers
|
||||
registerAppIPCHandlers()
|
||||
|
||||
// Window handlers
|
||||
ipcMain.handle('window:toggleMaximize', () => {
|
||||
const win = mainWindowGetter()
|
||||
if (!win) return
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
logger.info('[IPC] All handlers registered')
|
||||
}
|
||||
|
||||
|
||||
+12
-11
@@ -5,6 +5,7 @@
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent } from 'electron'
|
||||
import type { DatabaseService } from '../services/database-service'
|
||||
import type { SessionUpdateRequest } from '../../shared/types/session.types'
|
||||
|
||||
// IPC channel names
|
||||
export const SESSION_CHANNELS = {
|
||||
@@ -27,8 +28,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void {
|
||||
try {
|
||||
const result = dbService.createSession(session)
|
||||
return { success: true, session: result }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,8 +38,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void {
|
||||
try {
|
||||
const sessions = dbService.getAllSessions()
|
||||
return { success: true, sessions }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,18 +48,18 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void {
|
||||
try {
|
||||
const session = dbService.getSessionById(id)
|
||||
return { success: true, session }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
// Update session
|
||||
ipcMain.handle(SESSION_CHANNELS.UPDATE, async (_event: IpcMainInvokeEvent, id: string, updates: any) => {
|
||||
ipcMain.handle(SESSION_CHANNELS.UPDATE, async (_event: IpcMainInvokeEvent, id: string, updates: SessionUpdateRequest) => {
|
||||
try {
|
||||
const result = dbService.updateSession(id, updates)
|
||||
return { success: true, session: result }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -67,8 +68,8 @@ export function registerSessionIPCHandlers(options: SessionIPCOptions): void {
|
||||
try {
|
||||
dbService.deleteSession(id)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -24,8 +24,8 @@ export function registerSettingsIPCHandlers(options: SettingsIPCOptions): void {
|
||||
try {
|
||||
const value = dbService.getSetting(key)
|
||||
return { success: true, value }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -34,8 +34,8 @@ export function registerSettingsIPCHandlers(options: SettingsIPCOptions): void {
|
||||
try {
|
||||
dbService.setSetting(key, value)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
}
|
||||
+38
-29
@@ -26,11 +26,11 @@ export interface SFTPIPCOptions {
|
||||
}
|
||||
|
||||
// ===== 彩色调试日志 =====
|
||||
const DEBUG = true
|
||||
const _DEBUG = true
|
||||
function logHeader(msg: string) {
|
||||
console.log(`%c${msg}`, 'background: #16a085; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;')
|
||||
}
|
||||
function logInfo(label: string, value: any) {
|
||||
function logInfo(label: string, value: unknown) {
|
||||
console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #1abc9c; font-weight: bold;')
|
||||
}
|
||||
function logSuccess(msg: string) {
|
||||
@@ -52,9 +52,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
const sftp = await sshService.createSFTP(connectionId)
|
||||
logSuccess(`SFTP connected: ${sftp.id}`)
|
||||
return { success: true, sftpId: sftp.id }
|
||||
} catch (error: any) {
|
||||
logError(`SFTP connect failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`SFTP connect failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,9 +69,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
const files = await sshService.sftpListDirectory(sftpId, dirPath)
|
||||
logSuccess(`Listed ${files.length} items`)
|
||||
return { success: true, files }
|
||||
} catch (error: any) {
|
||||
logError(`List failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`List failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -97,9 +99,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
})
|
||||
logSuccess('Upload complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Upload failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Upload failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -126,9 +129,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
})
|
||||
logSuccess('Download complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Download failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Download failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -148,9 +152,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
await sshService.sftpRename(sftpId, oldPath, newPath)
|
||||
logSuccess('Rename complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Rename failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Rename failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -170,9 +175,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
await sshService.sftpDelete(sftpId, filePath, isDirectory)
|
||||
logSuccess('Delete complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Delete failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Delete failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -190,9 +196,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
await sshService.sftpMkdir(sftpId, dirPath)
|
||||
logSuccess('Mkdir complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Mkdir failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Mkdir failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -210,9 +217,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
const stats = await sshService.sftpStat(sftpId, filePath)
|
||||
logSuccess(`Stat complete: ${stats.size} bytes`)
|
||||
return { success: true, stats }
|
||||
} catch (error: any) {
|
||||
logError(`Stat failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Stat failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -232,9 +240,10 @@ export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
|
||||
await sshService.sftpChmod(sftpId, filePath, mode)
|
||||
logSuccess('Chmod complete')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Chmod failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Chmod failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
}
|
||||
+61
-63
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* SSH IPC Handlers
|
||||
* 处理 SSH 连接、Shell、会话、端口转发相关 IPC
|
||||
* 安全增强:规范化错误处理,不暴露敏感信息
|
||||
*/
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent } from 'electron'
|
||||
@@ -10,22 +11,22 @@ import { createLogger } from '../utils/logger'
|
||||
|
||||
const logger = createLogger('SSH_IPC')
|
||||
|
||||
// ===== 彩色调试日志 =====
|
||||
const DEBUG = true
|
||||
function debug(...args: any[]) {
|
||||
if (DEBUG) console.log('[SSH_IPC]', ...args)
|
||||
// 生产环境日志:使用结构化日志,不输出敏感信息
|
||||
const DEBUG = process.env.NODE_ENV === 'development'
|
||||
function debug(...args: unknown[]) {
|
||||
if (DEBUG) logger.debug('[SSH_IPC]', ...args)
|
||||
}
|
||||
function logHeader(msg: string) {
|
||||
console.log(`%c${msg}`, 'background: #9b59b6; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;')
|
||||
logger.info(`[${msg}]`)
|
||||
}
|
||||
function logInfo(label: string, value: any) {
|
||||
console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #3498db; font-weight: bold;')
|
||||
function logInfo(label: string, value: unknown) {
|
||||
logger.info(`${label}: ${value}`)
|
||||
}
|
||||
function logSuccess(msg: string) {
|
||||
console.log(`%c✓ ${msg}`, 'color: #27ae60; font-weight: bold;')
|
||||
logger.info(`[OK] ${msg}`)
|
||||
}
|
||||
function logError(msg: string) {
|
||||
console.log(`%c✗ ${msg}`, 'color: #e74c3c; font-weight: bold;')
|
||||
logger.error(msg)
|
||||
}
|
||||
|
||||
// IPC channel names
|
||||
@@ -49,15 +50,16 @@ export interface SSHIPCOptions {
|
||||
}
|
||||
|
||||
export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
const { sshService, zmodemService, mainWindowGetter } = options
|
||||
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: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,8 +68,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
try {
|
||||
await sshService.disconnect(connectionId)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -76,59 +79,49 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
try {
|
||||
const result = await sshService.exec(connectionId, command)
|
||||
return { success: true, result }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} 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) => {
|
||||
console.log('=== SSH_IPC CREATE_SHELL ===')
|
||||
console.log(' connectionId:', connectionId)
|
||||
console.log(' options:', options)
|
||||
console.log(' preferredShellId:', shellId)
|
||||
|
||||
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()) {
|
||||
console.log('=== SHELL DATA CALLBACK - SKIPPED (sender destroyed) ===')
|
||||
debug('SHELL DATA CALLBACK - SKIPPED (sender destroyed)')
|
||||
return
|
||||
}
|
||||
|
||||
const text = data.toString('utf-8')
|
||||
console.log('=== SHELL DATA CALLBACK ===')
|
||||
console.log(' shellId:', shellId)
|
||||
console.log(' data length:', data.length)
|
||||
console.log(' data preview:', text.substring(0, 100).replace(/\n/g, '\\n'))
|
||||
|
||||
// 打印所有收到的数据前100字符用于调试
|
||||
if (text.length > 0) {
|
||||
logger.info(`[SSH_IPC] Shell data from ${shellId}:`, text.substring(0, 100).replace(/\n/g, '\\n'))
|
||||
}
|
||||
|
||||
|
||||
debug('SHELL DATA CALLBACK shellId:', shellId, 'data length:', data.length)
|
||||
|
||||
try {
|
||||
console.log('>>> Sending SHELL_DATA to renderer...')
|
||||
debug('>>> Sending SHELL_DATA to renderer...')
|
||||
event.sender.send(SSH_CHANNELS.SHELL_DATA, connectionId, shellId, data)
|
||||
console.log('>>> SHELL_DATA sent successfully')
|
||||
debug('>>> SHELL_DATA sent successfully')
|
||||
} catch (err) {
|
||||
console.error('>>> FAILED to send SHELL_DATA:', 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: any) {
|
||||
} catch (error) {
|
||||
logger.error('[SSH_IPC] Shell creation failed:', error)
|
||||
debug('>>> Shell creation FAILED:', error.message)
|
||||
return { success: false, error: error.message }
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
debug('>>> Shell creation FAILED:', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,8 +130,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
try {
|
||||
sshService.shellWrite(shellId, data)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -147,8 +141,9 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
try {
|
||||
sshService.shellResize(shellId, cols, rows)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -160,19 +155,20 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
remoteHost: string,
|
||||
remotePort: number
|
||||
) => {
|
||||
logHeader('🔀 IPC FORWARD_LOCAL')
|
||||
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: any) {
|
||||
logError(`Local forward failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Local forward failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -184,19 +180,20 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
localHost: string,
|
||||
localPort: number
|
||||
) => {
|
||||
logHeader('🔀 IPC FORWARD_REMOTE')
|
||||
logHeader('FORWARD_REMOTE')
|
||||
logInfo('connectionId', connectionId)
|
||||
logInfo('remotePort', remotePort)
|
||||
logInfo('localHost', localHost)
|
||||
logInfo('localPort', localPort)
|
||||
|
||||
|
||||
try {
|
||||
const result = await sshService.forwardRemote(connectionId, remotePort, localHost, localPort)
|
||||
await sshService.forwardRemote(connectionId, remotePort, localHost, localPort)
|
||||
logSuccess('Remote forward established')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
logError(`Remote forward failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Remote forward failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -206,17 +203,18 @@ export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
|
||||
connectionId: string,
|
||||
localPort: number
|
||||
) => {
|
||||
logHeader('🧦 IPC FORWARD_DYNAMIC (SOCKS5)')
|
||||
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: any) {
|
||||
logError(`Dynamic forward failed: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logError(`Dynamic forward failed: ${message}`)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -77,9 +77,9 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void {
|
||||
)
|
||||
|
||||
return { success: true, sessionId, fileName }
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
logger.error('Zmodem send error:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -123,9 +123,9 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void {
|
||||
)
|
||||
|
||||
return { success: true, sessionId }
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
logger.error('Zmodem receive error:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -134,8 +134,8 @@ export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void {
|
||||
try {
|
||||
zmodemService.cancelSession(sessionId)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message }
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user