Compare commits

6 Commits

Author SHA1 Message Date
Serge NOEL
8ff22dfc2c Test peus concluants 2025-12-05 16:33:03 +01:00
Serge NOEL
0d0d52c93c Ajout batch 2025-12-05 14:54:08 +01:00
Serge NOEL
d04d1748d3 Ajout architecture multi-arch 2025-12-04 11:08:55 +01:00
Serge NOEL
2080559f46 Interactive list 2025-12-04 09:39:42 +01:00
Serge NOEL
cfe610c75f Simplification Web-Gateway 2025-12-04 09:32:45 +01:00
Serge NOEL
66ccf7a20e Repo initialisation 2025-12-03 13:16:35 +01:00
63 changed files with 6887 additions and 0 deletions

40
.gitignore vendored Normal file
View File

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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 RdpBroker Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

336
PROTOCOL.md Normal file
View File

@@ -0,0 +1,336 @@
# RdpBroker Protocol Specification
## Overview
This document describes the protocol between web-gateway and RdpBroker for user authentication and target management.
## Connection Flow
```
Web-Gateway RdpBroker Samba AD / Target RDP Servers
| | |
|--AUTH\n{user}\n{pass}\n----->| |
| |---LDAP Auth----------------->|
| |<----Auth Result--------------|
| | |
|<---{type:targets,targets:[]}| |
| OR | |
|<---{type:auth_failed}--------| |
| | |
|--SELECT\n{target_name}\n---->| |
| |---Connect to Target--------->|
|<---{type:rdp_ready}----------|<-----------------------------|
| | |
|<====== RDP Binary Data ======|<===== RDP Session ==========|
```
## Protocol Messages
### Phase 1: Authentication
#### Request Format (Web-Gateway → RdpBroker)
```
AUTH\n
username\n
password\n
```
**Example:**
```
AUTH
user@example.com
SecurePassword123
```
#### Success Response (RdpBroker → Web-Gateway)
**JSON Format:**
```json
{
"type": "auth_success",
"targets": [
{
"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"
}
]
}
```
**Notes:**
- The message must end with `\n\n` (double newline) to signal end of JSON message
- Targets list is personalized based on user permissions/groups in Samba AD
- Empty array means user is authenticated but has no authorized targets
#### Failure Response (RdpBroker → Web-Gateway)
**JSON Format:**
```json
{
"type": "auth_failed",
"message": "Invalid credentials"
}
```
Followed by connection close.
**Possible error messages:**
- "Invalid credentials"
- "User account disabled"
- "LDAP connection failed"
- "User not authorized for any targets"
### Phase 2: Target Selection
#### Request Format (Web-Gateway → RdpBroker)
```
SELECT\n
target_name\n
```
**Example:**
```
SELECT
Windows Server 2022
```
**Notes:**
- Target name must match exactly one of the names from the targets list
- Connection should be rejected if target name is invalid or not in user's authorized list
#### Success Response (RdpBroker → Web-Gateway)
**JSON Format:**
```json
{
"type": "rdp_ready"
}
```
Followed by `\n\n`, then RDP binary data stream begins.
After this message, the connection transitions to raw RDP protocol forwarding mode.
#### Failure Response (RdpBroker → Web-Gateway)
**JSON Format:**
```json
{
"type": "error",
"message": "Target not available"
}
```
Followed by connection close.
**Possible error messages:**
- "Target not found"
- "Target not authorized for user"
- "Failed to connect to target server"
- "Target server unreachable"
### Phase 3: RDP Session
After `rdp_ready` message, all subsequent data is raw RDP protocol:
- Web-Gateway forwards mouse/keyboard events as RDP protocol data
- RdpBroker forwards screen updates as RDP protocol data
- Connection is bidirectional binary stream
## Implementation Guidelines for RdpBroker
### 1. Accept Connection
Listen on port 3389 (configurable via RDP_LISTEN_PORT)
### 2. Read Authentication Message
```c
char buffer[4096];
int bytes = read(client_fd, buffer, sizeof(buffer));
// Parse "AUTH\n{username}\n{password}\n"
char *lines[3];
int line_count = 0;
char *token = strtok(buffer, "\n");
while (token != NULL && line_count < 3) {
lines[line_count++] = token;
token = strtok(NULL, "\n");
}
if (strcmp(lines[0], "AUTH") != 0) {
send_error(client_fd, "Invalid protocol");
close(client_fd);
return;
}
char *username = lines[1];
char *password = lines[2];
```
### 3. Authenticate with Samba AD
```c
int auth_result = authenticate_user(username, password,
config->samba_server,
config->samba_port,
config->base_dn);
```
### 4. Get User's Authorized Targets
```c
rdp_target_t *user_targets[MAX_TARGETS];
int target_count = get_user_targets(username, config, user_targets);
```
**Recommended approach:**
- Query user's groups from LDAP
- Filter targets based on group membership or user attributes
- Return only targets the user is authorized to access
**Example YAML configuration:**
```yaml
targets:
- name: "Windows Server 2022"
host: "ws2022.example.com"
port: 3389
description: "Production Windows Server"
authorized_groups:
- "Domain Admins"
- "Server Operators"
- name: "Development Server"
host: "dev.example.com"
port: 3389
description: "Development environment"
authorized_groups:
- "Developers"
- "Domain Admins"
```
### 5. Send Targets List
```c
char json_response[8192];
snprintf(json_response, sizeof(json_response),
"{\"type\":\"auth_success\",\"targets\":[");
for (int i = 0; i < target_count; i++) {
char target_json[512];
snprintf(target_json, sizeof(target_json),
"%s{\"name\":\"%s\",\"host\":\"%s\",\"port\":%d,\"description\":\"%s\"}",
(i > 0 ? "," : ""),
user_targets[i]->name,
user_targets[i]->host,
user_targets[i]->port,
user_targets[i]->description);
strcat(json_response, target_json);
}
strcat(json_response, "]}\n\n");
write(client_fd, json_response, strlen(json_response));
```
### 6. Read Target Selection
```c
bytes = read(client_fd, buffer, sizeof(buffer));
// Parse "SELECT\n{target_name}\n"
if (strncmp(buffer, "SELECT\n", 7) != 0) {
send_error(client_fd, "Invalid protocol");
close(client_fd);
return;
}
char *target_name = buffer + 7;
char *newline = strchr(target_name, '\n');
if (newline) *newline = '\0';
// Verify target is in user's authorized list
rdp_target_t *selected_target = find_target_in_list(target_name,
user_targets,
target_count);
if (!selected_target) {
send_error(client_fd, "Target not authorized");
close(client_fd);
return;
}
```
### 7. Connect to Target and Start Forwarding
```c
int target_fd = connect_to_rdp_target(selected_target->host,
selected_target->port);
// Send ready message
char *ready_msg = "{\"type\":\"rdp_ready\"}\n\n";
write(client_fd, ready_msg, strlen(ready_msg));
// Start bidirectional forwarding
forward_rdp_connection(client_fd, target_fd);
```
## Security Considerations
1. **Always validate target selection** - User must be authorized for selected target
2. **Close on protocol errors** - Invalid messages should immediately close connection
3. **Timeout authentication** - Implement timeout for AUTH phase (e.g., 30 seconds)
4. **Rate limiting** - Prevent brute force attacks on authentication
5. **Logging** - Log all authentication attempts and target selections
6. **TLS/SSL** - Consider wrapping connection in TLS for production
## Testing
### Test Authentication Success
```bash
(echo -e "AUTH\nuser@example.com\nPassword123\n"; sleep 1) | nc rdpbroker 3389
```
Expected response:
```json
{"type":"auth_success","targets":[...]}
```
### Test Authentication Failure
```bash
(echo -e "AUTH\nuser@example.com\nWrongPassword\n"; sleep 1) | nc rdpbroker 3389
```
Expected response:
```json
{"type":"auth_failed","message":"Invalid credentials"}
```
### Test Target Selection
```bash
(echo -e "AUTH\nuser@example.com\nPassword123\n"; sleep 1; echo -e "SELECT\nWindows Server 2022\n"; sleep 1) | nc rdpbroker 3389
```
Expected response:
```json
{"type":"auth_success","targets":[...]}
{"type":"rdp_ready"}
[RDP binary data follows]
```
## Migration Notes
Existing RdpBroker implementations that present login/menu screens via RDP protocol will need to be refactored to:
1. Accept the new text-based protocol on initial connection
2. Parse AUTH and SELECT commands
3. Return JSON responses instead of RDP login screens
4. Only start RDP forwarding after receiving SELECT command
The advantage is that authentication and target selection now happen via structured protocol before RDP session starts, allowing:
- Better error handling in web UI
- User-specific target lists
- Cleaner separation of concerns
- Easier debugging and monitoring

69
QUICKSTART.md Normal file
View File

@@ -0,0 +1,69 @@
# Quick Start Guide
This guide will help you get RdpBroker running quickly.
## 1. Build the Image
```bash
cd src/
docker build -t rdpbroker:latest .
```
## 2. Configure
Create `my-values.yaml`:
```yaml
image:
repository: rdpbroker
tag: latest
config:
sambaAD:
server: "YOUR_AD_SERVER"
port: 389
baseDN: "DC=example,DC=com"
targets:
data: |
targets:
- name: "Test Server"
host: "192.168.1.10"
port: 3389
description: "Test RDP Server"
```
## 3. Deploy
```bash
# Create namespace
kubectl create namespace rdpbroker
# Install with Helm
helm install rdpbroker ./chart/rdpbroker -f my-values.yaml -n rdpbroker
# Get service IP
kubectl get svc rdpbroker -n rdpbroker
```
## 4. Connect
```bash
# Get the external IP
export RDP_IP=$(kubectl get svc rdpbroker -n rdpbroker -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
# Connect with RDP client
xfreerdp /v:$RDP_IP:3389 /u:yourusername
```
## 5. Monitor
```bash
# View logs
kubectl logs -f deployment/rdpbroker -n rdpbroker
# Check pods
kubectl get pods -n rdpbroker
```
For detailed instructions, see [docs/deployment.md](docs/deployment.md).

View File

@@ -0,0 +1,18 @@
apiVersion: v2
name: rdpbroker
description: A Helm chart for RDP Connection Broker with Samba AD Authentication
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- rdp
- broker
- gateway
- authentication
- samba
- active-directory
maintainers:
- name: RdpBroker Team
home: https://github.com/yourusername/rdpbroker
sources:
- https://github.com/yourusername/rdpbroker

View File

@@ -0,0 +1,31 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "rdpbroker.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "RDP Broker available at: $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 "rdpbroker.fullname" . }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "rdpbroker.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo "RDP Broker available at: $SERVICE_IP:{{ .Values.service.port }}"
echo "Connect using your RDP client: xfreerdp /v:$SERVICE_IP:{{ .Values.service.port }} /u:yourusername"
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "rdpbroker.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:{{ .Values.service.port }} to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME {{ .Values.service.port }}:{{ .Values.service.targetPort }}
{{- end }}
2. View logs:
kubectl logs -f deployment/{{ include "rdpbroker.fullname" . }} -n {{ .Release.Namespace }}
3. Monitor active sessions:
kubectl exec -it deployment/{{ include "rdpbroker.fullname" . }} -n {{ .Release.Namespace }} -- cat /var/log/rdpbroker/sessions.log
Configuration:
- Samba AD Server: {{ .Values.config.sambaAD.server }}:{{ .Values.config.sambaAD.port }}
- Base DN: {{ .Values.config.sambaAD.baseDN }}
- RDP Port: {{ .Values.config.rdp.listenPort }}
- Log Level: {{ .Values.config.logging.level }}

View File

@@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "rdpbroker.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "rdpbroker.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 "rdpbroker.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "rdpbroker.labels" -}}
helm.sh/chart: {{ include "rdpbroker.chart" . }}
{{ include "rdpbroker.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "rdpbroker.selectorLabels" -}}
app.kubernetes.io/name: {{ include "rdpbroker.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "rdpbroker.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "rdpbroker.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,11 @@
{{- if not .Values.targets.existingConfigMap }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "rdpbroker.fullname" . }}-targets
labels:
{{- include "rdpbroker.labels" . | nindent 4 }}
data:
targets.yaml: |
{{ .Values.targets.data | indent 4 }}
{{- end }}

View File

@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rdpbroker.fullname" . }}
labels:
{{- include "rdpbroker.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "rdpbroker.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "rdpbroker.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "rdpbroker.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: SAMBA_AD_SERVER
value: {{ .Values.config.sambaAD.server | quote }}
- name: SAMBA_AD_PORT
value: {{ .Values.config.sambaAD.port | quote }}
- name: SAMBA_AD_BASE_DN
value: {{ .Values.config.sambaAD.baseDN | quote }}
- name: RDP_LISTEN_PORT
value: {{ .Values.config.rdp.listenPort | quote }}
- name: TARGETS_CONFIG_PATH
value: "/etc/rdpbroker/targets.yaml"
- name: LOG_LEVEL
value: {{ .Values.config.logging.level | quote }}
ports:
- name: rdp
containerPort: {{ .Values.config.rdp.listenPort }}
protocol: TCP
volumeMounts:
- name: targets-config
mountPath: /etc/rdpbroker
readOnly: true
{{- if .Values.persistence.enabled }}
- name: logs
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: targets-config
configMap:
name: {{ .Values.targets.existingConfigMap | default (printf "%s-targets" (include "rdpbroker.fullname" .)) }}
{{- if .Values.persistence.enabled }}
- name: logs
persistentVolumeClaim:
claimName: {{ include "rdpbroker.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 "rdpbroker.fullname" . }}
labels:
{{- include "rdpbroker.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "rdpbroker.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,18 @@
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "rdpbroker.fullname" . }}
labels:
{{- include "rdpbroker.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "rdpbroker.selectorLabels" . | nindent 6 }}
policyTypes:
{{- toYaml .Values.networkPolicy.policyTypes | nindent 4 }}
{{- with .Values.networkPolicy.ingress }}
ingress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "rdpbroker.fullname" . }}-logs
labels:
{{- include "rdpbroker.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,22 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "rdpbroker.fullname" . }}
labels:
{{- include "rdpbroker.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: rdp
selector:
{{- include "rdpbroker.selectorLabels" . | nindent 4 }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

View File

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

125
chart/rdpbroker/values.yaml Normal file
View File

@@ -0,0 +1,125 @@
# Default values for rdpbroker
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: rdpbroker
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
service:
type: LoadBalancer
port: 3389
targetPort: 3389
annotations: {}
# For cloud providers, you can specify loadBalancerIP
# loadBalancerIP: "10.0.0.100"
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# RdpBroker specific configuration
config:
# Samba AD Configuration
sambaAD:
server: "ad.example.com"
port: 389
baseDN: "DC=example,DC=com"
# For secure LDAP, use port 636
# useTLS: true
# RDP Server Configuration
rdp:
listenPort: 3389
# Logging Configuration
logging:
level: "INFO" # DEBUG, INFO, WARN, ERROR
# RDP Targets Configuration
targets:
# This will be mounted as targets.yaml
# You can also use an existing ConfigMap by setting existingConfigMap
existingConfigMap: ""
# Define targets here or use existingConfigMap
data: |
targets:
- 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"
# Persistence for logs (optional)
persistence:
enabled: false
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
mountPath: /var/log/rdpbroker
# Network Policy (optional)
networkPolicy:
enabled: false
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: default
ports:
- protocol: TCP
port: 3389

21
docker-compose.yaml Normal file
View File

@@ -0,0 +1,21 @@
services:
rdp-broker:
image: easylinux/rdp-broker:1.0-3
container_name: rdp-broker
restart: unless-stopped
env_file:
- ./rdp-broker.env
volumes:
- ./targets.yaml:/etc/rdpbroker/targets.yaml:ro
ports:
- "3389:3389"
rdp-web-gateway:
image: easylinux/web-gateway:1.4
container_name: web-gateway
restart: unless-stopped
env_file:
- ./rdp-web-gateway.env
ports:
- "8085:8080"
depends_on:
- rdp-broker

458
docs/deployment.md Normal file
View File

@@ -0,0 +1,458 @@
# RdpBroker Deployment Guide
This document provides detailed instructions for deploying RdpBroker to a Kubernetes cluster.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Building the Container Image](#building-the-container-image)
- [Preparing the Environment](#preparing-the-environment)
- [Deploying with Helm](#deploying-with-helm)
- [Manual Deployment](#manual-deployment)
- [Configuration](#configuration)
- [Testing the Deployment](#testing-the-deployment)
- [Troubleshooting](#troubleshooting)
- [Upgrading](#upgrading)
- [Uninstalling](#uninstalling)
## Prerequisites
### Required Tools
- **kubectl** (1.20+) - Kubernetes command-line tool
- **helm** (3.x) - Kubernetes package manager
- **docker** - Container runtime for building images
- **Kubernetes cluster** (1.20+) - Running cluster with appropriate access
### Required Services
- **Samba Active Directory server** - Accessible from the Kubernetes cluster
- **RDP target machines** - Reachable from Kubernetes pods
- **Container registry** - For storing the RdpBroker image (Docker Hub, GCR, ECR, etc.)
## Building the Container Image
### 1. Build the Image
Navigate to the source directory and build the Docker image:
```bash
cd src/
docker build -t rdpbroker:1.0.0 .
```
### 2. Tag for Your Registry
Tag the image for your container registry:
```bash
# Docker Hub
docker tag rdpbroker:1.0.0 yourusername/rdpbroker:1.0.0
# Google Container Registry
docker tag rdpbroker:1.0.0 gcr.io/your-project/rdpbroker:1.0.0
# AWS ECR
docker tag rdpbroker:1.0.0 123456789012.dkr.ecr.us-east-1.amazonaws.com/rdpbroker:1.0.0
```
### 3. Push to Registry
```bash
# Docker Hub
docker push yourusername/rdpbroker:1.0.0
# Google Container Registry
docker push gcr.io/your-project/rdpbroker:1.0.0
# AWS ECR
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/rdpbroker:1.0.0
```
## Preparing the Environment
### 1. Create Namespace
```bash
kubectl create namespace rdpbroker
```
### 2. Configure Targets
Edit the `targets.yaml` file to define your RDP targets:
```yaml
targets:
- name: "Production Server"
host: "192.168.1.10"
port: 3389
description: "Production Environment"
- name: "Development Server"
host: "192.168.1.20"
port: 3389
description: "Development Environment"
```
### 3. Create ConfigMap (Optional)
If you prefer to manage targets separately:
```bash
kubectl create configmap rdpbroker-targets \
--from-file=targets.yaml=targets.yaml \
-n rdpbroker
```
## Deploying with Helm
### 1. Create Custom Values File
Create a file named `my-values.yaml`:
```yaml
image:
repository: yourusername/rdpbroker
tag: "1.0.0"
config:
sambaAD:
server: "ad.example.com"
port: 389
baseDN: "DC=example,DC=com"
rdp:
listenPort: 3389
logging:
level: "INFO"
service:
type: LoadBalancer
# Optional: specify a static IP
# loadBalancerIP: "10.0.0.100"
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# If you created a ConfigMap for targets
targets:
existingConfigMap: "rdpbroker-targets"
# Or define inline
# data: |
# targets:
# - name: "Server 01"
# host: "192.168.1.10"
# port: 3389
# description: "Production"
```
### 2. Install the Chart
```bash
helm install rdpbroker ./chart/rdpbroker \
-f my-values.yaml \
-n rdpbroker
```
### 3. Verify Installation
```bash
# Check pod status
kubectl get pods -n rdpbroker
# Check service
kubectl get svc -n rdpbroker
# View logs
kubectl logs -f deployment/rdpbroker -n rdpbroker
```
## Manual Deployment
If you prefer not to use Helm, you can deploy manually:
### 1. Create ConfigMap
```bash
kubectl create configmap rdpbroker-targets \
--from-file=targets.yaml=targets.yaml \
-n rdpbroker
```
### 2. Create Deployment
Create `deployment.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rdpbroker
namespace: rdpbroker
spec:
replicas: 1
selector:
matchLabels:
app: rdpbroker
template:
metadata:
labels:
app: rdpbroker
spec:
containers:
- name: rdpbroker
image: yourusername/rdpbroker:1.0.0
env:
- name: SAMBA_AD_SERVER
value: "ad.example.com"
- name: SAMBA_AD_PORT
value: "389"
- name: SAMBA_AD_BASE_DN
value: "DC=example,DC=com"
- name: RDP_LISTEN_PORT
value: "3389"
- name: TARGETS_CONFIG_PATH
value: "/etc/rdpbroker/targets.yaml"
- name: LOG_LEVEL
value: "INFO"
ports:
- containerPort: 3389
name: rdp
volumeMounts:
- name: targets-config
mountPath: /etc/rdpbroker
readOnly: true
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
volumes:
- name: targets-config
configMap:
name: rdpbroker-targets
```
### 3. Create Service
Create `service.yaml`:
```yaml
apiVersion: v1
kind: Service
metadata:
name: rdpbroker
namespace: rdpbroker
spec:
type: LoadBalancer
ports:
- port: 3389
targetPort: 3389
protocol: TCP
name: rdp
selector:
app: rdpbroker
```
### 4. Apply Manifests
```bash
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
```
## Configuration
### Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `SAMBA_AD_SERVER` | Samba AD server hostname/IP | Yes | - |
| `SAMBA_AD_PORT` | LDAP port | No | 389 |
| `SAMBA_AD_BASE_DN` | LDAP base DN | Yes | - |
| `RDP_LISTEN_PORT` | Port to listen for RDP | No | 3389 |
| `TARGETS_CONFIG_PATH` | Path to targets.yaml | No | /etc/rdpbroker/targets.yaml |
| `LOG_LEVEL` | Logging level | No | INFO |
### Network Considerations
1. **Firewall Rules**: Ensure Kubernetes nodes can reach:
- Samba AD server (port 389 or 636)
- RDP target machines (port 3389)
2. **Load Balancer**: Configure your cloud provider's load balancer for RDP traffic
3. **Network Policies**: If using network policies, allow:
- Ingress on port 3389
- Egress to Samba AD and RDP targets
## Testing the Deployment
### 1. Get Service IP
```bash
kubectl get svc rdpbroker -n rdpbroker
# Wait for EXTERNAL-IP
export RDP_BROKER_IP=$(kubectl get svc rdpbroker -n rdpbroker -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $RDP_BROKER_IP
```
### 2. Connect with RDP Client
#### Linux (xfreerdp)
```bash
xfreerdp /v:$RDP_BROKER_IP:3389 /u:yourusername
```
#### Windows
```
mstsc /v:$RDP_BROKER_IP:3389
```
#### macOS
Use Microsoft Remote Desktop from the App Store.
### 3. Test Authentication
1. Enter your Samba AD credentials
2. Verify you see the target list
3. Select a target and verify connection
### 4. Monitor Sessions
```bash
# View logs
kubectl logs -f deployment/rdpbroker -n rdpbroker
# Check active sessions
kubectl exec -it deployment/rdpbroker -n rdpbroker -- ps aux
```
## Troubleshooting
### Pod Not Starting
```bash
# Check pod status
kubectl describe pod -l app=rdpbroker -n rdpbroker
# View events
kubectl get events -n rdpbroker --sort-by='.lastTimestamp'
```
### Authentication Failures
1. Verify Samba AD connectivity:
```bash
kubectl exec -it deployment/rdpbroker -n rdpbroker -- nc -zv ad.example.com 389
```
2. Check credentials and base DN configuration
3. Review logs:
```bash
kubectl logs deployment/rdpbroker -n rdpbroker | grep -i auth
```
### Target Connection Issues
1. Test target reachability:
```bash
kubectl exec -it deployment/rdpbroker -n rdpbroker -- nc -zv 192.168.1.10 3389
```
2. Verify targets.yaml configuration:
```bash
kubectl get configmap rdpbroker-targets -n rdpbroker -o yaml
```
### Performance Issues
1. Check resource usage:
```bash
kubectl top pod -n rdpbroker
```
2. Adjust resources in values.yaml
3. Enable horizontal pod autoscaling
## Upgrading
### Using Helm
```bash
# Update image tag in values
helm upgrade rdpbroker ./chart/rdpbroker \
-f my-values.yaml \
-n rdpbroker
```
### Manual Upgrade
```bash
# Update image
kubectl set image deployment/rdpbroker \
rdpbroker=yourusername/rdpbroker:1.1.0 \
-n rdpbroker
# Monitor rollout
kubectl rollout status deployment/rdpbroker -n rdpbroker
```
## Uninstalling
### Using Helm
```bash
helm uninstall rdpbroker -n rdpbroker
```
### Manual Uninstall
```bash
kubectl delete deployment rdpbroker -n rdpbroker
kubectl delete service rdpbroker -n rdpbroker
kubectl delete configmap rdpbroker-targets -n rdpbroker
kubectl delete namespace rdpbroker
```
## Production Recommendations
1. **Security**:
- Use TLS/SSL for RDP connections
- Enable network policies
- Use secrets for sensitive configuration
- Run security scans on container images
2. **High Availability**:
- Enable horizontal pod autoscaling
- Use multiple replicas
- Configure pod disruption budgets
3. **Monitoring**:
- Set up Prometheus metrics
- Configure alerting
- Enable logging aggregation
4. **Backups**:
- Back up ConfigMaps and values files
- Document custom configurations
- Version control all manifests
5. **Compliance**:
- Enable audit logging
- Implement session recording
- Regular security audits

19
rdp-broker.env Normal file
View File

@@ -0,0 +1,19 @@
# RdpBroker Configuration
# Samba Active Directory Server
SAMBA_AD_SERVER=192.168.100.240
SAMBA_AD_BASE_DN=DC=aipice,DC=local
SAMBA_PORT=389
# LDAP Base DN for user authentication
# Example: DC=easylinux,DC=lan
BASE_DN=DC=aipice,DC=local
# RDP Listen Port
RDP_LISTEN_PORT=3389
# Targets configuration file
TARGETS_CONFIG=/etc/rdpbroker/targets.yaml
# Log Level: 0=ERROR, 1=WARN, 2=INFO, 3=DEBUG
LOG_LEVEL=2

13
rdp-web-gateway.env Normal file
View File

@@ -0,0 +1,13 @@
# Web Gateway Configuration
# Server Port
PORT=8080
NODE_ENV=development
# RdpBroker Connection
RDP_BROKER_HOST=rdp-broker
RDP_BROKER_PORT=3389
# Optional: Pre-configure RDP Targets (JSON array)
# Leave empty to get targets dynamically from RdpBroker
# RDP_TARGETS=[{"name":"Server1","host":"192.168.1.100","port":3389,"description":"Test Server"}]

11
src/.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
# .dockerignore
build/
bin/
*.o
*.so
*.a
.git/
.gitignore
README.md
docs/
chart/

50
src/Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
# Build stage
FROM alpine:3.22 AS builder
# Install build dependencies
RUN apk add --no-cache \
gcc \
musl-dev \
make \
openldap-dev \
yaml-dev
# Set working directory
WORKDIR /build
# Copy source files
COPY *.c *.h Makefile ./
# Build the application
RUN make deps-alpine && make
# Runtime stage
FROM alpine:3.22
# Install runtime dependencies
RUN apk add --no-cache \
libldap \
yaml \
ca-certificates
# Create app user
RUN addgroup -g 1000 rdpbroker && \
adduser -D -u 1000 -G rdpbroker rdpbroker
# Create necessary directories
RUN mkdir -p /etc/rdpbroker /var/log/rdpbroker && \
chown -R rdpbroker:rdpbroker /etc/rdpbroker /var/log/rdpbroker
# Copy binary from builder
COPY --from=builder /build/bin/rdpbroker /usr/local/bin/rdpbroker
# Set permissions
RUN chmod +x /usr/local/bin/rdpbroker
# Switch to non-root user
USER rdpbroker
# Expose RDP port
EXPOSE 3389
# Set entrypoint
ENTRYPOINT ["/usr/local/bin/rdpbroker"]

62
src/Makefile Normal file
View File

@@ -0,0 +1,62 @@
# RdpBroker Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2 -pthread -D_GNU_SOURCE
LDFLAGS = -pthread -lldap -llber -lyaml
# Directories
SRC_DIR = .
BUILD_DIR = build
BIN_DIR = bin
# Source files
SOURCES = main.c config.c auth.c rdp_server.c session_manager.c
OBJECTS = $(SOURCES:%.c=$(BUILD_DIR)/%.o)
TARGET = $(BIN_DIR)/rdpbroker
# Default target
all: directories $(TARGET)
# Create necessary directories
directories:
@mkdir -p $(BUILD_DIR)
@mkdir -p $(BIN_DIR)
# Link
$(TARGET): $(OBJECTS)
@echo "Linking $(TARGET)..."
$(CC) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
@echo "Build complete: $(TARGET)"
# Compile
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@echo "Compiling $<..."
$(CC) $(CFLAGS) -c $< -o $@
# Clean
clean:
@echo "Cleaning build files..."
rm -rf $(BUILD_DIR) $(BIN_DIR)
@echo "Clean complete"
# Install dependencies (Debian/Ubuntu)
deps-debian:
@echo "Installing dependencies for Debian/Ubuntu..."
apt-get update
apt-get install -y build-essential libldap2-dev libyaml-dev
# Install dependencies (Alpine - for Docker)
deps-alpine:
@echo "Installing dependencies for Alpine..."
apk add --no-cache gcc musl-dev make openldap-dev yaml-dev
# Run
run: $(TARGET)
@echo "Running RdpBroker..."
$(TARGET)
# Debug build
debug: CFLAGS += -g -DDEBUG
debug: clean all
.PHONY: all clean directories deps-debian deps-alpine run debug

109
src/auth.c Normal file
View File

@@ -0,0 +1,109 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ldap.h>
#include "rdp_broker.h"
int authenticate_user(const char *username, const char *password,
const char *samba_server, int samba_port,
const char *base_dn) {
LOG(LOG_DEBUG, "Authenticating user: %s", username);
if (!username || !password || strlen(password) == 0) {
LOG(LOG_WARN, "Empty username or password");
return -1;
}
/* Perform LDAP bind to validate credentials */
int result = ldap_bind_check(samba_server, samba_port, username,
password, base_dn);
if (result == 0) {
LOG(LOG_INFO, "Authentication successful for user: %s", username);
return 0;
} else {
LOG(LOG_WARN, "Authentication failed for user: %s", username);
return -1;
}
}
int ldap_bind_check(const char *server, int port, const char *username,
const char *password, const char *base_dn) {
LDAP *ld = NULL;
int rc;
char ldap_uri[512];
char bind_dn[512];
int version = LDAP_VERSION3;
/* Construct LDAP URI */
snprintf(ldap_uri, sizeof(ldap_uri), "ldap://%s:%d", server, port);
/* Initialize LDAP connection */
rc = ldap_initialize(&ld, ldap_uri);
if (rc != LDAP_SUCCESS) {
LOG(LOG_ERROR, "LDAP initialization failed: %s", ldap_err2string(rc));
return -1;
}
/* Set LDAP version */
rc = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version);
if (rc != LDAP_OPT_SUCCESS) {
LOG(LOG_ERROR, "Failed to set LDAP version: %s", ldap_err2string(rc));
ldap_unbind_ext_s(ld, NULL, NULL);
return -1;
}
/* Construct bind DN - typically cn=username,base_dn or username@domain */
/* For Samba AD, we can use username@domain format or userPrincipalName */
/* Here we'll try simple bind with CN format first */
snprintf(bind_dn, sizeof(bind_dn), "cn=%s,%s", username, base_dn);
/* Attempt to bind */
struct berval cred;
cred.bv_val = (char *)password;
cred.bv_len = strlen(password);
rc = ldap_sasl_bind_s(ld, bind_dn, LDAP_SASL_SIMPLE, &cred,
NULL, NULL, NULL);
if (rc != LDAP_SUCCESS) {
/* Try alternative format: username@domain */
/* Extract domain from base_dn (DC=example,DC=com -> example.com) */
char domain[256] = {0};
const char *dc = strstr(base_dn, "DC=");
if (dc) {
const char *ptr = dc + 3;
char *out = domain;
while (*ptr && (out - domain) < sizeof(domain) - 1) {
if (*ptr == ',') {
ptr++;
if (strncmp(ptr, "DC=", 3) == 0) {
*out++ = '.';
ptr += 3;
continue;
}
break;
}
*out++ = *ptr++;
}
*out = '\0';
}
if (strlen(domain) > 0) {
snprintf(bind_dn, sizeof(bind_dn), "%s@%s", username, domain);
rc = ldap_sasl_bind_s(ld, bind_dn, LDAP_SASL_SIMPLE, &cred,
NULL, NULL, NULL);
}
}
ldap_unbind_ext_s(ld, NULL, NULL);
if (rc == LDAP_SUCCESS) {
LOG(LOG_DEBUG, "LDAP bind successful for: %s", username);
return 0;
} else {
LOG(LOG_DEBUG, "LDAP bind failed: %s", ldap_err2string(rc));
return -1;
}
}

162
src/config.c Normal file
View File

@@ -0,0 +1,162 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <yaml.h>
#include "rdp_broker.h"
int load_config(broker_config_t *config) {
const char *env_value;
/* Initialize config with defaults */
memset(config, 0, sizeof(broker_config_t));
/* Load from environment variables */
env_value = getenv("SAMBA_AD_SERVER");
if (env_value) {
strncpy(config->samba_server, env_value, MAX_HOSTNAME_LEN - 1);
} else {
LOG(LOG_ERROR, "SAMBA_AD_SERVER environment variable not set");
return -1;
}
env_value = getenv("SAMBA_AD_PORT");
config->samba_port = env_value ? atoi(env_value) : 389;
env_value = getenv("SAMBA_AD_BASE_DN");
if (env_value) {
strncpy(config->base_dn, env_value, MAX_PATH_LEN - 1);
} else {
LOG(LOG_ERROR, "SAMBA_AD_BASE_DN environment variable not set");
return -1;
}
env_value = getenv("RDP_LISTEN_PORT");
config->rdp_listen_port = env_value ? atoi(env_value) : 3389;
env_value = getenv("TARGETS_CONFIG_PATH");
if (env_value) {
strncpy(config->targets_config_path, env_value, MAX_PATH_LEN - 1);
} else {
strncpy(config->targets_config_path, "/etc/rdpbroker/targets.yaml",
MAX_PATH_LEN - 1);
}
env_value = getenv("LOG_LEVEL");
if (env_value) {
if (strcmp(env_value, "DEBUG") == 0) {
config->log_level = LOG_DEBUG;
} else if (strcmp(env_value, "INFO") == 0) {
config->log_level = LOG_INFO;
} else if (strcmp(env_value, "WARN") == 0) {
config->log_level = LOG_WARN;
} else if (strcmp(env_value, "ERROR") == 0) {
config->log_level = LOG_ERROR;
} else {
config->log_level = LOG_INFO;
}
} else {
config->log_level = LOG_INFO;
}
global_log_level = config->log_level;
/* Load targets configuration */
return load_targets(config, config->targets_config_path);
}
int load_targets(broker_config_t *config, const char *path) {
FILE *file;
yaml_parser_t parser;
yaml_event_t event;
int done = 0;
int in_targets = 0;
int in_target = 0;
char key[256] = {0};
rdp_target_t current_target;
memset(&current_target, 0, sizeof(rdp_target_t));
file = fopen(path, "r");
if (!file) {
LOG(LOG_ERROR, "Failed to open targets file: %s", path);
return -1;
}
if (!yaml_parser_initialize(&parser)) {
LOG(LOG_ERROR, "Failed to initialize YAML parser");
fclose(file);
return -1;
}
yaml_parser_set_input_file(&parser, file);
config->target_count = 0;
/* Simple YAML parsing - this is a basic implementation */
/* In production, use a more robust YAML library */
while (!done) {
if (!yaml_parser_parse(&parser, &event)) {
LOG(LOG_ERROR, "YAML parser error");
break;
}
switch (event.type) {
case YAML_SCALAR_EVENT:
if (strcmp((char *)event.data.scalar.value, "targets") == 0) {
in_targets = 1;
} else if (in_targets && strcmp(key, "name") == 0) {
strncpy(current_target.name,
(char *)event.data.scalar.value,
MAX_HOSTNAME_LEN - 1);
key[0] = '\0';
} else if (in_targets && strcmp(key, "host") == 0) {
strncpy(current_target.host,
(char *)event.data.scalar.value,
MAX_HOSTNAME_LEN - 1);
key[0] = '\0';
} else if (in_targets && strcmp(key, "port") == 0) {
current_target.port = atoi((char *)event.data.scalar.value);
key[0] = '\0';
} else if (in_targets && strcmp(key, "description") == 0) {
strncpy(current_target.description,
(char *)event.data.scalar.value,
MAX_DESCRIPTION_LEN - 1);
key[0] = '\0';
/* Target is complete, add it */
if (config->target_count < MAX_TARGETS) {
memcpy(&config->targets[config->target_count],
&current_target,
sizeof(rdp_target_t));
config->target_count++;
LOG(LOG_DEBUG, "Loaded target: %s (%s:%d)",
current_target.name, current_target.host,
current_target.port);
}
memset(&current_target, 0, sizeof(rdp_target_t));
} else if (in_targets) {
strncpy(key, (char *)event.data.scalar.value, sizeof(key) - 1);
}
break;
case YAML_STREAM_END_EVENT:
done = 1;
break;
default:
break;
}
yaml_event_delete(&event);
}
yaml_parser_delete(&parser);
fclose(file);
LOG(LOG_INFO, "Loaded %d targets from %s", config->target_count, path);
return 0;
}
void free_config(broker_config_t *config) {
/* Nothing to free for now, but placeholder for future use */
(void)config;
}

84
src/main.c Normal file
View File

@@ -0,0 +1,84 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include "rdp_broker.h"
int global_log_level = LOG_INFO;
volatile bool running = true; /* Global running flag */
void signal_handler(int signum) {
if (signum == SIGINT || signum == SIGTERM) {
LOG(LOG_INFO, "Received signal %d, shutting down...", signum);
running = false;
}
}
void print_banner(void) {
printf("\n");
printf("╔═══════════════════════════════════════════════════════╗\n");
printf("║ RDP Broker v1.0.0 ║\n");
printf("║ RDP Connection Broker with Samba AD Auth ║\n");
printf("╚═══════════════════════════════════════════════════════╝\n");
printf("\n");
}
int main(int argc, char *argv[]) {
broker_config_t config;
session_manager_t session_manager;
int ret;
print_banner();
/* Install signal handlers */
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGPIPE, SIG_IGN); /* Ignore broken pipe signals */
/* Load configuration */
LOG(LOG_INFO, "Loading configuration...");
ret = load_config(&config);
if (ret != 0) {
LOG(LOG_ERROR, "Failed to load configuration");
return 1;
}
LOG(LOG_INFO, "Configuration loaded successfully");
LOG(LOG_INFO, "Samba AD Server: %s:%d", config.samba_server, config.samba_port);
LOG(LOG_INFO, "RDP Listen Port: %d", config.rdp_listen_port);
LOG(LOG_INFO, "Loaded %d target(s)", config.target_count);
/* Initialize session manager */
LOG(LOG_INFO, "Initializing session manager...");
ret = init_session_manager(&session_manager);
if (ret != 0) {
LOG(LOG_ERROR, "Failed to initialize session manager");
free_config(&config);
return 1;
}
/* Start RDP server */
LOG(LOG_INFO, "Starting RDP server on port %d...", config.rdp_listen_port);
ret = start_rdp_server(&config, &session_manager);
if (ret != 0) {
LOG(LOG_ERROR, "Failed to start RDP server");
free_config(&config);
return 1;
}
/* Main loop - monitor sessions */
LOG(LOG_INFO, "RDP Broker is running. Press Ctrl+C to stop.");
while (running) {
sleep(30); /* Log every 30 seconds */
log_active_sessions(&session_manager);
cleanup_inactive_sessions(&session_manager, 3600); /* 1 hour timeout */
}
/* Cleanup */
LOG(LOG_INFO, "Cleaning up...");
free_config(&config);
LOG(LOG_INFO, "RDP Broker stopped");
return 0;
}

114
src/rdp_broker.h Normal file
View File

@@ -0,0 +1,114 @@
#ifndef RDP_BROKER_H
#define RDP_BROKER_H
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
/* Global running flag for graceful shutdown */
extern volatile bool running;
#define MAX_TARGETS 100
#define MAX_SESSIONS 1000
#define MAX_USERNAME_LEN 256
#define MAX_HOSTNAME_LEN 256
#define MAX_DESCRIPTION_LEN 512
#define MAX_PATH_LEN 1024
/* Configuration structures */
typedef struct {
char name[MAX_HOSTNAME_LEN];
char host[MAX_HOSTNAME_LEN];
int port;
char description[MAX_DESCRIPTION_LEN];
} rdp_target_t;
typedef struct {
char samba_server[MAX_HOSTNAME_LEN];
int samba_port;
char base_dn[MAX_PATH_LEN];
int rdp_listen_port;
char targets_config_path[MAX_PATH_LEN];
int log_level;
rdp_target_t targets[MAX_TARGETS];
int target_count;
} broker_config_t;
/* Session management structures */
typedef struct {
uint32_t session_id;
char username[MAX_USERNAME_LEN];
char client_ip[64];
char target_host[MAX_HOSTNAME_LEN];
int target_port;
time_t start_time;
time_t last_activity;
bool active;
int client_fd;
int target_fd;
} session_info_t;
typedef struct {
session_info_t sessions[MAX_SESSIONS];
int session_count;
uint32_t next_session_id;
} session_manager_t;
/* Function prototypes */
/* config.c */
int load_config(broker_config_t *config);
int load_targets(broker_config_t *config, const char *path);
void free_config(broker_config_t *config);
/* auth.c */
int authenticate_user(const char *username, const char *password,
const char *samba_server, int samba_port,
const char *base_dn);
int ldap_bind_check(const char *server, int port, const char *username,
const char *password, const char *base_dn);
/* rdp_server.c */
int start_rdp_server(broker_config_t *config, session_manager_t *sm);
int handle_rdp_connection(int client_fd, broker_config_t *config,
session_manager_t *sm);
int handle_auth_protocol(int client_fd, char *initial_buffer, int initial_len,
broker_config_t *config, session_manager_t *sm,
const char *client_ip);
void send_json_error(int fd, const char *message);
int send_targets_json(int fd, broker_config_t *config, const char *username);
int present_login_screen(int client_fd, char *username, char *password);
int present_target_menu(int client_fd, broker_config_t *config,
rdp_target_t **selected_target);
int forward_rdp_connection(int client_fd, const char *target_host,
int target_port, session_info_t *session);
/* session_manager.c */
int init_session_manager(session_manager_t *sm);
session_info_t* create_session(session_manager_t *sm, const char *username,
const char *client_ip, rdp_target_t *target,
int client_fd);
int update_session_activity(session_info_t *session);
int close_session(session_manager_t *sm, uint32_t session_id);
void log_active_sessions(session_manager_t *sm);
void cleanup_inactive_sessions(session_manager_t *sm, int timeout_seconds);
/* Logging macros */
typedef enum {
LOG_DEBUG = 0,
LOG_INFO = 1,
LOG_WARN = 2,
LOG_ERROR = 3
} log_level_t;
extern int global_log_level;
#define LOG(level, ...) do { \
if (level >= global_log_level) { \
fprintf(stderr, "[%s] ", #level); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
} \
} while(0)
#endif /* RDP_BROKER_H */

564
src/rdp_server.c Normal file
View File

@@ -0,0 +1,564 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include "rdp_broker.h"
typedef struct {
int client_fd;
broker_config_t *config;
session_manager_t *session_mgr;
} connection_handler_args_t;
void *handle_connection_thread(void *arg);
int create_listening_socket(int port);
int start_rdp_server(broker_config_t *config, session_manager_t *sm) {
int server_fd;
int client_fd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
pthread_t thread_id;
struct timeval timeout;
fd_set read_fds;
/* Create listening socket */
server_fd = create_listening_socket(config->rdp_listen_port);
if (server_fd < 0) {
LOG(LOG_ERROR, "Failed to create listening socket");
return -1;
}
LOG(LOG_INFO, "RDP server listening on port %d", config->rdp_listen_port);
/* Accept connections in a loop */
while (running) {
/* Use select with timeout to check running flag periodically */
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
timeout.tv_sec = 1; /* 1 second timeout */
timeout.tv_usec = 0;
int ready = select(server_fd + 1, &read_fds, NULL, NULL, &timeout);
if (ready < 0) {
if (!running) break; /* Shutting down */
LOG(LOG_ERROR, "Select failed: %s", strerror(errno));
continue;
}
if (ready == 0) {
/* Timeout - check running flag and continue */
continue;
}
/* Socket is ready for accept */
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
if (!running) break; /* Shutting down */
LOG(LOG_ERROR, "Accept failed: %s", strerror(errno));
continue;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
LOG(LOG_INFO, "New connection from %s:%d", client_ip,
ntohs(client_addr.sin_port));
/* Create thread to handle connection */
connection_handler_args_t *args = malloc(sizeof(connection_handler_args_t));
if (!args) {
LOG(LOG_ERROR, "Failed to allocate memory for connection handler");
close(client_fd);
continue;
}
args->client_fd = client_fd;
args->config = config;
args->session_mgr = sm;
if (pthread_create(&thread_id, NULL, handle_connection_thread, args) != 0) {
LOG(LOG_ERROR, "Failed to create thread: %s", strerror(errno));
free(args);
close(client_fd);
continue;
}
/* Detach thread so it cleans up automatically */
pthread_detach(thread_id);
}
/* Clean shutdown */
LOG(LOG_INFO, "Closing server socket...");
close(server_fd);
return 0;
}
void *handle_connection_thread(void *arg) {
connection_handler_args_t *args = (connection_handler_args_t *)arg;
int ret;
LOG(LOG_DEBUG, "Handling connection on fd %d", args->client_fd);
ret = handle_rdp_connection(args->client_fd, args->config, args->session_mgr);
if (ret != 0) {
LOG(LOG_WARN, "Connection handler failed");
}
close(args->client_fd);
free(args);
return NULL;
}
int create_listening_socket(int port) {
int server_fd;
struct sockaddr_in server_addr;
int opt = 1;
/* Create socket */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
LOG(LOG_ERROR, "Socket creation failed: %s", strerror(errno));
return -1;
}
/* Set socket options */
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
LOG(LOG_ERROR, "Setsockopt failed: %s", strerror(errno));
close(server_fd);
return -1;
}
/* Bind socket */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
LOG(LOG_ERROR, "Bind failed: %s", strerror(errno));
close(server_fd);
return -1;
}
/* Listen */
if (listen(server_fd, 10) < 0) {
LOG(LOG_ERROR, "Listen failed: %s", strerror(errno));
close(server_fd);
return -1;
}
return server_fd;
}
/* Handle AUTH/SELECT protocol from web-gateway */
int handle_auth_protocol(int client_fd, char *initial_buffer, int initial_len,
broker_config_t *config, session_manager_t *sm,
const char *client_ip) {
char username[MAX_USERNAME_LEN] = {0};
char password[MAX_USERNAME_LEN] = {0};
char target_name[MAX_HOSTNAME_LEN] = {0};
char buffer[4096];
rdp_target_t *selected_target = NULL;
session_info_t *session = NULL;
int ret;
/* Parse AUTH\nusername\npassword\n from initial_buffer */
char *line = initial_buffer;
char *lines[3];
int line_count = 0;
char *token = strtok(line, "\n");
while (token != NULL && line_count < 3) {
lines[line_count++] = token;
token = strtok(NULL, "\n");
}
if (line_count < 3 || strcmp(lines[0], "AUTH") != 0) {
LOG(LOG_ERROR, "Invalid AUTH protocol format");
send_json_error(client_fd, "Invalid protocol format");
return -1;
}
strncpy(username, lines[1], MAX_USERNAME_LEN - 1);
strncpy(password, lines[2], MAX_USERNAME_LEN - 1);
/* Authenticate user */
LOG(LOG_INFO, "Authenticating user: %s from %s", username, client_ip);
if (authenticate_user(username, password, config->samba_server,
config->samba_port, config->base_dn) != 0) {
LOG(LOG_WARN, "Authentication failed for user: %s", username);
send_json_error(client_fd, "Invalid credentials");
return -1;
}
LOG(LOG_INFO, "User %s authenticated successfully", username);
/* Send targets list as JSON */
if (send_targets_json(client_fd, config, username) != 0) {
LOG(LOG_ERROR, "Failed to send targets list");
return -1;
}
/* Wait for SELECT message */
memset(buffer, 0, sizeof(buffer));
ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (ret <= 0) {
LOG(LOG_ERROR, "Failed to receive SELECT message");
return -1;
}
/* Parse SELECT\ntarget_name\n */
line_count = 0;
line = buffer;
token = strtok(line, "\n");
while (token != NULL && line_count < 2) {
lines[line_count++] = token;
token = strtok(NULL, "\n");
}
if (line_count < 2 || strcmp(lines[0], "SELECT") != 0) {
LOG(LOG_ERROR, "Invalid SELECT protocol format");
send_json_error(client_fd, "Invalid protocol format");
return -1;
}
strncpy(target_name, lines[1], MAX_HOSTNAME_LEN - 1);
/* Find target by name */
for (int i = 0; i < config->target_count; i++) {
if (strcmp(config->targets[i].name, target_name) == 0) {
selected_target = &config->targets[i];
break;
}
}
if (!selected_target) {
LOG(LOG_ERROR, "Target not found: %s", target_name);
send_json_error(client_fd, "Target not found");
return -1;
}
LOG(LOG_INFO, "User %s selected target: %s (%s:%d)",
username, selected_target->name, selected_target->host, selected_target->port);
/* Send RDP ready message */
const char *ready_msg = "{\"type\":\"rdp_ready\"}\n\n";
send(client_fd, ready_msg, strlen(ready_msg), 0);
/* Create session */
session = create_session(sm, username, client_ip, selected_target, client_fd);
if (!session) {
LOG(LOG_ERROR, "Failed to create session");
return -1;
}
/* Connect to target RDP server and forward traffic */
LOG(LOG_INFO, "Connecting to target RDP server %s:%d",
selected_target->host, selected_target->port);
ret = forward_rdp_connection(client_fd, selected_target->host,
selected_target->port, session);
/* Cleanup session */
close_session(sm, session->session_id);
return ret;
}
/* Send JSON error message */
void send_json_error(int fd, const char *message) {
char buffer[512];
snprintf(buffer, sizeof(buffer),
"{\"type\":\"auth_failed\",\"message\":\"%s\"}\n\n", message);
send(fd, buffer, strlen(buffer), 0);
}
/* Send targets list as JSON */
int send_targets_json(int fd, broker_config_t *config, const char *username) {
char buffer[8192];
int offset = 0;
/* Start JSON */
offset += snprintf(buffer + offset, sizeof(buffer) - offset,
"{\"type\":\"auth_success\",\"targets\":[");
/* Add targets - for now, all targets (TODO: filter by user groups) */
for (int i = 0; i < config->target_count; i++) {
if (i > 0) {
offset += snprintf(buffer + offset, sizeof(buffer) - offset, ",");
}
offset += snprintf(buffer + offset, sizeof(buffer) - offset,
"{\"name\":\"%s\",\"host\":\"%s\",\"port\":%d,\"description\":\"%s\"}",
config->targets[i].name,
config->targets[i].host,
config->targets[i].port,
config->targets[i].description);
}
/* End JSON */
offset += snprintf(buffer + offset, sizeof(buffer) - offset, "]}\n\n");
LOG(LOG_DEBUG, "Sending %d targets to user %s", config->target_count, username);
return send(fd, buffer, strlen(buffer), 0) > 0 ? 0 : -1;
}
int handle_rdp_connection(int client_fd, broker_config_t *config,
session_manager_t *sm) {
char buffer[4096];
char username[MAX_USERNAME_LEN] = {0};
char password[MAX_USERNAME_LEN] = {0};
rdp_target_t *selected_target = NULL;
session_info_t *session = NULL;
struct sockaddr_in addr;
socklen_t addr_len = sizeof(addr);
char client_ip[INET_ADDRSTRLEN] = {0};
int ret;
/* Get client IP */
if (getpeername(client_fd, (struct sockaddr *)&addr, &addr_len) == 0) {
inet_ntop(AF_INET, &addr.sin_addr, client_ip, INET_ADDRSTRLEN);
}
/* Read first message to detect protocol type */
memset(buffer, 0, sizeof(buffer));
ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (ret <= 0) {
LOG(LOG_ERROR, "Failed to receive initial message");
return -1;
}
/* Check if this is AUTH protocol (web-gateway) */
if (strncmp(buffer, "AUTH\n", 5) == 0) {
LOG(LOG_INFO, "Detected AUTH protocol from web-gateway");
return handle_auth_protocol(client_fd, buffer, ret, config, sm, client_ip);
} else {
/* Legacy RDP protocol - not supported anymore */
LOG(LOG_WARN, "Legacy RDP protocol not supported, use web-gateway");
const char *msg = "ERROR: Please use web-gateway for access\n";
send(client_fd, msg, strlen(msg), 0);
return -1;
}
}
int present_login_screen(int client_fd, char *username, char *password) {
/* This is a simplified implementation */
/* In a real implementation, this would use RDP protocol to present a login UI */
/* For now, we'll use a simple text-based protocol */
const char *prompt_user = "RDP Broker Login\nUsername: ";
const char *prompt_pass = "Password: ";
char buffer[1024];
int n;
/* Send username prompt */
if (send(client_fd, prompt_user, strlen(prompt_user), 0) < 0) {
return -1;
}
/* Receive username */
n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
return -1;
}
buffer[n] = '\0';
/* Remove newline */
char *newline = strchr(buffer, '\n');
if (newline) *newline = '\0';
newline = strchr(buffer, '\r');
if (newline) *newline = '\0';
strncpy(username, buffer, MAX_USERNAME_LEN - 1);
/* Send password prompt */
if (send(client_fd, prompt_pass, strlen(prompt_pass), 0) < 0) {
return -1;
}
/* Receive password */
n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
return -1;
}
buffer[n] = '\0';
/* Remove newline */
newline = strchr(buffer, '\n');
if (newline) *newline = '\0';
newline = strchr(buffer, '\r');
if (newline) *newline = '\0';
strncpy(password, buffer, MAX_USERNAME_LEN - 1);
return 0;
}
int present_target_menu(int client_fd, broker_config_t *config,
rdp_target_t **selected_target) {
char buffer[4096];
char input[256];
int n, selection;
/* Build menu */
snprintf(buffer, sizeof(buffer), "\nAvailable RDP Targets:\n");
for (int i = 0; i < config->target_count; i++) {
char line[512];
snprintf(line, sizeof(line), "%d. %s - %s (%s:%d)\n",
i + 1,
config->targets[i].name,
config->targets[i].description,
config->targets[i].host,
config->targets[i].port);
strncat(buffer, line, sizeof(buffer) - strlen(buffer) - 1);
}
strncat(buffer, "\nSelect target (1-", sizeof(buffer) - strlen(buffer) - 1);
char num[16];
snprintf(num, sizeof(num), "%d): ", config->target_count);
strncat(buffer, num, sizeof(buffer) - strlen(buffer) - 1);
/* Send menu */
if (send(client_fd, buffer, strlen(buffer), 0) < 0) {
return -1;
}
/* Receive selection */
n = recv(client_fd, input, sizeof(input) - 1, 0);
if (n <= 0) {
return -1;
}
input[n] = '\0';
selection = atoi(input);
if (selection < 1 || selection > config->target_count) {
const char *msg = "Invalid selection\n";
send(client_fd, msg, strlen(msg), 0);
return -1;
}
*selected_target = &config->targets[selection - 1];
return 0;
}
int forward_rdp_connection(int client_fd, const char *target_host,
int target_port, session_info_t *session) {
int target_fd;
struct sockaddr_in target_addr;
fd_set read_fds;
int max_fd;
char buffer[8192];
int n;
/* Connect to target */
target_fd = socket(AF_INET, SOCK_STREAM, 0);
if (target_fd < 0) {
LOG(LOG_ERROR, "Failed to create target socket: %s", strerror(errno));
return -1;
}
memset(&target_addr, 0, sizeof(target_addr));
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(target_port);
if (inet_pton(AF_INET, target_host, &target_addr.sin_addr) <= 0) {
LOG(LOG_ERROR, "Invalid target address: %s", target_host);
close(target_fd);
return -1;
}
if (connect(target_fd, (struct sockaddr *)&target_addr,
sizeof(target_addr)) < 0) {
LOG(LOG_ERROR, "Failed to connect to target %s:%d: %s",
target_host, target_port, strerror(errno));
close(target_fd);
return -1;
}
LOG(LOG_INFO, "Connected to target %s:%d", target_host, target_port);
session->target_fd = target_fd;
/* Forward data bidirectionally - no messages sent, just raw RDP forwarding */
max_fd = (client_fd > target_fd) ? client_fd : target_fd;
LOG(LOG_INFO, "Starting bidirectional forwarding loop (client_fd=%d, target_fd=%d)",
client_fd, target_fd);
while (running) {
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
FD_ZERO(&read_fds);
FD_SET(client_fd, &read_fds);
FD_SET(target_fd, &read_fds);
int sel_ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (sel_ret < 0) {
LOG(LOG_ERROR, "Select failed: %s", strerror(errno));
break;
}
if (sel_ret == 0) {
/* Timeout - continue loop to check running flag */
continue;
}
/* Data from client to target */
if (FD_ISSET(client_fd, &read_fds)) {
n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n < 0) {
LOG(LOG_ERROR, "Error receiving from client: %s", strerror(errno));
break;
}
if (n == 0) {
LOG(LOG_INFO, "Client connection closed gracefully");
break;
}
LOG(LOG_DEBUG, "Forwarding %d bytes from client to target", n);
if (send(target_fd, buffer, n, 0) != n) {
LOG(LOG_ERROR, "Failed to forward to target");
break;
}
update_session_activity(session);
}
/* Data from target to client */
if (FD_ISSET(target_fd, &read_fds)) {
n = recv(target_fd, buffer, sizeof(buffer), 0);
if (n < 0) {
LOG(LOG_ERROR, "Error receiving from target: %s", strerror(errno));
break;
}
if (n == 0) {
LOG(LOG_INFO, "Target connection closed gracefully");
break;
}
LOG(LOG_DEBUG, "Forwarding %d bytes from target to client", n);
if (send(client_fd, buffer, n, 0) != n) {
LOG(LOG_ERROR, "Failed to forward to client");
break;
}
update_session_activity(session);
}
}
close(target_fd);
session->target_fd = -1;
return 0;
}

177
src/session_manager.c Normal file
View File

@@ -0,0 +1,177 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include "rdp_broker.h"
static pthread_mutex_t session_mutex = PTHREAD_MUTEX_INITIALIZER;
int init_session_manager(session_manager_t *sm) {
if (!sm) {
return -1;
}
memset(sm, 0, sizeof(session_manager_t));
sm->next_session_id = 1;
sm->session_count = 0;
LOG(LOG_DEBUG, "Session manager initialized");
return 0;
}
session_info_t* create_session(session_manager_t *sm, const char *username,
const char *client_ip, rdp_target_t *target,
int client_fd) {
session_info_t *session = NULL;
pthread_mutex_lock(&session_mutex);
if (sm->session_count >= MAX_SESSIONS) {
LOG(LOG_ERROR, "Maximum sessions reached (%d)", MAX_SESSIONS);
pthread_mutex_unlock(&session_mutex);
return NULL;
}
/* Find free slot */
for (int i = 0; i < MAX_SESSIONS; i++) {
if (!sm->sessions[i].active) {
session = &sm->sessions[i];
break;
}
}
if (!session) {
LOG(LOG_ERROR, "No free session slot available");
pthread_mutex_unlock(&session_mutex);
return NULL;
}
/* Initialize session */
memset(session, 0, sizeof(session_info_t));
session->session_id = sm->next_session_id++;
strncpy(session->username, username, MAX_USERNAME_LEN - 1);
strncpy(session->client_ip, client_ip, sizeof(session->client_ip) - 1);
strncpy(session->target_host, target->host, MAX_HOSTNAME_LEN - 1);
session->target_port = target->port;
session->start_time = time(NULL);
session->last_activity = session->start_time;
session->active = true;
session->client_fd = client_fd;
session->target_fd = -1;
sm->session_count++;
LOG(LOG_INFO, "Created session %u: %s@%s -> %s:%d",
session->session_id, username, client_ip,
target->host, target->port);
pthread_mutex_unlock(&session_mutex);
return session;
}
int update_session_activity(session_info_t *session) {
if (!session || !session->active) {
return -1;
}
pthread_mutex_lock(&session_mutex);
session->last_activity = time(NULL);
pthread_mutex_unlock(&session_mutex);
return 0;
}
int close_session(session_manager_t *sm, uint32_t session_id) {
session_info_t *session = NULL;
pthread_mutex_lock(&session_mutex);
/* Find session */
for (int i = 0; i < MAX_SESSIONS; i++) {
if (sm->sessions[i].active && sm->sessions[i].session_id == session_id) {
session = &sm->sessions[i];
break;
}
}
if (!session) {
LOG(LOG_WARN, "Session %u not found", session_id);
pthread_mutex_unlock(&session_mutex);
return -1;
}
time_t duration = time(NULL) - session->start_time;
LOG(LOG_INFO, "Closing session %u: %s -> %s:%d (duration: %ld seconds)",
session->session_id, session->username,
session->target_host, session->target_port, duration);
session->active = false;
sm->session_count--;
pthread_mutex_unlock(&session_mutex);
return 0;
}
void log_active_sessions(session_manager_t *sm) {
int count = 0;
time_t now = time(NULL);
pthread_mutex_lock(&session_mutex);
LOG(LOG_INFO, "=== Active Sessions Report ===");
for (int i = 0; i < MAX_SESSIONS; i++) {
if (sm->sessions[i].active) {
session_info_t *s = &sm->sessions[i];
time_t duration = now - s->start_time;
time_t idle = now - s->last_activity;
LOG(LOG_INFO, "Session %u: %s@%s -> %s:%d | Duration: %lds | Idle: %lds",
s->session_id, s->username, s->client_ip,
s->target_host, s->target_port, duration, idle);
count++;
}
}
if (count == 0) {
LOG(LOG_INFO, "No active sessions");
} else {
LOG(LOG_INFO, "Total active sessions: %d", count);
}
LOG(LOG_INFO, "=============================");
pthread_mutex_unlock(&session_mutex);
}
void cleanup_inactive_sessions(session_manager_t *sm, int timeout_seconds) {
time_t now = time(NULL);
int cleaned = 0;
pthread_mutex_lock(&session_mutex);
for (int i = 0; i < MAX_SESSIONS; i++) {
if (sm->sessions[i].active) {
time_t idle = now - sm->sessions[i].last_activity;
if (idle > timeout_seconds) {
LOG(LOG_WARN, "Cleaning up inactive session %u (idle: %ld seconds)",
sm->sessions[i].session_id, idle);
sm->sessions[i].active = false;
sm->session_count--;
cleaned++;
}
}
}
if (cleaned > 0) {
LOG(LOG_INFO, "Cleaned up %d inactive session(s)", cleaned);
}
pthread_mutex_unlock(&session_mutex);
}

2
src/test.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker container run -it -e SAMBA_AD_SERVER=192.168.100.240 -e SAMBA_AD_BASE_DN="DC=aipice,DC=local" -v /data/apps/RdpBroker/targets.yaml:/etc/rdpbroker/targets.yaml -p 3389:3389 easylinux/rdp-broker:1.0-1 sh

39
targets.yaml Normal file
View File

@@ -0,0 +1,39 @@
# RDP Broker - Target Configuration
# This file defines the available RDP targets that users can connect to
targets:
# Windows Server 01 - Production Web Server
- name: "Windows Server 01"
host: "192.168.1.10"
port: 3389
description: "Production Web Server"
# Windows Server 02 - Database Server
- name: "Windows Server 02"
host: "192.168.1.11"
port: 3389
description: "Database Server"
# Development Desktop
- name: "Development Desktop"
host: "192.168.100.135"
port: 3389
description: "Developer Workstation"
# Windows Server 03 - Application Server
- name: "Windows Server 03"
host: "192.168.1.20"
port: 3389
description: "Application Server"
# Terminal Server
- name: "Terminal Server"
host: "ts01.example.com"
port: 3389
description: "Shared Terminal Server"
# Test Environment
- name: "Test Server"
host: "192.168.100.50"
port: 3389
description: "Testing and QA Environment"

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/

15
web-gateway/.env.example Normal file
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
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

201
web-gateway/BUILD-ARM64.md Normal file
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

41
web-gateway/Dockerfile Normal file
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"]

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.

493
web-gateway/README.md Normal file
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
```

49
web-gateway/build-arm64.sh Executable file
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"

75
web-gateway/build-multiarch.sh Executable file
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

36
web-gateway/package.json Normal file
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;

208
web-gateway/src/server.js Normal file
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;

73
web-gateway/transfer-to-rpi.sh Executable file
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"