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 # Server
PORT=8080 PORT=8080
NODE_ENV=production NODE_ENV=production
LOG_LEVEL=info
# RDP Broker Connection # RDP Broker Connection
RDP_BROKER_HOST=rdpbroker RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389 RDP_BROKER_PORT=3389
# Session Configuration # Optional: Pre-configure RDP Targets
SESSION_TIMEOUT=3600000 # Format: JSON array of target objects
# If not set, RdpBroker will provide targets dynamically
# Security (set these in production) # Example:
# SESSION_SECRET=your-secret-key-here # 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 ./ COPY package*.json ./
# Install dependencies # Install dependencies
RUN npm ci --only=production RUN npm install \
&& npm ci --only=production
# Production stage # Production stage
FROM node:18-alpine FROM node:18-alpine
@@ -19,10 +20,6 @@ RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && \ RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 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 WORKDIR /app
# Copy dependencies from builder # 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 - 🌐 **Browser-Based Access** - Connect to RDP sessions from any modern web browser
- 🔒 **Secure WebSocket** - Real-time bidirectional communication - 🔒 **Secure WebSocket** - Real-time bidirectional communication
- 🎨 **Modern UI** - Clean, responsive interface - 🎨 **Modern UI** - Clean, responsive interface
- 🔑 **Session Management** - Automatic session cleanup and timeout - 🔑 **Simplified Authentication** - Credentials passed directly to RdpBroker
- 📊 **Activity Monitoring** - Track active connections - 📊 **Service Health Monitoring** - Automatic RdpBroker availability checks
- 🎯 **Dynamic Target Loading** - Targets fetched from configuration or RdpBroker
-**Low Latency** - Optimized for performance -**Low Latency** - Optimized for performance
- ☁️ **Kubernetes Native** - Console-only logging for cloud environments
## Architecture ## Architecture
@@ -63,10 +65,23 @@ Edit `.env` file:
PORT=8080 PORT=8080
RDP_BROKER_HOST=rdpbroker RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389 RDP_BROKER_PORT=3389
LOG_LEVEL=info NODE_ENV=production
SESSION_TIMEOUT=3600000
# 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 ## Usage
### Access the Web Interface ### Access the Web Interface
@@ -78,43 +93,55 @@ SESSION_TIMEOUT=3600000
### API Endpoints ### API Endpoints
#### POST /api/auth/login #### GET /health
Authenticate user and create session. Health check endpoint for monitoring the web gateway.
```json
{
"username": "user@domain.com",
"password": "password"
}
```
Response: Response:
```json ```json
{ {
"success": true, "status": "healthy",
"sessionId": "uuid" "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 /api/targets
Get available RDP targets (requires X-Session-ID header). Fetch available RDP targets.
Response: **Success Response (200):**
```json ```json
{ {
"targets": [ "targets": [
{ {
"name": "Server 01", "name": "Windows Server 2022",
"host": "192.168.1.10", "host": "ws2022.example.com",
"port": 3389, "port": 3389,
"description": "Production Server" "description": "Production Windows Server"
} }
] ],
"timestamp": "2025-12-04T10:30:00.000Z"
} }
``` ```
#### POST /api/auth/logout **Service Unavailable (503):**
Logout and destroy session (requires X-Session-ID header). ```json
{
"error": "RdpBroker service is unavailable. Please contact your administrator.",
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
### WebSocket Protocol ### WebSocket Protocol
@@ -126,7 +153,8 @@ Connect to `ws://localhost:8080/ws/rdp`
```json ```json
{ {
"type": "connect", "type": "connect",
"sessionId": "uuid", "username": "user@domain.com",
"password": "password123",
"target": { "target": {
"name": "Server 01", "name": "Server 01",
"host": "192.168.1.10", "host": "192.168.1.10",
@@ -209,20 +237,21 @@ helm install rdp-web-gateway ./chart/rdp-web-gateway -n rdpbroker
- Firefox 88+ - Firefox 88+
- Safari 14+ - Safari 14+
- Opera 76+ - Opera 76+
## Security Considerations ## Security Considerations
- Use HTTPS/WSS in production - Use HTTPS/WSS in production
- Implement rate limiting - Credentials are passed directly to RdpBroker (no storage in web-gateway)
- Set strong session secrets - Implement rate limiting at ingress level
- Enable CORS restrictions - Enable CORS restrictions
- Regular security audits - Regular security audits
- All authentication handled by RdpBroker → Samba ADs
- Regular security audits
## Performance Tuning ## Performance Tuning
- Adjust session timeout based on usage
- Configure WebSocket buffer sizes - 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 - Use CDN for static assets in production
## Troubleshooting ## Troubleshooting
@@ -253,9 +282,21 @@ location /ws/ {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
``` ```
### High memory usage ### 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. Reduce session timeout or implement session limits per user.
## Development ## Development

View File

@@ -33,5 +33,6 @@
Configuration: Configuration:
- RDP Broker: {{ .Values.config.rdpBroker.host }}:{{ .Values.config.rdpBroker.port }} - RDP Broker: {{ .Values.config.rdpBroker.host }}:{{ .Values.config.rdpBroker.port }}
- Server Port: {{ .Values.config.server.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 }} - 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 }} "port": {{ .Values.config.rdpBroker.port }}
}, },
"server": { "server": {
"port": {{ .Values.config.server.port }}, "port": {{ .Values.config.server.port }}
"logLevel": "{{ .Values.config.server.logLevel }}"
},
"session": {
"timeout": {{ .Values.config.session.timeout }}
} }
} }

View File

@@ -41,23 +41,16 @@ spec:
value: {{ .Values.config.rdpBroker.host | quote }} value: {{ .Values.config.rdpBroker.host | quote }}
- name: RDP_BROKER_PORT - name: RDP_BROKER_PORT
value: {{ .Values.config.rdpBroker.port | quote }} value: {{ .Values.config.rdpBroker.port | quote }}
- name: LOG_LEVEL {{- if .Values.config.rdpTargets }}
value: {{ .Values.config.server.logLevel | quote }} - name: RDP_TARGETS
- name: SESSION_TIMEOUT value: {{ .Values.config.rdpTargets | toJson | quote }}
value: {{ .Values.config.session.timeout | quote }} {{- end }}
- name: NODE_ENV - name: NODE_ENV
value: "production" value: "production"
{{- range .Values.env }} {{- range .Values.env }}
- name: {{ .name }} - name: {{ .name }}
value: {{ .value | quote }} value: {{ .value | quote }}
{{- end }} {{- end }}
{{- if .Values.secrets.sessionSecret }}
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: {{ include "rdp-web-gateway.fullname" . }}-secrets
key: sessionSecret
{{- end }}
ports: ports:
- name: http - name: http
containerPort: {{ .Values.config.server.port }} containerPort: {{ .Values.config.server.port }}
@@ -68,17 +61,6 @@ spec:
{{- toYaml .Values.readinessProbe | nindent 12 }} {{- toYaml .Values.readinessProbe | nindent 12 }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- 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 }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- 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 configuration
server: server:
port: 8080 port: 8080
logLevel: "info"
# Session configuration # Optional: Pre-configure RDP targets
session: # If not set, targets will be managed by RdpBroker
timeout: 3600000 # 1 hour in milliseconds # 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 # Environment variables
env: [] env: []
# - name: CUSTOM_VAR # - name: CUSTOM_VAR
# value: "value" # 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 # Liveness and readiness probes
livenessProbe: livenessProbe:
httpGet: httpGet:

View File

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

View File

@@ -5,7 +5,7 @@ class RDPWebGateway {
this.ctx = null; this.ctx = null;
this.currentUser = null; this.currentUser = null;
this.currentTarget = null; this.currentTarget = null;
this.sessionId = null; this.credentials = null;
this.init(); this.init();
} }
@@ -63,27 +63,24 @@ class RDPWebGateway {
errorMessage.style.display = 'none'; errorMessage.style.display = 'none';
try { try {
const response = await fetch('/api/auth/login', { // Check if RdpBroker service is available
method: 'POST', const statusResponse = await fetch('/api/broker-status');
headers: { const statusData = await statusResponse.json();
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json(); if (!statusData.available) {
this.showError(errorMessage, 'RDP service is currently unavailable. Please contact your administrator.');
if (response.ok) { return;
this.currentUser = username;
this.sessionId = data.sessionId;
await this.loadTargets();
this.showTargetsView();
} else {
this.showError(errorMessage, data.error || 'Authentication failed');
} }
// 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) { } catch (error) {
console.error('Login error:', 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 { } finally {
loginBtn.disabled = false; loginBtn.disabled = false;
btnText.style.display = 'block'; btnText.style.display = 'block';
@@ -93,25 +90,45 @@ class RDPWebGateway {
async loadTargets() { async loadTargets() {
try { try {
const response = await fetch('/api/targets', { const response = await fetch('/api/targets');
headers: {
'X-Session-ID': this.sessionId,
},
});
if (!response.ok) { 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'); throw new Error('Failed to load targets');
} }
const data = await response.json(); const data = await response.json();
this.displayTargets(data.targets); this.showTargetsView(data.targets);
} catch (error) { } catch (error) {
console.error('Load targets error:', error); console.error('Error loading targets:', error);
const targetsError = document.getElementById('targetsError'); // Show error in targets view
this.showError(targetsError, 'Failed to load available desktops'); 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) { displayTargets(targets) {
const targetsList = document.getElementById('targetsList'); const targetsList = document.getElementById('targetsList');
targetsList.innerHTML = ''; targetsList.innerHTML = '';
@@ -153,10 +170,11 @@ class RDPWebGateway {
this.ws.onopen = () => { this.ws.onopen = () => {
console.log('WebSocket connected'); console.log('WebSocket connected');
// Send connection request // Send credentials and connection request to RdpBroker
this.ws.send(JSON.stringify({ this.ws.send(JSON.stringify({
type: 'connect', type: 'connect',
sessionId: this.sessionId, username: this.credentials.username,
password: this.credentials.password,
target: target, target: target,
})); }));
}; };
@@ -324,7 +342,7 @@ class RDPWebGateway {
} }
this.currentUser = null; this.currentUser = null;
this.currentTarget = null; this.currentTarget = null;
this.sessionId = null; this.credentials = null;
this.showLoginView(); this.showLoginView();
} }
@@ -336,13 +354,6 @@ class RDPWebGateway {
document.getElementById('password').value = ''; 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() { showRDPViewer() {
document.getElementById('loginCard').style.display = 'none'; document.getElementById('loginCard').style.display = 'none';
document.getElementById('targetsCard').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 net = require('net');
const logger = require('./logger');
class RDPProxyHandler { class RDPProxyHandler {
constructor(websocket, sessionManager, rdpBrokerHost, rdpBrokerPort) { constructor(websocket, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket; this.ws = websocket;
this.sessionManager = sessionManager;
this.rdpBrokerHost = rdpBrokerHost; this.rdpBrokerHost = rdpBrokerHost;
this.rdpBrokerPort = rdpBrokerPort; this.rdpBrokerPort = rdpBrokerPort;
this.session = null;
this.rdpSocket = null; this.rdpSocket = null;
this.isConnected = false; this.isConnected = false;
} }
@@ -28,40 +25,33 @@ class RDPProxyHandler {
this.handleSpecialCommand(message); this.handleSpecialCommand(message);
break; break;
default: default:
logger.warn(`Unknown message type: ${message.type}`); console.warn(`${new Date().toISOString()} [WARN] Unknown message type: ${message.type}`);
} }
} catch (error) { } catch (error) {
logger.error('Error handling message:', error); console.error(`${new Date().toISOString()} [ERROR] Error handling message:`, error);
this.sendError('Failed to process message'); this.sendError('Failed to process message');
} }
} }
async handleConnect(message) { async handleConnect(message) {
const { sessionId, target } = message; const { username, password, target } = message;
// Validate session console.log(`${new Date().toISOString()} [INFO] Connecting to RDP Broker, target: ${target?.name || 'unknown'}`);
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}`);
try { try {
// Connect to RDP Broker // Connect to RDP Broker
this.rdpSocket = new net.Socket(); this.rdpSocket = new net.Socket();
this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => { 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; this.isConnected = true;
// Send authentication to RdpBroker // Send credentials to RdpBroker (it will handle authentication)
// In real implementation, this would follow the RDP protocol this.sendAuthToBroker(username, password);
this.sendAuthToBroker();
this.ws.send(JSON.stringify({ this.ws.send(JSON.stringify({
type: 'connected', type: 'connected',
target: target.name target: target?.name || 'RDP Server'
})); }));
// Set canvas size // Set canvas size
@@ -75,20 +65,19 @@ class RDPProxyHandler {
// Handle data from RDP Broker // Handle data from RDP Broker
this.rdpSocket.on('data', (data) => { this.rdpSocket.on('data', (data) => {
// Forward RDP data to WebSocket client // Forward RDP data to WebSocket client
// In production, this would be properly decoded RDP frames
if (this.ws.readyState === 1) { // WebSocket.OPEN if (this.ws.readyState === 1) { // WebSocket.OPEN
this.ws.send(data); this.ws.send(data);
} }
}); });
this.rdpSocket.on('error', (error) => { 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.sendError('Connection to RDP broker failed');
this.cleanup(); this.cleanup();
}); });
this.rdpSocket.on('close', () => { this.rdpSocket.on('close', () => {
logger.info('RDP connection closed'); console.log(`${new Date().toISOString()} [INFO] RDP connection closed`);
this.isConnected = false; this.isConnected = false;
if (this.ws.readyState === 1) { if (this.ws.readyState === 1) {
this.ws.close(); this.ws.close();
@@ -96,25 +85,19 @@ class RDPProxyHandler {
}); });
} catch (error) { } catch (error) {
logger.error('Connection error:', error); console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error);
this.sendError('Failed to connect to RDP broker'); this.sendError('Failed to connect to RDP broker');
} }
} }
sendAuthToBroker() { sendAuthToBroker(username, password) {
// This is a simplified version // Send credentials directly to RdpBroker
// In production, implement proper RDP protocol handshake // 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" // Format: "Username: <username>\nPassword: <password>\n"
const authMessage = `${authData.username}\n${authData.password}\n`; const authMessage = `${username}\n${password}\n`;
this.rdpSocket.write(authMessage); this.rdpSocket.write(authMessage);
} }
@@ -164,10 +147,10 @@ class RDPProxyHandler {
command: 'ctrl-alt-del' command: 'ctrl-alt-del'
}); });
this.rdpSocket.write(cadData + '\n'); this.rdpSocket.write(cadData + '\n');
logger.info('Sent Ctrl+Alt+Del'); console.log(`${new Date().toISOString()} [INFO] Sent Ctrl+Alt+Del`);
break; break;
default: 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.rdpSocket = null;
} }
this.isConnected = false; 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 http = require('http');
const WebSocket = require('ws'); const WebSocket = require('ws');
const path = require('path'); const path = require('path');
const net = require('net');
const compression = require('compression'); const compression = require('compression');
const helmet = require('helmet'); const helmet = require('helmet');
const cors = require('cors'); const cors = require('cors');
const logger = require('./logger');
const SessionManager = require('./sessionManager');
const RDPProxyHandler = require('./rdpProxyHandler'); const RDPProxyHandler = require('./rdpProxyHandler');
class RDPWebGatewayServer { class RDPWebGatewayServer {
@@ -18,7 +17,6 @@ class RDPWebGatewayServer {
path: '/ws/rdp' path: '/ws/rdp'
}); });
this.sessionManager = new SessionManager();
this.port = process.env.PORT || 8080; this.port = process.env.PORT || 8080;
this.rdpBrokerHost = process.env.RDP_BROKER_HOST || 'rdpbroker'; this.rdpBrokerHost = process.env.RDP_BROKER_HOST || 'rdpbroker';
this.rdpBrokerPort = process.env.RDP_BROKER_PORT || 3389; this.rdpBrokerPort = process.env.RDP_BROKER_PORT || 3389;
@@ -45,7 +43,7 @@ class RDPWebGatewayServer {
// Logging // Logging
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`); console.log(`${new Date().toISOString()} [INFO] ${req.method} ${req.url}`);
next(); next();
}); });
} }
@@ -56,70 +54,44 @@ class RDPWebGatewayServer {
res.json({ res.json({
status: 'healthy', status: 'healthy',
version: '1.0.0', version: '1.0.0',
sessions: this.sessionManager.getActiveSessionCount() uptime: process.uptime()
}); });
}); });
// Authentication endpoint // RdpBroker health check endpoint
this.app.post('/api/auth/login', async (req, res) => { this.app.get('/api/broker-status', async (req, res) => {
try { const isAvailable = await this.checkRdpBrokerHealth();
const { username, password } = req.body; res.json({
available: isAvailable,
if (!username || !password) { broker: `${this.rdpBrokerHost}:${this.rdpBrokerPort}`,
return res.status(400).json({ error: 'Username and password required' }); timestamp: new Date().toISOString()
} });
// 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' });
}
}); });
// Get available targets // Get targets list
this.app.get('/api/targets', async (req, res) => { this.app.get('/api/targets', async (req, res) => {
try { try {
const sessionId = req.headers['x-session-id']; const isAvailable = await this.checkRdpBrokerHealth();
if (!isAvailable) {
if (!sessionId) { return res.status(503).json({
return res.status(401).json({ error: 'Session ID required' }); error: 'RdpBroker service is unavailable. Please contact your administrator.',
timestamp: new Date().toISOString()
});
} }
const session = this.sessionManager.getSession(sessionId); // Parse targets from environment variable
if (!session) { const targets = this.parseTargetsFromEnv();
return res.status(401).json({ error: 'Invalid session' }); res.json({ targets, timestamp: new Date().toISOString() });
}
// In a real implementation, this would fetch from RdpBroker
// For now, return static list
const targets = await this.fetchTargetsFromBroker(session);
res.json({ targets });
} catch (error) { } catch (error) {
logger.error('Targets fetch error:', error); console.error(`${new Date().toISOString()} [ERROR] Error fetching targets:`, error);
res.status(500).json({ error: 'Failed to fetch targets' }); res.status(500).json({
error: 'Failed to fetch targets',
timestamp: new Date().toISOString()
});
} }
}); });
// Logout // API endpoints are removed - authentication handled by RdpBroker
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 });
});
// Catch all - serve index.html // Catch all - serve index.html
this.app.get('*', (req, res) => { this.app.get('*', (req, res) => {
@@ -129,11 +101,10 @@ class RDPWebGatewayServer {
setupWebSocket() { setupWebSocket() {
this.wss.on('connection', (ws, req) => { this.wss.on('connection', (ws, req) => {
logger.info('New WebSocket connection'); console.log(`${new Date().toISOString()} [INFO] New WebSocket connection`);
const proxyHandler = new RDPProxyHandler( const proxyHandler = new RDPProxyHandler(
ws, ws,
this.sessionManager,
this.rdpBrokerHost, this.rdpBrokerHost,
this.rdpBrokerPort this.rdpBrokerPort
); );
@@ -143,7 +114,7 @@ class RDPWebGatewayServer {
const message = JSON.parse(data.toString()); const message = JSON.parse(data.toString());
proxyHandler.handleMessage(message); proxyHandler.handleMessage(message);
} catch (error) { } catch (error) {
logger.error('WebSocket message error:', error); console.error(`${new Date().toISOString()} [ERROR] WebSocket message error:`, error);
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'error', type: 'error',
error: 'Invalid message format' error: 'Invalid message format'
@@ -152,50 +123,71 @@ class RDPWebGatewayServer {
}); });
ws.on('close', () => { ws.on('close', () => {
logger.info('WebSocket connection closed'); console.log(`${new Date().toISOString()} [INFO] WebSocket connection closed`);
proxyHandler.cleanup(); proxyHandler.cleanup();
}); });
ws.on('error', (error) => { ws.on('error', (error) => {
logger.error('WebSocket error:', error); console.error(`${new Date().toISOString()} [ERROR] WebSocket error:`, error);
proxyHandler.cleanup(); 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 [ // Check if RdpBroker is available
{ checkRdpBrokerHealth() {
name: "Windows Server 01", return new Promise((resolve) => {
host: "192.168.1.10", 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, port: 3389,
description: "Production Web Server" description: 'Targets will be provided by RdpBroker'
}, }];
{ }
name: "Windows Server 02",
host: "192.168.1.11", try {
port: 3389, return JSON.parse(targetsEnv);
description: "Database Server" } catch (error) {
}, console.error(`${new Date().toISOString()} [ERROR] Failed to parse RDP_TARGETS:`, error);
{ return [];
name: "Development Desktop", }
host: "dev-machine.local",
port: 3389,
description: "Developer Workstation"
}
];
} }
start() { start() {
this.server.listen(this.port, () => { this.server.listen(this.port, () => {
logger.info(`RDP Web Gateway server running on port ${this.port}`); console.log(`${new Date().toISOString()} [INFO] RDP Web Gateway server running on port ${this.port}`);
logger.info(`RDP Broker: ${this.rdpBrokerHost}:${this.rdpBrokerPort}`); console.log(`${new Date().toISOString()} [INFO] RDP Broker: ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
logger.info(`WebSocket endpoint: ws://localhost:${this.port}/ws/rdp`); console.log(`${new Date().toISOString()} [INFO] WebSocket endpoint: ws://localhost:${this.port}/ws/rdp`);
}); });
} }
} }
@@ -206,9 +198,9 @@ server.start();
// Graceful shutdown // Graceful shutdown
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully...'); console.log(`${new Date().toISOString()} [INFO] SIGTERM received, shutting down gracefully...`);
server.server.close(() => { server.server.close(() => {
logger.info('Server closed'); console.log(`${new Date().toISOString()} [INFO] Server closed`);
process.exit(0); 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;