This commit is contained in:
Baobhan Sith
2025-04-20 16:10:52 +08:00
parent 82808ca716
commit 80a461d337
6 changed files with 452 additions and 26 deletions
@@ -166,13 +166,13 @@ export const testConnection = async (req: Request, res: Response): Promise<void>
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<void>
}
};
/**
* 测试未保存的连接信息 (POST /api/v1/connections/test-unsaved)
*/
export const testUnsavedConnection = async (req: Request, res: Response): Promise<void> => {
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)
@@ -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;
+99 -4
View File
@@ -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<ClientChannel> => {
/**
* 测试给定 ID 的 SSH 连接(包括代理)
* @param connectionId 连接 ID
* @returns Promise<void> - 如果连接成功则 resolve,否则 reject
* @returns Promise<{ latency: number }> - 如果连接成功则 resolve 包含延迟的对象,否则 reject
* @throws Error 如果连接失败或配置错误
*/
export const testConnection = async (connectionId: number): Promise<void> => {
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<void> => {
// 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<void> => {
}
};
/**
* 测试未保存的 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