update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as ConnectionService from '../services/connection.service';
|
||||
import * as SshService from '../services/ssh.service';
|
||||
import * as GuacamoleService from '../services/guacamole.service'; // 导入 GuacamoleService
|
||||
import * as ImportExportService from '../services/import-export.service';
|
||||
import * as ConnectionRepository from '../repositories/connection.repository'; // +++ 导入 ConnectionRepository +++
|
||||
|
||||
@@ -269,10 +270,9 @@ export const importConnections = async (req: Request, res: Response): Promise<vo
|
||||
}
|
||||
}
|
||||
};
|
||||
import axios from 'axios'; // +++ Import axios +++
|
||||
import axios from 'axios'; // axios 仍可能用于错误检查类型
|
||||
|
||||
// TODO: Make RDP backend URL configurable
|
||||
const RDP_BACKEND_API_BASE = process.env.RDP_BACKEND_API_BASE || 'http://localhost:9090';
|
||||
// RDP_BACKEND_API_BASE and VNC_BACKEND_API_BASE are now handled in GuacamoleService
|
||||
|
||||
/**
|
||||
* 获取 RDP 会话的 Guacamole 令牌 (通过调用 RDP 后端)
|
||||
@@ -320,66 +320,143 @@ export const getRdpSessionToken = async (req: Request, res: Response): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 准备调用 RDP 后端的参数
|
||||
const rdpApiParams = new URLSearchParams({
|
||||
hostname: connection.host,
|
||||
port: connection.port.toString(),
|
||||
username: connection.username,
|
||||
password: decryptedPassword, // 使用解密后的密码
|
||||
// Add other RDP parameters from connection object if needed by rdp backend
|
||||
security: (connection as any).rdp_security || 'any',
|
||||
ignoreCert: String((connection as any).rdp_ignore_cert ?? true),
|
||||
});
|
||||
const rdpTokenUrl = `${RDP_BACKEND_API_BASE}/api/get-token?${rdpApiParams.toString()}`;
|
||||
// 4. 调用 GuacamoleService 获取 RDP 令牌
|
||||
const guacamoleToken = await GuacamoleService.getRdpToken(connection, decryptedPassword);
|
||||
|
||||
console.log(`[Controller:getRdpSessionToken] Received Guacamole token via GuacamoleService for RDP connection ${connectionId}`);
|
||||
|
||||
console.log(`[Controller:getRdpSessionToken] Calling RDP backend API: ${RDP_BACKEND_API_BASE}/api/get-token?...`);
|
||||
// 5. 将 Guacamole 令牌返回给前端
|
||||
res.status(200).json({ token: guacamoleToken });
|
||||
|
||||
// 5. 调用 RDP 后端 API 获取 Guacamole 令牌
|
||||
const rdpResponse = await axios.get<{ token: string }>(rdpTokenUrl, {
|
||||
timeout: 10000 // 设置 10 秒超时
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`Controller: 获取 RDP 会话令牌时发生错误 (ID: ${req.params.id}):`, error.message); // Log error message
|
||||
|
||||
if (rdpResponse.status !== 200 || !rdpResponse.data?.token) {
|
||||
console.error(`[Controller:getRdpSessionToken] RDP backend API call failed or returned invalid data. Status: ${rdpResponse.status}`, rdpResponse.data);
|
||||
throw new Error('从 RDP 后端获取令牌失败。');
|
||||
let statusCode = 500;
|
||||
let responseMessage = '获取 RDP 会话令牌时发生内部服务器错误。';
|
||||
|
||||
// 检查错误是否来自 GuacamoleService 或其内部的 axios 调用
|
||||
if (error.message.includes('调用 RDP 后端服务失败') || error.message.includes('从 RDP 后端获取令牌失败')) {
|
||||
responseMessage = error.message;
|
||||
// 尝试从错误消息中提取状态码,或者根据消息内容判断
|
||||
if (error.message.includes('(状态: 4')) statusCode = 400; // 例如 400, 401, 404
|
||||
else if (error.message.includes('(状态: 5')) statusCode = 502; // 例如 500, 502, 503, 504
|
||||
else statusCode = 503; // Service Unavailable or other specific error
|
||||
} else if (error.message.includes('RDP 连接需要使用密码认证') || error.message.includes('密码解密失败')) {
|
||||
responseMessage = error.message;
|
||||
statusCode = 400;
|
||||
} else if (error.message.includes('连接类型必须是 RDP')) {
|
||||
responseMessage = error.message;
|
||||
statusCode = 400;
|
||||
}
|
||||
// 可以保留对 axios.isAxiosError 的检查,以防 GuacamoleService 抛出未包装的 axios 错误
|
||||
// 但理想情况下,GuacamoleService 应该抛出更具体的错误。
|
||||
else if (axios.isAxiosError(error)) {
|
||||
responseMessage = '调用 RDP 后端服务时发生网络或请求错误。';
|
||||
if (error.response) {
|
||||
console.error('[Controller:getRdpSessionToken] RDP backend error response (unhandled by GuacamoleService):', error.response.data);
|
||||
responseMessage += ` (状态: ${error.response.status})`;
|
||||
statusCode = error.response.status >= 500 ? 502 : 400;
|
||||
} else if (error.request) {
|
||||
console.error('[Controller:getRdpSessionToken] No response from RDP backend (unhandled by GuacamoleService).');
|
||||
responseMessage += ' (无法连接或超时)';
|
||||
statusCode = 504;
|
||||
}
|
||||
} else if (error.message.includes('解密失败')) { // General decryption error from ConnectionService
|
||||
responseMessage = '获取 RDP 会话令牌时发生内部错误(凭证处理失败)。';
|
||||
}
|
||||
res.status(statusCode).json({ message: responseMessage });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 VNC 会话的 Guacamole 令牌 (通过调用 Guacamole 服务)
|
||||
* GET /api/v1/connections/:id/vnc-session
|
||||
*/
|
||||
export const getVncSessionToken = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.id, 10);
|
||||
if (isNaN(connectionId)) {
|
||||
res.status(400).json({ message: '无效的连接 ID。' });
|
||||
return;
|
||||
}
|
||||
|
||||
const guacamoleToken = rdpResponse.data.token;
|
||||
console.log(`[Controller:getRdpSessionToken] Received Guacamole token from RDP backend for connection ${connectionId}`);
|
||||
// 1. 获取连接信息和解密后的凭证
|
||||
const connectionData = await ConnectionService.getConnectionWithDecryptedCredentials(connectionId);
|
||||
|
||||
if (!connectionData) {
|
||||
res.status(404).json({ message: '连接未找到。' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { connection, decryptedPassword } = connectionData;
|
||||
|
||||
// 2. 验证连接类型是否为 VNC
|
||||
if (connection.type !== 'VNC') {
|
||||
res.status(400).json({ message: '此连接类型不是 VNC。' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 更新 last_connected_at
|
||||
try {
|
||||
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
||||
await ConnectionRepository.updateLastConnected(connectionId, currentTimeSeconds);
|
||||
console.log(`[Controller:getVncSessionToken] 已更新 VNC 连接 ${connectionId} 的 last_connected_at 为 ${currentTimeSeconds}`);
|
||||
} catch (updateError) {
|
||||
console.error(`[Controller:getVncSessionToken] 更新 VNC 连接 ${connectionId} 的 last_connected_at 时出错:`, updateError);
|
||||
}
|
||||
|
||||
// 4. 验证 VNC 连接是否使用密码认证 (VNC 通常总是需要密码)
|
||||
if (connection.auth_method !== 'password' || !decryptedPassword) {
|
||||
console.warn(`[Controller:getVncSessionToken] VNC connection ${connectionId} does not use password auth or password decryption failed.`);
|
||||
res.status(400).json({ message: 'VNC 连接需要使用密码认证,或密码解密失败。' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 调用 GuacamoleService 获取 VNC 令牌
|
||||
const guacamoleToken = await GuacamoleService.getVncToken(connection, decryptedPassword);
|
||||
|
||||
console.log(`[Controller:getVncSessionToken] Received Guacamole token via GuacamoleService for VNC connection ${connectionId}`);
|
||||
|
||||
// 6. 将 Guacamole 令牌返回给前端
|
||||
res.status(200).json({ token: guacamoleToken });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`Controller: 获取 RDP 会话令牌时发生错误 (ID: ${req.params.id}):`, error);
|
||||
console.error(`Controller: 获取 VNC 会话令牌时发生错误 (ID: ${req.params.id}):`, error.message); // Log error message
|
||||
|
||||
let statusCode = 500;
|
||||
let message = '获取 RDP 会话令牌时发生内部服务器错误。';
|
||||
let responseMessage = '获取 VNC 会话令牌时发生内部服务器错误。';
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
message = '调用 RDP 后端服务时出错。';
|
||||
if (error.response) {
|
||||
// RDP 后端返回了错误响应
|
||||
console.error('[Controller:getRdpSessionToken] RDP backend error response:', error.response.data);
|
||||
message += ` (状态: ${error.response.status})`;
|
||||
statusCode = error.response.status >= 500 ? 502 : 400; // Bad Gateway or Bad Request
|
||||
} else if (error.request) {
|
||||
// 请求已发出但没有收到响应 (网络问题、超时)
|
||||
console.error('[Controller:getRdpSessionToken] No response from RDP backend.');
|
||||
message += ' (无法连接或超时)';
|
||||
statusCode = 504; // Gateway Timeout
|
||||
} else {
|
||||
// 设置请求时发生错误
|
||||
console.error('[Controller:getRdpSessionToken] Axios request setup error:', error.message);
|
||||
}
|
||||
} else if (error.message.includes('解密失败')) {
|
||||
message = '获取 RDP 会话令牌时发生内部错误(凭证处理失败)。';
|
||||
// 检查错误是否来自 GuacamoleService 或其内部的 axios 调用
|
||||
if (error.message.includes('调用 VNC 后端服务失败') || error.message.includes('从 VNC 后端获取令牌失败')) {
|
||||
responseMessage = error.message;
|
||||
if (error.message.includes('(状态: 4')) statusCode = 400;
|
||||
else if (error.message.includes('(状态: 5')) statusCode = 502;
|
||||
else statusCode = 503;
|
||||
} else if (error.message.includes('VNC 连接需要使用密码认证') || error.message.includes('密码解密失败')) {
|
||||
responseMessage = error.message;
|
||||
statusCode = 400;
|
||||
} else if (error.message.includes('连接类型必须是 VNC')) {
|
||||
responseMessage = error.message;
|
||||
statusCode = 400;
|
||||
}
|
||||
|
||||
res.status(statusCode).json({ message });
|
||||
// 可以保留对 axios.isAxiosError 的检查
|
||||
else if (axios.isAxiosError(error)) {
|
||||
responseMessage = '调用 VNC 后端服务时发生网络或请求错误。';
|
||||
if (error.response) {
|
||||
console.error('[Controller:getVncSessionToken] VNC backend error response (unhandled by GuacamoleService):', error.response.data);
|
||||
responseMessage += ` (状态: ${error.response.status})`;
|
||||
statusCode = error.response.status >= 500 ? 502 : 400;
|
||||
} else if (error.request) {
|
||||
console.error('[Controller:getVncSessionToken] No response from VNC backend (unhandled by GuacamoleService).');
|
||||
responseMessage += ' (无法连接或超时)';
|
||||
statusCode = 504;
|
||||
}
|
||||
} else if (error.message.includes('解密失败')) { // General decryption error from ConnectionService
|
||||
responseMessage = '获取 VNC 会话令牌时发生内部错误(凭证处理失败)。';
|
||||
}
|
||||
res.status(statusCode).json({ message: responseMessage });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 克隆连接 (POST /api/v1/connections/:id/clone)
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
exportConnections,
|
||||
importConnections,
|
||||
getRdpSessionToken, // Import the new controller function
|
||||
getVncSessionToken, // Import the VNC session token controller function
|
||||
cloneConnection, // +++ Import the clone controller function +++
|
||||
// updateConnectionTags, // No longer directly used by primary flow
|
||||
addTagToConnections // +++ Import the new controller function for adding tag to multiple connections +++
|
||||
@@ -83,6 +84,9 @@ router.post('/test-unsaved', testUnsavedConnection);
|
||||
// POST /api/v1/connections/:id/rdp-session - Get RDP session token via backend
|
||||
router.post('/:id/rdp-session', getRdpSessionToken);
|
||||
|
||||
// POST /api/v1/connections/:id/vnc-session - Get VNC session token
|
||||
router.post('/:id/vnc-session', getVncSessionToken);
|
||||
|
||||
// +++ POST /api/v1/connections/:id/clone - 克隆连接 +++
|
||||
router.post('/:id/clone', cloneConnection);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const createConnectionsTableSQL = `
|
||||
CREATE TABLE IF NOT EXISTS connections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NULL, -- 允许 name 为空
|
||||
type TEXT NOT NULL CHECK(type IN ('SSH', 'RDP')) DEFAULT 'SSH',
|
||||
type TEXT NOT NULL CHECK(type IN ('SSH', 'RDP', 'VNC')) DEFAULT 'SSH',
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn
|
||||
interface ConnectionBase {
|
||||
id: number;
|
||||
name: string | null;
|
||||
type: 'SSH' | 'RDP'; // Add type field
|
||||
type: 'SSH' | 'RDP' | 'VNC'; // Add type field
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
|
||||
@@ -43,9 +43,9 @@ export const createConnection = async (input: CreateConnectionInput): Promise<Co
|
||||
console.log('[Service:createConnection] Received input:', JSON.stringify(input, null, 2)); // Log input
|
||||
// 1. 验证输入 (包含 type)
|
||||
// Convert type to uppercase for validation and consistency
|
||||
const connectionType = input.type?.toUpperCase() as 'SSH' | 'RDP' | undefined; // Ensure type safety
|
||||
if (!connectionType || !['SSH', 'RDP'].includes(connectionType)) {
|
||||
throw new Error('必须提供有效的连接类型 (SSH 或 RDP)。');
|
||||
const connectionType = input.type?.toUpperCase() as 'SSH' | 'RDP' | 'VNC' | undefined; // Ensure type safety
|
||||
if (!connectionType || !['SSH', 'RDP', 'VNC'].includes(connectionType)) {
|
||||
throw new Error('必须提供有效的连接类型 (SSH, RDP 或 VNC)。');
|
||||
}
|
||||
if (!input.host || !input.username) {
|
||||
throw new Error('缺少必要的连接信息 (host, username)。');
|
||||
@@ -70,6 +70,18 @@ export const createConnection = async (input: CreateConnectionInput): Promise<Co
|
||||
throw new Error('RDP 连接需要提供 password。');
|
||||
}
|
||||
// For RDP, we'll ignore auth_method, private_key, passphrase from input if provided
|
||||
} else if (connectionType === 'VNC') {
|
||||
if (!input.password) {
|
||||
throw new Error('VNC 连接需要提供 password。');
|
||||
}
|
||||
// For VNC, auth_method is implicitly 'password'.
|
||||
// ssh_key_id, private_key, passphrase are not applicable.
|
||||
if (input.auth_method && input.auth_method !== 'password') {
|
||||
throw new Error('VNC 连接的认证方式必须是 password。');
|
||||
}
|
||||
if (input.ssh_key_id || input.private_key) {
|
||||
throw new Error('VNC 连接不支持 SSH 密钥认证。');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 处理凭证和 ssh_key_id (根据 type)
|
||||
@@ -108,16 +120,27 @@ export const createConnection = async (input: CreateConnectionInput): Promise<Co
|
||||
throw new Error('SSH 密钥认证方式内部错误:未提供 private_key 或 ssh_key_id。');
|
||||
}
|
||||
}
|
||||
} else { // RDP (connectionType is 'RDP')
|
||||
} else if (connectionType === 'RDP') { // RDP
|
||||
encryptedPassword = encrypt(input.password!);
|
||||
// authMethodForDb remains 'password' for RDP to satisfy DB constraint
|
||||
// Ensure SSH specific fields are null for RDP
|
||||
// authMethodForDb remains 'password' for RDP
|
||||
encryptedPrivateKey = null;
|
||||
encryptedPassphrase = null;
|
||||
sshKeyIdToSave = null;
|
||||
} else { // VNC
|
||||
encryptedPassword = encrypt(input.password!);
|
||||
authMethodForDb = 'password'; // VNC always uses password auth
|
||||
encryptedPrivateKey = null;
|
||||
encryptedPassphrase = null;
|
||||
sshKeyIdToSave = null;
|
||||
}
|
||||
|
||||
// 3. 准备仓库数据
|
||||
const defaultPort = input.type === 'RDP' ? 3389 : 22;
|
||||
let defaultPort = 22; // Default for SSH
|
||||
if (connectionType === 'RDP') {
|
||||
defaultPort = 3389;
|
||||
} else if (connectionType === 'VNC') {
|
||||
defaultPort = 5900; // Default VNC port
|
||||
}
|
||||
// +++ Explicitly type connectionData using the local alias +++
|
||||
const connectionData: ConnectionDataForRepo = {
|
||||
name: input.name || '',
|
||||
@@ -178,7 +201,7 @@ export const updateConnection = async (id: number, input: UpdateConnectionInput)
|
||||
const dataToUpdate: Partial<Omit<ConnectionRepository.FullConnectionData & { ssh_key_id?: number | null }, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>> = {};
|
||||
let needsCredentialUpdate = false;
|
||||
// Determine the final type, converting input type to uppercase if provided
|
||||
const targetType = input.type?.toUpperCase() as 'SSH' | 'RDP' | undefined || currentFullConnection.type;
|
||||
const targetType = input.type?.toUpperCase() as 'SSH' | 'RDP' | 'VNC' | undefined || currentFullConnection.type;
|
||||
|
||||
// 更新非凭证字段
|
||||
if (input.name !== undefined) dataToUpdate.name = input.name || '';
|
||||
@@ -280,20 +303,32 @@ if (input.notes !== undefined) dataToUpdate.notes = input.notes; // Add notes up
|
||||
dataToUpdate.encrypted_password = null;
|
||||
}
|
||||
}
|
||||
} else { // targetType is 'RDP'
|
||||
} else if (targetType === 'RDP') { // targetType is 'RDP'
|
||||
// RDP only uses password
|
||||
if (input.password !== undefined) { // Check if password was provided
|
||||
// Encrypt if password is not empty, otherwise set to null (to clear)
|
||||
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
|
||||
needsCredentialUpdate = true;
|
||||
}
|
||||
// Ensure SSH specific fields are nullified if switching to RDP or updating RDP
|
||||
if (targetType !== currentFullConnection.type || needsCredentialUpdate) {
|
||||
if (targetType !== currentFullConnection.type || needsCredentialUpdate || Object.keys(dataToUpdate).includes('type')) {
|
||||
dataToUpdate.auth_method = 'password'; // RDP uses password auth method in DB
|
||||
dataToUpdate.encrypted_private_key = null;
|
||||
dataToUpdate.encrypted_passphrase = null;
|
||||
dataToUpdate.ssh_key_id = null; // RDP cannot use ssh_key_id
|
||||
}
|
||||
} else { // targetType is 'VNC'
|
||||
// VNC only uses password
|
||||
if (input.password !== undefined) { // Check if password was provided
|
||||
dataToUpdate.encrypted_password = input.password ? encrypt(input.password) : null;
|
||||
needsCredentialUpdate = true;
|
||||
}
|
||||
// Ensure SSH specific fields are nullified if switching to VNC or updating VNC
|
||||
if (targetType !== currentFullConnection.type || needsCredentialUpdate || Object.keys(dataToUpdate).includes('type')) {
|
||||
dataToUpdate.auth_method = 'password'; // VNC uses password auth method in DB
|
||||
dataToUpdate.encrypted_private_key = null;
|
||||
dataToUpdate.encrypted_passphrase = null;
|
||||
dataToUpdate.ssh_key_id = null; // VNC cannot use ssh_key_id
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果有更改,则更新连接记录
|
||||
|
||||
@@ -1 +1,110 @@
|
||||
// This file is intentionally left blank as Guacamole logic is handled by the separate rdp package.
|
||||
import axios from 'axios';
|
||||
import { ConnectionWithTags } from '../types/connection.types';
|
||||
|
||||
// RDP 后端服务的 Base URL,从环境变量读取,提供默认值
|
||||
const RDP_BACKEND_API_BASE = process.env.RDP_BACKEND_API_BASE || 'http://nexus-rdp:9090'; // 假设 RDP 服务名为 nexus-rdp
|
||||
|
||||
// VNC 后端服务的 Base URL,从环境变量读取,提供默认值
|
||||
const VNC_BACKEND_API_BASE = process.env.VNC_BACKEND_API_BASE || 'http://nexus-vnc:9091'; // 假设 VNC 服务名为 nexus-vnc,端口为 9091
|
||||
|
||||
/**
|
||||
* 从 RDP 后端服务获取 Guacamole 令牌
|
||||
* @param connection 连接对象
|
||||
* @param decryptedPassword 解密后的密码
|
||||
* @returns Guacamole 令牌
|
||||
*/
|
||||
export const getRdpToken = async (connection: ConnectionWithTags, decryptedPassword?: string): Promise<string> => {
|
||||
if (connection.type !== 'RDP') {
|
||||
throw new Error('连接类型必须是 RDP。');
|
||||
}
|
||||
if (connection.auth_method !== 'password' || !decryptedPassword) {
|
||||
console.warn(`[GuacamoleService:getRdpToken] RDP connection ${connection.id} does not use password auth or password decryption failed.`);
|
||||
throw new Error('RDP 连接需要使用密码认证,或密码解密失败。');
|
||||
}
|
||||
|
||||
const rdpApiParams = new URLSearchParams({
|
||||
hostname: connection.host,
|
||||
port: connection.port.toString(),
|
||||
username: connection.username,
|
||||
password: decryptedPassword,
|
||||
// 确保传递 RDP 特定的参数,如果存在的话
|
||||
security: (connection as any).rdp_security || 'any', // 从连接对象中获取,如果存在
|
||||
ignoreCert: String((connection as any).rdp_ignore_cert ?? true), // 从连接对象中获取,如果存在
|
||||
// 可以根据需要添加更多参数,例如 domain, width, height, dpi 等
|
||||
});
|
||||
const rdpTokenUrl = `${RDP_BACKEND_API_BASE}/api/get-token?${rdpApiParams.toString()}`;
|
||||
|
||||
console.log(`[GuacamoleService:getRdpToken] Calling RDP backend API: ${RDP_BACKEND_API_BASE}/api/get-token?... for connection ${connection.id}`);
|
||||
|
||||
try {
|
||||
const rdpResponse = await axios.get<{ token: string }>(rdpTokenUrl, {
|
||||
timeout: 10000 // 10 秒超时
|
||||
});
|
||||
|
||||
if (rdpResponse.status !== 200 || !rdpResponse.data?.token) {
|
||||
console.error(`[GuacamoleService:getRdpToken] RDP backend API call failed or returned invalid data. Status: ${rdpResponse.status}`, rdpResponse.data);
|
||||
throw new Error('从 RDP 后端获取令牌失败。');
|
||||
}
|
||||
console.log(`[GuacamoleService:getRdpToken] Received Guacamole token from RDP backend for connection ${connection.id}`);
|
||||
return rdpResponse.data.token;
|
||||
} catch (error: any) {
|
||||
console.error(`[GuacamoleService:getRdpToken] Error calling RDP backend for connection ${connection.id}:`, error.message);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
throw new Error(`调用 RDP 后端服务失败 (状态: ${error.response.status}): ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`调用 RDP 后端服务时发生错误: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 VNC 后端服务获取 Guacamole 令牌
|
||||
* @param connection 连接对象
|
||||
* @param decryptedPassword 解密后的密码 (VNC 通常需要密码)
|
||||
* @returns Guacamole 令牌
|
||||
*/
|
||||
export const getVncToken = async (connection: ConnectionWithTags, decryptedPassword?: string): Promise<string> => {
|
||||
if (connection.type !== 'VNC') {
|
||||
throw new Error('连接类型必须是 VNC。');
|
||||
}
|
||||
// VNC 通常总是需要密码,并且 auth_method 应该被设置为 'password'
|
||||
if (connection.auth_method !== 'password' || !decryptedPassword) {
|
||||
console.warn(`[GuacamoleService:getVncToken] VNC connection ${connection.id} does not use password auth or password decryption failed.`);
|
||||
throw new Error('VNC 连接需要使用密码认证,或密码解密失败。');
|
||||
}
|
||||
|
||||
const vncApiParams = new URLSearchParams({
|
||||
hostname: connection.host,
|
||||
port: connection.port.toString(),
|
||||
password: decryptedPassword, // VNC 通常只需要密码
|
||||
// VNC 特有的参数可以根据 @nexus-terminal/vnc 的 API 进行添加
|
||||
// 例如: username (如果 VNC 服务器需要), colorDepth, etc.
|
||||
// username: connection.username, // 如果 VNC 服务支持用户名
|
||||
});
|
||||
|
||||
// 如果 VNC 服务也支持用户名,可以取消注释上面的 username 参数
|
||||
// 注意:标准的 VNC 协议主要通过密码进行认证,用户名不是标准部分,但某些实现可能支持。
|
||||
// 这里假设 @nexus-terminal/vnc 的 /api/get-vnc-token 接受这些参数。
|
||||
|
||||
const vncTokenUrl = `${VNC_BACKEND_API_BASE}/api/get-vnc-token?${vncApiParams.toString()}`;
|
||||
|
||||
console.log(`[GuacamoleService:getVncToken] Calling VNC backend API: ${VNC_BACKEND_API_BASE}/api/get-vnc-token?... for connection ${connection.id}`);
|
||||
|
||||
try {
|
||||
const vncResponse = await axios.get<{ token: string }>(vncTokenUrl, {
|
||||
timeout: 10000 // 10 秒超时
|
||||
});
|
||||
|
||||
if (vncResponse.status !== 200 || !vncResponse.data?.token) {
|
||||
console.error(`[GuacamoleService:getVncToken] VNC backend API call failed or returned invalid data. Status: ${vncResponse.status}`, vncResponse.data);
|
||||
throw new Error('从 VNC 后端获取令牌失败。');
|
||||
}
|
||||
console.log(`[GuacamoleService:getVncToken] Received Guacamole token from VNC backend for connection ${connection.id}`);
|
||||
return vncResponse.data.token;
|
||||
} catch (error: any) {
|
||||
console.error(`[GuacamoleService:getVncToken] Error calling VNC backend for connection ${connection.id}:`, error.message);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
throw new Error(`调用 VNC 后端服务失败 (状态: ${error.response.status}): ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`调用 VNC 后端服务时发生错误: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
export interface ConnectionBase {
|
||||
id: number;
|
||||
name: string | null;
|
||||
type: 'SSH' | 'RDP';
|
||||
type: 'SSH' | 'RDP' | 'VNC';
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
@@ -22,7 +22,7 @@ export interface ConnectionWithTags extends ConnectionBase {
|
||||
|
||||
export interface CreateConnectionInput {
|
||||
name?: string;
|
||||
type: 'SSH' | 'RDP';
|
||||
type: 'SSH' | 'RDP' | 'VNC';
|
||||
host: string;
|
||||
port?: number;
|
||||
username: string;
|
||||
@@ -39,7 +39,7 @@ notes?: string | null; // 新增备注字段
|
||||
|
||||
export interface UpdateConnectionInput {
|
||||
name?: string;
|
||||
type?: 'SSH' | 'RDP';
|
||||
type?: 'SSH' | 'RDP' | 'VNC';
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
@@ -57,7 +57,7 @@ notes?: string | null; // 新增备注字段
|
||||
export interface FullConnectionData {
|
||||
id: number;
|
||||
name: string | null;
|
||||
type: 'SSH' | 'RDP';
|
||||
type: 'SSH' | 'RDP' | 'VNC';
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
|
||||
Reference in New Issue
Block a user