update
This commit is contained in:
@@ -257,3 +257,102 @@ export const importConnections = async (req: Request, res: Response): Promise<vo
|
||||
}
|
||||
}
|
||||
};
|
||||
import axios from 'axios'; // +++ Import axios +++
|
||||
|
||||
// TODO: Make RDP backend URL configurable
|
||||
const RDP_BACKEND_API_BASE = process.env.RDP_BACKEND_API_BASE || 'http://localhost:9090';
|
||||
|
||||
/**
|
||||
* 获取 RDP 会话的 Guacamole 令牌 (通过调用 RDP 后端)
|
||||
* GET /api/v1/connections/:id/rdp-session
|
||||
*/
|
||||
export const getRdpSessionToken = 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;
|
||||
}
|
||||
|
||||
// 1. 获取连接信息和解密后的凭证
|
||||
const connectionData = await ConnectionService.getConnectionWithDecryptedCredentials(connectionId);
|
||||
|
||||
if (!connectionData) {
|
||||
res.status(404).json({ message: '连接未找到。' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { connection, decryptedPassword } = connectionData;
|
||||
|
||||
// 2. 验证连接类型是否为 RDP
|
||||
if (connection.type !== 'RDP') {
|
||||
res.status(400).json({ message: '此连接类型不是 RDP。' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 验证 RDP 连接是否使用密码认证
|
||||
if (connection.auth_method !== 'password' || !decryptedPassword) {
|
||||
console.warn(`[Controller:getRdpSessionToken] RDP connection ${connectionId} does not use password auth or password decryption failed.`);
|
||||
res.status(400).json({ message: 'RDP 连接需要使用密码认证,或密码解密失败。' });
|
||||
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()}`;
|
||||
|
||||
console.log(`[Controller:getRdpSessionToken] Calling RDP backend API: ${RDP_BACKEND_API_BASE}/api/get-token?...`);
|
||||
|
||||
// 5. 调用 RDP 后端 API 获取 Guacamole 令牌
|
||||
const rdpResponse = await axios.get<{ token: string }>(rdpTokenUrl, {
|
||||
timeout: 10000 // 设置 10 秒超时
|
||||
});
|
||||
|
||||
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 后端获取令牌失败。');
|
||||
}
|
||||
|
||||
const guacamoleToken = rdpResponse.data.token;
|
||||
console.log(`[Controller:getRdpSessionToken] Received Guacamole token from RDP backend for connection ${connectionId}`);
|
||||
|
||||
// 6. 将 Guacamole 令牌返回给前端
|
||||
res.status(200).json({ token: guacamoleToken });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`Controller: 获取 RDP 会话令牌时发生错误 (ID: ${req.params.id}):`, error);
|
||||
|
||||
let statusCode = 500;
|
||||
let message = '获取 RDP 会话令牌时发生内部服务器错误。';
|
||||
|
||||
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 会话令牌时发生内部错误(凭证处理失败)。';
|
||||
}
|
||||
|
||||
res.status(statusCode).json({ message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
testConnection,
|
||||
testUnsavedConnection,
|
||||
exportConnections,
|
||||
importConnections
|
||||
importConnections, // <-- Add comma here
|
||||
getRdpSessionToken // +++ Import getRdpSessionToken +++
|
||||
} from './connections.controller';
|
||||
|
||||
const router = Router();
|
||||
@@ -76,4 +77,6 @@ router.post('/:id/test', testConnection);
|
||||
// POST /api/v1/connections/test-unsaved - 测试未保存的连接信息
|
||||
router.post('/test-unsaved', testUnsavedConnection);
|
||||
|
||||
// Removed GET /:id/rdp-token route
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -56,7 +56,24 @@ const initializeEnvironment = async () => {
|
||||
keysGenerated = true;
|
||||
}
|
||||
|
||||
// 4. 如果生成了新密钥,则追加到 .env 文件
|
||||
// 4. 检查 GUACD_HOST 和 GUACD_PORT
|
||||
if (!process.env.GUACD_HOST) {
|
||||
console.warn('[ENV Init] GUACD_HOST 未设置,将使用默认值 "localhost"');
|
||||
process.env.GUACD_HOST = 'localhost';
|
||||
// Optionally add to keysToAppend if you want to save the default
|
||||
// keysToAppend += `\nGUACD_HOST=localhost`;
|
||||
// keysGenerated = true; // Mark if you want to save
|
||||
}
|
||||
if (!process.env.GUACD_PORT) {
|
||||
console.warn('[ENV Init] GUACD_PORT 未设置,将使用默认值 "4822"');
|
||||
process.env.GUACD_PORT = '4822';
|
||||
// Optionally add to keysToAppend
|
||||
// keysToAppend += `\nGUACD_PORT=4822`;
|
||||
// keysGenerated = true; // Mark if you want to save
|
||||
}
|
||||
|
||||
|
||||
// 5. 如果生成了新密钥或添加了默认值,则追加到 .env 文件
|
||||
if (keysGenerated) {
|
||||
try {
|
||||
// 确保追加前有换行符 (如果文件非空)
|
||||
@@ -88,6 +105,20 @@ const initializeEnvironment = async () => {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 最终检查 (包括 Guacamole 相关)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (!process.env.ENCRYPTION_KEY) {
|
||||
console.error('错误:生产环境中 ENCRYPTION_KEY 最终未能设置!');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
console.error('错误:生产环境中 SESSION_SECRET 最终未能设置!');
|
||||
process.exit(1);
|
||||
}
|
||||
// Guacd host/port are less critical to halt on, defaults might work
|
||||
}
|
||||
|
||||
};
|
||||
// --- 结束环境变量和密钥初始化 ---
|
||||
|
||||
@@ -193,7 +224,8 @@ const startServer = () => {
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`后端服务器正在监听 http://localhost:${port}`);
|
||||
initializeWebSocket(server, sessionMiddleware as RequestHandler);
|
||||
initializeWebSocket(server, sessionMiddleware as RequestHandler); // Initialize existing WebSocket
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -271,5 +271,76 @@ export const deleteConnection = async (id: number): Promise<boolean> => {
|
||||
return deleted;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取连接信息(包含标签)以及解密后的凭证(如果适用)
|
||||
* @param id 连接 ID
|
||||
* @returns 包含 ConnectionWithTags 和解密后密码/密钥的对象,或 null
|
||||
*/
|
||||
export const getConnectionWithDecryptedCredentials = async (
|
||||
id: number
|
||||
): Promise<{ connection: ConnectionWithTags; decryptedPassword?: string; decryptedPrivateKey?: string; decryptedPassphrase?: string } | null> => {
|
||||
// 1. 获取完整的连接数据(包含加密字段)
|
||||
// Assuming findFullConnectionById exists and returns FullConnectionDbRow or null
|
||||
const fullConnectionDbRow = await ConnectionRepository.findFullConnectionById(id);
|
||||
if (!fullConnectionDbRow) {
|
||||
console.log(`[Service:getConnWithDecrypt] Connection not found for ID: ${id}`);
|
||||
return null;
|
||||
}
|
||||
// Convert DbRow to the stricter FullConnectionData type expected by the service/types file
|
||||
// Handle potential undefined by defaulting to null
|
||||
const fullConnection: FullConnectionData = {
|
||||
...fullConnectionDbRow,
|
||||
encrypted_password: fullConnectionDbRow.encrypted_password ?? null,
|
||||
encrypted_private_key: fullConnectionDbRow.encrypted_private_key ?? null,
|
||||
encrypted_passphrase: fullConnectionDbRow.encrypted_passphrase ?? null,
|
||||
// Ensure other fields match FullConnectionData if necessary
|
||||
// (Assuming FullConnectionDbRow includes all fields of FullConnectionData)
|
||||
};
|
||||
|
||||
// 2. 获取带标签的连接数据(用于返回给调用者)
|
||||
const connectionWithTags: ConnectionWithTags | null = await ConnectionRepository.findConnectionByIdWithTags(id);
|
||||
if (!connectionWithTags) {
|
||||
// This shouldn't happen if findFullConnectionById succeeded, but good practice to check
|
||||
console.error(`[Service:getConnWithDecrypt] Mismatch: Full connection found but tagged connection not found for ID: ${id}`);
|
||||
// Consider throwing an error or returning a specific error state
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 解密凭证
|
||||
let decryptedPassword: string | undefined = undefined;
|
||||
let decryptedPrivateKey: string | undefined = undefined;
|
||||
let decryptedPassphrase: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
// Decrypt password if method is 'password' and encrypted password exists
|
||||
if (fullConnection.auth_method === 'password' && fullConnection.encrypted_password) {
|
||||
decryptedPassword = decrypt(fullConnection.encrypted_password);
|
||||
}
|
||||
// Decrypt key and passphrase if method is 'key'
|
||||
else if (fullConnection.auth_method === 'key') {
|
||||
if (fullConnection.encrypted_private_key) {
|
||||
decryptedPrivateKey = decrypt(fullConnection.encrypted_private_key);
|
||||
}
|
||||
// Only decrypt passphrase if it exists
|
||||
if (fullConnection.encrypted_passphrase) {
|
||||
decryptedPassphrase = decrypt(fullConnection.encrypted_passphrase);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Service:getConnWithDecrypt] Failed to decrypt credentials for connection ID ${id}:`, error);
|
||||
// Decide how to handle decryption errors. Throw? Return null password?
|
||||
// For now, we'll log and continue, returning undefined credentials.
|
||||
// Consider throwing an error if credentials are required but decryption fails.
|
||||
// Or return a specific error structure: return { error: 'Decryption failed' };
|
||||
}
|
||||
|
||||
console.log(`[Service:getConnWithDecrypt] Returning data for ID: ${id}, Auth Method: ${fullConnection.auth_method}`);
|
||||
return {
|
||||
connection: connectionWithTags,
|
||||
decryptedPassword,
|
||||
decryptedPrivateKey,
|
||||
decryptedPassphrase,
|
||||
};
|
||||
};
|
||||
// 注意:testConnection、importConnections、exportConnections 逻辑
|
||||
// 将分别移至 SshService 和 ImportExportService。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
// This file is intentionally left blank as Guacamole logic is handled by the separate rdp package.
|
||||
Reference in New Issue
Block a user