Simplification Web-Gateway
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal 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/
|
||||
@@ -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"}]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user