127 lines
4.0 KiB
TypeScript
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));
|
|
}
|
|
}
|
|
}
|