refactor: 重构后端
This commit is contained in:
@@ -1,45 +1,15 @@
|
||||
import { Client, ClientChannel, ConnectConfig } from 'ssh2'; // Import ClientChannel and ConnectConfig
|
||||
import { SocksClient, SocksClientOptions } from 'socks'; // Import SocksClientOptions
|
||||
import { Client, ClientChannel, ConnectConfig } from 'ssh2';
|
||||
import { SocksClient, SocksClientOptions } from 'socks';
|
||||
import http from 'http';
|
||||
import net from 'net';
|
||||
import WebSocket from 'ws'; // Import WebSocket for type hint
|
||||
import * as ConnectionRepository from '../repositories/connection.repository';
|
||||
import { decrypt } from '../utils/crypto';
|
||||
// Import SftpService if needed later for initialization
|
||||
// import * as SftpService from './sftp.service';
|
||||
// Import StatusMonitorService if needed later for initialization
|
||||
// import * as StatusMonitorService from './status-monitor.service';
|
||||
|
||||
|
||||
const CONNECT_TIMEOUT = 20000; // 连接超时时间 (毫秒)
|
||||
const TEST_TIMEOUT = 15000; // 测试连接超时时间 (毫秒)
|
||||
|
||||
// Define AuthenticatedWebSocket interface (or import from websocket.ts if refactored there)
|
||||
// This is needed to associate SSH clients with specific WS connections
|
||||
interface AuthenticatedWebSocket extends WebSocket {
|
||||
isAlive?: boolean;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
// sshClient?: Client; // Managed by the service now
|
||||
// sshShellStream?: ClientChannel; // Managed by the service now
|
||||
}
|
||||
|
||||
// Structure to hold active SSH connection details managed by this service
|
||||
interface ActiveSshSession {
|
||||
client: Client;
|
||||
shell: ClientChannel;
|
||||
// sftp?: SFTPWrapper; // SFTP will be managed by SftpService
|
||||
// statusIntervalId?: NodeJS.Timeout; // Status polling managed by StatusMonitorService
|
||||
connectionInfo: DecryptedConnectionDetails; // Store connection info for context (Fix typo)
|
||||
}
|
||||
|
||||
// Map to store active sessions associated with WebSocket clients
|
||||
const activeSessions = new Map<AuthenticatedWebSocket, ActiveSshSession>();
|
||||
|
||||
|
||||
// 辅助接口:定义解密后的凭证和代理信息结构 (可以共享到 types 文件)
|
||||
// Renamed to avoid conflict if imported later
|
||||
interface DecryptedConnectionDetails {
|
||||
// 辅助接口:定义解密后的凭证和代理信息结构 (导出以便 websocket.ts 使用)
|
||||
export interface DecryptedConnectionDetails {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
@@ -57,30 +27,27 @@ interface DecryptedConnectionDetails {
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string; // Decrypted
|
||||
auth_method?: string; // Proxy auth method
|
||||
privateKey?: string; // Decrypted proxy key
|
||||
passphrase?: string; // Decrypted proxy passphrase
|
||||
// auth_method?: string; // Proxy auth method (如果需要可以保留)
|
||||
// privateKey?: string; // Decrypted proxy key (如果需要可以保留)
|
||||
// passphrase?: string; // Decrypted proxy passphrase (如果需要可以保留)
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试给定 ID 的 SSH 连接(包括代理)
|
||||
* 获取并解密指定 ID 的完整连接信息(包括代理)
|
||||
* @param connectionId 连接 ID
|
||||
* @returns Promise<void> - 如果连接成功则 resolve,否则 reject
|
||||
* @throws Error 如果连接失败或配置错误
|
||||
* @returns Promise<DecryptedConnectionDetails> 解密后的连接详情
|
||||
* @throws Error 如果连接配置未找到或解密失败
|
||||
*/
|
||||
export const testConnection = async (connectionId: number): Promise<void> => {
|
||||
console.log(`SshService: Testing connection ${connectionId}...`);
|
||||
// 1. 获取完整的连接信息(包括加密凭证和代理信息)
|
||||
const rawConnInfo = await ConnectionRepository.findFullConnectionById(connectionId); // Assuming this fetches proxy details too
|
||||
export const getConnectionDetails = async (connectionId: number): Promise<DecryptedConnectionDetails> => {
|
||||
console.log(`SshService: 获取连接 ${connectionId} 的详细信息...`);
|
||||
const rawConnInfo = await ConnectionRepository.findFullConnectionById(connectionId);
|
||||
if (!rawConnInfo) {
|
||||
throw new Error('连接配置未找到。');
|
||||
throw new Error(`连接配置 ID ${connectionId} 未找到。`);
|
||||
}
|
||||
|
||||
// 2. 解密凭证并构建结构化的连接信息
|
||||
let fullConnInfo: DecryptedConnectionDetails;
|
||||
try {
|
||||
fullConnInfo = {
|
||||
const fullConnInfo: DecryptedConnectionDetails = {
|
||||
id: rawConnInfo.id,
|
||||
name: rawConnInfo.name,
|
||||
host: rawConnInfo.host,
|
||||
@@ -101,99 +68,91 @@ export const testConnection = async (connectionId: number): Promise<void> => {
|
||||
host: rawConnInfo.proxy_host,
|
||||
port: rawConnInfo.proxy_port,
|
||||
username: rawConnInfo.proxy_username || undefined,
|
||||
auth_method: rawConnInfo.proxy_auth_method, // Include proxy auth method
|
||||
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined,
|
||||
privateKey: rawConnInfo.proxy_encrypted_private_key ? decrypt(rawConnInfo.proxy_encrypted_private_key) : undefined, // Decrypt proxy key
|
||||
passphrase: rawConnInfo.proxy_encrypted_passphrase ? decrypt(rawConnInfo.proxy_encrypted_passphrase) : undefined, // Decrypt proxy passphrase
|
||||
// 可以根据需要解密代理的其他凭证
|
||||
};
|
||||
}
|
||||
console.log(`SshService: 连接 ${connectionId} 的详细信息获取并解密成功。`);
|
||||
return fullConnInfo;
|
||||
} catch (decryptError: any) {
|
||||
console.error(`Service: 处理连接 ${connectionId} 凭证或代理凭证失败:`, decryptError);
|
||||
console.error(`SshService: 处理连接 ${connectionId} 凭证或代理凭证失败:`, decryptError);
|
||||
throw new Error(`处理凭证失败: ${decryptError.message}`);
|
||||
}
|
||||
|
||||
// 3. 构建 ssh2 连接配置
|
||||
const connectConfig: ConnectConfig = { // Use ConnectConfig type
|
||||
host: fullConnInfo.host,
|
||||
port: fullConnInfo.port,
|
||||
username: fullConnInfo.username,
|
||||
password: fullConnInfo.password,
|
||||
privateKey: fullConnInfo.privateKey,
|
||||
passphrase: fullConnInfo.passphrase,
|
||||
readyTimeout: TEST_TIMEOUT,
|
||||
keepaliveInterval: 0, // 测试连接不需要 keepalive
|
||||
};
|
||||
|
||||
// 4. 应用代理配置并执行连接 (Refactored into helper)
|
||||
const sshClient = new Client();
|
||||
try {
|
||||
await establishSshConnection(sshClient, connectConfig, fullConnInfo.proxy); // Use helper
|
||||
console.log(`SshService: Test connection ${connectionId} successful.`);
|
||||
// Test successful, void promise resolves implicitly
|
||||
} catch (error) {
|
||||
console.error(`SshService: Test connection ${connectionId} failed:`, error);
|
||||
throw error; // Re-throw the specific error
|
||||
} finally {
|
||||
// 无论成功失败,都关闭 SSH 客户端
|
||||
sshClient.end();
|
||||
console.log(`SshService: Test connection ${connectionId} client closed.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- NEW FUNCTIONS FOR MANAGING LIVE CONNECTIONS ---
|
||||
|
||||
/**
|
||||
* Establishes an SSH connection, handling proxies.
|
||||
* Internal helper function.
|
||||
* @param sshClient - The ssh2 Client instance.
|
||||
* @param connectConfig - Base SSH connection config.
|
||||
* @param proxyInfo - Optional proxy details.
|
||||
* @returns Promise that resolves when SSH is ready, or rejects on error.
|
||||
* 根据解密后的连接详情建立 SSH 连接(处理代理)
|
||||
* @param connDetails - 解密后的连接详情
|
||||
* @param timeout - 连接超时时间 (毫秒),可选
|
||||
* @returns Promise<Client> 连接成功的 SSH Client 实例
|
||||
* @throws Error 如果连接失败
|
||||
*/
|
||||
const establishSshConnection = (
|
||||
sshClient: Client,
|
||||
connectConfig: ConnectConfig,
|
||||
proxyInfo: DecryptedConnectionDetails['proxy']
|
||||
): Promise<void> => {
|
||||
export const establishSshConnection = (
|
||||
connDetails: DecryptedConnectionDetails,
|
||||
timeout: number = CONNECT_TIMEOUT
|
||||
): Promise<Client> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const readyHandler = () => {
|
||||
sshClient.removeListener('error', errorHandler); // Clean up error listener on success
|
||||
resolve();
|
||||
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: 30000, // 保持连接
|
||||
keepaliveCountMax: 3,
|
||||
};
|
||||
|
||||
const readyHandler = () => {
|
||||
console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`);
|
||||
sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器
|
||||
resolve(sshClient); // 返回 Client 实例
|
||||
};
|
||||
|
||||
const errorHandler = (err: Error) => {
|
||||
sshClient.removeListener('ready', readyHandler); // Clean up ready listener on error
|
||||
reject(err); // Reject with the specific 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); // Generic error handler for direct connect issues
|
||||
sshClient.once('error', errorHandler);
|
||||
|
||||
if (proxyInfo) {
|
||||
const proxy = proxyInfo;
|
||||
console.log(`SshService: Applying proxy ${proxy.name} (${proxy.type})`);
|
||||
// --- 处理代理 ---
|
||||
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 }, // Type 5 is implicit
|
||||
proxy: { host: proxy.host, port: proxy.port, type: 5, userId: proxy.username, password: proxy.password },
|
||||
command: 'connect',
|
||||
destination: { host: connectConfig.host!, port: connectConfig.port! }, // Use base config host/port
|
||||
timeout: connectConfig.readyTimeout ?? CONNECT_TIMEOUT, // Use connection timeout
|
||||
destination: { host: connectConfig.host!, port: connectConfig.port! },
|
||||
timeout: connectConfig.readyTimeout,
|
||||
};
|
||||
SocksClient.createConnection(socksOptions)
|
||||
.then(({ socket }) => {
|
||||
console.log(`SshService: SOCKS5 proxy connection successful.`);
|
||||
console.log(`SshService: SOCKS5 代理连接成功 (目标: ${connDetails.host}:${connDetails.port})。`);
|
||||
connectConfig.sock = socket;
|
||||
sshClient.connect(connectConfig); // Connect SSH via proxy socket
|
||||
sshClient.connect(connectConfig);
|
||||
})
|
||||
.catch(socksError => {
|
||||
console.error(`SshService: SOCKS5 proxy connection failed:`, socksError);
|
||||
// Reject the main promise, remove listeners handled by errorHandler
|
||||
errorHandler(new Error(`SOCKS5 代理连接失败: ${socksError.message}`));
|
||||
errorHandler(new Error(`SOCKS5 代理 ${proxy.host}:${proxy.port} 连接失败: ${socksError.message}`));
|
||||
});
|
||||
|
||||
} else if (proxy.type === 'HTTP') {
|
||||
console.log(`SshService: Attempting HTTP proxy tunnel via ${proxy.host}:${proxy.port}...`);
|
||||
const reqOptions: http.RequestOptions = { method: 'CONNECT', host: proxy.host, port: proxy.port, path: `${connectConfig.host}:${connectConfig.port}`, timeout: connectConfig.readyTimeout ?? CONNECT_TIMEOUT, agent: false };
|
||||
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}` };
|
||||
@@ -201,233 +160,86 @@ const establishSshConnection = (
|
||||
const req = http.request(reqOptions);
|
||||
req.on('connect', (res, socket, head) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log(`SshService: HTTP proxy tunnel established.`);
|
||||
console.log(`SshService: HTTP 代理隧道建立成功 (目标: ${connDetails.host}:${connDetails.port})。`);
|
||||
connectConfig.sock = socket;
|
||||
sshClient.connect(connectConfig); // Connect SSH via tunnel socket
|
||||
sshClient.connect(connectConfig);
|
||||
} else {
|
||||
console.error(`SshService: HTTP proxy CONNECT request failed, status code: ${res.statusCode}`);
|
||||
socket.destroy();
|
||||
errorHandler(new Error(`HTTP 代理连接失败 (状态码: ${res.statusCode})`)); // Reject main promise
|
||||
} // <-- Added missing closing parenthesis here
|
||||
errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 连接失败 (状态码: ${res.statusCode})`));
|
||||
}
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
console.error(`SshService: HTTP proxy request error:`, err);
|
||||
errorHandler(new Error(`HTTP 代理连接错误: ${err.message}`)); // Reject main promise
|
||||
errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 请求错误: ${err.message}`));
|
||||
});
|
||||
req.on('timeout', () => {
|
||||
console.error(`SshService: HTTP proxy request timeout.`);
|
||||
req.destroy();
|
||||
errorHandler(new Error('HTTP 代理连接超时')); // Reject main promise
|
||||
errorHandler(new Error(`HTTP 代理 ${proxy.host}:${proxy.port} 连接超时`));
|
||||
});
|
||||
req.end(); // Send the CONNECT request
|
||||
req.end();
|
||||
} else {
|
||||
errorHandler(new Error(`不支持的代理类型: ${proxy.type}`)); // Reject main promise
|
||||
errorHandler(new Error(`不支持的代理类型: ${proxy.type}`));
|
||||
}
|
||||
} else {
|
||||
// No proxy, connect directly
|
||||
console.log(`SshService: No proxy detected, connecting directly...`);
|
||||
// 无代理,直接连接
|
||||
console.log(`SshService: 无代理,直接连接到 ${connDetails.host}:${connDetails.port}`);
|
||||
sshClient.connect(connectConfig);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 在已连接的 SSH Client 上打开 Shell 通道
|
||||
* @param sshClient - 已连接的 SSH Client 实例
|
||||
* @returns Promise<ClientChannel> Shell 通道实例
|
||||
* @throws Error 如果打开 Shell 失败
|
||||
*/
|
||||
export const openShell = (sshClient: Client): Promise<ClientChannel> => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects to SSH, opens a shell, and sets up event forwarding via WebSocket.
|
||||
* @param connectionId - The ID of the connection config in the database.
|
||||
* @param ws - The authenticated WebSocket client instance.
|
||||
* 测试给定 ID 的 SSH 连接(包括代理)
|
||||
* @param connectionId 连接 ID
|
||||
* @returns Promise<void> - 如果连接成功则 resolve,否则 reject
|
||||
* @throws Error 如果连接失败或配置错误
|
||||
*/
|
||||
export const connectAndOpenShell = async (connectionId: number, ws: AuthenticatedWebSocket): Promise<void> => {
|
||||
console.log(`SshService: User ${ws.username} requested connection to ID: ${connectionId}`);
|
||||
if (activeSessions.has(ws)) {
|
||||
console.warn(`SshService: User ${ws.username} already has an active session.`);
|
||||
throw new Error('已存在活动的 SSH 连接。');
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({ type: 'ssh:status', payload: '正在获取连接信息...' }));
|
||||
|
||||
// 1. Get connection info
|
||||
const rawConnInfo = await ConnectionRepository.findFullConnectionById(connectionId);
|
||||
if (!rawConnInfo) {
|
||||
throw new Error('连接配置未找到。');
|
||||
}
|
||||
|
||||
// 2. Decrypt and prepare connection details
|
||||
let fullConnInfo: DecryptedConnectionDetails;
|
||||
export const testConnection = async (connectionId: number): Promise<void> => {
|
||||
console.log(`SshService: 测试连接 ${connectionId}...`);
|
||||
let sshClient: Client | null = null;
|
||||
try {
|
||||
// (Decryption logic similar to testConnection, could be refactored)
|
||||
fullConnInfo = { /* ... decryption ... */
|
||||
id: rawConnInfo.id, name: rawConnInfo.name, host: rawConnInfo.host, port: rawConnInfo.port, username: rawConnInfo.username, auth_method: rawConnInfo.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) {
|
||||
fullConnInfo.proxy = { /* ... proxy decryption ... */
|
||||
id: rawConnInfo.proxy_db_id, name: rawConnInfo.proxy_name, type: rawConnInfo.proxy_type, host: rawConnInfo.proxy_host, port: rawConnInfo.proxy_port, username: rawConnInfo.proxy_username || undefined, auth_method: rawConnInfo.proxy_auth_method,
|
||||
password: rawConnInfo.proxy_encrypted_password ? decrypt(rawConnInfo.proxy_encrypted_password) : undefined,
|
||||
privateKey: rawConnInfo.proxy_encrypted_private_key ? decrypt(rawConnInfo.proxy_encrypted_private_key) : undefined,
|
||||
passphrase: rawConnInfo.proxy_encrypted_passphrase ? decrypt(rawConnInfo.proxy_encrypted_passphrase) : undefined,
|
||||
};
|
||||
// 1. 获取并解密连接信息
|
||||
const connDetails = await getConnectionDetails(connectionId);
|
||||
|
||||
// 2. 尝试建立连接 (使用较短的测试超时时间)
|
||||
sshClient = await establishSshConnection(connDetails, TEST_TIMEOUT);
|
||||
|
||||
console.log(`SshService: 测试连接 ${connectionId} 成功。`);
|
||||
// 测试成功,Promise 自动 resolve void
|
||||
} catch (error) {
|
||||
console.error(`SshService: 测试连接 ${connectionId} 失败:`, error);
|
||||
throw error; // 将错误向上抛出
|
||||
} finally {
|
||||
// 无论成功失败,都关闭 SSH 客户端
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
console.log(`SshService: 测试连接 ${connectionId} 的客户端已关闭。`);
|
||||
}
|
||||
} catch (decryptError: any) {
|
||||
console.error(`SshService: Handling credentials failed for ${connectionId}:`, decryptError);
|
||||
throw new Error(`无法处理连接凭证: ${decryptError.message}`);
|
||||
}
|
||||
|
||||
// 3. Prepare SSH config
|
||||
const connectConfig: ConnectConfig = {
|
||||
host: fullConnInfo.host,
|
||||
port: fullConnInfo.port,
|
||||
username: fullConnInfo.username,
|
||||
password: fullConnInfo.password,
|
||||
privateKey: fullConnInfo.privateKey,
|
||||
passphrase: fullConnInfo.passphrase,
|
||||
readyTimeout: CONNECT_TIMEOUT,
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
};
|
||||
|
||||
// 4. Establish connection and open shell
|
||||
const sshClient = new Client();
|
||||
|
||||
// Generic error/close handlers for the client
|
||||
const clientCloseHandler = () => {
|
||||
console.log(`SshService: SSH client for ${ws.username} closed.`);
|
||||
if (activeSessions.has(ws)) { // Check if cleanup wasn't already called
|
||||
if (!ws.CLOSED && !ws.CLOSING) {
|
||||
ws.send(JSON.stringify({ type: 'ssh:disconnected', payload: 'SSH 连接已关闭。' }));
|
||||
}
|
||||
cleanupConnection(ws); // Ensure cleanup
|
||||
}
|
||||
};
|
||||
const clientErrorHandler = (err: Error) => {
|
||||
console.error(`SshService: SSH client error for ${ws.username}:`, err);
|
||||
if (activeSessions.has(ws)) { // Check if cleanup wasn't already called
|
||||
if (!ws.CLOSED && !ws.CLOSING) {
|
||||
ws.send(JSON.stringify({ type: 'ssh:error', payload: `SSH 连接错误: ${err.message}` }));
|
||||
}
|
||||
cleanupConnection(ws); // Ensure cleanup
|
||||
}
|
||||
};
|
||||
sshClient.on('close', clientCloseHandler);
|
||||
sshClient.on('error', clientErrorHandler);
|
||||
|
||||
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'ssh:status', payload: `正在连接到 ${fullConnInfo.host}...` }));
|
||||
await establishSshConnection(sshClient, connectConfig, fullConnInfo.proxy); // Use helper
|
||||
|
||||
ws.send(JSON.stringify({ type: 'ssh:status', payload: 'SSH 连接成功,正在打开 Shell...' }));
|
||||
|
||||
// 5. Open Shell Stream
|
||||
const shellStream = await new Promise<ClientChannel>((resolve, reject) => {
|
||||
sshClient.shell((err, stream) => {
|
||||
if (err) {
|
||||
console.error(`SshService: User ${ws.username} failed to open shell:`, err);
|
||||
return reject(new Error(`打开 Shell 失败: ${err.message}`));
|
||||
}
|
||||
console.log(`SshService: User ${ws.username} shell channel opened.`);
|
||||
resolve(stream);
|
||||
});
|
||||
});
|
||||
|
||||
// 6. Store active session
|
||||
const session: ActiveSshSession = { client: sshClient, shell: shellStream, connectionInfo: fullConnInfo };
|
||||
activeSessions.set(ws, session);
|
||||
console.log(`SshService: Active session stored for ${ws.username}.`);
|
||||
|
||||
// 7. Setup event forwarding for the shell stream
|
||||
shellStream.on('data', (data: Buffer) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
||||
}
|
||||
});
|
||||
shellStream.stderr.on('data', (data: Buffer) => {
|
||||
console.error(`SSH Stderr (${ws.username}): ${data.toString('utf8').substring(0,100)}...`);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' })); // Send stderr as output
|
||||
}
|
||||
});
|
||||
shellStream.on('close', () => {
|
||||
console.log(`SshService: Shell stream for ${ws.username} closed.`);
|
||||
if (activeSessions.has(ws)) { // Check if cleanup wasn't already called by client close
|
||||
if (!ws.CLOSED && !ws.CLOSING) {
|
||||
ws.send(JSON.stringify({ type: 'ssh:disconnected', payload: 'Shell 通道已关闭。' }));
|
||||
}
|
||||
cleanupConnection(ws); // Trigger cleanup if shell closes independently
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Initialize SFTP (TODO: Move to SftpService) and Status Polling (TODO: Move to StatusMonitorService)
|
||||
// For now, just notify connection success
|
||||
ws.send(JSON.stringify({ type: 'ssh:connected' }));
|
||||
|
||||
// TODO: Call SftpService.initializeSftpSession(ws, sshClient);
|
||||
// TODO: Call StatusMonitorService.startStatusPolling(ws, sshClient);
|
||||
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`SshService: Failed to connect or open shell for ${ws.username}:`, error);
|
||||
// Ensure client listeners are removed and client is ended on failure
|
||||
sshClient.removeListener('close', clientCloseHandler);
|
||||
sshClient.removeListener('error', clientErrorHandler);
|
||||
sshClient.end();
|
||||
cleanupConnection(ws); // Clean up any partial state
|
||||
throw error; // Re-throw for the controller
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends input data to the SSH shell stream associated with a WebSocket connection.
|
||||
* @param ws - The authenticated WebSocket client.
|
||||
* @param data - The data string to send.
|
||||
*/
|
||||
export const sendInput = (ws: AuthenticatedWebSocket, data: string): void => {
|
||||
const session = activeSessions.get(ws);
|
||||
if (session?.shell && session.shell.writable) {
|
||||
session.shell.write(data);
|
||||
} else {
|
||||
console.warn(`SshService: Cannot send input for ${ws.username}, no active/writable shell stream found.`);
|
||||
// Optionally notify the client ws.send(...)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes the pseudo-terminal associated with a WebSocket connection.
|
||||
* @param ws - The authenticated WebSocket client.
|
||||
* @param cols - Terminal width in columns.
|
||||
* @param rows - Terminal height in rows.
|
||||
*/
|
||||
export const resizeTerminal = (ws: AuthenticatedWebSocket, cols: number, rows: number): void => {
|
||||
const session = activeSessions.get(ws);
|
||||
if (session?.shell) {
|
||||
console.log(`SshService: Resizing terminal for ${ws.username} to ${cols}x${rows}`);
|
||||
session.shell.setWindow(rows, cols, 0, 0); // Note: rows, cols order
|
||||
} else {
|
||||
console.warn(`SshService: Cannot resize terminal for ${ws.username}, no active shell stream found.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans up SSH resources associated with a WebSocket connection.
|
||||
* @param ws - The authenticated WebSocket client.
|
||||
*/
|
||||
export const cleanupConnection = (ws: AuthenticatedWebSocket): void => {
|
||||
const session = activeSessions.get(ws);
|
||||
if (session) {
|
||||
console.log(`SshService: Cleaning up SSH session for ${ws.username}...`);
|
||||
// TODO: Call StatusMonitorService.stopStatusPolling(ws);
|
||||
// TODO: Call SftpService.cleanupSftpSession(ws);
|
||||
|
||||
// End streams and client
|
||||
session.shell?.end(); // End the shell stream first
|
||||
session.client?.end(); // End the main SSH client connection
|
||||
|
||||
activeSessions.delete(ws); // Remove from active sessions map
|
||||
console.log(`SshService: SSH session for ${ws.username} cleaned up.`);
|
||||
} else {
|
||||
// console.log(`SshService: No active SSH session found for ${ws.username} during cleanup.`);
|
||||
}
|
||||
};
|
||||
// --- 移除旧的函数 ---
|
||||
// - connectAndOpenShell
|
||||
// - sendInput
|
||||
// - resizeTerminal
|
||||
// - cleanupConnection
|
||||
// - activeSessions Map
|
||||
// - AuthenticatedWebSocket interface (如果仅在此文件使用)
|
||||
|
||||
Reference in New Issue
Block a user