fix(rdp): convert to manual ws.Server, fix URL path, fix double session
This commit is contained in:
parent
3b5e5e0d36
commit
f124d4b7d2
@ -5,6 +5,7 @@ import { AppModule } from './app.module';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { TerminalGateway } from './terminal/terminal.gateway';
|
||||
import { SftpGateway } from './terminal/sftp.gateway';
|
||||
import { RdpGateway } from './rdp/rdp.gateway';
|
||||
|
||||
// Crash handlers — catch whatever is killing the process
|
||||
process.on('uncaughtException', (err) => {
|
||||
@ -30,9 +31,11 @@ async function bootstrap() {
|
||||
const server = app.getHttpServer();
|
||||
const terminalGateway = app.get(TerminalGateway);
|
||||
const sftpGateway = app.get(SftpGateway);
|
||||
const rdpGateway = app.get(RdpGateway);
|
||||
|
||||
const terminalWss = new WebSocketServer({ noServer: true });
|
||||
const sftpWss = new WebSocketServer({ noServer: true });
|
||||
const rdpWss = new WebSocketServer({ noServer: true });
|
||||
|
||||
terminalWss.on('connection', (ws, req) => {
|
||||
try {
|
||||
@ -52,6 +55,15 @@ async function bootstrap() {
|
||||
}
|
||||
});
|
||||
|
||||
rdpWss.on('connection', (ws, req) => {
|
||||
try {
|
||||
console.log(`[WS] RDP connection established`);
|
||||
rdpGateway.handleConnection(ws, req);
|
||||
} catch (err: any) {
|
||||
console.error(`[FATAL] RDP handleConnection crashed: ${err.message}\n${err.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove ALL existing upgrade listeners (WsAdapter's) so we handle upgrades first
|
||||
const existingListeners = server.listeners('upgrade');
|
||||
server.removeAllListeners('upgrade');
|
||||
@ -72,8 +84,13 @@ async function bootstrap() {
|
||||
console.log(`[WS] SFTP upgrade complete`);
|
||||
sftpWss.emit('connection', ws, req);
|
||||
});
|
||||
} else if (pathname === '/api/ws/rdp') {
|
||||
rdpWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
console.log(`[WS] RDP upgrade complete`);
|
||||
rdpWss.emit('connection', ws, req);
|
||||
});
|
||||
} else {
|
||||
// Pass to WsAdapter's original handlers (for RDP etc)
|
||||
// Pass to WsAdapter's original handlers
|
||||
for (const listener of existingListeners) {
|
||||
listener.call(server, req, socket, head);
|
||||
}
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
import {
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Server } from 'ws';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as net from 'net';
|
||||
import { WsAuthGuard } from '../auth/ws-auth.guard';
|
||||
import { GuacamoleService } from './guacamole.service';
|
||||
@ -13,9 +6,8 @@ import { CredentialsService } from '../vault/credentials.service';
|
||||
import { HostsService } from '../connections/hosts.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@WebSocketGateway({ path: '/ws/rdp' })
|
||||
export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer() server: Server;
|
||||
@Injectable()
|
||||
export class RdpGateway {
|
||||
private readonly logger = new Logger(RdpGateway.name);
|
||||
|
||||
// Maps browser WebSocket client → live guacd TCP socket
|
||||
@ -29,8 +21,8 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
private wsAuth: WsAuthGuard,
|
||||
) {}
|
||||
|
||||
handleConnection(client: any) {
|
||||
const user = this.wsAuth.validateClient(client);
|
||||
handleConnection(client: any, req: any) {
|
||||
const user = this.wsAuth.validateClient(client, req);
|
||||
if (!user) {
|
||||
client.close(4001, 'Unauthorized');
|
||||
return;
|
||||
@ -41,6 +33,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
client.on('message', async (raw: Buffer) => {
|
||||
try {
|
||||
const msg = JSON.parse(raw.toString());
|
||||
this.logger.log(`[RDP] Message: ${msg.type}`);
|
||||
|
||||
if (msg.type === 'connect') {
|
||||
await this.handleConnect(client, msg);
|
||||
@ -56,15 +49,16 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
this.send(client, { type: 'error', message: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDisconnect(client: any) {
|
||||
const socket = this.clientSockets.get(client);
|
||||
if (socket) {
|
||||
socket.destroy();
|
||||
this.clientSockets.delete(client);
|
||||
this.logger.log('RDP WS disconnected — guacd socket destroyed');
|
||||
}
|
||||
client.on('close', () => {
|
||||
this.logger.log('RDP WS disconnected');
|
||||
const socket = this.clientSockets.get(client);
|
||||
if (socket) {
|
||||
socket.destroy();
|
||||
this.clientSockets.delete(client);
|
||||
this.logger.log('guacd socket destroyed on WS close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleConnect(client: any, msg: any) {
|
||||
@ -90,7 +84,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
dpi: msg.dpi || 96,
|
||||
security: msg.security || 'any',
|
||||
colorDepth: msg.colorDepth || 24,
|
||||
ignoreCert: true, // default permissive — expose as setting in Task 15+
|
||||
ignoreCert: true,
|
||||
});
|
||||
|
||||
this.clientSockets.set(client, socket);
|
||||
@ -115,7 +109,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
this.send(client, { type: 'error', message: err.message });
|
||||
});
|
||||
|
||||
// Connection tracking — same pattern as SSH gateway
|
||||
// Connection tracking
|
||||
this.hosts.touchLastConnected(host.id).catch(() => {});
|
||||
this.prisma.connectionLog
|
||||
.create({ data: { hostId: host.id, protocol: 'rdp' } })
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRdp } from '~/composables/useRdp'
|
||||
const props = defineProps<{
|
||||
hostId: number
|
||||
hostName: string
|
||||
sessionId: string
|
||||
color?: string | null
|
||||
}>()
|
||||
|
||||
@ -27,6 +28,7 @@ onMounted(() => {
|
||||
props.hostId,
|
||||
props.hostName,
|
||||
props.color ?? null,
|
||||
props.sessionId,
|
||||
{
|
||||
width: container.value.clientWidth,
|
||||
height: container.value.clientHeight,
|
||||
|
||||
@ -70,6 +70,7 @@ function handleRdpClipboard(sessionId: string, text: string) {
|
||||
:ref="(el) => setRdpRef(session.id, el)"
|
||||
:host-id="session.hostId"
|
||||
:host-name="session.hostName"
|
||||
:session-id="session.id"
|
||||
:color="session.color"
|
||||
/>
|
||||
<RdpToolbar
|
||||
|
||||
@ -42,9 +42,11 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
||||
}
|
||||
|
||||
connect(_data?: string) {
|
||||
console.log('[RDP] Tunnel opening WebSocket:', this.wsUrl)
|
||||
this.ws = new WebSocket(this.wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[RDP] WebSocket open, sending connect handshake')
|
||||
// Step 1: send our JSON connect handshake
|
||||
this.ws!.send(JSON.stringify(this.connectMsg))
|
||||
}
|
||||
@ -159,14 +161,17 @@ export function useRdp() {
|
||||
hostId: number,
|
||||
hostName: string,
|
||||
color: string | null,
|
||||
pendingSessionId: string,
|
||||
options?: { width?: number; height?: number },
|
||||
) {
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const wsUrl = `${proto}://${location.host}/ws/rdp?token=${auth.token}`
|
||||
const wsUrl = `${proto}://${location.host}/api/ws/rdp?token=${auth.token}`
|
||||
|
||||
const width = options?.width || container.clientWidth || 1920
|
||||
const height = options?.height || container.clientHeight || 1080
|
||||
|
||||
console.log(`[RDP] Connecting to ${wsUrl} for hostId=${hostId} (${width}x${height})`)
|
||||
|
||||
const tunnel = new JsonWsTunnel(wsUrl, {
|
||||
type: 'connect',
|
||||
hostId,
|
||||
@ -176,12 +181,14 @@ export function useRdp() {
|
||||
|
||||
const client = new Guacamole.Client(tunnel)
|
||||
|
||||
// Session store ID — created optimistically, will be confirmed on 'connected'
|
||||
const sessionId = `rdp-${hostId}-${Date.now()}`
|
||||
// Session ID is passed in by the caller (the pending session created in connectHost)
|
||||
const sessionId = pendingSessionId
|
||||
|
||||
// Wire tunnel callbacks
|
||||
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
||||
sessions.addSession({
|
||||
console.log(`[RDP] Connected to ${resolvedHostName}`)
|
||||
// Update the pending session with the resolved host name
|
||||
sessions.replaceSession(sessionId, {
|
||||
key: sessionId,
|
||||
id: sessionId,
|
||||
hostId,
|
||||
@ -192,7 +199,8 @@ export function useRdp() {
|
||||
})
|
||||
}
|
||||
|
||||
tunnel.onDisconnected = (_reason: string) => {
|
||||
tunnel.onDisconnected = (reason: string) => {
|
||||
console.log(`[RDP] Disconnected: ${reason}`)
|
||||
sessions.removeSession(sessionId)
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user