diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26c2cd6 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..ffbbaf7 --- /dev/null +++ b/QUICKSTART.md @@ -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). diff --git a/chart/rdpbroker/Chart.yaml b/chart/rdpbroker/Chart.yaml new file mode 100644 index 0000000..73ee6aa --- /dev/null +++ b/chart/rdpbroker/Chart.yaml @@ -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 diff --git a/chart/rdpbroker/templates/NOTES.txt b/chart/rdpbroker/templates/NOTES.txt new file mode 100644 index 0000000..026a9b8 --- /dev/null +++ b/chart/rdpbroker/templates/NOTES.txt @@ -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 }} diff --git a/chart/rdpbroker/templates/_helpers.tpl b/chart/rdpbroker/templates/_helpers.tpl new file mode 100644 index 0000000..f80c824 --- /dev/null +++ b/chart/rdpbroker/templates/_helpers.tpl @@ -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 }} diff --git a/chart/rdpbroker/templates/configmap.yaml b/chart/rdpbroker/templates/configmap.yaml new file mode 100644 index 0000000..1b620b4 --- /dev/null +++ b/chart/rdpbroker/templates/configmap.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/deployment.yaml b/chart/rdpbroker/templates/deployment.yaml new file mode 100644 index 0000000..944a49d --- /dev/null +++ b/chart/rdpbroker/templates/deployment.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/hpa.yaml b/chart/rdpbroker/templates/hpa.yaml new file mode 100644 index 0000000..c5b2055 --- /dev/null +++ b/chart/rdpbroker/templates/hpa.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/networkpolicy.yaml b/chart/rdpbroker/templates/networkpolicy.yaml new file mode 100644 index 0000000..5b16175 --- /dev/null +++ b/chart/rdpbroker/templates/networkpolicy.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/pvc.yaml b/chart/rdpbroker/templates/pvc.yaml new file mode 100644 index 0000000..3d78e9d --- /dev/null +++ b/chart/rdpbroker/templates/pvc.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/service.yaml b/chart/rdpbroker/templates/service.yaml new file mode 100644 index 0000000..2be7b01 --- /dev/null +++ b/chart/rdpbroker/templates/service.yaml @@ -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 }} diff --git a/chart/rdpbroker/templates/serviceaccount.yaml b/chart/rdpbroker/templates/serviceaccount.yaml new file mode 100644 index 0000000..f272d6d --- /dev/null +++ b/chart/rdpbroker/templates/serviceaccount.yaml @@ -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 }} diff --git a/chart/rdpbroker/values.yaml b/chart/rdpbroker/values.yaml new file mode 100644 index 0000000..81a5e80 --- /dev/null +++ b/chart/rdpbroker/values.yaml @@ -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 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..93c4c03 --- /dev/null +++ b/docs/deployment.md @@ -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 diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..dba5965 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,11 @@ +# .dockerignore +build/ +bin/ +*.o +*.so +*.a +.git/ +.gitignore +README.md +docs/ +chart/ diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..686b78e --- /dev/null +++ b/src/Dockerfile @@ -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"] diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..68f6c32 --- /dev/null +++ b/src/Makefile @@ -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 diff --git a/src/auth.c b/src/auth.c new file mode 100644 index 0000000..61f9e6b --- /dev/null +++ b/src/auth.c @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#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; + } +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..5d78677 --- /dev/null +++ b/src/config.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#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(¤t_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], + ¤t_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(¤t_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; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e96716c --- /dev/null +++ b/src/main.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#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; +} diff --git a/src/rdp_broker.h b/src/rdp_broker.h new file mode 100644 index 0000000..80e4b5e --- /dev/null +++ b/src/rdp_broker.h @@ -0,0 +1,106 @@ +#ifndef RDP_BROKER_H +#define RDP_BROKER_H + +#include +#include +#include + +#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 */ diff --git a/src/rdp_server.c b/src/rdp_server.c new file mode 100644 index 0000000..06ac89f --- /dev/null +++ b/src/rdp_server.c @@ -0,0 +1,400 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/src/session_manager.c b/src/session_manager.c new file mode 100644 index 0000000..ff15cdf --- /dev/null +++ b/src/session_manager.c @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#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); +} diff --git a/targets.yaml b/targets.yaml new file mode 100644 index 0000000..eaa72d8 --- /dev/null +++ b/targets.yaml @@ -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" diff --git a/web-gateway/.dockerignore b/web-gateway/.dockerignore new file mode 100644 index 0000000..aaeea44 --- /dev/null +++ b/web-gateway/.dockerignore @@ -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/ diff --git a/web-gateway/.env.example b/web-gateway/.env.example new file mode 100644 index 0000000..2390c67 --- /dev/null +++ b/web-gateway/.env.example @@ -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 diff --git a/web-gateway/.gitignore b/web-gateway/.gitignore new file mode 100644 index 0000000..24d315f --- /dev/null +++ b/web-gateway/.gitignore @@ -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 diff --git a/web-gateway/Dockerfile b/web-gateway/Dockerfile new file mode 100644 index 0000000..cf44eb8 --- /dev/null +++ b/web-gateway/Dockerfile @@ -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"] diff --git a/web-gateway/INTEGRATION.md b/web-gateway/INTEGRATION.md new file mode 100644 index 0000000..7ee30ef --- /dev/null +++ b/web-gateway/INTEGRATION.md @@ -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. diff --git a/web-gateway/README.md b/web-gateway/README.md new file mode 100644 index 0000000..39164e9 --- /dev/null +++ b/web-gateway/README.md @@ -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 +``` diff --git a/web-gateway/chart/rdp-web-gateway/Chart.yaml b/web-gateway/chart/rdp-web-gateway/Chart.yaml new file mode 100644 index 0000000..7f2d44f --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/Chart.yaml @@ -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 diff --git a/web-gateway/chart/rdp-web-gateway/templates/NOTES.txt b/web-gateway/chart/rdp-web-gateway/templates/NOTES.txt new file mode 100644 index 0000000..5420906 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/NOTES.txt @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/_helpers.tpl b/web-gateway/chart/rdp-web-gateway/templates/_helpers.tpl new file mode 100644 index 0000000..64abbc6 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/_helpers.tpl @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/configmap.yaml b/web-gateway/chart/rdp-web-gateway/templates/configmap.yaml new file mode 100644 index 0000000..5470418 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/configmap.yaml @@ -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 }} + } + } diff --git a/web-gateway/chart/rdp-web-gateway/templates/deployment.yaml b/web-gateway/chart/rdp-web-gateway/templates/deployment.yaml new file mode 100644 index 0000000..7255b2f --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/deployment.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/hpa.yaml b/web-gateway/chart/rdp-web-gateway/templates/hpa.yaml new file mode 100644 index 0000000..a749d08 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/hpa.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/ingress.yaml b/web-gateway/chart/rdp-web-gateway/templates/ingress.yaml new file mode 100644 index 0000000..05d5830 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/ingress.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/networkpolicy.yaml b/web-gateway/chart/rdp-web-gateway/templates/networkpolicy.yaml new file mode 100644 index 0000000..99715f5 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/networkpolicy.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/pvc.yaml b/web-gateway/chart/rdp-web-gateway/templates/pvc.yaml new file mode 100644 index 0000000..0f3f1f8 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/pvc.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/secret.yaml b/web-gateway/chart/rdp-web-gateway/templates/secret.yaml new file mode 100644 index 0000000..67b813e --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/secret.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/service.yaml b/web-gateway/chart/rdp-web-gateway/templates/service.yaml new file mode 100644 index 0000000..b1995f6 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/service.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/templates/serviceaccount.yaml b/web-gateway/chart/rdp-web-gateway/templates/serviceaccount.yaml new file mode 100644 index 0000000..7f624f2 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/templates/serviceaccount.yaml @@ -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 }} diff --git a/web-gateway/chart/rdp-web-gateway/values.yaml b/web-gateway/chart/rdp-web-gateway/values.yaml new file mode 100644 index 0000000..6b8fd47 --- /dev/null +++ b/web-gateway/chart/rdp-web-gateway/values.yaml @@ -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 diff --git a/web-gateway/package.json b/web-gateway/package.json new file mode 100644 index 0000000..70f65e5 --- /dev/null +++ b/web-gateway/package.json @@ -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" + } +} diff --git a/web-gateway/public/css/style.css b/web-gateway/public/css/style.css new file mode 100644 index 0000000..96e1f8b --- /dev/null +++ b/web-gateway/public/css/style.css @@ -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; + } +} diff --git a/web-gateway/public/index.html b/web-gateway/public/index.html new file mode 100644 index 0000000..d756273 --- /dev/null +++ b/web-gateway/public/index.html @@ -0,0 +1,92 @@ + + + + + + RDP Web Gateway - Login + + + +
+ + + + + +
+ +
+

RDP Web Gateway v1.0.0 | Powered by FreeRDP-WebConnect

+
+ + + + diff --git a/web-gateway/public/js/app.js b/web-gateway/public/js/app.js new file mode 100644 index 0000000..019079a --- /dev/null +++ b/web-gateway/public/js/app.js @@ -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 = '

No remote desktops available

'; + return; + } + + targets.forEach(target => { + const targetItem = document.createElement('div'); + targetItem.className = 'target-item'; + targetItem.innerHTML = ` +
${this.escapeHtml(target.name)}
+
${this.escapeHtml(target.description)}
+
${this.escapeHtml(target.host)}:${target.port}
+ `; + 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 = ` +
+ + + + +

Connection Failed

+

${this.escapeHtml(message)}

+ +
+ `; + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +// Initialize the app +const rdpGateway = new RDPWebGateway(); diff --git a/web-gateway/src/logger.js b/web-gateway/src/logger.js new file mode 100644 index 0000000..e332f2f --- /dev/null +++ b/web-gateway/src/logger.js @@ -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; diff --git a/web-gateway/src/rdpProxyHandler.js b/web-gateway/src/rdpProxyHandler.js new file mode 100644 index 0000000..9fcb52f --- /dev/null +++ b/web-gateway/src/rdpProxyHandler.js @@ -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: \nPassword: \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; diff --git a/web-gateway/src/server.js b/web-gateway/src/server.js new file mode 100644 index 0000000..cafff07 --- /dev/null +++ b/web-gateway/src/server.js @@ -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; diff --git a/web-gateway/src/sessionManager.js b/web-gateway/src/sessionManager.js new file mode 100644 index 0000000..43994ea --- /dev/null +++ b/web-gateway/src/sessionManager.js @@ -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;