import { createRequire } from 'node:module'; import { randomBytes } from 'node:crypto'; import { path } from '../utils/pathUtils.js'; import express from 'express'; import { db, visibleClause } from '../db.js'; import { decryptSecret } from './cryptoStore.js'; import { logger } from './logger.js'; import { normalizeOsFamily } from '../utils/osFamily.js'; const require = createRequire(import.meta.url); const rdp = require('node-rdpjs'); const socketIo = require('socket.io'); const mstscPackagePath = require.resolve('mstsc.js/package.json'); const mstscClientDir = path.join(path.dirname(mstscPackagePath), 'client'); const SESSION_TTL_MS = 5 * 60 * 1000; const sessions = new Map(); let socketServer = null; function token() { return randomBytes(24).toString('base64url'); } function escapeHtml(value = '') { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function splitDomainUsername(username = '') { const value = String(username || ''); const match = value.match(/^([^\\]+)\\(.+)$/); if (!match) return { domain: '', username: value }; return { domain: match[1], username: match[2] }; } function cleanupExpiredSessions() { const cutoff = Date.now() - SESSION_TTL_MS; for (const [sessionToken, session] of sessions.entries()) { if (session.createdAt < cutoff) sessions.delete(sessionToken); } } function loadRdpTarget(hostId, userId) { return db.prepare(` SELECT h.*, c.name AS credential_name, c.kind AS credential_kind, c.username AS credential_username, c.secret_cipher, c.secret_iv, c.secret_tag FROM hosts h JOIN credentials c ON c.id = h.credential_id WHERE h.id = ? AND ${visibleClause('h')} AND ${visibleClause('c')} `).get(hostId, userId, userId, userId, userId); } export function createRdpLaunchSession(hostId, userId) { cleanupExpiredSessions(); const target = loadRdpTarget(hostId, userId); if (!target) { const error = new Error('Windows host with an assigned visible credential is required for RDP launch.'); error.statusCode = 404; throw error; } if (normalizeOsFamily(target.os_family, 'other') !== 'windows') { const error = new Error('RDP launch is only available for Windows hosts.'); error.statusCode = 400; throw error; } if (target.credential_kind !== 'username_password') { const error = new Error('RDP launch requires a username/password credential assigned to the host.'); error.statusCode = 400; throw error; } const password = decryptSecret(target); if (!password) { const error = new Error('Assigned host credential does not have a decryptable password.'); error.statusCode = 400; throw error; } const sessionToken = token(); const parsedUsername = splitDomainUsername(target.credential_username); const session = { token: sessionToken, userId, hostId: target.id, hostName: target.name, address: target.fqdn || target.address, port: 3389, domain: parsedUsername.domain, username: parsedUsername.username, password, credentialName: target.credential_name, createdAt: Date.now() }; sessions.set(sessionToken, session); logger.info('rdp session created', { hostId: target.id, hostName: target.name, userId, credentialId: target.credential_id }); return { token: sessionToken, url: `/rdp/${sessionToken}`, expiresInSeconds: Math.floor(SESSION_TTL_MS / 1000), host: { id: target.id, name: target.name, address: session.address } }; } function renderSessionPage(session) { const safeToken = escapeHtml(session.token); const safeHost = escapeHtml(session.hostName); const safeAddress = escapeHtml(session.address); return ` RDP - ${safeHost}
POSHManager RDP

${safeHost}

Opening a browser RDP session to ${safeAddress} using the assigned host credential. Close this tab to end the session.

Preparing secure session...
`; } export function mountRdpGateway(app) { app.use('/rdp/assets', express.static(mstscClientDir, { fallthrough: false, immutable: true, maxAge: '1h' })); app.get('/rdp/sessions/:token', (req, res) => { res.redirect(302, `/rdp/${encodeURIComponent(req.params.token)}`); }); app.get('/rdp/:token', (req, res) => { cleanupExpiredSessions(); const session = sessions.get(req.params.token); if (!session) return res.status(404).send('RDP session expired or not found.'); res.type('html').send(renderSessionPage(session)); }); } export function startRdpGateway(httpServer) { if (socketServer) return socketServer; socketServer = socketIo(httpServer, { path: '/rdp/socket.io' }); socketServer.on('connection', (client) => { let rdpClient = null; client.on('infos', (infos = {}) => { cleanupExpiredSessions(); const sessionToken = infos.token || infos.sessionToken || infos.password; const session = sessions.get(sessionToken); if (!session) { client.emit('rdp-error', { code: 'POSHM_RDP_SESSION', message: 'RDP session expired or invalid.' }); client.disconnect(); return; } if (rdpClient) rdpClient.close(); const screen = { width: Math.max(640, Math.min(Number(infos.screen?.width || 1280), 3840)), height: Math.max(480, Math.min(Number(infos.screen?.height || 800), 2160)) }; logger.info('rdp connection starting', { hostId: session.hostId, hostName: session.hostName, address: session.address, userId: session.userId }); rdpClient = rdp.createClient({ domain: session.domain, userName: session.username, password: session.password, enablePerf: true, autoLogin: true, screen, locale: infos.locale, logLevel: 'ERROR' }).on('connect', () => { client.emit('rdp-connect'); logger.info('rdp connection established', { hostId: session.hostId, hostName: session.hostName, userId: session.userId }); }).on('bitmap', (bitmap) => { client.emit('rdp-bitmap', bitmap); }).on('close', () => { client.emit('rdp-close'); sessions.delete(sessionToken); logger.info('rdp connection closed', { hostId: session.hostId, hostName: session.hostName, userId: session.userId }); }).on('error', (err) => { client.emit('rdp-error', { code: err.code || 'RDP_ERROR', message: err.message || String(err) }); logger.error('rdp connection failed', { hostId: session.hostId, hostName: session.hostName, userId: session.userId, error: err.message || String(err) }); }).connect(session.address, session.port); }); client.on('mouse', (x, y, button, isPressed) => { if (rdpClient) rdpClient.sendPointerEvent(x, y, button, isPressed); }); client.on('wheel', (x, y, step, isNegative, isHorizontal) => { if (rdpClient) rdpClient.sendWheelEvent(x, y, step, isNegative, isHorizontal); }); client.on('scancode', (code, isPressed) => { if (rdpClient) rdpClient.sendKeyEventScancode(code, isPressed); }); client.on('unicode', (code, isPressed) => { if (rdpClient) rdpClient.sendKeyEventUnicode(code, isPressed); }); client.on('disconnect', () => { if (rdpClient) rdpClient.close(); }); }); logger.info('POSHManager RDP gateway mounted at /rdp'); return socketServer; }