Initialisation depot

This commit is contained in:
Serge NOEL
2026-02-10 12:12:11 +01:00
commit c3176e8d79
818 changed files with 52573 additions and 0 deletions

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/

View File

@@ -0,0 +1,15 @@
# RDP Web Gateway Environment Configuration
# Server
PORT=8080
NODE_ENV=production
# RDP Broker Connection
RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389
# 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"}]

39
RdpBroker/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

View File

@@ -0,0 +1,201 @@
# Building for Raspberry Pi 4 (ARM64)
## Quick Start
The web-gateway is **fully compatible** with Raspberry Pi 4 running K3s! 🎉
### Option 1: Build Multi-Arch Image (Recommended)
Build once, run on both x86_64 and ARM64:
```bash
chmod +x build-multiarch.sh
# Build and push to Docker Hub
IMAGE_NAME=yourusername/web-gateway IMAGE_TAG=1.0.0 ./build-multiarch.sh
# Or use default (easylinux/web-gateway:latest)
./build-multiarch.sh
```
### Option 2: Build ARM64 Only
Faster if you only need ARM64:
```bash
chmod +x build-arm64.sh
# Build for ARM64
IMAGE_NAME=yourusername/web-gateway IMAGE_TAG=1.0.0 ./build-arm64.sh
# Push to registry
docker push yourusername/web-gateway:1.0.0
```
### Option 3: Build Natively on Raspberry Pi
Build directly on your RPi 4 (slower but simpler):
```bash
# On your Raspberry Pi 4
docker build -t yourusername/web-gateway:1.0.0 .
docker push yourusername/web-gateway:1.0.0
```
## Deploy to K3s on Raspberry Pi
```bash
# Update values.yaml or use --set
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
--create-namespace \
--set image.repository=yourusername/web-gateway \
--set image.tag=1.0.0 \
--set service.type=ClusterIP \
--set traefik.enabled=true \
--set traefik.host=rdp.yourdomain.com \
--set traefik.tls.enabled=true \
--set traefik.tls.certResolver=letsencrypt
```
## Resource Recommendations for Raspberry Pi 4
The default values.yaml may be too high for RPi 4. Use this configuration:
```yaml
# values-rpi4.yaml
resources:
limits:
cpu: 500m # Down from 1000m
memory: 512Mi # Down from 1Gi
requests:
cpu: 100m # Down from 200m
memory: 128Mi # Down from 256Mi
autoscaling:
enabled: true
minReplicas: 1 # Down from 2 (save memory)
maxReplicas: 3 # Down from 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
replicaCount: 1 # Start with 1 replica
```
Deploy with adjusted resources:
```bash
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
-f chart/rdp-web-gateway/examples/traefik-letsencrypt.yaml \
-f values-rpi4.yaml
```
## Performance Notes
- **Node.js Alpine** images are very lightweight (~50MB compressed)
- **Memory footprint**: ~100-200MB per pod under normal load
- **CPU usage**: Very low when idle, spikes during RDP streaming
- **Network**: WebSocket is efficient, ~1-5Mbps per active session
- **Recommended**: 2-4GB RAM Raspberry Pi 4 can handle 3-5 concurrent sessions
## Verify Architecture
After building, verify the image supports ARM64:
```bash
docker buildx imagetools inspect yourusername/web-gateway:1.0.0
```
Expected output:
```
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:...
Manifests:
Name: linux/amd64
Digest: sha256:...
Platform: linux/amd64
Name: linux/arm64
Digest: sha256:...
Platform: linux/arm64
```
## Troubleshooting
### Image pull fails on ARM64
```bash
# Check current architecture
uname -m # Should show: aarch64
# Verify image manifest
docker manifest inspect yourusername/web-gateway:1.0.0 | grep architecture
```
### OOMKilled (Out of Memory)
Reduce memory limits or number of replicas:
```yaml
resources:
limits:
memory: 256Mi # Lower if needed
autoscaling:
minReplicas: 1
```
### Build too slow
- Use `build-arm64.sh` instead of `build-multiarch.sh`
- Build on Raspberry Pi 4 itself (native build)
- Use GitHub Actions or CI/CD to build multi-arch images
## GitHub Actions Example
Add `.github/workflows/build.yml`:
```yaml
name: Build Multi-Arch
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./web-gateway
platforms: linux/amd64,linux/arm64
push: true
tags: |
yourusername/web-gateway:latest
yourusername/web-gateway:${{ github.ref_name }}
```
## Additional Notes
- **K3s on RPi 4**: Works perfectly! K3s is optimized for ARM
- **Storage**: Use SSD instead of SD card for better I/O performance
- **Network**: Gigabit Ethernet recommended for RDP streaming
- **Cooling**: Consider a fan/heatsink for sustained load

View File

@@ -0,0 +1,41 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install \
&& 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
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"]

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.

View File

@@ -0,0 +1,493 @@
# 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
- 🔑 **User-Specific Targets** - Each user sees only their authorized RDP servers
- 📊 **Service Health Monitoring** - Automatic RdpBroker availability checks
- 🎯 **Dynamic Target Loading** - Personalized targets from RdpBroker based on user permissions
-**Low Latency** - Optimized for performance
- ☁️ **Kubernetes Native** - Console-only logging for cloud environments
- 🔐 **Samba AD Integration** - Authentication via RdpBroker with Samba Active Directory
## Architecture
```
User Browser (HTML5/WebSocket)
RDP Web Gateway (Node.js)
↓ [WebSocket Protocol]
↓ 1. AUTH → receives user-specific targets
↓ 2. SELECT → connects to chosen target
RdpBroker (C)
↓ [Samba AD Auth]
↓ [Target Authorization]
↓ [RDP Forwarding]
Target RDP Servers
```
## Authentication Flow
1. **User Login** - User enters credentials in web interface
2. **Health Check** - Web-gateway verifies RdpBroker is available
3. **WebSocket Auth** - Credentials sent via WebSocket to RdpBroker
4. **LDAP Authentication** - RdpBroker authenticates against Samba AD
5. **Target Authorization** - RdpBroker determines user's authorized targets based on groups/permissions
6. **Targets Display** - User-specific target list sent back to web-gateway
7. **Target Selection** - User chooses from their authorized servers
8. **RDP Session** - RdpBroker establishes connection to selected target
## 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
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
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
#### GET /health
Health check endpoint for monitoring the web gateway.
Response:
```json
{
"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
Fetch available RDP targets.
**Success Response (200):**
```json
{
"targets": [
{
"name": "Windows Server 2022",
"host": "ws2022.example.com",
"port": 3389,
"description": "Production Windows Server"
}
],
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
**Service Unavailable (503):**
```json
{
"error": "RdpBroker service is unavailable. Please contact your administrator.",
"timestamp": "2025-12-04T10:30:00.000Z"
}
```
### WebSocket Protocol
Connect to `ws://localhost:8080/ws/rdp`
The protocol follows a two-phase approach:
1. **Authentication Phase**: User authenticates and receives personalized target list
2. **Connection Phase**: User selects target and establishes RDP session
#### Phase 1: Authentication
**Client → Server - Authenticate:**
```json
{
"type": "authenticate",
"username": "user@domain.com",
"password": "password123"
}
```
**Server → Client - Authentication Success with Targets:**
```json
{
"type": "targets",
"targets": [
{
"name": "Windows Server 2022",
"host": "ws2022.example.com",
"port": 3389,
"description": "Production Windows Server (user-specific)"
},
{
"name": "Development Server",
"host": "dev.example.com",
"port": 3389,
"description": "Development environment"
}
]
}
```
**Server → Client - Authentication Failed:**
```json
{
"type": "error",
"error": "Invalid credentials"
}
```
#### Phase 2: Connection
**Client → Server - Connect to Target:**
```json
{
"type": "connect",
"target": {
"name": "Windows Server 2022",
"host": "ws2022.example.com",
"port": 3389
}
}
```
**Server → Client - RDP Session Ready:**
```json
{
"type": "connected",
"target": "Windows Server 2022"
}
```
#### Client → Server Messages
**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
### Kubernetes with Helm
#### Option 1: LoadBalancer (Default)
```bash
# Deploy with LoadBalancer service
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
--create-namespace \
--set service.type=LoadBalancer
```
#### Option 2: Traefik IngressRoute with Let's Encrypt
**Recommended for production with automatic HTTPS**
1. **Apply Traefik middlewares** (one time):
```bash
kubectl apply -f chart/rdp-web-gateway/examples/traefik-middlewares.yaml -n rdpbroker
```
2. **Deploy with Traefik IngressRoute**:
```bash
# Edit the host in examples/traefik-letsencrypt.yaml
# Then deploy:
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
--create-namespace \
-f chart/rdp-web-gateway/examples/traefik-letsencrypt.yaml
```
Or directly with values:
```bash
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
--create-namespace \
--set service.type=ClusterIP \
--set traefik.enabled=true \
--set traefik.host=rdp.yourdomain.com \
--set traefik.tls.enabled=true \
--set traefik.tls.certResolver=letsencrypt
```
3. **Verify deployment**:
```bash
# Check IngressRoute
kubectl get ingressroute -n rdpbroker
# Check certificate (after a few seconds)
kubectl get certificate -n rdpbroker
# Access your gateway
https://rdp.yourdomain.com
```
#### Option 3: Standard Ingress (nginx, etc.)
```bash
helm install rdp-web-gateway ./chart/rdp-web-gateway \
--namespace rdpbroker \
--create-namespace \
--set service.type=ClusterIP \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set ingress.hosts[0].host=rdp.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix
```
### Important Notes for Traefik
**WebSocket Support**: Traefik automatically handles WebSocket upgrades, no special configuration needed!
**Let's Encrypt Certificate Resolver**: Ensure your Traefik has a certResolver named `letsencrypt` configured. Example:
```yaml
# Traefik values.yaml or static config
certificatesResolvers:
letsencrypt:
acme:
email: admin@yourdomain.com
storage: /data/acme.json
httpChallenge:
entryPoint: web
```
**Middlewares**: Apply the recommended middlewares for security:
- `redirect-to-https` - Force HTTPS
- `security-headers` - Security headers including WebSocket support
- `rate-limit` - Prevent abuse
- `compression` - Reduce bandwidth
## Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Opera 76+
## Security Considerations
- Use HTTPS/WSS in production
- 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
- Configure WebSocket buffer sizes
- 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
### 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 proxies/load balancers.
**For Traefik**: Already handled automatically! ✅
**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";
}
```
**For Traefik middlewares**: Ensure security-headers middleware includes:
```yaml
customResponseHeaders:
Connection: "upgrade"
Upgrade: "$http_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
### 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,49 @@
#!/bin/bash
# Build ARM64-only image for Raspberry Pi 4
# Faster build when you only need ARM64
set -e
IMAGE_NAME="${IMAGE_NAME:-easylinux/web-gateway}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
echo "============================================"
echo "Building ARM64 Image for Raspberry Pi 4"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo "Platform: linux/arm64"
echo "============================================"
# Check if buildx is available
if ! docker buildx version >/dev/null 2>&1; then
echo "Error: docker buildx not found. Please install Docker Buildx."
exit 1
fi
# Create builder if needed
if ! docker buildx ls | grep -q arm64-builder; then
echo "Creating arm64-builder..."
docker buildx create --name arm64-builder --use --bootstrap
else
echo "Using existing arm64-builder..."
docker buildx use arm64-builder
fi
# Build ARM64 image
echo "Building for ARM64..."
docker buildx build \
--platform linux/arm64 \
--tag "${IMAGE_NAME}:${IMAGE_TAG}" \
--load \
.
echo "============================================"
echo "✅ ARM64 image built successfully!"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Push to registry:"
echo " docker push ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Or save as tar:"
echo " docker save ${IMAGE_NAME}:${IMAGE_TAG} | gzip > web-gateway-arm64.tar.gz"

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Build multi-architecture Docker image for web-gateway
# Supports: amd64 (x86_64) and arm64 (Raspberry Pi 4, Apple Silicon)
set -e
# Configuration
IMAGE_NAME="${IMAGE_NAME:-easylinux/web-gateway}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
echo "============================================"
echo "Building Multi-Arch Docker Image"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo "Platforms: ${PLATFORMS}"
echo "============================================"
# Check if buildx is available
if ! docker buildx version >/dev/null 2>&1; then
echo "Error: docker buildx not found. Please install Docker Buildx."
echo "See: https://docs.docker.com/buildx/working-with-buildx/"
exit 1
fi
# Create builder instance if it doesn't exist
if ! docker buildx ls | grep -q multiarch-builder; then
echo "Creating multiarch-builder..."
docker buildx create --name multiarch-builder --use --bootstrap
else
echo "Using existing multiarch-builder..."
docker buildx use multiarch-builder
fi
# Build and push multi-arch image
echo "Building for platforms: ${PLATFORMS}..."
# Check if PUSH is set to true
if [ "${PUSH}" = "true" ]; then
echo "Building and pushing to registry..."
docker buildx build \
--platform "${PLATFORMS}" \
--tag "${IMAGE_NAME}:${IMAGE_TAG}" \
--push \
.
echo "============================================"
echo "✅ Multi-arch image built and pushed!"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Verify architectures:"
echo " docker buildx imagetools inspect ${IMAGE_NAME}:${IMAGE_TAG}"
else
# For multi-arch, save to local registry cache
echo "Building locally (use PUSH=true to push to registry)..."
docker buildx build \
--platform "${PLATFORMS}" \
--tag "${IMAGE_NAME}:${IMAGE_TAG}" \
.
echo "============================================"
echo "✅ Multi-arch image built successfully!"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Note: Multi-arch images are in buildx cache."
echo "To push to registry:"
echo " PUSH=true ./build-multiarch.sh"
echo ""
echo "To load single arch locally:"
echo " docker buildx build --platform linux/amd64 -t ${IMAGE_NAME}:${IMAGE_TAG} --load ."
echo " or"
echo " docker buildx build --platform linux/arm64 -t ${IMAGE_NAME}:${IMAGE_TAG} --load ."
fi

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,108 @@
# Raspberry Pi 4 optimized values for K3s cluster
# Deploy with: helm install rdp-web-gateway ./chart/rdp-web-gateway -f examples/rpi4-k3s.yaml
# Use ClusterIP with Traefik (common on K3s)
service:
type: ClusterIP
port: 80
targetPort: 8080
# Traefik IngressRoute (K3s includes Traefik by default)
traefik:
enabled: true
host: rdp.yourdomain.com
entryPoints:
- websecure
tls:
enabled: true
certResolver: letsencrypt
# Reduced resources for Raspberry Pi 4
resources:
limits:
cpu: 500m # 0.5 CPU core
memory: 512Mi # 512MB RAM
requests:
cpu: 100m # 0.1 CPU core minimum
memory: 128Mi # 128MB RAM minimum
# Conservative autoscaling for RPi cluster
autoscaling:
enabled: true
minReplicas: 1 # Start with 1 pod
maxReplicas: 3 # Max 3 pods (adjust based on cluster size)
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
# Start with single replica
replicaCount: 1
# RDP Broker connection (internal ClusterIP)
config:
rdpBroker:
host: "rdpbroker"
port: 3389
server:
port: 8080
# Spread pods across nodes if you have multiple RPi
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- rdp-web-gateway
topologyKey: kubernetes.io/hostname
# Optimize for ARM64
podAnnotations:
cluster.autoscaler.kubernetes.io/safe-to-evict: "true"
# Security context
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1001
allowPrivilegeEscalation: false
podSecurityContext:
fsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
# Health checks with longer delays for slower RPi startup
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 45 # Increased from 30
periodSeconds: 15 # Increased from 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15 # Increased from 10
periodSeconds: 10 # Increased from 5
timeoutSeconds: 3
failureThreshold: 3
# Optional: Node selector for ARM64 nodes only
# nodeSelector:
# kubernetes.io/arch: arm64
# Optional: Tolerate RPi-specific taints
# tolerations:
# - key: "node.kubernetes.io/arm64"
# operator: "Exists"
# effect: "NoSchedule"

View File

@@ -0,0 +1,71 @@
# Example: Traefik with multiple middlewares and custom cert
# Deploy with: helm install rdp-web-gateway ./chart/rdp-web-gateway -f examples/traefik-advanced.yaml
service:
type: ClusterIP
port: 80
targetPort: 8080
traefik:
enabled: true
host: rdp.yourdomain.com
annotations:
# Optional annotations
kubernetes.io/ingress.class: traefik
entryPoints:
- web # HTTP (will redirect to HTTPS)
- websecure # HTTPS
middlewares:
# Redirect HTTP to HTTPS
- name: redirect-to-https
# Add security headers
- name: security-headers
# Rate limiting
- name: rate-limit
tls:
enabled: true
certResolver: letsencrypt
# Specify multiple domains/SANs
domains:
- main: rdp.yourdomain.com
sans:
- www.rdp.yourdomain.com
- rdp-gateway.yourdomain.com
config:
rdpBroker:
host: "rdpbroker"
port: 3389
server:
port: 8080
# Production resource limits
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 500m
memory: 512Mi
# Autoscaling for production
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 60
targetMemoryUtilizationPercentage: 70
# Pod anti-affinity for high availability
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- rdp-web-gateway
topologyKey: kubernetes.io/hostname

View File

@@ -0,0 +1,63 @@
# Example: Traefik with Let's Encrypt
# Deploy with: helm install rdp-web-gateway ./chart/rdp-web-gateway -f examples/traefik-letsencrypt.yaml
# Disable LoadBalancer, use IngressRoute instead
service:
type: ClusterIP
port: 80
targetPort: 8080
# Enable Traefik IngressRoute
traefik:
enabled: true
host: rdp.yourdomain.com
entryPoints:
- websecure # HTTPS entry point
tls:
enabled: true
certResolver: letsencrypt # Must match your Traefik certResolver name
# Optional: Add middlewares
# middlewares:
# - name: redirect-to-https
# - name: rate-limit
# RDP Broker connection (internal ClusterIP)
config:
rdpBroker:
host: "rdpbroker" # Kubernetes service name
port: 3389
server:
port: 8080
# Recommended: Enable network policies for security
networkPolicy:
enabled: true
policyTypes:
- Ingress
- Egress
ingress:
# Allow traffic from Traefik
- from:
- namespaceSelector:
matchLabels:
name: traefik # Adjust to your Traefik namespace
ports:
- protocol: TCP
port: 8080
egress:
# Allow traffic to RdpBroker
- to:
- podSelector:
matchLabels:
app: rdpbroker
ports:
- protocol: TCP
port: 3389
# Allow DNS resolution
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53

View File

@@ -0,0 +1,71 @@
# Recommended Traefik Middlewares for RDP Web Gateway
# Apply these in your Traefik namespace or the same namespace as web-gateway
---
# Redirect HTTP to HTTPS
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: redirect-to-https
spec:
redirectScheme:
scheme: https
permanent: true
---
# Security Headers
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: security-headers
spec:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
frameDeny: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
customResponseHeaders:
X-Forwarded-Proto: "https"
# Allow WebSocket upgrade
Connection: "upgrade"
Upgrade: "$http_upgrade"
---
# Rate Limiting (adjust as needed)
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: rate-limit
spec:
rateLimit:
average: 100
burst: 50
period: 1s
---
# IP Whitelist (optional - restrict to specific IPs/ranges)
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: ip-whitelist
spec:
ipWhiteList:
sourceRange:
- 192.168.1.0/24
- 10.0.0.0/8
# For use behind a proxy/load balancer
ipStrategy:
depth: 1
---
# Compression
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: compression
spec:
compress: {}

View File

@@ -0,0 +1,38 @@
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 }}
- Replicas: {{ if .Values.autoscaling.enabled }}{{ .Values.autoscaling.minReplicas }}-{{ .Values.autoscaling.maxReplicas }} (autoscaling){{ else }}{{ .Values.replicaCount }}{{ end }}
Note: Authentication is handled by RdpBroker. Logs are sent to stdout for Kubernetes.

View File

@@ -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,17 @@
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 }}
}
}

View File

@@ -0,0 +1,75 @@
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 }}
{{- 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 }}
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 }}
{{- 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,44 @@
{{- if .Values.traefik.enabled -}}
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
{{- with .Values.traefik.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
entryPoints:
{{- toYaml .Values.traefik.entryPoints | nindent 4 }}
routes:
- match: Host(`{{ .Values.traefik.host }}`)
kind: Rule
services:
- name: {{ include "rdp-web-gateway.fullname" . }}
port: {{ .Values.service.port }}
{{- if .Values.traefik.middlewares }}
middlewares:
{{- toYaml .Values.traefik.middlewares | nindent 6 }}
{{- end }}
{{- if .Values.traefik.tls.enabled }}
tls:
{{- if .Values.traefik.tls.certResolver }}
certResolver: {{ .Values.traefik.tls.certResolver }}
{{- end }}
{{- if .Values.traefik.tls.secretName }}
secretName: {{ .Values.traefik.tls.secretName }}
{{- end }}
{{- if .Values.traefik.tls.domains }}
domains:
{{- range .Values.traefik.tls.domains }}
- main: {{ .main }}
{{- if .sans }}
sans:
{{- toYaml .sans | nindent 10 }}
{{- end }}
{{- end }}
{{- 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,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,171 @@
# 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
# Traefik IngressRoute configuration (alternative to standard Ingress)
traefik:
enabled: false
annotations: {}
# Host for the IngressRoute
host: rdp.example.com
# Traefik entryPoints
entryPoints:
- websecure
# Optional middlewares
middlewares: []
# - name: redirect-to-https
# - name: rate-limit
# TLS configuration
tls:
enabled: true
# Use Let's Encrypt cert resolver
certResolver: letsencrypt
# Or use existing secret
secretName: ""
# Optional: Specify domains
domains: []
# - main: rdp.example.com
# sans:
# - www.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
# 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"
# 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

View File

@@ -0,0 +1,36 @@
{
"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",
"dotenv": "^16.3.1",
"compression": "^1.7.4",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"node-rdpjs-2": "^0.3.4",
"pngjs": "^7.0.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,463 @@
class RDPWebGateway {
constructor() {
this.ws = null;
this.canvas = null;
this.ctx = null;
this.currentUser = null;
this.currentTarget = null;
this.credentials = 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 {
// 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 and authenticate via WebSocket
this.currentUser = username;
this.credentials = { username, password };
// Authenticate and get user-specific targets from RdpBroker
await this.authenticateAndLoadTargets();
} catch (error) {
console.error('Login error:', error);
// Show specific error message if available
const errorMsg = error.message || 'Connection error. Please check your network and try again.';
this.showError(errorMessage, errorMsg);
loginBtn.disabled = false;
btnText.style.display = 'block';
spinner.style.display = 'none';
}
}
authenticateAndLoadTargets() {
return new Promise((resolve, reject) => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/rdp`;
// Create WebSocket connection for authentication
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
const timeout = setTimeout(() => {
if (this.ws) {
this.ws.close();
reject(new Error('Authentication timeout'));
}
}, 10000); // 10 second timeout
this.ws.onopen = () => {
console.log('WebSocket connected for authentication');
// Send authentication request to RdpBroker
this.ws.send(JSON.stringify({
type: 'authenticate',
username: this.credentials.username,
password: this.credentials.password
}));
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'targets') {
// Received user-specific targets from RdpBroker
clearTimeout(timeout);
console.log('Received targets from RdpBroker:', message.targets);
this.showTargetsView(message.targets);
// Reset login button
const loginBtn = document.getElementById('loginBtn');
const btnText = loginBtn.querySelector('.btn-text');
const spinner = loginBtn.querySelector('.spinner');
loginBtn.disabled = false;
btnText.style.display = 'block';
spinner.style.display = 'none';
resolve();
} else if (message.type === 'error') {
clearTimeout(timeout);
this.ws.close();
this.ws = null;
reject(new Error(message.error || 'Authentication failed'));
}
} catch (e) {
console.error('Error parsing WebSocket message:', e);
}
};
this.ws.onerror = (error) => {
clearTimeout(timeout);
console.error('WebSocket error:', error);
reject(new Error('WebSocket connection failed'));
};
this.ws.onclose = () => {
clearTimeout(timeout);
if (this.ws) {
console.log('WebSocket closed during authentication');
}
};
});
}
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 = '';
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) {
// WebSocket already connected from authentication
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket not connected');
this.showConnectionError('Connection lost. Please login again.');
return;
}
this.canvas = document.getElementById('rdpCanvas');
this.ctx = this.canvas.getContext('2d');
// Update message handler for RDP session
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();
};
// Send target selection to RdpBroker
console.log('Connecting to target:', target.name);
this.ws.send(JSON.stringify({
type: 'connect',
target: target
}));
// Setup canvas input handlers
this.setupCanvasInputHandlers();
}
handleWebSocketMessage(event) {
try {
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}`;
console.log('RDP connection established');
break;
case 'screen':
// Render screen update (PNG image data)
this.renderScreenUpdate(message);
break;
case 'disconnected':
this.showConnectionError('RDP connection closed');
break;
case 'error':
this.showConnectionError(message.error);
break;
default:
console.warn('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error handling WebSocket message:', error);
}
}
renderScreenUpdate(update) {
try {
// Decode base64 PNG image
const img = new Image();
img.onload = () => {
// Draw image to canvas at specified position
this.ctx.drawImage(img, update.x, update.y, update.width, update.height);
};
img.onerror = (error) => {
console.error('Failed to load screen update image:', error);
};
img.src = 'data:image/png;base64,' + update.data;
} catch (error) {
console.error('Error rendering screen update:', error);
}
}
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));
// Map button: 0=left, 1=middle, 2=right
let button = 0;
if (type === 'down' || type === 'up') {
button = event.button;
}
this.ws.send(JSON.stringify({
type: 'mouse',
action: type,
x: x,
y: y,
button: button
}));
}
sendKeyEvent(type, event) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({
type: 'keyboard',
action: type,
code: event.keyCode || event.which
}));
}
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.credentials = 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 = '';
}
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();
// v1.4

View File

@@ -0,0 +1,252 @@
const net = require('net');
class RDPProxyHandler {
constructor(websocket, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket;
this.rdpBrokerHost = rdpBrokerHost;
this.rdpBrokerPort = rdpBrokerPort;
this.rdpSocket = null;
this.isConnected = false;
this.isAuthenticated = false;
this.isRdpMode = false; // Track when in binary RDP forwarding mode
this.dataBuffer = '';
this.pendingTarget = null;
}
async handleMessage(message) {
try {
switch (message.type) {
case 'authenticate':
await this.handleAuthenticate(message);
break;
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:
console.warn(`${new Date().toISOString()} [WARN] Unknown message type: ${message.type}`);
}
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Error handling message:`, error);
this.sendError('Failed to process message');
}
}
async handleAuthenticate(message) {
const { username, password } = message;
console.log(`${new Date().toISOString()} [INFO] Authenticating user: ${username}`);
try {
// Connect to RDP Broker for authentication
this.rdpSocket = new net.Socket();
this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => {
console.log(`${new Date().toISOString()} [INFO] Connected to RDP Broker for authentication`);
this.isConnected = true;
// Send authentication request to RdpBroker
// Protocol: AUTH\nusername\npassword\n
const authMessage = `AUTH\n${username}\n${password}\n`;
this.rdpSocket.write(authMessage);
});
// Handle data from RDP Broker
this.rdpSocket.on('data', (data) => {
this.handleBrokerData(data);
});
this.rdpSocket.on('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', () => {
console.log(`${new Date().toISOString()} [INFO] RDP connection closed`);
this.isConnected = false;
if (!this.isAuthenticated && this.ws.readyState === 1) {
this.sendError('Authentication failed');
}
});
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Authentication error:`, error);
this.sendError('Failed to authenticate with RDP broker');
}
}
handleBrokerData(data) {
// If in RDP mode, forward binary data directly
if (this.isRdpMode) {
if (this.ws.readyState === 1) {
this.ws.send(data);
}
return;
}
// Otherwise, accumulate and parse JSON messages
this.dataBuffer += data.toString();
// Check if we have complete message (ends with \n\n or specific delimiter)
if (this.dataBuffer.includes('\n\n')) {
const messages = this.dataBuffer.split('\n\n');
this.dataBuffer = messages.pop(); // Keep incomplete part in buffer
messages.forEach(msg => {
if (msg.trim()) {
this.processBrokerMessage(msg.trim());
}
});
}
}
processBrokerMessage(message) {
try {
// Try to parse as JSON first (for structured messages)
const data = JSON.parse(message);
if (data.type === 'auth_success') {
// Authentication successful, targets list received
console.log(`${new Date().toISOString()} [INFO] Authentication successful`);
this.isAuthenticated = true;
// Send targets to client
this.ws.send(JSON.stringify({
type: 'targets',
targets: data.targets || []
}));
} else if (data.type === 'auth_failed') {
// Authentication failed
console.log(`${new Date().toISOString()} [WARN] Authentication failed: ${data.message}`);
this.sendError(data.message || 'Invalid credentials');
this.cleanup();
} else if (data.type === 'rdp_ready') {
// RDP session ready, start forwarding
console.log(`${new Date().toISOString()} [INFO] RDP session ready`);
this.ws.send(JSON.stringify({
type: 'connected',
target: this.pendingTarget
}));
this.pendingTarget = null;
this.isRdpMode = true; // Switch to binary RDP forwarding mode
this.dataBuffer = ''; // Clear JSON buffer
} else {
console.warn(`${new Date().toISOString()} [WARN] Unknown broker message type:`, data.type);
}
} catch (e) {
// Not JSON, might be raw RDP data - ignore during auth phase
if (this.isAuthenticated) {
console.debug(`${new Date().toISOString()} [DEBUG] Non-JSON data from broker (RDP traffic)`);
}
}
}
async handleConnect(message) {
const { target } = message;
if (!this.isAuthenticated) {
this.sendError('Must authenticate before connecting to target');
return;
}
console.log(`${new Date().toISOString()} [INFO] Connecting to target: ${target?.name || 'unknown'}`);
this.pendingTarget = target?.name || 'RDP Server';
try {
// Send target selection to RdpBroker
// Protocol: SELECT\ntarget_name\n
const selectMessage = `SELECT\n${target?.name}\n`;
this.rdpSocket.write(selectMessage);
// RdpBroker will respond with rdp_ready message
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error);
this.sendError('Failed to connect to target');
}
}
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');
console.log(`${new Date().toISOString()} [INFO] Sent Ctrl+Alt+Del`);
break;
default:
console.warn(`${new Date().toISOString()} [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;
}
}
module.exports = RDPProxyHandler;

View File

@@ -0,0 +1,355 @@
const net = require('net');
const rdp = require('node-rdpjs-2');
const { PNG } = require('pngjs');
/**
* Clean RDP Proxy Handler - v1.3
* Handles: Authentication with broker → Target selection → RDP connection with screen updates
*/
class RDPProxyHandler {
constructor(websocket, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket;
this.brokerHost = rdpBrokerHost;
this.brokerPort = rdpBrokerPort;
// State
this.state = 'INIT'; // INIT → AUTH → TARGETS → CONNECTED
this.username = null;
this.currentTarget = null;
// Promise callbacks for async operations
this.authResolve = null;
this.authReject = null;
// Broker connection (for AUTH/SELECT protocol)
this.brokerSocket = null;
this.brokerBuffer = '';
// RDP client (for actual RDP connection to target)
this.rdpClient = null;
this.rdpConnected = false;
}
/**
* Handle incoming WebSocket message from browser
*/
async handleMessage(message) {
try {
const msg = JSON.parse(message);
switch (msg.type) {
case 'authenticate':
await this.handleAuthenticate(msg.username, msg.password);
break;
case 'connect':
await this.handleConnectToTarget(msg.target);
break;
case 'mouse':
this.handleMouseInput(msg);
break;
case 'keyboard':
this.handleKeyboardInput(msg);
break;
default:
console.warn(`Unknown message type: ${msg.type}`);
}
} catch (error) {
console.error('Error handling message:', error);
this.sendError('Failed to process message');
}
}
/**
* Phase 1: Authenticate with RdpBroker
*/
async handleAuthenticate(username, password) {
console.log(`[AUTH] Authenticating user: ${username}`);
this.username = username;
this.state = 'AUTH';
return new Promise((resolve, reject) => {
this.authResolve = resolve;
this.authReject = reject;
this.brokerSocket = new net.Socket();
this.brokerSocket.connect(this.brokerPort, this.brokerHost, () => {
console.log('[AUTH] Connected to broker');
// Send AUTH protocol message
const authMsg = `AUTH\n${username}\n${password}\n`;
this.brokerSocket.write(authMsg);
});
this.brokerSocket.on('data', (data) => {
this.brokerBuffer += data.toString();
// Check for complete JSON message (ends with \n\n)
if (this.brokerBuffer.includes('\n\n')) {
const messages = this.brokerBuffer.split('\n\n');
this.brokerBuffer = messages.pop();
messages.forEach(msgText => {
if (msgText.trim()) {
try {
const msg = JSON.parse(msgText.trim());
this.handleBrokerMessage(msg);
} catch (e) {
console.error('Failed to parse broker message:', e);
}
}
});
}
});
this.brokerSocket.on('error', (error) => {
console.error('[AUTH] Broker connection error:', error);
this.sendError('Failed to connect to authentication server');
if (this.authReject) {
this.authReject(error);
this.authReject = null;
this.authResolve = null;
}
});
this.brokerSocket.on('close', () => {
console.log('[AUTH] Broker connection closed');
});
});
}
/**
* Handle messages from RdpBroker (auth results, target lists, etc)
*/
handleBrokerMessage(msg) {
console.log('[BROKER] Received:', msg.type);
switch (msg.type) {
case 'auth_success':
console.log(`[AUTH] Success - ${msg.targets.length} targets available`);
this.state = 'TARGETS';
this.sendToClient({
type: 'targets',
targets: msg.targets
});
// Resolve the authentication promise
if (this.authResolve) {
this.authResolve();
this.authResolve = null;
this.authReject = null;
}
break;
case 'auth_failed':
console.log('[AUTH] Failed:', msg.message);
this.sendError(msg.message || 'Authentication failed');
// Reject the authentication promise
if (this.authReject) {
this.authReject(new Error(msg.message));
this.authResolve = null;
this.authReject = null;
}
this.cleanup();
break;
case 'rdp_ready':
console.log('[BROKER] RDP session ready signal received');
// Broker has connected to target, but we won't use this connection
// We'll create our own RDP client connection
break;
default:
console.warn('[BROKER] Unknown message type:', msg.type);
}
}
/**
* Phase 2: Connect to selected target via RDP
*/
async handleConnectToTarget(target) {
if (this.state !== 'TARGETS') {
this.sendError('Must authenticate first');
return;
}
console.log(`[RDP] Connecting to target: ${target.name}`);
this.currentTarget = target;
// Send SELECT message to broker (to maintain session tracking)
const selectMsg = `SELECT\n${target.name}\n`;
if (this.brokerSocket && !this.brokerSocket.destroyed) {
this.brokerSocket.write(selectMsg);
}
// Create direct RDP connection to target
await this.connectRDP(target.host, target.port);
}
/**
* Create RDP client connection and handle screen updates
*/
async connectRDP(host, port) {
return new Promise((resolve, reject) => {
try {
console.log(`[RDP] Creating client for ${host}:${port}`);
this.rdpClient = rdp.createClient({
domain: '',
userName: this.username,
password: '', // Already authenticated via broker
enablePerf: true,
autoLogin: true,
screen: { width: 1024, height: 768 },
locale: 'en',
logLevel: 'INFO'
}).on('connect', () => {
console.log('[RDP] Connected');
this.rdpConnected = true;
this.state = 'CONNECTED';
this.sendToClient({
type: 'connected',
target: this.currentTarget
});
resolve();
}).on('bitmap', (bitmap) => {
// Received screen update from RDP server
this.handleScreenUpdate(bitmap);
}).on('close', () => {
console.log('[RDP] Connection closed');
this.rdpConnected = false;
this.sendToClient({ type: 'disconnected' });
}).on('error', (error) => {
console.error('[RDP] Error:', error);
this.sendError('RDP connection failed: ' + error.message);
reject(error);
});
// Connect to RDP server
this.rdpClient.connect(host, port);
} catch (error) {
console.error('[RDP] Failed to create client:', error);
this.sendError('Failed to initialize RDP client');
reject(error);
}
});
}
/**
* Handle screen bitmap updates from RDP server
*/
handleScreenUpdate(bitmap) {
try {
// Convert bitmap to PNG and send to browser
const png = new PNG({
width: bitmap.width,
height: bitmap.height
});
// Copy bitmap data (assuming RGBA format)
png.data = Buffer.from(bitmap.data);
const buffer = PNG.sync.write(png);
const base64Data = buffer.toString('base64');
this.sendToClient({
type: 'screen',
x: bitmap.destLeft || 0,
y: bitmap.destTop || 0,
width: bitmap.width,
height: bitmap.height,
data: base64Data
});
} catch (error) {
console.error('[RDP] Error processing screen update:', error);
}
}
/**
* Handle mouse input from browser
*/
handleMouseInput(msg) {
if (!this.rdpConnected || !this.rdpClient) return;
try {
this.rdpClient.sendPointerEvent(
msg.x,
msg.y,
msg.button || 0,
msg.action === 'down'
);
} catch (error) {
console.error('[RDP] Mouse input error:', error);
}
}
/**
* Handle keyboard input from browser
*/
handleKeyboardInput(msg) {
if (!this.rdpConnected || !this.rdpClient) return;
try {
this.rdpClient.sendKeyEventScancode(
msg.code,
msg.action === 'down'
);
} catch (error) {
console.error('[RDP] Keyboard input error:', error);
}
}
/**
* Send message to browser client
*/
sendToClient(message) {
if (this.ws.readyState === 1) {
this.ws.send(JSON.stringify(message));
}
}
/**
* Send error to browser client
*/
sendError(message) {
this.sendToClient({
type: 'error',
error: message
});
}
/**
* Cleanup all connections
*/
cleanup() {
console.log('[CLEANUP] Closing connections');
if (this.rdpClient) {
try {
this.rdpClient.close();
} catch (e) {
console.error('[CLEANUP] Error closing RDP client:', e);
}
this.rdpClient = null;
}
if (this.brokerSocket && !this.brokerSocket.destroyed) {
this.brokerSocket.destroy();
this.brokerSocket = null;
}
this.rdpConnected = false;
this.state = 'INIT';
}
}
module.exports = RDPProxyHandler;

View File

@@ -0,0 +1,208 @@
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 RDPProxyHandler = require('./rdpProxyHandler.new');
class RDPWebGatewayServer {
constructor() {
this.app = express();
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({
server: this.server,
path: '/ws/rdp'
});
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) => {
console.log(`${new Date().toISOString()} [INFO] ${req.method} ${req.url}`);
next();
});
}
setupRoutes() {
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
version: '1.0.0',
uptime: process.uptime()
});
});
// 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 targets list
this.app.get('/api/targets', async (req, res) => {
try {
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()
});
}
// Parse targets from environment variable
const targets = this.parseTargetsFromEnv();
res.json({ targets, timestamp: new Date().toISOString() });
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Error fetching targets:`, error);
res.status(500).json({
error: 'Failed to fetch targets',
timestamp: new Date().toISOString()
});
}
});
// API endpoints are removed - authentication handled by RdpBroker
// 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) => {
console.log(`${new Date().toISOString()} [INFO] New WebSocket connection`);
const proxyHandler = new RDPProxyHandler(
ws,
this.rdpBrokerHost,
this.rdpBrokerPort
);
ws.on('message', (data) => {
try {
// All messages are JSON (no binary mode)
proxyHandler.handleMessage(data.toString());
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] WebSocket message error:`, error);
ws.send(JSON.stringify({
type: 'error',
error: 'Invalid message format'
}));
}
});
ws.on('close', () => {
console.log(`${new Date().toISOString()} [INFO] WebSocket connection closed`);
proxyHandler.cleanup();
});
ws.on('error', (error) => {
console.error(`${new Date().toISOString()} [ERROR] WebSocket error:`, error);
proxyHandler.cleanup();
});
});
}
// 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: '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, () => {
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`);
});
}
}
// Start server
const server = new RDPWebGatewayServer();
server.start();
// Graceful shutdown
process.on('SIGTERM', () => {
console.log(`${new Date().toISOString()} [INFO] SIGTERM received, shutting down gracefully...`);
server.server.close(() => {
console.log(`${new Date().toISOString()} [INFO] Server closed`);
process.exit(0);
});
});
module.exports = RDPWebGatewayServer;

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Transfer Docker image to Raspberry Pi and load into K3s
set -e
IMAGE_NAME="${IMAGE_NAME:-easylinux/web-gateway}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
RPI_HOST="${RPI_HOST:-}"
RPI_USER="${RPI_USER:-pi}"
if [ -z "$RPI_HOST" ]; then
echo "Error: RPI_HOST not set"
echo "Usage: RPI_HOST=192.168.1.100 ./transfer-to-rpi.sh"
echo " or: RPI_HOST=rpi4.local RPI_USER=myuser ./transfer-to-rpi.sh"
exit 1
fi
echo "============================================"
echo "Transfer Docker Image to Raspberry Pi"
echo "============================================"
echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
echo "Target: ${RPI_USER}@${RPI_HOST}"
echo "============================================"
# Step 1: Build ARM64 image locally
echo "Step 1/4: Building ARM64 image..."
docker buildx build \
--platform linux/arm64 \
--tag "${IMAGE_NAME}:${IMAGE_TAG}" \
--load \
.
# Step 2: Save image to tar.gz
echo "Step 2/4: Saving image to tar.gz..."
docker save "${IMAGE_NAME}:${IMAGE_TAG}" | gzip > /tmp/web-gateway-arm64.tar.gz
echo "Saved to /tmp/web-gateway-arm64.tar.gz ($(du -h /tmp/web-gateway-arm64.tar.gz | cut -f1))"
# Step 3: Transfer to Raspberry Pi
echo "Step 3/4: Transferring to ${RPI_USER}@${RPI_HOST}..."
scp /tmp/web-gateway-arm64.tar.gz "${RPI_USER}@${RPI_HOST}:/tmp/"
# Step 4: Load image on Raspberry Pi
echo "Step 4/4: Loading image into K3s..."
ssh "${RPI_USER}@${RPI_HOST}" << EOF
echo "Loading image into Docker/K3s..."
gunzip -c /tmp/web-gateway-arm64.tar.gz | sudo k3s ctr images import -
# Alternative if using docker instead of containerd:
# gunzip -c /tmp/web-gateway-arm64.tar.gz | docker load
echo "Cleaning up..."
rm /tmp/web-gateway-arm64.tar.gz
echo "Verifying image..."
sudo k3s ctr images ls | grep web-gateway || echo "Image not found!"
EOF
# Cleanup local file
rm /tmp/web-gateway-arm64.tar.gz
echo "============================================"
echo "✅ Image transferred successfully!"
echo "============================================"
echo "Image is now available on ${RPI_HOST}"
echo ""
echo "Deploy with Helm:"
echo " helm install rdp-web-gateway ./chart/rdp-web-gateway \\"
echo " --namespace rdpbroker \\"
echo " --create-namespace \\"
echo " --set image.repository=${IMAGE_NAME} \\"
echo " --set image.tag=${IMAGE_TAG} \\"
echo " --set image.pullPolicy=IfNotPresent \\"
echo " -f chart/rdp-web-gateway/examples/rpi4-k3s.yaml"