Simplification Web-Gateway

This commit is contained in:
Serge NOEL
2025-12-04 09:32:45 +01:00
parent 66ccf7a20e
commit cfe610c75f
16 changed files with 292 additions and 427 deletions

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# .gitignore
# Build artifacts
src/build/
src/bin/
*.o
*.so
*.a
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Environment files
.env
.env.local
# Custom values (may contain sensitive info)
my-values.yaml
*-values.yaml
!values.yaml
# Test files
test/
*.test
# Temporary files
tmp/
temp/

View File

@@ -3,14 +3,13 @@
# Server
PORT=8080
NODE_ENV=production
LOG_LEVEL=info
# RDP Broker Connection
RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389
# Session Configuration
SESSION_TIMEOUT=3600000
# Security (set these in production)
# SESSION_SECRET=your-secret-key-here
# Optional: Pre-configure RDP Targets
# Format: JSON array of target objects
# If not set, RdpBroker will provide targets dynamically
# Example:
# RDP_TARGETS=[{"name":"Server1","host":"srv1.example.com","port":3389,"description":"Production Server"},{"name":"Server2","host":"srv2.example.com","port":3389,"description":"Development Server"}]

View File

@@ -7,7 +7,8 @@ WORKDIR /app
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
RUN npm install \
&& npm ci --only=production
# Production stage
FROM node:18-alpine
@@ -19,10 +20,6 @@ RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Create necessary directories
RUN mkdir -p /var/log/rdp-web-gateway && \
chown -R nodejs:nodejs /var/log/rdp-web-gateway
WORKDIR /app
# Copy dependencies from builder

View File

@@ -7,9 +7,11 @@ HTML5 WebSocket-based gateway for accessing RDP connections through a web browse
- 🌐 **Browser-Based Access** - Connect to RDP sessions from any modern web browser
- 🔒 **Secure WebSocket** - Real-time bidirectional communication
- 🎨 **Modern UI** - Clean, responsive interface
- 🔑 **Session Management** - Automatic session cleanup and timeout
- 📊 **Activity Monitoring** - Track active connections
- 🔑 **Simplified Authentication** - Credentials passed directly to RdpBroker
- 📊 **Service Health Monitoring** - Automatic RdpBroker availability checks
- 🎯 **Dynamic Target Loading** - Targets fetched from configuration or RdpBroker
-**Low Latency** - Optimized for performance
- ☁️ **Kubernetes Native** - Console-only logging for cloud environments
## Architecture
@@ -63,10 +65,23 @@ Edit `.env` file:
PORT=8080
RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389
LOG_LEVEL=info
SESSION_TIMEOUT=3600000
NODE_ENV=production
# Optional: Pre-configure RDP targets (JSON array)
# If not set, RdpBroker will provide targets dynamically
RDP_TARGETS=[{"name":"Server1","host":"srv1.example.com","port":3389,"description":"Production Server"}]
```
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Web server listening port | `8080` |
| `RDP_BROKER_HOST` | RdpBroker hostname | `rdpbroker` |
| `RDP_BROKER_PORT` | RdpBroker port | `3389` |
| `RDP_TARGETS` | JSON array of pre-configured targets | `null` |
| `NODE_ENV` | Environment mode | `development` |
## Usage
### Access the Web Interface
@@ -78,43 +93,55 @@ SESSION_TIMEOUT=3600000
### API Endpoints
#### POST /api/auth/login
Authenticate user and create session.
```json
{
"username": "user@domain.com",
"password": "password"
}
```
#### GET /health
Health check endpoint for monitoring the web gateway.
Response:
```json
{
"success": true,
"sessionId": "uuid"
"status": "healthy",
"version": "1.0.0",
"uptime": 12345
}
```
#### GET /api/broker-status
Check if RdpBroker service is available.
Response:
```json
{
"available": true,
"broker": "rdpbroker:3389",
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
#### GET /api/targets
Get available RDP targets (requires X-Session-ID header).
Fetch available RDP targets.
Response:
**Success Response (200):**
```json
{
"targets": [
{
"name": "Server 01",
"host": "192.168.1.10",
"name": "Windows Server 2022",
"host": "ws2022.example.com",
"port": 3389,
"description": "Production Server"
"description": "Production Windows Server"
}
]
],
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
#### POST /api/auth/logout
Logout and destroy session (requires X-Session-ID header).
**Service Unavailable (503):**
```json
{
"error": "RdpBroker service is unavailable. Please contact your administrator.",
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
### WebSocket Protocol
@@ -126,7 +153,8 @@ Connect to `ws://localhost:8080/ws/rdp`
```json
{
"type": "connect",
"sessionId": "uuid",
"username": "user@domain.com",
"password": "password123",
"target": {
"name": "Server 01",
"host": "192.168.1.10",
@@ -209,20 +237,21 @@ helm install rdp-web-gateway ./chart/rdp-web-gateway -n rdpbroker
- Firefox 88+
- Safari 14+
- Opera 76+
## Security Considerations
- Use HTTPS/WSS in production
- Implement rate limiting
- Set strong session secrets
- Credentials are passed directly to RdpBroker (no storage in web-gateway)
- Implement rate limiting at ingress level
- Enable CORS restrictions
- Regular security audits
- All authentication handled by RdpBroker → Samba ADs
- Regular security audits
## Performance Tuning
- Adjust session timeout based on usage
- Configure WebSocket buffer sizes
- Enable compression for HTTP responses
- Use CDN for static assets in production
- Enable HTTP compression (already included)
- Adjust resource limits in Kubernetes
- Use CDN for static assets in production
## Troubleshooting
@@ -253,9 +282,21 @@ location /ws/ {
proxy_set_header Connection "upgrade";
}
```
### High memory usage
Adjust resource limits in Kubernetes values.yaml
## Logging
All logs go to stdout/stderr for Kubernetes:
```bash
# View logs
kubectl logs -f deployment/rdp-web-gateway -n rdpbroker
# Follow logs for all pods
kubectl logs -f -l app=rdp-web-gateway -n rdpbroker
```
Reduce session timeout or implement session limits per user.
## Development

View File

@@ -33,5 +33,6 @@
Configuration:
- RDP Broker: {{ .Values.config.rdpBroker.host }}:{{ .Values.config.rdpBroker.port }}
- Server Port: {{ .Values.config.server.port }}
- Log Level: {{ .Values.config.server.logLevel }}
- Replicas: {{ if .Values.autoscaling.enabled }}{{ .Values.autoscaling.minReplicas }}-{{ .Values.autoscaling.maxReplicas }} (autoscaling){{ else }}{{ .Values.replicaCount }}{{ end }}
Note: Authentication is handled by RdpBroker. Logs are sent to stdout for Kubernetes.

View File

@@ -12,10 +12,6 @@ data:
"port": {{ .Values.config.rdpBroker.port }}
},
"server": {
"port": {{ .Values.config.server.port }},
"logLevel": "{{ .Values.config.server.logLevel }}"
},
"session": {
"timeout": {{ .Values.config.session.timeout }}
"port": {{ .Values.config.server.port }}
}
}

View File

@@ -41,23 +41,16 @@ spec:
value: {{ .Values.config.rdpBroker.host | quote }}
- name: RDP_BROKER_PORT
value: {{ .Values.config.rdpBroker.port | quote }}
- name: LOG_LEVEL
value: {{ .Values.config.server.logLevel | quote }}
- name: SESSION_TIMEOUT
value: {{ .Values.config.session.timeout | quote }}
{{- if .Values.config.rdpTargets }}
- name: RDP_TARGETS
value: {{ .Values.config.rdpTargets | toJson | quote }}
{{- end }}
- name: NODE_ENV
value: "production"
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- if .Values.secrets.sessionSecret }}
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: {{ include "rdp-web-gateway.fullname" . }}-secrets
key: sessionSecret
{{- end }}
ports:
- name: http
containerPort: {{ .Values.config.server.port }}
@@ -68,17 +61,6 @@ spec:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.persistence.enabled }}
volumeMounts:
- name: logs
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}
{{- if .Values.persistence.enabled }}
volumes:
- name: logs
persistentVolumeClaim:
claimName: {{ include "rdp-web-gateway.fullname" . }}-logs
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View File

@@ -1,17 +0,0 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}-logs
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}

View File

@@ -1,13 +0,0 @@
{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}-secrets
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
type: Opaque
data:
{{- if .Values.secrets.sessionSecret }}
sessionSecret: {{ .Values.secrets.sessionSecret | b64enc | quote }}
{{- end }}
{{- end }}

View File

@@ -82,29 +82,26 @@ config:
# Server configuration
server:
port: 8080
logLevel: "info"
# Session configuration
session:
timeout: 3600000 # 1 hour in milliseconds
# Optional: Pre-configure RDP targets
# If not set, targets will be managed by RdpBroker
# Format: JSON array of target objects
rdpTargets: null
# Example:
# - name: "Windows Server 2022"
# host: "ws2022.example.com"
# port: 3389
# description: "Production Windows Server"
# - name: "Development Server"
# host: "dev.example.com"
# port: 3389
# description: "Development environment"
# Environment variables
env: []
# - name: CUSTOM_VAR
# value: "value"
# Secrets (for sensitive configuration)
secrets: {}
# sessionSecret: "your-secret-key"
# Persistence for logs
persistence:
enabled: false
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
mountPath: /var/log/rdp-web-gateway
# Liveness and readiness probes
livenessProbe:
httpGet:

View File

@@ -20,13 +20,10 @@
"dependencies": {
"express": "^4.18.2",
"ws": "^8.14.2",
"node-rdpjs": "^0.3.2",
"dotenv": "^16.3.1",
"compression": "^1.7.4",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"uuid": "^9.0.1",
"winston": "^3.11.0"
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.2"

View File

@@ -5,7 +5,7 @@ class RDPWebGateway {
this.ctx = null;
this.currentUser = null;
this.currentTarget = null;
this.sessionId = null;
this.credentials = null;
this.init();
}
@@ -63,27 +63,24 @@ class RDPWebGateway {
errorMessage.style.display = 'none';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
this.currentUser = username;
this.sessionId = data.sessionId;
await this.loadTargets();
this.showTargetsView();
} else {
this.showError(errorMessage, data.error || 'Authentication failed');
// Check if RdpBroker service is available
const statusResponse = await fetch('/api/broker-status');
const statusData = await statusResponse.json();
if (!statusData.available) {
this.showError(errorMessage, 'RDP service is currently unavailable. Please contact your administrator.');
return;
}
// Store credentials temporarily - will be sent to RdpBroker
this.currentUser = username;
this.credentials = { username, password };
// Load targets and show targets view
await this.loadTargets();
} catch (error) {
console.error('Login error:', error);
this.showError(errorMessage, 'Connection error. Please try again.');
this.showError(errorMessage, 'Connection error. Please check your network and try again.');
} finally {
loginBtn.disabled = false;
btnText.style.display = 'block';
@@ -93,25 +90,45 @@ class RDPWebGateway {
async loadTargets() {
try {
const response = await fetch('/api/targets', {
headers: {
'X-Session-ID': this.sessionId,
},
});
const response = await fetch('/api/targets');
if (!response.ok) {
if (response.status === 503) {
const error = await response.json();
throw new Error(error.error || 'Service unavailable');
}
throw new Error('Failed to load targets');
}
const data = await response.json();
this.displayTargets(data.targets);
this.showTargetsView(data.targets);
} catch (error) {
console.error('Load targets error:', error);
const targetsError = document.getElementById('targetsError');
this.showError(targetsError, 'Failed to load available desktops');
console.error('Error loading targets:', error);
// Show error in targets view
this.showTargetsView(null, error.message);
}
}
showTargetsView(targets = null, errorMsg = null) {
document.getElementById('loginCard').style.display = 'none';
document.getElementById('targetsCard').style.display = 'block';
document.getElementById('rdpViewer').style.display = 'none';
document.getElementById('currentUser').textContent = this.currentUser;
if (errorMsg) {
const targetsList = document.getElementById('targetsList');
targetsList.innerHTML = `
<div style="text-align: center; padding: 20px;">
<p style="color: var(--error-color); margin-bottom: 10px;">⚠️ ${this.escapeHtml(errorMsg)}</p>
<button onclick="location.reload()" class="btn btn-secondary">Retry</button>
</div>
`;
return;
}
this.displayTargets(targets);
}
displayTargets(targets) {
const targetsList = document.getElementById('targetsList');
targetsList.innerHTML = '';
@@ -153,10 +170,11 @@ class RDPWebGateway {
this.ws.onopen = () => {
console.log('WebSocket connected');
// Send connection request
// Send credentials and connection request to RdpBroker
this.ws.send(JSON.stringify({
type: 'connect',
sessionId: this.sessionId,
username: this.credentials.username,
password: this.credentials.password,
target: target,
}));
};
@@ -324,7 +342,7 @@ class RDPWebGateway {
}
this.currentUser = null;
this.currentTarget = null;
this.sessionId = null;
this.credentials = null;
this.showLoginView();
}
@@ -336,13 +354,6 @@ class RDPWebGateway {
document.getElementById('password').value = '';
}
showTargetsView() {
document.getElementById('loginCard').style.display = 'none';
document.getElementById('targetsCard').style.display = 'block';
document.getElementById('rdpViewer').style.display = 'none';
document.getElementById('currentUser').textContent = this.currentUser;
}
showRDPViewer() {
document.getElementById('loginCard').style.display = 'none';
document.getElementById('targetsCard').style.display = 'none';

View File

@@ -1,41 +0,0 @@
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'rdp-web-gateway' },
transports: [
// Write to console
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
})
)
}),
// Write to file
new winston.transports.File({
filename: '/var/log/rdp-web-gateway/error.log',
level: 'error',
handleExceptions: true
}),
new winston.transports.File({
filename: '/var/log/rdp-web-gateway/combined.log',
handleExceptions: true
})
]
});
module.exports = logger;

View File

@@ -1,13 +1,10 @@
const net = require('net');
const logger = require('./logger');
class RDPProxyHandler {
constructor(websocket, sessionManager, rdpBrokerHost, rdpBrokerPort) {
constructor(websocket, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket;
this.sessionManager = sessionManager;
this.rdpBrokerHost = rdpBrokerHost;
this.rdpBrokerPort = rdpBrokerPort;
this.session = null;
this.rdpSocket = null;
this.isConnected = false;
}
@@ -28,40 +25,33 @@ class RDPProxyHandler {
this.handleSpecialCommand(message);
break;
default:
logger.warn(`Unknown message type: ${message.type}`);
console.warn(`${new Date().toISOString()} [WARN] Unknown message type: ${message.type}`);
}
} catch (error) {
logger.error('Error handling message:', error);
console.error(`${new Date().toISOString()} [ERROR] Error handling message:`, error);
this.sendError('Failed to process message');
}
}
async handleConnect(message) {
const { sessionId, target } = message;
const { username, password, target } = message;
// Validate session
this.session = this.sessionManager.getSession(sessionId);
if (!this.session) {
return this.sendError('Invalid session');
}
logger.info(`Connecting to RDP Broker for session ${sessionId}, target: ${target.name}`);
console.log(`${new Date().toISOString()} [INFO] Connecting to RDP Broker, target: ${target?.name || 'unknown'}`);
try {
// Connect to RDP Broker
this.rdpSocket = new net.Socket();
this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => {
logger.info(`Connected to RDP Broker at ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
console.log(`${new Date().toISOString()} [INFO] Connected to RDP Broker at ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
this.isConnected = true;
// Send authentication to RdpBroker
// In real implementation, this would follow the RDP protocol
this.sendAuthToBroker();
// Send credentials to RdpBroker (it will handle authentication)
this.sendAuthToBroker(username, password);
this.ws.send(JSON.stringify({
type: 'connected',
target: target.name
target: target?.name || 'RDP Server'
}));
// Set canvas size
@@ -75,20 +65,19 @@ class RDPProxyHandler {
// Handle data from RDP Broker
this.rdpSocket.on('data', (data) => {
// Forward RDP data to WebSocket client
// In production, this would be properly decoded RDP frames
if (this.ws.readyState === 1) { // WebSocket.OPEN
this.ws.send(data);
}
});
this.rdpSocket.on('error', (error) => {
logger.error('RDP socket error:', error);
console.error(`${new Date().toISOString()} [ERROR] RDP socket error:`, error);
this.sendError('Connection to RDP broker failed');
this.cleanup();
});
this.rdpSocket.on('close', () => {
logger.info('RDP connection closed');
console.log(`${new Date().toISOString()} [INFO] RDP connection closed`);
this.isConnected = false;
if (this.ws.readyState === 1) {
this.ws.close();
@@ -96,25 +85,19 @@ class RDPProxyHandler {
});
} catch (error) {
logger.error('Connection error:', error);
console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error);
this.sendError('Failed to connect to RDP broker');
}
}
sendAuthToBroker() {
// This is a simplified version
// In production, implement proper RDP protocol handshake
sendAuthToBroker(username, password) {
// Send credentials directly to RdpBroker
// RdpBroker will handle Samba AD authentication
if (!this.rdpSocket || !this.session) return;
if (!this.rdpSocket) return;
const authData = {
username: this.session.username,
password: this.session.data.password
};
// Send authentication data
// Format: "Username: <username>\nPassword: <password>\n"
const authMessage = `${authData.username}\n${authData.password}\n`;
const authMessage = `${username}\n${password}\n`;
this.rdpSocket.write(authMessage);
}
@@ -164,10 +147,10 @@ class RDPProxyHandler {
command: 'ctrl-alt-del'
});
this.rdpSocket.write(cadData + '\n');
logger.info('Sent Ctrl+Alt+Del');
console.log(`${new Date().toISOString()} [INFO] Sent Ctrl+Alt+Del`);
break;
default:
logger.warn(`Unknown special command: ${message.action}`);
console.warn(`${new Date().toISOString()} [WARN] Unknown special command: ${message.action}`);
}
}
@@ -186,12 +169,6 @@ class RDPProxyHandler {
this.rdpSocket = null;
}
this.isConnected = false;
if (this.session) {
this.sessionManager.updateSession(this.session.id, {
rdpConnection: null
});
}
}
}

View File

@@ -2,11 +2,10 @@ const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const net = require('net');
const compression = require('compression');
const helmet = require('helmet');
const cors = require('cors');
const logger = require('./logger');
const SessionManager = require('./sessionManager');
const RDPProxyHandler = require('./rdpProxyHandler');
class RDPWebGatewayServer {
@@ -18,7 +17,6 @@ class RDPWebGatewayServer {
path: '/ws/rdp'
});
this.sessionManager = new SessionManager();
this.port = process.env.PORT || 8080;
this.rdpBrokerHost = process.env.RDP_BROKER_HOST || 'rdpbroker';
this.rdpBrokerPort = process.env.RDP_BROKER_PORT || 3389;
@@ -45,7 +43,7 @@ class RDPWebGatewayServer {
// Logging
this.app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
console.log(`${new Date().toISOString()} [INFO] ${req.method} ${req.url}`);
next();
});
}
@@ -56,70 +54,44 @@ class RDPWebGatewayServer {
res.json({
status: 'healthy',
version: '1.0.0',
sessions: this.sessionManager.getActiveSessionCount()
uptime: process.uptime()
});
});
// Authentication endpoint
this.app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
// Create session
const session = this.sessionManager.createSession(username, {
password,
ipAddress: req.ip
});
logger.info(`User ${username} authenticated, session: ${session.id}`);
res.json({
success: true,
sessionId: session.id
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
// RdpBroker health check endpoint
this.app.get('/api/broker-status', async (req, res) => {
const isAvailable = await this.checkRdpBrokerHealth();
res.json({
available: isAvailable,
broker: `${this.rdpBrokerHost}:${this.rdpBrokerPort}`,
timestamp: new Date().toISOString()
});
});
// Get available targets
// Get targets list
this.app.get('/api/targets', async (req, res) => {
try {
const sessionId = req.headers['x-session-id'];
if (!sessionId) {
return res.status(401).json({ error: 'Session ID required' });
const isAvailable = await this.checkRdpBrokerHealth();
if (!isAvailable) {
return res.status(503).json({
error: 'RdpBroker service is unavailable. Please contact your administrator.',
timestamp: new Date().toISOString()
});
}
const session = this.sessionManager.getSession(sessionId);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
// In a real implementation, this would fetch from RdpBroker
// For now, return static list
const targets = await this.fetchTargetsFromBroker(session);
res.json({ targets });
// Parse targets from environment variable
const targets = this.parseTargetsFromEnv();
res.json({ targets, timestamp: new Date().toISOString() });
} catch (error) {
logger.error('Targets fetch error:', error);
res.status(500).json({ error: 'Failed to fetch targets' });
console.error(`${new Date().toISOString()} [ERROR] Error fetching targets:`, error);
res.status(500).json({
error: 'Failed to fetch targets',
timestamp: new Date().toISOString()
});
}
});
// Logout
this.app.post('/api/auth/logout', (req, res) => {
const sessionId = req.headers['x-session-id'];
if (sessionId) {
this.sessionManager.destroySession(sessionId);
}
res.json({ success: true });
});
// API endpoints are removed - authentication handled by RdpBroker
// Catch all - serve index.html
this.app.get('*', (req, res) => {
@@ -129,11 +101,10 @@ class RDPWebGatewayServer {
setupWebSocket() {
this.wss.on('connection', (ws, req) => {
logger.info('New WebSocket connection');
console.log(`${new Date().toISOString()} [INFO] New WebSocket connection`);
const proxyHandler = new RDPProxyHandler(
ws,
this.sessionManager,
ws,
this.rdpBrokerHost,
this.rdpBrokerPort
);
@@ -143,7 +114,7 @@ class RDPWebGatewayServer {
const message = JSON.parse(data.toString());
proxyHandler.handleMessage(message);
} catch (error) {
logger.error('WebSocket message error:', error);
console.error(`${new Date().toISOString()} [ERROR] WebSocket message error:`, error);
ws.send(JSON.stringify({
type: 'error',
error: 'Invalid message format'
@@ -152,50 +123,71 @@ class RDPWebGatewayServer {
});
ws.on('close', () => {
logger.info('WebSocket connection closed');
console.log(`${new Date().toISOString()} [INFO] WebSocket connection closed`);
proxyHandler.cleanup();
});
ws.on('error', (error) => {
logger.error('WebSocket error:', error);
console.error(`${new Date().toISOString()} [ERROR] WebSocket error:`, error);
proxyHandler.cleanup();
});
});
}
async fetchTargetsFromBroker(session) {
// This is a simplified version
// In production, this would communicate with RdpBroker
// to get the actual list of targets
// For now, return example targets
return [
{
name: "Windows Server 01",
host: "192.168.1.10",
// Check if RdpBroker is available
checkRdpBrokerHealth() {
return new Promise((resolve) => {
const socket = new net.Socket();
const timeout = 3000; // 3 second timeout
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.destroy();
resolve(true);
});
socket.on('timeout', () => {
socket.destroy();
resolve(false);
});
socket.on('error', () => {
resolve(false);
});
socket.connect(this.rdpBrokerPort, this.rdpBrokerHost);
});
}
// Parse targets from environment variable
parseTargetsFromEnv() {
const targetsEnv = process.env.RDP_TARGETS;
if (!targetsEnv) {
// Return default message if no targets configured
return [{
name: 'Default',
host: 'via-rdpbroker',
port: 3389,
description: "Production Web Server"
},
{
name: "Windows Server 02",
host: "192.168.1.11",
port: 3389,
description: "Database Server"
},
{
name: "Development Desktop",
host: "dev-machine.local",
port: 3389,
description: "Developer Workstation"
}
];
description: 'Targets will be provided by RdpBroker'
}];
}
try {
return JSON.parse(targetsEnv);
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Failed to parse RDP_TARGETS:`, error);
return [];
}
}
start() {
this.server.listen(this.port, () => {
logger.info(`RDP Web Gateway server running on port ${this.port}`);
logger.info(`RDP Broker: ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
logger.info(`WebSocket endpoint: ws://localhost:${this.port}/ws/rdp`);
console.log(`${new Date().toISOString()} [INFO] RDP Web Gateway server running on port ${this.port}`);
console.log(`${new Date().toISOString()} [INFO] RDP Broker: ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
console.log(`${new Date().toISOString()} [INFO] WebSocket endpoint: ws://localhost:${this.port}/ws/rdp`);
});
}
}
@@ -206,9 +198,9 @@ server.start();
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully...');
console.log(`${new Date().toISOString()} [INFO] SIGTERM received, shutting down gracefully...`);
server.server.close(() => {
logger.info('Server closed');
console.log(`${new Date().toISOString()} [INFO] Server closed`);
process.exit(0);
});
});

View File

@@ -1,94 +0,0 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('./logger');
class SessionManager {
constructor() {
this.sessions = new Map();
this.sessionTimeout = 3600000; // 1 hour
// Cleanup inactive sessions every 5 minutes
setInterval(() => this.cleanupSessions(), 300000);
}
createSession(username, userData = {}) {
const sessionId = uuidv4();
const session = {
id: sessionId,
username,
createdAt: Date.now(),
lastActivity: Date.now(),
data: userData,
rdpConnection: null
};
this.sessions.set(sessionId, session);
logger.info(`Session created: ${sessionId} for user ${username}`);
return session;
}
getSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.lastActivity = Date.now();
}
return session;
}
updateSession(sessionId, data) {
const session = this.sessions.get(sessionId);
if (session) {
session.data = { ...session.data, ...data };
session.lastActivity = Date.now();
}
return session;
}
destroySession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
logger.info(`Session destroyed: ${sessionId}`);
// Cleanup any active RDP connection
if (session.rdpConnection) {
session.rdpConnection.close();
}
this.sessions.delete(sessionId);
return true;
}
return false;
}
cleanupSessions() {
const now = Date.now();
let cleaned = 0;
for (const [sessionId, session] of this.sessions.entries()) {
if (now - session.lastActivity > this.sessionTimeout) {
logger.info(`Cleaning up inactive session: ${sessionId}`);
this.destroySession(sessionId);
cleaned++;
}
}
if (cleaned > 0) {
logger.info(`Cleaned up ${cleaned} inactive session(s)`);
}
}
getActiveSessionCount() {
return this.sessions.size;
}
getAllSessions() {
return Array.from(this.sessions.values()).map(session => ({
id: session.id,
username: session.username,
createdAt: session.createdAt,
lastActivity: session.lastActivity
}));
}
}
module.exports = SessionManager;