+170
-164
@@ -36,6 +36,7 @@ export interface ClientState { // 导出以便 Service 可以导入
|
|||||||
statusIntervalId?: NodeJS.Timeout; // 添加状态轮询 ID (由 StatusMonitorService 管理)
|
statusIntervalId?: NodeJS.Timeout; // 添加状态轮询 ID (由 StatusMonitorService 管理)
|
||||||
dockerStatusIntervalId?: NodeJS.Timeout; // NEW: Docker 状态轮询 ID
|
dockerStatusIntervalId?: NodeJS.Timeout; // NEW: Docker 状态轮询 ID
|
||||||
ipAddress?: string; // 添加 IP 地址字段
|
ipAddress?: string; // 添加 IP 地址字段
|
||||||
|
isShellReady?: boolean; // 新增:标记 Shell 是否已准备好处理输入和调整大小
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -524,7 +525,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
// 根据宽高的简单 DPI 计算逻辑 (如果宽度 > 1920,则 DPI=120,否则 DPI=96)
|
// 根据宽高的简单 DPI 计算逻辑 (如果宽度 > 1920,则 DPI=120,否则 DPI=96)
|
||||||
const calculatedDpi = rdpWidth > 1920 ? 120 : 96;
|
const calculatedDpi = rdpWidth > 1920 ? 120 : 96;
|
||||||
console.log(`WebSocket: RDP Proxy calculated DPI for ${ws.username} based on width ${rdpWidth}: ${calculatedDpi}`);
|
console.log(`WebSocket: RDP Proxy calculated DPI for ${ws.username} based on width ${rdpWidth}: ${calculatedDpi}`);
|
||||||
// --- 结束新增 ---
|
|
||||||
|
|
||||||
|
|
||||||
// Determine RDP target URL based on deployment mode
|
// Determine RDP target URL based on deployment mode
|
||||||
@@ -541,9 +542,8 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
console.warn(`[WebSocket RDP Proxy] Unknown deployment mode '${deploymentMode}'. Defaulting to safe fallback RDP Target Base: ${rdpBaseUrl}`);
|
console.warn(`[WebSocket RDP Proxy] Unknown deployment mode '${deploymentMode}'. Defaulting to safe fallback RDP Target Base: ${rdpBaseUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure base URL doesn't end with a slash before appending query params
|
|
||||||
const cleanRdpBaseUrl = rdpBaseUrl.endsWith('/') ? rdpBaseUrl.slice(0, -1) : rdpBaseUrl;
|
const cleanRdpBaseUrl = rdpBaseUrl.endsWith('/') ? rdpBaseUrl.slice(0, -1) : rdpBaseUrl;
|
||||||
// Append ALL parameters to the target URL, using calculated DPI
|
|
||||||
const rdpTargetUrl = `${cleanRdpBaseUrl}/?token=${encodeURIComponent(rdpToken)}&width=${encodeURIComponent(rdpWidth)}&height=${encodeURIComponent(rdpHeight)}&dpi=${encodeURIComponent(calculatedDpi)}`; // 使用 calculatedDpi
|
const rdpTargetUrl = `${cleanRdpBaseUrl}/?token=${encodeURIComponent(rdpToken)}&width=${encodeURIComponent(rdpWidth)}&height=${encodeURIComponent(rdpHeight)}&dpi=${encodeURIComponent(calculatedDpi)}`; // 使用 calculatedDpi
|
||||||
|
|
||||||
console.log(`WebSocket: RDP Proxy for ${ws.username} attempting to connect to ${rdpTargetUrl}`);
|
console.log(`WebSocket: RDP Proxy for ${ws.username} attempting to connect to ${rdpTargetUrl}`);
|
||||||
@@ -555,10 +555,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
// --- 消息转发: Client -> RDP ---
|
// --- 消息转发: Client -> RDP ---
|
||||||
ws.on('message', (message: RawData) => {
|
ws.on('message', (message: RawData) => {
|
||||||
if (rdpWs.readyState === WebSocket.OPEN) {
|
if (rdpWs.readyState === WebSocket.OPEN) {
|
||||||
// --- 添加中文日志 ---
|
|
||||||
const messageString = message.toString('utf-8'); // 尝试解码为 UTF-8
|
const messageString = message.toString('utf-8'); // 尝试解码为 UTF-8
|
||||||
// console.log(`[RDP 代理 C->S] 用户: ${ws.username}, 会话: ${ws.sessionId}, 转发消息 (前 100 字符): ${messageString.substring(0, 100)}`);
|
|
||||||
// --- 结束日志 ---
|
|
||||||
rdpWs.send(message);
|
rdpWs.send(message);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[RDP 代理 C->S] 用户: ${ws.username}, 会话: ${ws.sessionId}, RDP WS 未打开,丢弃消息。`);
|
console.warn(`[RDP 代理 C->S] 用户: ${ws.username}, 会话: ${ws.sessionId}, RDP WS 未打开,丢弃消息。`);
|
||||||
@@ -570,9 +567,6 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
// 将 RawData (可能是 Buffer) 转换为 UTF-8 字符串再发送
|
// 将 RawData (可能是 Buffer) 转换为 UTF-8 字符串再发送
|
||||||
const messageString = message.toString('utf-8');
|
const messageString = message.toString('utf-8');
|
||||||
// --- 添加中文日志 ---
|
|
||||||
// console.log(`[RDP 代理 S->C] 用户: ${ws.username}, 会话: ${ws.sessionId}, 转发消息 (前 100 字符): ${messageString.substring(0, 100)}`);
|
|
||||||
// --- 结束日志 ---
|
|
||||||
ws.send(messageString);
|
ws.send(messageString);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[RDP 代理 S->C] 用户: ${ws.username}, 会话: ${ws.sessionId}, 客户端 WS 未打开,丢弃消息。`);
|
console.warn(`[RDP 代理 S->C] 用户: ${ws.username}, 会话: ${ws.sessionId}, 客户端 WS 未打开,丢弃消息。`);
|
||||||
@@ -581,9 +575,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
|
|
||||||
// --- 错误处理 ---
|
// --- 错误处理 ---
|
||||||
ws.on('error', (error) => {
|
ws.on('error', (error) => {
|
||||||
// --- 添加中文日志 ---
|
|
||||||
console.error(`[RDP 代理 客户端 WS 错误] 用户: ${ws.username}, 会话: ${ws.sessionId}, 错误:`, error);
|
console.error(`[RDP 代理 客户端 WS 错误] 用户: ${ws.username}, 会话: ${ws.sessionId}, 错误:`, error);
|
||||||
// --- 结束日志 ---
|
|
||||||
if (!rdpWsClosed && rdpWs.readyState !== WebSocket.CLOSED && rdpWs.readyState !== WebSocket.CLOSING) {
|
if (!rdpWsClosed && rdpWs.readyState !== WebSocket.CLOSED && rdpWs.readyState !== WebSocket.CLOSING) {
|
||||||
console.log(`[RDP 代理] 因客户端 WS 错误关闭 RDP WS。会话: ${ws.sessionId}`);
|
console.log(`[RDP 代理] 因客户端 WS 错误关闭 RDP WS。会话: ${ws.sessionId}`);
|
||||||
rdpWs.close(1011, 'Client WS Error');
|
rdpWs.close(1011, 'Client WS Error');
|
||||||
@@ -592,9 +584,8 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
clientWsClosed = true;
|
clientWsClosed = true;
|
||||||
});
|
});
|
||||||
rdpWs.on('error', (error) => {
|
rdpWs.on('error', (error) => {
|
||||||
// --- 添加中文日志 ---
|
|
||||||
console.error(`[RDP 代理 RDP WS 错误] 用户: ${ws.username}, 会话: ${ws.sessionId}, 连接到 ${rdpTargetUrl} 时出错:`, error);
|
console.error(`[RDP 代理 RDP WS 错误] 用户: ${ws.username}, 会话: ${ws.sessionId}, 连接到 ${rdpTargetUrl} 时出错:`, error);
|
||||||
// --- 结束日志 ---
|
|
||||||
if (!clientWsClosed && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
if (!clientWsClosed && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
||||||
console.log(`[RDP 代理] 因 RDP WS 错误关闭客户端 WS。会话: ${ws.sessionId}`);
|
console.log(`[RDP 代理] 因 RDP WS 错误关闭客户端 WS。会话: ${ws.sessionId}`);
|
||||||
ws.close(1011, `RDP WS Error: ${error.message}`);
|
ws.close(1011, `RDP WS Error: ${error.message}`);
|
||||||
@@ -628,10 +619,7 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
});
|
});
|
||||||
|
|
||||||
rdpWs.on('open', () => {
|
rdpWs.on('open', () => {
|
||||||
// --- 添加中文日志 ---
|
|
||||||
console.log(`[RDP 代理 RDP WS 打开] 用户: ${ws.username}, 会话: ${ws.sessionId}, 到 ${rdpTargetUrl} 的连接已建立。开始转发消息。`);
|
console.log(`[RDP 代理 RDP WS 打开] 用户: ${ws.username}, 会话: ${ws.sessionId}, 到 ${rdpTargetUrl} 的连接已建立。开始转发消息。`);
|
||||||
// --- 结束日志 ---
|
|
||||||
// Do not send custom message, let Guacamole protocol flow directly
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 标准 (SSH/SFTP/Docker) 连接处理 ---
|
// --- 标准 (SSH/SFTP/Docker) 连接处理 ---
|
||||||
@@ -693,175 +681,185 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
sshClient: sshClient,
|
sshClient: sshClient,
|
||||||
dbConnectionId: dbConnectionId,
|
dbConnectionId: dbConnectionId,
|
||||||
ipAddress: clientIp, // 存储 IP 地址
|
ipAddress: clientIp, // 存储 IP 地址
|
||||||
|
isShellReady: false, // 初始化 Shell 状态为未就绪
|
||||||
};
|
};
|
||||||
clientStates.set(newSessionId, newState);
|
clientStates.set(newSessionId, newState);
|
||||||
console.log(`WebSocket: 为用户 ${ws.username} (IP: ${clientIp}) 创建新会话 ${newSessionId} (DB ID: ${dbConnectionId})`);
|
console.log(`WebSocket: 为用户 ${ws.username} (IP: ${clientIp}) 创建新会话 ${newSessionId} (DB ID: ${dbConnectionId})`);
|
||||||
|
|
||||||
// 4. 打开 Shell
|
// 4. 立即打开 Shell (使用默认尺寸)
|
||||||
ws.send(JSON.stringify({ type: 'ssh:status', payload: 'SSH 连接成功,正在打开 Shell...' }));
|
ws.send(JSON.stringify({ type: 'ssh:status', payload: 'SSH 连接成功,正在打开 Shell...' }));
|
||||||
try {
|
try {
|
||||||
const shellStream = await SshService.openShell(sshClient);
|
// 使用默认尺寸 80x24 打开 Shell
|
||||||
newState.sshShellStream = shellStream;
|
const defaultCols = 80;
|
||||||
|
const defaultRows = 24;
|
||||||
// 5. 设置 Shell 事件转发
|
sshClient.shell({ term: 'xterm-256color', cols: defaultCols, rows: defaultRows }, (err, stream) => {
|
||||||
shellStream.on('data', (data: Buffer) => {
|
if (err) {
|
||||||
// --- 添加日志:打印收到的原始数据 ---
|
console.error(`SSH: 会话 ${newSessionId} 打开 Shell 失败:`, err);
|
||||||
// console.log(`SSH Data (会话: ${newSessionId}, 原始): `, data.toString()); // 添加原始数据日志 (尝试 utf8)
|
// 记录审计日志:打开 Shell 失败
|
||||||
// console.log(`SSH Data (会话: ${newSessionId}, Hex): `, data.toString('hex')); // 添加 Hex 日志
|
auditLogService.logAction('SSH_SHELL_FAILURE', {
|
||||||
|
userId: ws.userId,
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
username: ws.username,
|
||||||
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
connectionId: dbConnectionId,
|
||||||
|
sessionId: newSessionId,
|
||||||
|
ip: newState.ipAddress,
|
||||||
|
reason: err.message
|
||||||
|
});
|
||||||
|
notificationService.sendNotification('SSH_SHELL_FAILURE', { // 添加通知调用
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
connectionId: dbConnectionId,
|
||||||
|
sessionId: newSessionId,
|
||||||
|
ip: newState.ipAddress,
|
||||||
|
reason: err.message
|
||||||
|
});
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ssh:error', payload: `打开 Shell 失败: ${err.message}` }));
|
||||||
|
}
|
||||||
|
cleanupClientConnection(newSessionId); // 清理连接
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
shellStream.stderr.on('data', (data: Buffer) => {
|
|
||||||
console.error(`SSH Stderr (会话: ${newSessionId}): ${data.toString('utf8').substring(0, 100)}...`);
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
shellStream.on('close', () => {
|
|
||||||
console.log(`SSH: 会话 ${newSessionId} 的 Shell 通道已关闭。`);
|
|
||||||
ws.send(JSON.stringify({ type: 'ssh:disconnected', payload: 'Shell 通道已关闭。' }));
|
|
||||||
cleanupClientConnection(newSessionId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. 发送 SSH 连接成功消息
|
// Shell 打开成功
|
||||||
ws.send(JSON.stringify({
|
console.log(`WebSocket: 会话 ${newSessionId} Shell 打开成功 (使用默认尺寸 ${defaultCols}x${defaultRows})。`);
|
||||||
type: 'ssh:connected',
|
newState.sshShellStream = stream;
|
||||||
payload: {
|
newState.isShellReady = true; // 标记 Shell 已就绪
|
||||||
|
|
||||||
|
// 5. 立即设置 Shell 事件转发 (捕获初始输出)
|
||||||
|
stream.on('data', (data: Buffer) => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.stderr.on('data', (data: Buffer) => {
|
||||||
|
console.error(`SSH Stderr (会话: ${newSessionId}): ${data.toString('utf8').substring(0, 100)}...`);
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.on('close', () => {
|
||||||
|
console.log(`SSH: 会话 ${newSessionId} 的 Shell 通道已关闭。`);
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ssh:disconnected', payload: 'Shell 通道已关闭。' }));
|
||||||
|
}
|
||||||
|
cleanupClientConnection(newSessionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 发送 SSH 连接成功消息 (现在 Shell 也已打开)
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'ssh:connected',
|
||||||
|
payload: {
|
||||||
|
connectionId: dbConnectionId,
|
||||||
|
sessionId: newSessionId
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
console.log(`WebSocket: 会话 ${newSessionId} SSH 连接和 Shell 建立成功。`);
|
||||||
|
// 记录审计日志:SSH 连接成功
|
||||||
|
auditLogService.logAction('SSH_CONNECT_SUCCESS', {
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
connectionId: dbConnectionId,
|
connectionId: dbConnectionId,
|
||||||
sessionId: newSessionId
|
sessionId: newSessionId,
|
||||||
}
|
ip: newState.ipAddress
|
||||||
}));
|
});
|
||||||
console.log(`WebSocket: 会话 ${newSessionId} SSH 连接和 Shell 建立成功。`);
|
notificationService.sendNotification('SSH_CONNECT_SUCCESS', { // 添加通知调用
|
||||||
// 记录审计日志:SSH 连接成功
|
userId: ws.userId,
|
||||||
auditLogService.logAction('SSH_CONNECT_SUCCESS', {
|
username: ws.username,
|
||||||
userId: ws.userId,
|
connectionId: dbConnectionId,
|
||||||
username: ws.username,
|
sessionId: newSessionId,
|
||||||
connectionId: dbConnectionId,
|
ip: newState.ipAddress
|
||||||
sessionId: newSessionId,
|
});
|
||||||
ip: newState.ipAddress
|
|
||||||
});
|
|
||||||
notificationService.sendNotification('SSH_CONNECT_SUCCESS', { // 添加通知调用
|
|
||||||
userId: ws.userId,
|
|
||||||
username: ws.username,
|
|
||||||
connectionId: dbConnectionId,
|
|
||||||
sessionId: newSessionId,
|
|
||||||
ip: newState.ipAddress
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7. 异步初始化 SFTP 和启动状态监控
|
// 7. 启动异步任务 (SFTP, Status Monitor, Docker)
|
||||||
console.log(`WebSocket: 会话 ${newSessionId} 正在异步初始化 SFTP...`);
|
console.log(`WebSocket: 会话 ${newSessionId} 正在异步初始化 SFTP...`);
|
||||||
sftpService.initializeSftpSession(newSessionId)
|
sftpService.initializeSftpSession(newSessionId)
|
||||||
.then(() => console.log(`SFTP: 会话 ${newSessionId} 异步初始化成功。`))
|
.then(() => console.log(`SFTP: 会话 ${newSessionId} 异步初始化成功。`))
|
||||||
.catch(sftpInitError => console.error(`WebSocket: 会话 ${newSessionId} 异步初始化 SFTP 失败:`, sftpInitError));
|
.catch(sftpInitError => console.error(`WebSocket: 会话 ${newSessionId} 异步初始化 SFTP 失败:`, sftpInitError));
|
||||||
|
|
||||||
console.log(`WebSocket: 会话 ${newSessionId} 正在启动状态监控...`);
|
console.log(`WebSocket: 会话 ${newSessionId} 正在启动状态监控...`);
|
||||||
statusMonitorService.startStatusPolling(newSessionId);
|
statusMonitorService.startStatusPolling(newSessionId);
|
||||||
|
|
||||||
|
console.log(`WebSocket: 会话 ${newSessionId} 正在启动 Docker 状态轮询...`);
|
||||||
console.log(`WebSocket: 会话 ${newSessionId} 正在启动 Docker 状态轮询...`);
|
let dockerPollIntervalMs = 2000;
|
||||||
|
(async () => { // 使用 IIFE 获取设置
|
||||||
let dockerPollIntervalMs = 2000;
|
|
||||||
try {
|
|
||||||
const intervalSetting = await settingsService.getSetting('dockerStatusIntervalSeconds');
|
|
||||||
if (intervalSetting) {
|
|
||||||
const intervalSeconds = parseInt(intervalSetting, 10);
|
|
||||||
if (!isNaN(intervalSeconds) && intervalSeconds >= 1) {
|
|
||||||
dockerPollIntervalMs = intervalSeconds * 1000;
|
|
||||||
console.log(`[Docker Polling] Using interval from settings: ${intervalSeconds}s (${dockerPollIntervalMs}ms) for session ${newSessionId}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`[Docker Polling] Invalid interval setting '${intervalSetting}' found. Using default ${dockerPollIntervalMs}ms for session ${newSessionId}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[Docker Polling] No interval setting found. Using default ${dockerPollIntervalMs}ms for session ${newSessionId}`);
|
|
||||||
}
|
|
||||||
} catch (settingError) {
|
|
||||||
console.error(`[Docker Polling] Error fetching interval setting for session ${newSessionId}. Using default ${dockerPollIntervalMs}ms:`, settingError);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const dockerIntervalId = setInterval(async () => {
|
|
||||||
const currentState = clientStates.get(newSessionId);
|
|
||||||
if (!currentState || currentState.ws.readyState !== WebSocket.OPEN) {
|
|
||||||
console.log(`[Docker Polling] Session ${newSessionId} no longer valid or WS closed. Stopping poll.`);
|
|
||||||
clearInterval(dockerIntervalId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
|
|
||||||
const statusPayload = await fetchRemoteDockerStatus(currentState);
|
|
||||||
if (currentState.ws.readyState === WebSocket.OPEN) {
|
|
||||||
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload }));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`[Docker Polling] Error fetching Docker status for session ${newSessionId}:`, error);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}, dockerPollIntervalMs);
|
|
||||||
newState.dockerStatusIntervalId = dockerIntervalId;
|
|
||||||
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const currentState = clientStates.get(newSessionId);
|
|
||||||
if (currentState && currentState.ws.readyState === WebSocket.OPEN) {
|
|
||||||
try {
|
try {
|
||||||
console.log(`[Docker Initial Fetch] Fetching status for session ${newSessionId}...`);
|
const intervalSetting = await settingsService.getSetting('dockerStatusIntervalSeconds');
|
||||||
const statusPayload = await fetchRemoteDockerStatus(currentState);
|
if (intervalSetting) {
|
||||||
if (currentState.ws.readyState === WebSocket.OPEN) {
|
const intervalSeconds = parseInt(intervalSetting, 10);
|
||||||
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload }));
|
if (!isNaN(intervalSeconds) && intervalSeconds >= 1) {
|
||||||
|
dockerPollIntervalMs = intervalSeconds * 1000;
|
||||||
|
console.log(`[Docker Polling] Using interval from settings: ${intervalSeconds}s (${dockerPollIntervalMs}ms) for session ${newSessionId}`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[Docker Polling] Invalid interval setting '${intervalSetting}' found. Using default ${dockerPollIntervalMs}ms for session ${newSessionId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[Docker Polling] No interval setting found. Using default ${dockerPollIntervalMs}ms for session ${newSessionId}`);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (settingError) {
|
||||||
console.error(`[Docker Initial Fetch] Error fetching Docker status for session ${newSessionId}:`, error);
|
console.error(`[Docker Polling] Error fetching interval setting for session ${newSessionId}. Using default ${dockerPollIntervalMs}ms:`, settingError);
|
||||||
if (currentState.ws.readyState === WebSocket.OPEN) {
|
}
|
||||||
|
|
||||||
const errorMessage = error.message || 'Unknown error during initial fetch';
|
const dockerIntervalId = setInterval(async () => {
|
||||||
const isUnavailable = errorMessage.includes('command not found') || errorMessage.includes('Cannot connect to the Docker daemon');
|
const currentState = clientStates.get(newSessionId);
|
||||||
if (isUnavailable) {
|
if (!currentState || currentState.ws.readyState !== WebSocket.OPEN) {
|
||||||
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: false, containers: [] } }));
|
console.log(`[Docker Polling] Session ${newSessionId} no longer valid or WS closed. Stopping poll.`);
|
||||||
} else {
|
clearInterval(dockerIntervalId);
|
||||||
currentState.ws.send(JSON.stringify({ type: 'docker:status:error', payload: { message: `Initial Docker status fetch failed: ${errorMessage}` } }));
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const statusPayload = await fetchRemoteDockerStatus(currentState);
|
||||||
|
if (currentState.ws.readyState === WebSocket.OPEN) {
|
||||||
|
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload }));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[Docker Polling] Error fetching Docker status for session ${newSessionId}:`, error);
|
||||||
|
}
|
||||||
|
}, dockerPollIntervalMs);
|
||||||
|
if (newState) newState.dockerStatusIntervalId = dockerIntervalId; // 确保 newState 仍然存在
|
||||||
|
|
||||||
|
// 立即触发一次 Docker 状态获取
|
||||||
|
const currentState = clientStates.get(newSessionId);
|
||||||
|
if (currentState && currentState.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
console.log(`[Docker Initial Fetch] Fetching status for session ${newSessionId}...`);
|
||||||
|
const statusPayload = await fetchRemoteDockerStatus(currentState);
|
||||||
|
if (currentState.ws.readyState === WebSocket.OPEN) {
|
||||||
|
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: statusPayload }));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[Docker Initial Fetch] Error fetching Docker status for session ${newSessionId}:`, error);
|
||||||
|
if (currentState.ws.readyState === WebSocket.OPEN) {
|
||||||
|
const errorMessage = error.message || 'Unknown error during initial fetch';
|
||||||
|
const isUnavailable = errorMessage.includes('command not found') || errorMessage.includes('Cannot connect to the Docker daemon');
|
||||||
|
if (isUnavailable) {
|
||||||
|
currentState.ws.send(JSON.stringify({ type: 'docker:status:update', payload: { available: false, containers: [] } }));
|
||||||
|
} else {
|
||||||
|
currentState.ws.send(JSON.stringify({ type: 'docker:status:error', payload: { message: `Initial Docker status fetch failed: ${errorMessage}` } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
})();
|
}); // End of sshClient.shell callback
|
||||||
|
|
||||||
|
|
||||||
} catch (shellError: any) {
|
} catch (shellError: any) {
|
||||||
console.error(`SSH: 会话 ${newSessionId} 打开 Shell 失败:`, shellError);
|
// 这个 catch 块理论上不会被触发,因为错误在 shell 回调中处理,但保留以防万一
|
||||||
// 记录审计日志:打开 Shell 失败
|
console.error(`SSH: 会话 ${newSessionId} 打开 Shell 时发生意外错误:`, shellError);
|
||||||
auditLogService.logAction('SSH_SHELL_FAILURE', {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
userId: ws.userId,
|
ws.send(JSON.stringify({ type: 'ssh:error', payload: `打开 Shell 时发生意外错误: ${shellError.message}` }));
|
||||||
username: ws.username,
|
}
|
||||||
connectionId: dbConnectionId,
|
cleanupClientConnection(newSessionId);
|
||||||
sessionId: newSessionId,
|
|
||||||
ip: newState.ipAddress,
|
|
||||||
reason: shellError.message
|
|
||||||
});
|
|
||||||
notificationService.sendNotification('SSH_SHELL_FAILURE', { // 添加通知调用
|
|
||||||
userId: ws.userId,
|
|
||||||
username: ws.username,
|
|
||||||
connectionId: dbConnectionId,
|
|
||||||
sessionId: newSessionId,
|
|
||||||
ip: newState.ipAddress,
|
|
||||||
reason: shellError.message
|
|
||||||
});
|
|
||||||
ws.send(JSON.stringify({ type: 'ssh:error', payload: `打开 Shell 失败: ${shellError.message}` }));
|
|
||||||
cleanupClientConnection(newSessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. 设置 SSH Client 的关闭和错误处理 (移到 Shell 成功打开之后)
|
// 8. 设置 SSH Client 的关闭和错误处理
|
||||||
sshClient.on('close', () => {
|
sshClient.on('close', () => {
|
||||||
console.log(`SSH: 会话 ${newSessionId} 的客户端连接已关闭。`);
|
console.log(`SSH: 会话 ${newSessionId} 的客户端连接已关闭。`);
|
||||||
cleanupClientConnection(newSessionId);
|
cleanupClientConnection(newSessionId);
|
||||||
});
|
});
|
||||||
sshClient.on('error', (err: Error) => {
|
sshClient.on('error', (err: Error) => {
|
||||||
console.error(`SSH: 会话 ${newSessionId} 的客户端连接错误:`, err);
|
console.error(`SSH: 会话 ${newSessionId} 的客户端连接错误:`, err);
|
||||||
ws.send(JSON.stringify({ type: 'ssh:error', payload: `SSH 连接错误: ${err.message}` }));
|
// 确保在发送错误消息前检查 WebSocket 是否仍然打开
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ssh:error', payload: `SSH 连接错误: ${err.message}` }));
|
||||||
|
}
|
||||||
cleanupClientConnection(newSessionId);
|
cleanupClientConnection(newSessionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -904,14 +902,22 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re
|
|||||||
|
|
||||||
// --- SSH 终端大小调整 ---
|
// --- SSH 终端大小调整 ---
|
||||||
case 'ssh:resize': {
|
case 'ssh:resize': {
|
||||||
if (!state || !state.sshShellStream) {
|
if (!sessionId || !state || !state.sshClient) {
|
||||||
console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的调整大小请求,但无活动 Shell。`);
|
console.warn(`WebSocket: 收到来自 ${ws.username} 的调整大小请求,但无有效会话或 SSH 客户端。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cols, rows } = payload || {};
|
const { cols, rows } = payload || {};
|
||||||
if (typeof cols === 'number' && typeof rows === 'number') {
|
if (typeof cols !== 'number' || typeof rows !== 'number' || cols <= 0 || rows <= 0) {
|
||||||
|
console.warn(`WebSocket: 收到来自 ${ws.username} (会话: ${sessionId}) 的无效调整大小请求:`, payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isShellReady && state.sshShellStream) {
|
||||||
console.log(`SSH: 会话 ${sessionId} 调整终端大小: ${cols}x${rows}`);
|
console.log(`SSH: 会话 ${sessionId} 调整终端大小: ${cols}x${rows}`);
|
||||||
state.sshShellStream.setWindow(rows, cols, 0, 0);
|
state.sshShellStream.setWindow(rows, cols, 0, 0); // ssh2 使用 (rows, cols)
|
||||||
|
} else {
|
||||||
|
console.warn(`WebSocket: 会话 ${sessionId} 收到调整大小请求,但 Shell 尚未就绪或流不存在 (isShellReady: ${state.isShellReady})。`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user