import { Client, ClientChannel, ConnectConfig } from 'ssh2'; 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; // 连接超时时间 (毫秒) const TEST_TIMEOUT = 15000; // 测试连接超时时间 (毫秒) // 辅助接口:定义解密后的凭证和代理信息结构 (导出以便 websocket.ts 使用) export interface DecryptedConnectionDetails { id: number; name: string; host: string; port: number; username: string; auth_method: 'password' | 'key'; password?: string; // Decrypted privateKey?: string; // Decrypted passphrase?: string; // Decrypted proxy?: { id: number; name: string; type: 'SOCKS5' | 'HTTP'; host: string; port: number; username?: string; password?: string; // Decrypted // auth_method?: string; // Proxy auth method (如果需要可以保留) // privateKey?: string; // Decrypted proxy key (如果需要可以保留) // passphrase?: string; // Decrypted proxy passphrase (如果需要可以保留) } | null; } /** * 获取并解密指定 ID 的完整连接信息(包括代理) * @param connectionId 连接 ID * @returns Promise 解密后的连接详情 * @throws Error 如果连接配置未找到或解密失败 */ export const getConnectionDetails = async (connectionId: number): Promise => { console.log(`SshService: 获取连接 ${connectionId} 的详细信息...`); const rawConnInfo = await ConnectionRepository.findFullConnectionById(connectionId); if (!rawConnInfo) { throw new Error(`连接配置 ID ${connectionId} 未找到。`); } try { const fullConnInfo: DecryptedConnectionDetails = { id: rawConnInfo.id, // Add null check for required fields from rawConnInfo name: rawConnInfo.name ?? (() => { throw new Error(`Connection ID ${connectionId} has null name.`); })(), host: rawConnInfo.host ?? (() => { throw new Error(`Connection ID ${connectionId} has null host.`); })(), port: rawConnInfo.port ?? (() => { throw new Error(`Connection ID ${connectionId} has null port.`); })(), username: rawConnInfo.username ?? (() => { throw new Error(`Connection ID ${connectionId} has null username.`); })(), auth_method: rawConnInfo.auth_method ?? (() => { throw new Error(`Connection ID ${connectionId} has null auth_method.`); })(), password: (rawConnInfo.auth_method === 'password' && rawConnInfo.encrypted_password) ? decrypt(rawConnInfo.encrypted_password) : undefined, privateKey: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_private_key) ? decrypt(rawConnInfo.encrypted_private_key) : undefined, passphrase: (rawConnInfo.auth_method === 'key' && rawConnInfo.encrypted_passphrase) ? decrypt(rawConnInfo.encrypted_passphrase) : undefined, proxy: null, }; if (rawConnInfo.proxy_db_id) { // Add null checks for required proxy fields inside the if block const proxyName = rawConnInfo.proxy_name ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null name.`); })(); const proxyType = rawConnInfo.proxy_type ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null type.`); })(); const proxyHost = rawConnInfo.proxy_host ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null host.`); })(); const proxyPort = rawConnInfo.proxy_port ?? (() => { throw new Error(`Proxy for Connection ID ${connectionId} has null port.`); })(); // Ensure proxyType is one of the allowed values if (proxyType !== 'SOCKS5' && proxyType !== 'HTTP') { throw new Error(`Proxy for Connection ID ${connectionId} has invalid type: ${proxyType}`); } fullConnInfo.proxy = { id: rawConnInfo.proxy_db_id, // Already checked by the if condition name: proxyName, type: proxyType, // Already validated host: proxyHost, port: proxyPort, username: rawConnInfo.proxy_username || undefined, // Optional, defaults to undefined password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined, // Optional, handled by decrypt logic // 可以根据需要解密代理的其他凭证 }; } console.log(`SshService: 连接 ${connectionId} 的详细信息获取并解密成功。`); return fullConnInfo; } catch (decryptError: any) { console.error(`SshService: 处理连接 ${connectionId} 凭证或代理凭证失败:`, decryptError); throw new Error(`处理凭证失败: ${decryptError.message}`); } }; /** * 根据解密后的连接详情建立 SSH 连接(处理代理) * @param connDetails - 解密后的连接详情 * @param timeout - 连接超时时间 (毫秒),可选 * @returns Promise 连接成功的 SSH Client 实例 * @throws Error 如果连接失败 */ export const establishSshConnection = ( connDetails: DecryptedConnectionDetails, timeout: number = CONNECT_TIMEOUT ): Promise => { return new Promise((resolve, reject) => { const sshClient = new Client(); const connectConfig: ConnectConfig = { host: connDetails.host, port: connDetails.port, username: connDetails.username, password: connDetails.password, privateKey: connDetails.privateKey, passphrase: connDetails.passphrase, readyTimeout: timeout, keepaliveInterval: 10000, // 保持连接 keepaliveCountMax: 10, }; const readyHandler = () => { console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`); sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器 resolve(sshClient); // 返回 Client 实例 }; const errorHandler = (err: Error) => { console.error(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 失败:`, err); sshClient.removeListener('ready', readyHandler); // 失败后移除成功监听器 sshClient.end(); // 确保关闭客户端 reject(err); }; sshClient.once('ready', readyHandler); sshClient.once('error', errorHandler); // --- 处理代理 --- if (connDetails.proxy) { const proxy = connDetails.proxy; console.log(`SshService: 应用代理 ${proxy.name} (${proxy.type}) 连接到 ${connDetails.host}:${connDetails.port}`); if (proxy.type === 'SOCKS5') { const socksOptions: SocksClientOptions = { proxy: { host: proxy.host, port: proxy.port, type: 5, userId: proxy.username, password: proxy.password }, command: 'connect', destination: { host: connectConfig.host!, port: connectConfig.port! }, timeout: connectConfig.readyTimeout, }; SocksClient.createConnection(socksOptions) .then(({ socket }) => { console.log(`SshService: SOCKS5 代理连接成功 (目标: ${connDetails.host}:${connDetails.port})。`); connectConfig.sock = socket; sshClient.connect(connectConfig); }) .catch(socksError => { errorHandler(new Error(`SOCKS5 代理 ${proxy.host}:${proxy.port} 连接失败: ${socksError.message}`)); }); } else if (proxy.type === 'HTTP') { console.log(`SshService: 尝试通过 HTTP 代理 ${proxy.host}:${proxy.port} 建立隧道到 ${connDetails.host}:${connDetails.port}...`); const reqOptions: http.RequestOptions = { method: 'CONNECT', host: proxy.host, port: proxy.port, path: `${connectConfig.host}:${connectConfig.port}`, timeout: connectConfig.readyTimeout, agent: false }; if (proxy.username) { const auth = 'Basic ' + Buffer.from(proxy.username + ':' + (proxy.password || '')).toString('base64'); reqOptions.headers = { ...reqOptions.headers, 'Proxy-Authorization': auth, 'Proxy-Connection': 'Keep-Alive', 'Host': `${connectConfig.host}:${connectConfig.port}` }; } const req = http.request(reqOptions); req.on('connect', (res, socket, head) => { if (res.statusCode === 200) { console.log(`SshService: HTTP 代理隧道建立成功 (目标: ${connDetails.host}:${connDetails.port})。`); connectConfig.sock = socket; sshClient.connect(connectConfig); } else { socket.destroy(); errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 连接失败 (状态码: ${res.statusCode})`)); } }); req.on('error', (err) => { errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 请求错误: ${err.message}`)); }); req.on('timeout', () => { req.destroy(); errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 连接超时`)); }); req.end(); } else { errorHandler(new Error(`不支持的代理类型: ${proxy.type}`)); } } else { // 无代理,直接连接 console.log(`SshService: 无代理,直接连接到 ${connDetails.host}:${connDetails.port}`); sshClient.connect(connectConfig); } }); }; /** * 在已连接的 SSH Client 上打开 Shell 通道 * @param sshClient - 已连接的 SSH Client 实例 * @returns Promise Shell 通道实例 * @throws Error 如果打开 Shell 失败 */ export const openShell = (sshClient: Client): Promise => { return new Promise((resolve, reject) => { sshClient.shell((err, stream) => { if (err) { console.error(`SshService: 打开 Shell 失败:`, err); return reject(new Error(`打开 Shell 失败: ${err.message}`)); } console.log(`SshService: Shell 通道已打开。`); resolve(stream); }); }); }; /** * 测试给定 ID 的 SSH 连接(包括代理) * @param connectionId 连接 ID * @returns Promise<{ latency: number }> - 如果连接成功则 resolve 包含延迟的对象,否则 reject * @throws Error 如果连接失败或配置错误 */ 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); // 2. 尝试建立连接 (使用较短的测试超时时间) sshClient = await establishSshConnection(connDetails, TEST_TIMEOUT); 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; // 将错误向上抛出 } finally { // 无论成功失败,都关闭 SSH 客户端 if (sshClient) { sshClient.end(); console.log(`SshService: 测试连接 ${connectionId} 的客户端已关闭。`); } } }; /** * 测试未保存的 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 // - resizeTerminal // - cleanupConnection // - activeSessions Map // - AuthenticatedWebSocket interface (如果仅在此文件使用)