Repo initialisation

This commit is contained in:
Serge NOEL
2025-12-03 13:16:35 +01:00
parent 21b6c855d2
commit 66ccf7a20e
51 changed files with 5011 additions and 0 deletions

32
web-gateway/.dockerignore Normal file
View File

@@ -0,0 +1,32 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment
.env
.env.local
.env.*.local
# Logs
logs/
*.log
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Build
dist/
build/
# Test
coverage/
.nyc_output/

16
web-gateway/.env.example Normal file
View File

@@ -0,0 +1,16 @@
# RDP Web Gateway Environment Configuration
# 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

39
web-gateway/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# .gitignore
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
# Environment
.env
.env.local
.env.*.local
# Logs
logs/
*.log
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Build
dist/
build/
# Test
coverage/
.nyc_output/
# Helm
*-values.yaml
!values.yaml

44
web-gateway/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Production stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create app user
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
COPY --from=builder /app/node_modules ./node_modules
# Copy application files
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 8080
# Use dumb-init to handle signals
ENTRYPOINT ["dumb-init", "--"]
# Start application
CMD ["node", "src/server.js"]

454
web-gateway/INTEGRATION.md Normal file
View File

@@ -0,0 +1,454 @@
# RDP Web Gateway Integration Guide
This guide explains how to integrate the RDP Web Gateway with RdpBroker for a complete browser-based RDP solution.
## Architecture Overview
```
┌─────────────────┐
│ User Browser │
│ (HTML5/WS) │
└────────┬────────┘
│ HTTP/WebSocket
│ Port 80/443
┌─────────────────────┐
│ RDP Web Gateway │
│ (Node.js) │
│ - Session Mgmt │
│ - WebSocket Proxy │
└────────┬────────────┘
│ RDP Protocol
│ Port 3389
┌─────────────────────┐
│ RdpBroker │
│ (C Application) │
│ - Samba AD Auth │
│ - Target Selection │
│ - RDP Forwarding │
└────────┬────────────┘
│ RDP Protocol
│ Port 3389
┌─────────────────────┐
│ Target RDP Servers │
│ (Windows/Linux) │
└─────────────────────┘
```
## Deployment Steps
### 1. Deploy RdpBroker
First, ensure RdpBroker is running:
```bash
# Deploy RdpBroker
cd /data/apps/RdpBroker
helm install rdpbroker ./chart/rdpbroker \
-f rdpbroker-values.yaml \
-n rdpbroker \
--create-namespace
# Verify deployment
kubectl get pods -n rdpbroker
```
### 2. Deploy RDP Web Gateway
```bash
# Build the web gateway image
cd /data/apps/RdpBroker/web-gateway
docker build -t rdp-web-gateway:1.0.0 .
# Tag and push to registry
docker tag rdp-web-gateway:1.0.0 yourusername/rdp-web-gateway:1.0.0
docker push yourusername/rdp-web-gateway:1.0.0
# Deploy with Helm
helm install rdp-web-gateway ./chart/rdp-web-gateway \
-f web-gateway-values.yaml \
-n rdpbroker
```
### 3. Configure Integration
Create `web-gateway-values.yaml`:
```yaml
image:
repository: yourusername/rdp-web-gateway
tag: "1.0.0"
replicaCount: 2
config:
rdpBroker:
host: "rdpbroker" # Service name in Kubernetes
port: 3389
server:
port: 8080
logLevel: "info"
session:
timeout: 3600000 # 1 hour
service:
type: LoadBalancer
port: 80
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
# Optional: Enable ingress for HTTPS
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: rdp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: rdp-tls
hosts:
- rdp.example.com
```
## Network Configuration
### Service Communication
The web gateway needs to communicate with RdpBroker:
```yaml
# In RdpBroker values
service:
type: ClusterIP # Internal only
port: 3389
# In Web Gateway values
config:
rdpBroker:
host: "rdpbroker" # Kubernetes service name
port: 3389
```
### Network Policies (Optional)
For enhanced security, configure network policies:
```yaml
# Web Gateway can access RdpBroker
networkPolicy:
enabled: true
egress:
- to:
- podSelector:
matchLabels:
app: rdpbroker
ports:
- protocol: TCP
port: 3389
```
## Testing the Integration
### 1. Verify Services
```bash
# Check both services are running
kubectl get svc -n rdpbroker
# Expected output:
# NAME TYPE PORT(S)
# rdpbroker ClusterIP 3389/TCP
# rdp-web-gateway LoadBalancer 80:xxxxx/TCP
```
### 2. Test Connectivity
```bash
# Get web gateway URL
export WEB_GATEWAY_IP=$(kubectl get svc rdp-web-gateway -n rdpbroker -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Web Gateway: http://$WEB_GATEWAY_IP"
# Test health endpoint
curl http://$WEB_GATEWAY_IP/health
```
### 3. Test Web Interface
1. Open browser to `http://$WEB_GATEWAY_IP`
2. Login with Samba AD credentials
3. Select a target from the list
4. Verify RDP connection works
### 4. Monitor Logs
```bash
# Web gateway logs
kubectl logs -f deployment/rdp-web-gateway -n rdpbroker
# RdpBroker logs
kubectl logs -f deployment/rdpbroker -n rdpbroker
```
## Flow Diagram
### Authentication Flow
```
Browser → Web Gateway: POST /api/auth/login
{username, password}
← Web Gateway: {sessionId: "uuid"}
Web Gateway → RdpBroker: RDP Connection
Auth: username/password
← RdpBroker: → Samba AD: LDAP Bind
← Auth Result
Web Gateway → Browser: Login Success
```
### Connection Flow
```
Browser → Web Gateway: WebSocket /ws/rdp
{type: "connect", target}
Web Gateway → RdpBroker: TCP Socket (port 3389)
Auth + Target Selection
← RdpBroker: Target Menu Response
Web Gateway → RdpBroker: Selected Target
← RdpBroker: → Target RDP Server
← RDP Stream
Web Gateway → Browser: RDP Frames (Binary WebSocket)
Browser → Web Gateway: Mouse/Keyboard Events
Web Gateway → RdpBroker: RDP Protocol Events
RdpBroker → Target: Forward Events
```
## Production Configuration
### Enable HTTPS/WSS
```yaml
# values.yaml
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/websocket-services: "rdp-web-gateway"
hosts:
- host: rdp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: rdp-tls
hosts:
- rdp.example.com
```
### Session Security
```yaml
secrets:
sessionSecret: "your-secure-random-key-here"
```
Generate secure key:
```bash
openssl rand -base64 32
```
### Resource Limits
```yaml
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
```
## Troubleshooting
### Web Gateway Can't Connect to RdpBroker
```bash
# Test from web gateway pod
kubectl exec -it deployment/rdp-web-gateway -n rdpbroker -- sh
# Inside pod
nc -zv rdpbroker 3389
nslookup rdpbroker
```
### WebSocket Connection Fails
Check ingress configuration for WebSocket support:
```yaml
# For nginx ingress
annotations:
nginx.ingress.kubernetes.io/websocket-services: "rdp-web-gateway"
```
### Authentication Fails
Check logs on both services:
```bash
# Web gateway
kubectl logs deployment/rdp-web-gateway -n rdpbroker | grep -i auth
# RdpBroker
kubectl logs deployment/rdpbroker -n rdpbroker | grep -i auth
```
### High Latency
1. Check network latency between services
2. Ensure services are in same cluster/region
3. Consider increasing resources
4. Enable connection pooling
## Monitoring
### Metrics to Monitor
- Active WebSocket connections
- RDP session count
- Authentication success/failure rate
- Response times
- Resource usage (CPU/Memory)
### Prometheus Integration
```yaml
# serviceMonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: rdp-web-gateway
spec:
selector:
matchLabels:
app: rdp-web-gateway
endpoints:
- port: http
path: /metrics
```
## Security Best Practices
1. **Always use HTTPS/WSS in production**
2. **Implement rate limiting** on authentication endpoints
3. **Use strong session secrets**
4. **Enable network policies** to restrict traffic
5. **Regular security audits** and updates
6. **Monitor for suspicious activity**
7. **Implement session timeout** and cleanup
8. **Use CSP headers** for XSS protection
## Performance Optimization
1. **Enable compression** for HTTP responses
2. **Use CDN** for static assets
3. **Implement caching** where appropriate
4. **Optimize WebSocket buffer sizes**
5. **Use horizontal pod autoscaling**
6. **Consider using Redis** for session storage in multi-replica setup
## Upgrading
### Rolling Update
```bash
# Update web gateway
helm upgrade rdp-web-gateway ./chart/rdp-web-gateway \
-f web-gateway-values.yaml \
-n rdpbroker
# Monitor rollout
kubectl rollout status deployment/rdp-web-gateway -n rdpbroker
```
### Zero-Downtime Deployment
Ensure proper liveness/readiness probes:
```yaml
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
```
## Complete Example Deployment
```bash
#!/bin/bash
# 1. Deploy RdpBroker
helm install rdpbroker ./chart/rdpbroker \
--set image.repository=yourusername/rdpbroker \
--set image.tag=1.0.0 \
--set config.sambaAD.server=ad.example.com \
--set config.sambaAD.baseDN="DC=example,DC=com" \
-n rdpbroker --create-namespace
# 2. Wait for RdpBroker to be ready
kubectl wait --for=condition=available --timeout=300s \
deployment/rdpbroker -n rdpbroker
# 3. Deploy Web Gateway
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--set image.repository=yourusername/rdp-web-gateway \
--set image.tag=1.0.0 \
--set config.rdpBroker.host=rdpbroker \
--set ingress.enabled=true \
--set ingress.hosts[0].host=rdp.example.com \
-n rdpbroker
# 4. Get access URL
kubectl get ingress -n rdpbroker
```
## Support
For issues:
1. Check logs on both services
2. Verify network connectivity
3. Review configuration
4. Check resource usage
5. Consult documentation
For questions, open an issue on the project repository.

287
web-gateway/README.md Normal file
View File

@@ -0,0 +1,287 @@
# RDP Web Gateway
HTML5 WebSocket-based gateway for accessing RDP connections through a web browser. This service sits in front of RdpBroker and provides a modern web interface for remote desktop access.
## Features
- 🌐 **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
-**Low Latency** - Optimized for performance
## Architecture
```
User Browser (HTML5/WebSocket)
RDP Web Gateway (Node.js)
RdpBroker (RDP)
Target RDP Servers
```
## Prerequisites
- Node.js 18+
- RdpBroker service running
- Modern web browser with WebSocket support
## Installation
### Local Development
```bash
cd web-gateway
# Install dependencies
npm install
# Copy environment file
cp .env.example .env
# Edit configuration
nano .env
# Start development server
npm run dev
```
### Docker Build
```bash
docker build -t rdp-web-gateway:latest .
```
## Configuration
Edit `.env` file:
```env
PORT=8080
RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389
LOG_LEVEL=info
SESSION_TIMEOUT=3600000
```
## Usage
### Access the Web Interface
1. Open your browser to `http://localhost:8080`
2. Enter your credentials (validated against Samba AD via RdpBroker)
3. Select a target from the list
4. Connect and use the remote desktop
### API Endpoints
#### POST /api/auth/login
Authenticate user and create session.
```json
{
"username": "user@domain.com",
"password": "password"
}
```
Response:
```json
{
"success": true,
"sessionId": "uuid"
}
```
#### GET /api/targets
Get available RDP targets (requires X-Session-ID header).
Response:
```json
{
"targets": [
{
"name": "Server 01",
"host": "192.168.1.10",
"port": 3389,
"description": "Production Server"
}
]
}
```
#### POST /api/auth/logout
Logout and destroy session (requires X-Session-ID header).
### WebSocket Protocol
Connect to `ws://localhost:8080/ws/rdp`
#### Client → Server Messages
**Connect to target:**
```json
{
"type": "connect",
"sessionId": "uuid",
"target": {
"name": "Server 01",
"host": "192.168.1.10",
"port": 3389
}
}
```
**Mouse event:**
```json
{
"type": "mouse",
"action": "move|down|up|wheel",
"x": 100,
"y": 200,
"button": 0,
"deltaY": 0
}
```
**Keyboard event:**
```json
{
"type": "keyboard",
"action": "down|up",
"key": "a",
"code": "KeyA",
"ctrlKey": false,
"altKey": false,
"shiftKey": false
}
```
**Special command:**
```json
{
"type": "special",
"action": "ctrl-alt-del"
}
```
#### Server → Client Messages
**Connected:**
```json
{
"type": "connected",
"target": "Server 01"
}
```
**Resize canvas:**
```json
{
"type": "resize",
"width": 1920,
"height": 1080
}
```
**Error:**
```json
{
"type": "error",
"error": "Error message"
}
```
## Deployment
See the Helm chart in `chart/rdp-web-gateway/` for Kubernetes deployment.
```bash
helm install rdp-web-gateway ./chart/rdp-web-gateway -n rdpbroker
```
## Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Opera 76+
## Security Considerations
- Use HTTPS/WSS in production
- Implement rate limiting
- Set strong session secrets
- Enable CORS restrictions
- 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
## Troubleshooting
### Can't connect to RdpBroker
Check environment variables:
```bash
echo $RDP_BROKER_HOST
echo $RDP_BROKER_PORT
```
Test connectivity:
```bash
nc -zv rdpbroker 3389
```
### WebSocket connection fails
Ensure WebSocket upgrade is allowed through any proxies/load balancers.
For nginx:
```nginx
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
### High memory usage
Reduce session timeout or implement session limits per user.
## Development
### Running Tests
```bash
npm test
```
### Code Style
```bash
npm run lint
```
## License
MIT License - see LICENSE file
## Support
For issues and questions, check the logs:
```bash
# View logs
kubectl logs -f deployment/rdp-web-gateway -n rdpbroker
# Check health
curl http://localhost:8080/health
```

View File

@@ -0,0 +1,18 @@
apiVersion: v2
name: rdp-web-gateway
description: HTML5 WebSocket-based RDP Web Gateway
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- rdp
- websocket
- html5
- gateway
- web
dependencies: []
maintainers:
- name: RdpBroker Team
home: https://github.com/yourusername/rdpbroker
sources:
- https://github.com/yourusername/rdpbroker

View File

@@ -0,0 +1,37 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "rdp-web-gateway.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "RDP Web Gateway available at: http://$NODE_IP:$NODE_PORT"
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running:
kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "rdp-web-gateway.fullname" . }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "rdp-web-gateway.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo "RDP Web Gateway available at: http://$SERVICE_IP:{{ .Values.service.port }}"
echo "Open in your browser to access the web interface"
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "rdp-web-gateway.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use the application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:{{ .Values.config.server.port }}
{{- end }}
2. View logs:
kubectl logs -f deployment/{{ include "rdp-web-gateway.fullname" . }} -n {{ .Release.Namespace }}
3. Check health:
kubectl exec -it deployment/{{ include "rdp-web-gateway.fullname" . }} -n {{ .Release.Namespace }} -- curl http://localhost:{{ .Values.config.server.port }}/health
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 }}

View File

@@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "rdp-web-gateway.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "rdp-web-gateway.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "rdp-web-gateway.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "rdp-web-gateway.labels" -}}
helm.sh/chart: {{ include "rdp-web-gateway.chart" . }}
{{ include "rdp-web-gateway.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "rdp-web-gateway.selectorLabels" -}}
app.kubernetes.io/name: {{ include "rdp-web-gateway.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "rdp-web-gateway.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "rdp-web-gateway.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}-config
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
data:
config.json: |
{
"rdpBroker": {
"host": "{{ .Values.config.rdpBroker.host }}",
"port": {{ .Values.config.rdpBroker.port }}
},
"server": {
"port": {{ .Values.config.server.port }},
"logLevel": "{{ .Values.config.server.logLevel }}"
},
"session": {
"timeout": {{ .Values.config.session.timeout }}
}
}

View File

@@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "rdp-web-gateway.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: PORT
value: {{ .Values.config.server.port | quote }}
- name: RDP_BROKER_HOST
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 }}
- 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 }}
protocol: TCP
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- 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 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "rdp-web-gateway.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "rdp-web-gateway.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,22 @@
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 6 }}
policyTypes:
{{- toYaml .Values.networkPolicy.policyTypes | nindent 4 }}
{{- with .Values.networkPolicy.ingress }}
ingress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.networkPolicy.egress }}
egress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,17 @@
{{- 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

@@ -0,0 +1,13 @@
{{- 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

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "rdp-web-gateway.serviceAccountName" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,148 @@
# Default values for rdp-web-gateway
replicaCount: 2
image:
repository: rdp-web-gateway
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 1001
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1001
service:
type: LoadBalancer
port: 80
targetPort: 8080
annotations: {}
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: rdp.example.com
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: rdp-tls
# hosts:
# - rdp.example.com
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Application configuration
config:
# RDP Broker connection
rdpBroker:
host: "rdpbroker"
port: 3389
# Server configuration
server:
port: 8080
logLevel: "info"
# Session configuration
session:
timeout: 3600000 # 1 hour in milliseconds
# 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:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# Network Policy
networkPolicy:
enabled: false
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: default
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: rdpbroker
ports:
- protocol: TCP
port: 3389

37
web-gateway/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "rdp-web-gateway",
"version": "1.0.0",
"description": "HTML5 WebSocket-based RDP Gateway for RdpBroker",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"rdp",
"websocket",
"gateway",
"html5",
"remote-desktop"
],
"author": "RdpBroker Team",
"license": "MIT",
"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"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -0,0 +1,370 @@
:root {
--primary-color: #007acc;
--primary-hover: #005a9e;
--danger-color: #e74c3c;
--danger-hover: #c0392b;
--success-color: #27ae60;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-color: #2c3e50;
--text-secondary: #7f8c8d;
--border-color: #e1e8ed;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: var(--text-color);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-card, .targets-card {
background: var(--card-bg);
border-radius: 12px;
box-shadow: var(--shadow);
padding: 40px;
max-width: 450px;
width: 100%;
animation: slideUp 0.4s ease-out;
}
.targets-card {
max-width: 800px;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
text-align: center;
margin-bottom: 20px;
}
h1 {
text-align: center;
color: var(--text-color);
font-size: 28px;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: var(--text-secondary);
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 15px;
transition: all 0.3s ease;
background: #fafafa;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--primary-color);
background: white;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: var(--primary-color);
color: white;
width: 100%;
}
.btn-primary:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--text-secondary);
color: white;
}
.btn-secondary:hover {
background: #6c7a7b;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-danger:hover {
background: var(--danger-hover);
}
.btn-sm {
padding: 8px 16px;
font-size: 13px;
}
.btn-icon {
padding: 8px;
background: transparent;
color: var(--text-color);
}
.btn-icon:hover {
background: rgba(0, 0, 0, 0.05);
}
.error-message {
background: #fee;
color: var(--danger-color);
padding: 12px;
border-radius: 8px;
margin-top: 16px;
font-size: 14px;
border-left: 4px solid var(--danger-color);
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner.large {
width: 48px;
height: 48px;
border-width: 4px;
border-color: rgba(0, 122, 204, 0.3);
border-top-color: var(--primary-color);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Targets Card */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid var(--border-color);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
color: var(--text-secondary);
}
.targets-list {
display: grid;
gap: 12px;
}
.target-item {
background: #fafafa;
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
}
.target-item:hover {
border-color: var(--primary-color);
background: white;
box-shadow: var(--shadow);
transform: translateY(-2px);
}
.target-name {
font-weight: 600;
font-size: 16px;
color: var(--text-color);
margin-bottom: 4px;
}
.target-description {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.target-host {
font-size: 13px;
color: var(--text-secondary);
font-family: 'Courier New', monospace;
}
/* RDP Viewer */
.rdp-viewer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #1e1e1e;
z-index: 1000;
}
.viewer-header {
background: #2d2d2d;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #3d3d3d;
}
.connection-info {
display: flex;
align-items: center;
gap: 12px;
color: white;
font-size: 14px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success-color);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.viewer-controls {
display: flex;
gap: 8px;
align-items: center;
}
.viewer-container {
position: relative;
width: 100%;
height: calc(100% - 60px);
display: flex;
align-items: center;
justify-content: center;
background: #1e1e1e;
}
#rdpCanvas {
max-width: 100%;
max-height: 100%;
background: #000;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
color: white;
}
footer {
text-align: center;
padding: 20px;
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
}
/* Responsive */
@media (max-width: 768px) {
.login-card, .targets-card {
padding: 30px 20px;
}
.header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.viewer-header {
flex-direction: column;
gap: 8px;
}
}

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RDP Web Gateway - Login</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<div class="login-card" id="loginCard">
<div class="logo">
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="5" width="50" height="50" rx="8" stroke="#007acc" stroke-width="3" fill="none"/>
<rect x="15" y="15" width="30" height="20" rx="2" fill="#007acc"/>
<circle cx="30" cy="45" r="3" fill="#007acc"/>
</svg>
</div>
<h1>RDP Web Gateway</h1>
<p class="subtitle">Connect to remote desktops through your browser</p>
<form id="loginForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username" autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary" id="loginBtn">
<span class="btn-text">Sign In</span>
<span class="spinner" style="display: none;"></span>
</button>
<div class="error-message" id="errorMessage" style="display: none;"></div>
</form>
</div>
<div class="targets-card" id="targetsCard" style="display: none;">
<div class="header">
<h2>Select Remote Desktop</h2>
<div class="user-info">
<span id="currentUser"></span>
<button class="btn btn-secondary btn-sm" id="logoutBtn">Logout</button>
</div>
</div>
<div class="targets-list" id="targetsList"></div>
<div class="error-message" id="targetsError" style="display: none;"></div>
</div>
<div class="rdp-viewer" id="rdpViewer" style="display: none;">
<div class="viewer-header">
<div class="connection-info">
<span class="status-indicator"></span>
<span id="connectionInfo"></span>
</div>
<div class="viewer-controls">
<button class="btn btn-icon" id="fullscreenBtn" title="Fullscreen">
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M3 3h5v2H5v3H3V3zm9 0h5v5h-2V5h-3V3zM3 12h2v3h3v2H3v-5zm14 0h2v5h-5v-2h3v-3z"/>
</svg>
</button>
<button class="btn btn-icon" id="ctrlAltDelBtn" title="Send Ctrl+Alt+Del">
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M3 3h14v2H3V3zm0 4h14v2H3V7zm0 4h14v2H3v-2zm0 4h14v2H3v-2z"/>
</svg>
</button>
<button class="btn btn-danger btn-sm" id="disconnectBtn">Disconnect</button>
</div>
</div>
<div class="viewer-container">
<canvas id="rdpCanvas"></canvas>
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner large"></div>
<p>Connecting to remote desktop...</p>
</div>
</div>
</div>
</div>
<footer>
<p>RDP Web Gateway v1.0.0 | Powered by FreeRDP-WebConnect</p>
</footer>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,383 @@
class RDPWebGateway {
constructor() {
this.ws = null;
this.canvas = null;
this.ctx = null;
this.currentUser = null;
this.currentTarget = null;
this.sessionId = null;
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
// Login form
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
}
// Logout button
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => this.handleLogout());
}
// Disconnect button
const disconnectBtn = document.getElementById('disconnectBtn');
if (disconnectBtn) {
disconnectBtn.addEventListener('click', () => this.handleDisconnect());
}
// Fullscreen button
const fullscreenBtn = document.getElementById('fullscreenBtn');
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
}
// Ctrl+Alt+Del button
const ctrlAltDelBtn = document.getElementById('ctrlAltDelBtn');
if (ctrlAltDelBtn) {
ctrlAltDelBtn.addEventListener('click', () => this.sendCtrlAltDel());
}
}
async handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
const btnText = loginBtn.querySelector('.btn-text');
const spinner = loginBtn.querySelector('.spinner');
const errorMessage = document.getElementById('errorMessage');
// Show loading state
loginBtn.disabled = true;
btnText.style.display = 'none';
spinner.style.display = 'block';
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');
}
} catch (error) {
console.error('Login error:', error);
this.showError(errorMessage, 'Connection error. Please try again.');
} finally {
loginBtn.disabled = false;
btnText.style.display = 'block';
spinner.style.display = 'none';
}
}
async loadTargets() {
try {
const response = await fetch('/api/targets', {
headers: {
'X-Session-ID': this.sessionId,
},
});
if (!response.ok) {
throw new Error('Failed to load targets');
}
const data = await response.json();
this.displayTargets(data.targets);
} catch (error) {
console.error('Load targets error:', error);
const targetsError = document.getElementById('targetsError');
this.showError(targetsError, 'Failed to load available desktops');
}
}
displayTargets(targets) {
const targetsList = document.getElementById('targetsList');
targetsList.innerHTML = '';
if (!targets || targets.length === 0) {
targetsList.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No remote desktops available</p>';
return;
}
targets.forEach(target => {
const targetItem = document.createElement('div');
targetItem.className = 'target-item';
targetItem.innerHTML = `
<div class="target-name">${this.escapeHtml(target.name)}</div>
<div class="target-description">${this.escapeHtml(target.description)}</div>
<div class="target-host">${this.escapeHtml(target.host)}:${target.port}</div>
`;
targetItem.addEventListener('click', () => this.connectToTarget(target));
targetsList.appendChild(targetItem);
});
}
async connectToTarget(target) {
this.currentTarget = target;
this.showRDPViewer();
this.initializeRDPConnection(target);
}
initializeRDPConnection(target) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/rdp`;
this.canvas = document.getElementById('rdpCanvas');
this.ctx = this.canvas.getContext('2d');
// Connect WebSocket
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
console.log('WebSocket connected');
// Send connection request
this.ws.send(JSON.stringify({
type: 'connect',
sessionId: this.sessionId,
target: target,
}));
};
this.ws.onmessage = (event) => {
this.handleWebSocketMessage(event);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.showConnectionError('Connection error occurred');
};
this.ws.onclose = () => {
console.log('WebSocket closed');
this.handleDisconnect();
};
// Setup canvas input handlers
this.setupCanvasInputHandlers();
}
handleWebSocketMessage(event) {
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
switch (message.type) {
case 'connected':
document.getElementById('loadingOverlay').style.display = 'none';
document.getElementById('connectionInfo').textContent =
`Connected to ${this.currentTarget.name}`;
break;
case 'error':
this.showConnectionError(message.error);
break;
case 'resize':
this.canvas.width = message.width;
this.canvas.height = message.height;
break;
}
} else {
// Binary data - frame update
this.renderFrame(event.data);
}
}
renderFrame(data) {
// This is a simplified version
// In production, you'd decode the RDP frame data properly
const imageData = new Uint8ClampedArray(data);
if (imageData.length === this.canvas.width * this.canvas.height * 4) {
const imgData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
imgData.data.set(imageData);
this.ctx.putImageData(imgData, 0, 0);
}
}
setupCanvasInputHandlers() {
const canvas = this.canvas;
// Mouse events
canvas.addEventListener('mousemove', (e) => {
this.sendMouseEvent('move', e);
});
canvas.addEventListener('mousedown', (e) => {
this.sendMouseEvent('down', e);
});
canvas.addEventListener('mouseup', (e) => {
this.sendMouseEvent('up', e);
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
this.sendMouseEvent('wheel', e);
});
// Keyboard events
window.addEventListener('keydown', (e) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
e.preventDefault();
this.sendKeyEvent('down', e);
}
});
window.addEventListener('keyup', (e) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
e.preventDefault();
this.sendKeyEvent('up', e);
}
});
// Prevent context menu
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
sendMouseEvent(type, event) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
const rect = this.canvas.getBoundingClientRect();
const x = Math.floor((event.clientX - rect.left) * (this.canvas.width / rect.width));
const y = Math.floor((event.clientY - rect.top) * (this.canvas.height / rect.height));
this.ws.send(JSON.stringify({
type: 'mouse',
action: type,
x: x,
y: y,
button: event.button,
deltaY: event.deltaY || 0,
}));
}
sendKeyEvent(type, event) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({
type: 'keyboard',
action: type,
key: event.key,
code: event.code,
keyCode: event.keyCode,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
}));
}
sendCtrlAltDel() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({
type: 'special',
action: 'ctrl-alt-del',
}));
}
toggleFullscreen() {
const viewer = document.getElementById('rdpViewer');
if (!document.fullscreenElement) {
viewer.requestFullscreen().catch(err => {
console.error('Error attempting to enable fullscreen:', err);
});
} else {
document.exitFullscreen();
}
}
handleDisconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.currentTarget = null;
this.showTargetsView();
}
handleLogout() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.currentUser = null;
this.currentTarget = null;
this.sessionId = null;
this.showLoginView();
}
showLoginView() {
document.getElementById('loginCard').style.display = 'block';
document.getElementById('targetsCard').style.display = 'none';
document.getElementById('rdpViewer').style.display = 'none';
document.getElementById('username').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() {
document.getElementById('loginCard').style.display = 'none';
document.getElementById('targetsCard').style.display = 'none';
document.getElementById('rdpViewer').style.display = 'block';
document.getElementById('loadingOverlay').style.display = 'flex';
}
showError(element, message) {
element.textContent = message;
element.style.display = 'block';
}
showConnectionError(message) {
const overlay = document.getElementById('loadingOverlay');
overlay.innerHTML = `
<div style="text-align: center;">
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" stroke="#e74c3c" stroke-width="4" fill="none"/>
<path d="M32 16v20M32 44v4" stroke="#e74c3c" stroke-width="4" stroke-linecap="round"/>
</svg>
<h3 style="color: white; margin-top: 16px;">Connection Failed</h3>
<p style="color: rgba(255,255,255,0.8); margin-top: 8px;">${this.escapeHtml(message)}</p>
<button class="btn btn-primary" onclick="rdpGateway.handleDisconnect()" style="margin-top: 20px;">
Back to Desktop Selection
</button>
</div>
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize the app
const rdpGateway = new RDPWebGateway();

41
web-gateway/src/logger.js Normal file
View File

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,198 @@
const net = require('net');
const logger = require('./logger');
class RDPProxyHandler {
constructor(websocket, sessionManager, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket;
this.sessionManager = sessionManager;
this.rdpBrokerHost = rdpBrokerHost;
this.rdpBrokerPort = rdpBrokerPort;
this.session = null;
this.rdpSocket = null;
this.isConnected = false;
}
async handleMessage(message) {
try {
switch (message.type) {
case 'connect':
await this.handleConnect(message);
break;
case 'mouse':
this.handleMouseEvent(message);
break;
case 'keyboard':
this.handleKeyboardEvent(message);
break;
case 'special':
this.handleSpecialCommand(message);
break;
default:
logger.warn(`Unknown message type: ${message.type}`);
}
} catch (error) {
logger.error('Error handling message:', error);
this.sendError('Failed to process message');
}
}
async handleConnect(message) {
const { sessionId, 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}`);
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}`);
this.isConnected = true;
// Send authentication to RdpBroker
// In real implementation, this would follow the RDP protocol
this.sendAuthToBroker();
this.ws.send(JSON.stringify({
type: 'connected',
target: target.name
}));
// Set canvas size
this.ws.send(JSON.stringify({
type: 'resize',
width: 1920,
height: 1080
}));
});
// 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);
this.sendError('Connection to RDP broker failed');
this.cleanup();
});
this.rdpSocket.on('close', () => {
logger.info('RDP connection closed');
this.isConnected = false;
if (this.ws.readyState === 1) {
this.ws.close();
}
});
} catch (error) {
logger.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
if (!this.rdpSocket || !this.session) 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`;
this.rdpSocket.write(authMessage);
}
handleMouseEvent(message) {
if (!this.isConnected || !this.rdpSocket) return;
// Convert mouse event to RDP protocol
// This is simplified - real implementation would use RDP protocol
const mouseData = JSON.stringify({
type: 'mouse',
x: message.x,
y: message.y,
button: message.button,
action: message.action
});
this.rdpSocket.write(mouseData + '\n');
}
handleKeyboardEvent(message) {
if (!this.isConnected || !this.rdpSocket) return;
// Convert keyboard event to RDP protocol
const keyData = JSON.stringify({
type: 'key',
action: message.action,
key: message.key,
code: message.code,
modifiers: {
ctrl: message.ctrlKey,
alt: message.altKey,
shift: message.shiftKey
}
});
this.rdpSocket.write(keyData + '\n');
}
handleSpecialCommand(message) {
if (!this.isConnected || !this.rdpSocket) return;
switch (message.action) {
case 'ctrl-alt-del':
// Send Ctrl+Alt+Del sequence
const cadData = JSON.stringify({
type: 'special',
command: 'ctrl-alt-del'
});
this.rdpSocket.write(cadData + '\n');
logger.info('Sent Ctrl+Alt+Del');
break;
default:
logger.warn(`Unknown special command: ${message.action}`);
}
}
sendError(message) {
if (this.ws.readyState === 1) {
this.ws.send(JSON.stringify({
type: 'error',
error: message
}));
}
}
cleanup() {
if (this.rdpSocket) {
this.rdpSocket.destroy();
this.rdpSocket = null;
}
this.isConnected = false;
if (this.session) {
this.sessionManager.updateSession(this.session.id, {
rdpConnection: null
});
}
}
}
module.exports = RDPProxyHandler;

216
web-gateway/src/server.js Normal file
View File

@@ -0,0 +1,216 @@
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
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 {
constructor() {
this.app = express();
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({
server: this.server,
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;
this.setupMiddleware();
this.setupRoutes();
this.setupWebSocket();
}
setupMiddleware() {
// Security
this.app.use(helmet({
contentSecurityPolicy: false, // Disable for WebSocket
}));
this.app.use(cors());
this.app.use(compression());
// Body parsing
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// Static files
this.app.use(express.static(path.join(__dirname, '../public')));
// Logging
this.app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
}
setupRoutes() {
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
version: '1.0.0',
sessions: this.sessionManager.getActiveSessionCount()
});
});
// 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' });
}
});
// Get available targets
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 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 });
} catch (error) {
logger.error('Targets fetch error:', error);
res.status(500).json({ error: 'Failed to fetch targets' });
}
});
// 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 });
});
// Catch all - serve index.html
this.app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
}
setupWebSocket() {
this.wss.on('connection', (ws, req) => {
logger.info('New WebSocket connection');
const proxyHandler = new RDPProxyHandler(
ws,
this.sessionManager,
this.rdpBrokerHost,
this.rdpBrokerPort
);
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
proxyHandler.handleMessage(message);
} catch (error) {
logger.error('WebSocket message error:', error);
ws.send(JSON.stringify({
type: 'error',
error: 'Invalid message format'
}));
}
});
ws.on('close', () => {
logger.info('WebSocket connection closed');
proxyHandler.cleanup();
});
ws.on('error', (error) => {
logger.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",
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"
}
];
}
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`);
});
}
}
// Start server
const server = new RDPWebGatewayServer();
server.start();
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully...');
server.server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
module.exports = RDPWebGatewayServer;

View File

@@ -0,0 +1,94 @@
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;