diff --git a/packages/backend/src/connections/connections.controller.ts b/packages/backend/src/connections/connections.controller.ts index 9d3c2ab..8e66e83 100644 --- a/packages/backend/src/connections/connections.controller.ts +++ b/packages/backend/src/connections/connections.controller.ts @@ -166,13 +166,13 @@ export const testConnection = async (req: Request, res: Response): Promise return; } - // 调用 SshService 进行连接测试 - await SshService.testConnection(connectionId); + // 调用 SshService 进行连接测试,现在它会返回延迟 + const { latency } = await SshService.testConnection(connectionId); // 如果 SshService.testConnection 没有抛出错误,则表示成功 // 记录审计日志 (可选,看是否需要记录测试操作) // auditLogService.logAction('CONNECTION_TESTED', { connectionId, success: true }); - res.status(200).json({ success: true, message: '连接测试成功。' }); + res.status(200).json({ success: true, message: '连接测试成功。', latency }); // 返回延迟 } catch (error: any) { // 记录审计日志 (可选) @@ -183,6 +183,70 @@ export const testConnection = async (req: Request, res: Response): Promise } }; + +/** + * 测试未保存的连接信息 (POST /api/v1/connections/test-unsaved) + */ +export const testUnsavedConnection = async (req: Request, res: Response): Promise => { + try { + // 从请求体中提取连接信息 + const { host, port, username, auth_method, password, private_key, passphrase, proxy_id } = req.body; + + // 基本验证 + if (!host || !port || !username || !auth_method) { + res.status(400).json({ success: false, message: '缺少必要的连接信息 (host, port, username, auth_method)。' }); + return; + } + // 密码认证时,password 字段必须存在,但可以为空字符串 + if (auth_method === 'password' && password === undefined) { + res.status(400).json({ success: false, message: '密码认证方式需要提供 password 字段 (可以为空字符串)。' }); + return; + } + // 密钥认证时,private_key 必须存在且不为空 + if (auth_method === 'key' && !private_key) { + res.status(400).json({ success: false, message: '密钥认证方式需要提供 private_key。' }); + return; + } + + // 构建传递给服务层的连接配置对象 + // 注意:这里传递的是未经验证和加密处理的原始数据 + const connectionConfig = { + host, + port: parseInt(port, 10), // 确保 port 是数字 + username, + auth_method, + password, // 传递原始密码 + private_key, // 传递原始私钥 + passphrase, // 传递原始密码短语 + proxy_id: proxy_id ? parseInt(proxy_id, 10) : null // 确保 proxy_id 是数字或 null + }; + + // 验证 port 和 proxy_id 是否为有效数字 + if (isNaN(connectionConfig.port)) { + res.status(400).json({ success: false, message: '端口号必须是有效的数字。' }); + return; + } + if (proxy_id && isNaN(connectionConfig.proxy_id as number)) { + res.status(400).json({ success: false, message: '代理 ID 必须是有效的数字。' }); + return; + } + + + // 调用 SshService 进行连接测试,现在它会返回延迟 + // 注意:SshService.testUnsavedConnection 需要处理原始凭证 + const { latency } = await SshService.testUnsavedConnection(connectionConfig); + + // 如果 SshService.testUnsavedConnection 没有抛出错误,则表示成功 + res.status(200).json({ success: true, message: '连接测试成功。', latency }); + + } catch (error: any) { + console.error(`Controller: 测试未保存连接时发生错误:`, error); + // SshService 会抛出包含具体原因的 Error + res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' }); + } +}; + + // --- TODO: 将以下逻辑迁移到 ImportExportService --- /** * 导出所有连接配置 (GET /api/v1/connections/export) diff --git a/packages/backend/src/connections/connections.routes.ts b/packages/backend/src/connections/connections.routes.ts index 71a430e..270ef9c 100644 --- a/packages/backend/src/connections/connections.routes.ts +++ b/packages/backend/src/connections/connections.routes.ts @@ -8,6 +8,7 @@ import { updateConnection, // 引入更新连接的控制器 deleteConnection, // 引入删除连接的控制器 testConnection, // 引入测试连接的控制器 + testUnsavedConnection, // 添加导入: 引入测试未保存连接的控制器 exportConnections, // 引入导出连接的控制器 importConnections // 引入导入连接的控制器 } from './connections.controller'; @@ -81,4 +82,7 @@ router.delete('/:id', deleteConnection); // POST /api/v1/connections/:id/test - 测试连接 router.post('/:id/test', testConnection); +// POST /api/v1/connections/test-unsaved - 测试未保存的连接信息 +router.post('/test-unsaved', testUnsavedConnection); + export default router; diff --git a/packages/backend/src/services/ssh.service.ts b/packages/backend/src/services/ssh.service.ts index 3bf9014..bb86b66 100644 --- a/packages/backend/src/services/ssh.service.ts +++ b/packages/backend/src/services/ssh.service.ts @@ -3,6 +3,7 @@ import { SocksClient, SocksClientOptions } from 'socks'; import http from 'http'; import net from 'net'; import * as ConnectionRepository from '../repositories/connection.repository'; +import * as ProxyRepository from '../repositories/proxy.repository'; // 引入 ProxyRepository import { decrypt } from '../utils/crypto'; const CONNECT_TIMEOUT = 20000; // 连接超时时间 (毫秒) @@ -221,12 +222,13 @@ export const openShell = (sshClient: Client): Promise => { /** * 测试给定 ID 的 SSH 连接(包括代理) * @param connectionId 连接 ID - * @returns Promise - 如果连接成功则 resolve,否则 reject + * @returns Promise<{ latency: number }> - 如果连接成功则 resolve 包含延迟的对象,否则 reject * @throws Error 如果连接失败或配置错误 */ -export const testConnection = async (connectionId: number): Promise => { +export const testConnection = async (connectionId: number): Promise<{ latency: number }> => { console.log(`SshService: 测试连接 ${connectionId}...`); let sshClient: Client | null = null; + const startTime = Date.now(); // 开始计时 try { // 1. 获取并解密连接信息 const connDetails = await getConnectionDetails(connectionId); @@ -234,8 +236,10 @@ export const testConnection = async (connectionId: number): Promise => { // 2. 尝试建立连接 (使用较短的测试超时时间) sshClient = await establishSshConnection(connDetails, TEST_TIMEOUT); - console.log(`SshService: 测试连接 ${connectionId} 成功。`); - // 测试成功,Promise 自动 resolve void + const endTime = Date.now(); // 结束计时 + const latency = endTime - startTime; + console.log(`SshService: 测试连接 ${connectionId} 成功,延迟: ${latency}ms。`); + return { latency }; // 返回延迟 } catch (error) { console.error(`SshService: 测试连接 ${connectionId} 失败:`, error); throw error; // 将错误向上抛出 @@ -248,6 +252,97 @@ export const testConnection = async (connectionId: number): Promise => { } }; + +/** + * 测试未保存的 SSH 连接信息(包括代理) + * @param connectionConfig - 包含连接参数的对象 (host, port, username, auth_method, password?, private_key?, passphrase?, proxy_id?) + * @returns Promise<{ latency: number }> - 如果连接成功则 resolve 包含延迟的对象,否则 reject + * @throws Error 如果连接失败或配置错误 + */ +export const testUnsavedConnection = async (connectionConfig: { + host: string; + port: number; + username: string; + auth_method: 'password' | 'key'; + password?: string; + private_key?: string; // 注意这里是 private_key + passphrase?: string; + proxy_id?: number | null; +}): Promise<{ latency: number }> => { + console.log(`SshService: 测试未保存的连接到 ${connectionConfig.host}:${connectionConfig.port}...`); + let sshClient: Client | null = null; + const startTime = Date.now(); // 开始计时 + try { + // 1. 构建临时的 DecryptedConnectionDetails 结构 + const tempConnDetails: DecryptedConnectionDetails = { + id: -1, // 临时 ID,不实际使用 + name: `Test-${connectionConfig.host}`, // 临时名称 + host: connectionConfig.host, + port: connectionConfig.port, + username: connectionConfig.username, + auth_method: connectionConfig.auth_method, + // 直接使用传入的凭证,因为它们是未加密的 + password: connectionConfig.password, + privateKey: connectionConfig.private_key, // 映射 private_key + passphrase: connectionConfig.passphrase, + proxy: null, // 稍后填充 + }; + + // 2. 如果提供了 proxy_id,获取并解密代理信息 + if (connectionConfig.proxy_id) { + console.log(`SshService: 测试连接需要获取代理 ${connectionConfig.proxy_id} 的信息...`); + const rawProxyInfo = await ProxyRepository.findProxyById(connectionConfig.proxy_id); + if (!rawProxyInfo) { + throw new Error(`代理 ID ${connectionConfig.proxy_id} 未找到。`); + } + try { + // Add null checks for required proxy fields + const proxyName = rawProxyInfo.name ?? (() => { throw new Error(`Proxy ID ${connectionConfig.proxy_id} has null name.`); })(); + const proxyType = rawProxyInfo.type ?? (() => { throw new Error(`Proxy ID ${connectionConfig.proxy_id} has null type.`); })(); + const proxyHost = rawProxyInfo.host ?? (() => { throw new Error(`Proxy ID ${connectionConfig.proxy_id} has null host.`); })(); + const proxyPort = rawProxyInfo.port ?? (() => { throw new Error(`Proxy ID ${connectionConfig.proxy_id} has null port.`); })(); + + // Ensure proxyType is one of the allowed values + if (proxyType !== 'SOCKS5' && proxyType !== 'HTTP') { + throw new Error(`Proxy ID ${connectionConfig.proxy_id} has invalid type: ${proxyType}`); + } + + tempConnDetails.proxy = { + id: rawProxyInfo.id, + name: proxyName, + type: proxyType, + host: proxyHost, + port: proxyPort, + username: rawProxyInfo.username || undefined, + password: rawProxyInfo.encrypted_password ? decrypt(rawProxyInfo.encrypted_password) : undefined, + }; + console.log(`SshService: 代理 ${connectionConfig.proxy_id} 信息获取并解密成功。`); + } catch (decryptError: any) { + console.error(`SshService: 处理代理 ${connectionConfig.proxy_id} 凭证失败:`, decryptError); + throw new Error(`处理代理凭证失败: ${decryptError.message}`); + } + } + + // 3. 尝试建立连接 (使用较短的测试超时时间) + sshClient = await establishSshConnection(tempConnDetails, TEST_TIMEOUT); + + const endTime = Date.now(); // 结束计时 + const latency = endTime - startTime; + console.log(`SshService: 测试未保存的连接到 ${connectionConfig.host}:${connectionConfig.port} 成功,延迟: ${latency}ms。`); + return { latency }; // 返回延迟 + } catch (error) { + console.error(`SshService: 测试未保存的连接到 ${connectionConfig.host}:${connectionConfig.port} 失败:`, error); + throw error; // 将错误向上抛出 + } finally { + // 无论成功失败,都关闭 SSH 客户端 + if (sshClient) { + sshClient.end(); + console.log(`SshService: 测试未保存连接的客户端已关闭。`); + } + } +}; + + // --- 移除旧的函数 --- // - connectAndOpenShell // - sendInput diff --git a/packages/frontend/src/components/AddConnectionForm.vue b/packages/frontend/src/components/AddConnectionForm.vue index 4775fbf..a12ec66 100644 --- a/packages/frontend/src/components/AddConnectionForm.vue +++ b/packages/frontend/src/components/AddConnectionForm.vue @@ -1,7 +1,8 @@