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