Repo initialisation

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

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.

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

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

11
src/.dockerignore Normal file
View File

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

51
src/Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Build stage
FROM alpine:3.18 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.18
# 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;
static volatile bool running = true;
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;
}

106
src/rdp_broker.h Normal file
View File

@@ -0,0 +1,106 @@
#ifndef RDP_BROKER_H
#define RDP_BROKER_H
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#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 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, rdp_target_t *target,
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 */

400
src/rdp_server.c Normal file
View File

@@ -0,0 +1,400 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.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;
/* 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 (1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
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);
}
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;
}
int handle_rdp_connection(int client_fd, broker_config_t *config,
session_manager_t *sm) {
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};
/* Get client IP */
if (getpeername(client_fd, (struct sockaddr *)&addr, &addr_len) == 0) {
inet_ntop(AF_INET, &addr.sin_addr, client_ip, INET_ADDRSTRLEN);
}
/* Step 1: Present login screen and get credentials */
LOG(LOG_INFO, "Presenting login screen to client");
if (present_login_screen(client_fd, username, password) != 0) {
LOG(LOG_ERROR, "Failed to get login credentials");
return -1;
}
/* Step 2: 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);
const char *msg = "Authentication failed\n";
send(client_fd, msg, strlen(msg), 0);
return -1;
}
LOG(LOG_INFO, "User %s authenticated successfully", username);
/* Step 3: Present target menu */
LOG(LOG_INFO, "Presenting target menu to user: %s", username);
if (present_target_menu(client_fd, config, &selected_target) != 0) {
LOG(LOG_ERROR, "Failed to get target selection");
return -1;
}
if (!selected_target) {
LOG(LOG_ERROR, "No target selected");
return -1;
}
LOG(LOG_INFO, "User %s selected target: %s (%s:%d)", username,
selected_target->name, selected_target->host, selected_target->port);
/* Step 4: Create session */
session = create_session(sm, username, client_ip, selected_target, client_fd);
if (!session) {
LOG(LOG_ERROR, "Failed to create session");
return -1;
}
LOG(LOG_INFO, "Created session %u for %s -> %s", session->session_id,
username, selected_target->name);
/* Step 5: Forward RDP connection */
LOG(LOG_INFO, "Forwarding RDP connection for session %u", session->session_id);
if (forward_rdp_connection(client_fd, selected_target, session) != 0) {
LOG(LOG_ERROR, "Failed to forward RDP connection");
close_session(sm, session->session_id);
return -1;
}
/* Connection closed */
LOG(LOG_INFO, "Session %u ended", session->session_id);
close_session(sm, session->session_id);
return 0;
}
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, rdp_target_t *target,
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;
const char *msg = "Connected to target. Starting RDP session...\n";
send(client_fd, msg, strlen(msg), 0);
/* Forward data bidirectionally */
max_fd = (client_fd > target_fd) ? client_fd : target_fd;
while (1) {
FD_ZERO(&read_fds);
FD_SET(client_fd, &read_fds);
FD_SET(target_fd, &read_fds);
if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
LOG(LOG_ERROR, "Select failed: %s", strerror(errno));
break;
}
/* 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_DEBUG, "Client connection closed");
break;
}
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_DEBUG, "Target connection closed");
break;
}
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);
}

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: "dev-machine.local"
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/

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

@@ -0,0 +1,16 @@
# RDP Web Gateway Environment Configuration
# Server
PORT=8080
NODE_ENV=production
LOG_LEVEL=info
# RDP Broker Connection
RDP_BROKER_HOST=rdpbroker
RDP_BROKER_PORT=3389
# Session Configuration
SESSION_TIMEOUT=3600000
# Security (set these in production)
# SESSION_SECRET=your-secret-key-here

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

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

44
web-gateway/Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Production stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create app user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Create necessary directories
RUN mkdir -p /var/log/rdp-web-gateway && \
chown -R nodejs:nodejs /var/log/rdp-web-gateway
WORKDIR /app
# Copy dependencies from builder
COPY --from=builder /app/node_modules ./node_modules
# Copy application files
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 8080
# Use dumb-init to handle signals
ENTRYPOINT ["dumb-init", "--"]
# Start application
CMD ["node", "src/server.js"]

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "rdp-web-gateway.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "rdp-web-gateway.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: PORT
value: {{ .Values.config.server.port | quote }}
- name: RDP_BROKER_HOST
value: {{ .Values.config.rdpBroker.host | quote }}
- name: RDP_BROKER_PORT
value: {{ .Values.config.rdpBroker.port | quote }}
- name: LOG_LEVEL
value: {{ .Values.config.server.logLevel | quote }}
- name: SESSION_TIMEOUT
value: {{ .Values.config.session.timeout | quote }}
- name: NODE_ENV
value: "production"
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- if .Values.secrets.sessionSecret }}
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: {{ include "rdp-web-gateway.fullname" . }}-secrets
key: sessionSecret
{{- end }}
ports:
- name: http
containerPort: {{ .Values.config.server.port }}
protocol: TCP
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.persistence.enabled }}
volumeMounts:
- name: logs
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}
{{- if .Values.persistence.enabled }}
volumes:
- name: logs
persistentVolumeClaim:
claimName: {{ include "rdp-web-gateway.fullname" . }}-logs
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}-logs
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "rdp-web-gateway.fullname" . }}-secrets
labels:
{{- include "rdp-web-gateway.labels" . | nindent 4 }}
type: Opaque
data:
{{- if .Values.secrets.sessionSecret }}
sessionSecret: {{ .Values.secrets.sessionSecret | b64enc | quote }}
{{- end }}
{{- end }}

View File

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

View File

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

View File

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

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

@@ -0,0 +1,37 @@
{
"name": "rdp-web-gateway",
"version": "1.0.0",
"description": "HTML5 WebSocket-based RDP Gateway for RdpBroker",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"rdp",
"websocket",
"gateway",
"html5",
"remote-desktop"
],
"author": "RdpBroker Team",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.14.2",
"node-rdpjs": "^0.3.2",
"dotenv": "^16.3.1",
"compression": "^1.7.4",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"uuid": "^9.0.1",
"winston": "^3.11.0"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

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

View File

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

View File

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

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

@@ -0,0 +1,41 @@
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'rdp-web-gateway' },
transports: [
// Write to console
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
})
)
}),
// Write to file
new winston.transports.File({
filename: '/var/log/rdp-web-gateway/error.log',
level: 'error',
handleExceptions: true
}),
new winston.transports.File({
filename: '/var/log/rdp-web-gateway/combined.log',
handleExceptions: true
})
]
});
module.exports = logger;

View File

@@ -0,0 +1,198 @@
const net = require('net');
const logger = require('./logger');
class RDPProxyHandler {
constructor(websocket, sessionManager, rdpBrokerHost, rdpBrokerPort) {
this.ws = websocket;
this.sessionManager = sessionManager;
this.rdpBrokerHost = rdpBrokerHost;
this.rdpBrokerPort = rdpBrokerPort;
this.session = null;
this.rdpSocket = null;
this.isConnected = false;
}
async handleMessage(message) {
try {
switch (message.type) {
case 'connect':
await this.handleConnect(message);
break;
case 'mouse':
this.handleMouseEvent(message);
break;
case 'keyboard':
this.handleKeyboardEvent(message);
break;
case 'special':
this.handleSpecialCommand(message);
break;
default:
logger.warn(`Unknown message type: ${message.type}`);
}
} catch (error) {
logger.error('Error handling message:', error);
this.sendError('Failed to process message');
}
}
async handleConnect(message) {
const { sessionId, target } = message;
// Validate session
this.session = this.sessionManager.getSession(sessionId);
if (!this.session) {
return this.sendError('Invalid session');
}
logger.info(`Connecting to RDP Broker for session ${sessionId}, target: ${target.name}`);
try {
// Connect to RDP Broker
this.rdpSocket = new net.Socket();
this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => {
logger.info(`Connected to RDP Broker at ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
this.isConnected = true;
// Send authentication to RdpBroker
// In real implementation, this would follow the RDP protocol
this.sendAuthToBroker();
this.ws.send(JSON.stringify({
type: 'connected',
target: target.name
}));
// Set canvas size
this.ws.send(JSON.stringify({
type: 'resize',
width: 1920,
height: 1080
}));
});
// Handle data from RDP Broker
this.rdpSocket.on('data', (data) => {
// Forward RDP data to WebSocket client
// In production, this would be properly decoded RDP frames
if (this.ws.readyState === 1) { // WebSocket.OPEN
this.ws.send(data);
}
});
this.rdpSocket.on('error', (error) => {
logger.error('RDP socket error:', error);
this.sendError('Connection to RDP broker failed');
this.cleanup();
});
this.rdpSocket.on('close', () => {
logger.info('RDP connection closed');
this.isConnected = false;
if (this.ws.readyState === 1) {
this.ws.close();
}
});
} catch (error) {
logger.error('Connection error:', error);
this.sendError('Failed to connect to RDP broker');
}
}
sendAuthToBroker() {
// This is a simplified version
// In production, implement proper RDP protocol handshake
if (!this.rdpSocket || !this.session) return;
const authData = {
username: this.session.username,
password: this.session.data.password
};
// Send authentication data
// Format: "Username: <username>\nPassword: <password>\n"
const authMessage = `${authData.username}\n${authData.password}\n`;
this.rdpSocket.write(authMessage);
}
handleMouseEvent(message) {
if (!this.isConnected || !this.rdpSocket) return;
// Convert mouse event to RDP protocol
// This is simplified - real implementation would use RDP protocol
const mouseData = JSON.stringify({
type: 'mouse',
x: message.x,
y: message.y,
button: message.button,
action: message.action
});
this.rdpSocket.write(mouseData + '\n');
}
handleKeyboardEvent(message) {
if (!this.isConnected || !this.rdpSocket) return;
// Convert keyboard event to RDP protocol
const keyData = JSON.stringify({
type: 'key',
action: message.action,
key: message.key,
code: message.code,
modifiers: {
ctrl: message.ctrlKey,
alt: message.altKey,
shift: message.shiftKey
}
});
this.rdpSocket.write(keyData + '\n');
}
handleSpecialCommand(message) {
if (!this.isConnected || !this.rdpSocket) return;
switch (message.action) {
case 'ctrl-alt-del':
// Send Ctrl+Alt+Del sequence
const cadData = JSON.stringify({
type: 'special',
command: 'ctrl-alt-del'
});
this.rdpSocket.write(cadData + '\n');
logger.info('Sent Ctrl+Alt+Del');
break;
default:
logger.warn(`Unknown special command: ${message.action}`);
}
}
sendError(message) {
if (this.ws.readyState === 1) {
this.ws.send(JSON.stringify({
type: 'error',
error: message
}));
}
}
cleanup() {
if (this.rdpSocket) {
this.rdpSocket.destroy();
this.rdpSocket = null;
}
this.isConnected = false;
if (this.session) {
this.sessionManager.updateSession(this.session.id, {
rdpConnection: null
});
}
}
}
module.exports = RDPProxyHandler;

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

@@ -0,0 +1,216 @@
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const compression = require('compression');
const helmet = require('helmet');
const cors = require('cors');
const logger = require('./logger');
const SessionManager = require('./sessionManager');
const RDPProxyHandler = require('./rdpProxyHandler');
class RDPWebGatewayServer {
constructor() {
this.app = express();
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({
server: this.server,
path: '/ws/rdp'
});
this.sessionManager = new SessionManager();
this.port = process.env.PORT || 8080;
this.rdpBrokerHost = process.env.RDP_BROKER_HOST || 'rdpbroker';
this.rdpBrokerPort = process.env.RDP_BROKER_PORT || 3389;
this.setupMiddleware();
this.setupRoutes();
this.setupWebSocket();
}
setupMiddleware() {
// Security
this.app.use(helmet({
contentSecurityPolicy: false, // Disable for WebSocket
}));
this.app.use(cors());
this.app.use(compression());
// Body parsing
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// Static files
this.app.use(express.static(path.join(__dirname, '../public')));
// Logging
this.app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
}
setupRoutes() {
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
version: '1.0.0',
sessions: this.sessionManager.getActiveSessionCount()
});
});
// Authentication endpoint
this.app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
// Create session
const session = this.sessionManager.createSession(username, {
password,
ipAddress: req.ip
});
logger.info(`User ${username} authenticated, session: ${session.id}`);
res.json({
success: true,
sessionId: session.id
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// Get available targets
this.app.get('/api/targets', async (req, res) => {
try {
const sessionId = req.headers['x-session-id'];
if (!sessionId) {
return res.status(401).json({ error: 'Session ID required' });
}
const session = this.sessionManager.getSession(sessionId);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
// In a real implementation, this would fetch from RdpBroker
// For now, return static list
const targets = await this.fetchTargetsFromBroker(session);
res.json({ targets });
} catch (error) {
logger.error('Targets fetch error:', error);
res.status(500).json({ error: 'Failed to fetch targets' });
}
});
// Logout
this.app.post('/api/auth/logout', (req, res) => {
const sessionId = req.headers['x-session-id'];
if (sessionId) {
this.sessionManager.destroySession(sessionId);
}
res.json({ success: true });
});
// Catch all - serve index.html
this.app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
}
setupWebSocket() {
this.wss.on('connection', (ws, req) => {
logger.info('New WebSocket connection');
const proxyHandler = new RDPProxyHandler(
ws,
this.sessionManager,
this.rdpBrokerHost,
this.rdpBrokerPort
);
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
proxyHandler.handleMessage(message);
} catch (error) {
logger.error('WebSocket message error:', error);
ws.send(JSON.stringify({
type: 'error',
error: 'Invalid message format'
}));
}
});
ws.on('close', () => {
logger.info('WebSocket connection closed');
proxyHandler.cleanup();
});
ws.on('error', (error) => {
logger.error('WebSocket error:', error);
proxyHandler.cleanup();
});
});
}
async fetchTargetsFromBroker(session) {
// This is a simplified version
// In production, this would communicate with RdpBroker
// to get the actual list of targets
// For now, return example targets
return [
{
name: "Windows Server 01",
host: "192.168.1.10",
port: 3389,
description: "Production Web Server"
},
{
name: "Windows Server 02",
host: "192.168.1.11",
port: 3389,
description: "Database Server"
},
{
name: "Development Desktop",
host: "dev-machine.local",
port: 3389,
description: "Developer Workstation"
}
];
}
start() {
this.server.listen(this.port, () => {
logger.info(`RDP Web Gateway server running on port ${this.port}`);
logger.info(`RDP Broker: ${this.rdpBrokerHost}:${this.rdpBrokerPort}`);
logger.info(`WebSocket endpoint: ws://localhost:${this.port}/ws/rdp`);
});
}
}
// Start server
const server = new RDPWebGatewayServer();
server.start();
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully...');
server.server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
module.exports = RDPWebGatewayServer;

View File

@@ -0,0 +1,94 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('./logger');
class SessionManager {
constructor() {
this.sessions = new Map();
this.sessionTimeout = 3600000; // 1 hour
// Cleanup inactive sessions every 5 minutes
setInterval(() => this.cleanupSessions(), 300000);
}
createSession(username, userData = {}) {
const sessionId = uuidv4();
const session = {
id: sessionId,
username,
createdAt: Date.now(),
lastActivity: Date.now(),
data: userData,
rdpConnection: null
};
this.sessions.set(sessionId, session);
logger.info(`Session created: ${sessionId} for user ${username}`);
return session;
}
getSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.lastActivity = Date.now();
}
return session;
}
updateSession(sessionId, data) {
const session = this.sessions.get(sessionId);
if (session) {
session.data = { ...session.data, ...data };
session.lastActivity = Date.now();
}
return session;
}
destroySession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
logger.info(`Session destroyed: ${sessionId}`);
// Cleanup any active RDP connection
if (session.rdpConnection) {
session.rdpConnection.close();
}
this.sessions.delete(sessionId);
return true;
}
return false;
}
cleanupSessions() {
const now = Date.now();
let cleaned = 0;
for (const [sessionId, session] of this.sessions.entries()) {
if (now - session.lastActivity > this.sessionTimeout) {
logger.info(`Cleaning up inactive session: ${sessionId}`);
this.destroySession(sessionId);
cleaned++;
}
}
if (cleaned > 0) {
logger.info(`Cleaned up ${cleaned} inactive session(s)`);
}
}
getActiveSessionCount() {
return this.sessions.size;
}
getAllSessions() {
return Array.from(this.sessions.values()).map(session => ({
id: session.id,
username: session.username,
createdAt: session.createdAt,
lastActivity: session.lastActivity
}));
}
}
module.exports = SessionManager;