wraith/backend/src/rdp/rdp.gateway.ts

127 lines
4.0 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import * as net from 'net';
import { WsAuthGuard } from '../auth/ws-auth.guard';
import { GuacamoleService } from './guacamole.service';
import { CredentialsService } from '../vault/credentials.service';
import { HostsService } from '../connections/hosts.service';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class RdpGateway {
private readonly logger = new Logger(RdpGateway.name);
// Maps browser WebSocket client → live guacd TCP socket
private clientSockets = new Map<any, net.Socket>();
constructor(
private guacamole: GuacamoleService,
private credentials: CredentialsService,
private hosts: HostsService,
private prisma: PrismaService,
private wsAuth: WsAuthGuard,
) {}
handleConnection(client: any, req: any) {
const user = this.wsAuth.validateClient(client, req);
if (!user) {
client.close(4001, 'Unauthorized');
return;
}
this.logger.log(`RDP WS connected: ${user.email}`);
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);
} else if (msg.type === 'guac') {
// Forward raw Guacamole instruction from browser to guacd TCP socket
const socket = this.clientSockets.get(client);
if (socket && !socket.destroyed) {
socket.write(msg.instruction);
}
}
} catch (err: any) {
this.logger.error(`RDP message error: ${err.message}`);
this.send(client, { type: 'error', message: err.message });
}
});
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) {
const host = await this.hosts.findOne(msg.hostId);
// Decrypt credentials if attached to host
const cred = host.credentialId
? await this.credentials.decryptForConnection(host.credentialId)
: null;
this.logger.log(
`Opening RDP tunnel: ${host.hostname}:${host.port} for host "${host.name}"`,
);
const socket = await this.guacamole.connect({
hostname: host.hostname,
port: host.port,
username: cred?.username || '',
password: cred?.password || '',
domain: cred?.domain || undefined,
width: msg.width || 1920,
height: msg.height || 1080,
dpi: msg.dpi || 96,
security: msg.security || 'any',
colorDepth: msg.colorDepth || 24,
ignoreCert: true,
});
this.clientSockets.set(client, socket);
// Pipe guacd → browser: wrap raw Guacamole instruction bytes in JSON envelope
socket.on('data', (data: Buffer) => {
if (client.readyState === 1 /* WebSocket.OPEN */) {
client.send(
JSON.stringify({ type: 'guac', instruction: data.toString('utf-8') }),
);
}
});
socket.on('close', () => {
this.logger.log(`guacd socket closed for host ${host.id}`);
this.send(client, { type: 'disconnected', reason: 'RDP session closed' });
this.clientSockets.delete(client);
});
socket.on('error', (err) => {
this.logger.error(`guacd socket error for host ${host.id}: ${err.message}`);
this.send(client, { type: 'error', message: err.message });
});
// Connection tracking
this.hosts.touchLastConnected(host.id).catch(() => {});
this.prisma.connectionLog
.create({ data: { hostId: host.id, protocol: 'rdp' } })
.catch(() => {});
this.send(client, { type: 'connected', hostId: host.id, hostName: host.name });
}
private send(client: any, data: any) {
if (client.readyState === 1 /* WebSocket.OPEN */) {
client.send(JSON.stringify(data));
}
}
}