From 2080559f46ce0ef90ef1a83e14d14a97a5b95d38 Mon Sep 17 00:00:00 2001 From: Serge NOEL Date: Thu, 4 Dec 2025 09:39:42 +0100 Subject: [PATCH] Interactive list --- PROTOCOL.md | 336 +++++++++++++++++++++++++++++ web-gateway/README.md | 88 +++++++- web-gateway/public/js/app.js | 124 +++++++---- web-gateway/src/rdpProxyHandler.js | 143 +++++++++--- 4 files changed, 608 insertions(+), 83 deletions(-) create mode 100644 PROTOCOL.md diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..0845939 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,336 @@ +# RdpBroker Protocol Specification + +## Overview + +This document describes the protocol between web-gateway and RdpBroker for user authentication and target management. + +## Connection Flow + +``` +Web-Gateway RdpBroker Samba AD / Target RDP Servers + | | | + |--AUTH\n{user}\n{pass}\n----->| | + | |---LDAP Auth----------------->| + | |<----Auth Result--------------| + | | | + |<---{type:targets,targets:[]}| | + | OR | | + |<---{type:auth_failed}--------| | + | | | + |--SELECT\n{target_name}\n---->| | + | |---Connect to Target--------->| + |<---{type:rdp_ready}----------|<-----------------------------| + | | | + |<====== RDP Binary Data ======|<===== RDP Session ==========| +``` + +## Protocol Messages + +### Phase 1: Authentication + +#### Request Format (Web-Gateway → RdpBroker) + +``` +AUTH\n +username\n +password\n +``` + +**Example:** +``` +AUTH +user@example.com +SecurePassword123 +``` + +#### Success Response (RdpBroker → Web-Gateway) + +**JSON Format:** +```json +{ + "type": "auth_success", + "targets": [ + { + "name": "Windows Server 2022", + "host": "ws2022.example.com", + "port": 3389, + "description": "Production Windows Server" + }, + { + "name": "Development Server", + "host": "dev.example.com", + "port": 3389, + "description": "Development environment" + } + ] +} +``` + +**Notes:** +- The message must end with `\n\n` (double newline) to signal end of JSON message +- Targets list is personalized based on user permissions/groups in Samba AD +- Empty array means user is authenticated but has no authorized targets + +#### Failure Response (RdpBroker → Web-Gateway) + +**JSON Format:** +```json +{ + "type": "auth_failed", + "message": "Invalid credentials" +} +``` + +Followed by connection close. + +**Possible error messages:** +- "Invalid credentials" +- "User account disabled" +- "LDAP connection failed" +- "User not authorized for any targets" + +### Phase 2: Target Selection + +#### Request Format (Web-Gateway → RdpBroker) + +``` +SELECT\n +target_name\n +``` + +**Example:** +``` +SELECT +Windows Server 2022 +``` + +**Notes:** +- Target name must match exactly one of the names from the targets list +- Connection should be rejected if target name is invalid or not in user's authorized list + +#### Success Response (RdpBroker → Web-Gateway) + +**JSON Format:** +```json +{ + "type": "rdp_ready" +} +``` + +Followed by `\n\n`, then RDP binary data stream begins. + +After this message, the connection transitions to raw RDP protocol forwarding mode. + +#### Failure Response (RdpBroker → Web-Gateway) + +**JSON Format:** +```json +{ + "type": "error", + "message": "Target not available" +} +``` + +Followed by connection close. + +**Possible error messages:** +- "Target not found" +- "Target not authorized for user" +- "Failed to connect to target server" +- "Target server unreachable" + +### Phase 3: RDP Session + +After `rdp_ready` message, all subsequent data is raw RDP protocol: +- Web-Gateway forwards mouse/keyboard events as RDP protocol data +- RdpBroker forwards screen updates as RDP protocol data +- Connection is bidirectional binary stream + +## Implementation Guidelines for RdpBroker + +### 1. Accept Connection +Listen on port 3389 (configurable via RDP_LISTEN_PORT) + +### 2. Read Authentication Message +```c +char buffer[4096]; +int bytes = read(client_fd, buffer, sizeof(buffer)); + +// Parse "AUTH\n{username}\n{password}\n" +char *lines[3]; +int line_count = 0; +char *token = strtok(buffer, "\n"); +while (token != NULL && line_count < 3) { + lines[line_count++] = token; + token = strtok(NULL, "\n"); +} + +if (strcmp(lines[0], "AUTH") != 0) { + send_error(client_fd, "Invalid protocol"); + close(client_fd); + return; +} + +char *username = lines[1]; +char *password = lines[2]; +``` + +### 3. Authenticate with Samba AD +```c +int auth_result = authenticate_user(username, password, + config->samba_server, + config->samba_port, + config->base_dn); +``` + +### 4. Get User's Authorized Targets +```c +rdp_target_t *user_targets[MAX_TARGETS]; +int target_count = get_user_targets(username, config, user_targets); +``` + +**Recommended approach:** +- Query user's groups from LDAP +- Filter targets based on group membership or user attributes +- Return only targets the user is authorized to access + +**Example YAML configuration:** +```yaml +targets: + - name: "Windows Server 2022" + host: "ws2022.example.com" + port: 3389 + description: "Production Windows Server" + authorized_groups: + - "Domain Admins" + - "Server Operators" + + - name: "Development Server" + host: "dev.example.com" + port: 3389 + description: "Development environment" + authorized_groups: + - "Developers" + - "Domain Admins" +``` + +### 5. Send Targets List +```c +char json_response[8192]; +snprintf(json_response, sizeof(json_response), + "{\"type\":\"auth_success\",\"targets\":["); + +for (int i = 0; i < target_count; i++) { + char target_json[512]; + snprintf(target_json, sizeof(target_json), + "%s{\"name\":\"%s\",\"host\":\"%s\",\"port\":%d,\"description\":\"%s\"}", + (i > 0 ? "," : ""), + user_targets[i]->name, + user_targets[i]->host, + user_targets[i]->port, + user_targets[i]->description); + strcat(json_response, target_json); +} + +strcat(json_response, "]}\n\n"); +write(client_fd, json_response, strlen(json_response)); +``` + +### 6. Read Target Selection +```c +bytes = read(client_fd, buffer, sizeof(buffer)); + +// Parse "SELECT\n{target_name}\n" +if (strncmp(buffer, "SELECT\n", 7) != 0) { + send_error(client_fd, "Invalid protocol"); + close(client_fd); + return; +} + +char *target_name = buffer + 7; +char *newline = strchr(target_name, '\n'); +if (newline) *newline = '\0'; + +// Verify target is in user's authorized list +rdp_target_t *selected_target = find_target_in_list(target_name, + user_targets, + target_count); +if (!selected_target) { + send_error(client_fd, "Target not authorized"); + close(client_fd); + return; +} +``` + +### 7. Connect to Target and Start Forwarding +```c +int target_fd = connect_to_rdp_target(selected_target->host, + selected_target->port); + +// Send ready message +char *ready_msg = "{\"type\":\"rdp_ready\"}\n\n"; +write(client_fd, ready_msg, strlen(ready_msg)); + +// Start bidirectional forwarding +forward_rdp_connection(client_fd, target_fd); +``` + +## Security Considerations + +1. **Always validate target selection** - User must be authorized for selected target +2. **Close on protocol errors** - Invalid messages should immediately close connection +3. **Timeout authentication** - Implement timeout for AUTH phase (e.g., 30 seconds) +4. **Rate limiting** - Prevent brute force attacks on authentication +5. **Logging** - Log all authentication attempts and target selections +6. **TLS/SSL** - Consider wrapping connection in TLS for production + +## Testing + +### Test Authentication Success +```bash +(echo -e "AUTH\nuser@example.com\nPassword123\n"; sleep 1) | nc rdpbroker 3389 +``` + +Expected response: +```json +{"type":"auth_success","targets":[...]} +``` + +### Test Authentication Failure +```bash +(echo -e "AUTH\nuser@example.com\nWrongPassword\n"; sleep 1) | nc rdpbroker 3389 +``` + +Expected response: +```json +{"type":"auth_failed","message":"Invalid credentials"} +``` + +### Test Target Selection +```bash +(echo -e "AUTH\nuser@example.com\nPassword123\n"; sleep 1; echo -e "SELECT\nWindows Server 2022\n"; sleep 1) | nc rdpbroker 3389 +``` + +Expected response: +```json +{"type":"auth_success","targets":[...]} + +{"type":"rdp_ready"} + +[RDP binary data follows] +``` + +## Migration Notes + +Existing RdpBroker implementations that present login/menu screens via RDP protocol will need to be refactored to: + +1. Accept the new text-based protocol on initial connection +2. Parse AUTH and SELECT commands +3. Return JSON responses instead of RDP login screens +4. Only start RDP forwarding after receiving SELECT command + +The advantage is that authentication and target selection now happen via structured protocol before RDP session starts, allowing: +- Better error handling in web UI +- User-specific target lists +- Cleaner separation of concerns +- Easier debugging and monitoring diff --git a/web-gateway/README.md b/web-gateway/README.md index 77df151..7130e4f 100644 --- a/web-gateway/README.md +++ b/web-gateway/README.md @@ -7,11 +7,12 @@ HTML5 WebSocket-based gateway for accessing RDP connections through a web browse - 🌐 **Browser-Based Access** - Connect to RDP sessions from any modern web browser - 🔒 **Secure WebSocket** - Real-time bidirectional communication - 🎨 **Modern UI** - Clean, responsive interface -- 🔑 **Simplified Authentication** - Credentials passed directly to RdpBroker +- 🔑 **User-Specific Targets** - Each user sees only their authorized RDP servers - 📊 **Service Health Monitoring** - Automatic RdpBroker availability checks -- 🎯 **Dynamic Target Loading** - Targets fetched from configuration or RdpBroker +- 🎯 **Dynamic Target Loading** - Personalized targets from RdpBroker based on user permissions - ⚡ **Low Latency** - Optimized for performance - ☁️ **Kubernetes Native** - Console-only logging for cloud environments +- 🔐 **Samba AD Integration** - Authentication via RdpBroker with Samba Active Directory ## Architecture @@ -19,12 +20,29 @@ HTML5 WebSocket-based gateway for accessing RDP connections through a web browse User Browser (HTML5/WebSocket) ↓ RDP Web Gateway (Node.js) + ↓ [WebSocket Protocol] + ↓ 1. AUTH → receives user-specific targets + ↓ 2. SELECT → connects to chosen target ↓ - RdpBroker (RDP) + RdpBroker (C) + ↓ [Samba AD Auth] + ↓ [Target Authorization] + ↓ [RDP Forwarding] ↓ Target RDP Servers ``` +## Authentication Flow + +1. **User Login** - User enters credentials in web interface +2. **Health Check** - Web-gateway verifies RdpBroker is available +3. **WebSocket Auth** - Credentials sent via WebSocket to RdpBroker +4. **LDAP Authentication** - RdpBroker authenticates against Samba AD +5. **Target Authorization** - RdpBroker determines user's authorized targets based on groups/permissions +6. **Targets Display** - User-specific target list sent back to web-gateway +7. **Target Selection** - User chooses from their authorized servers +8. **RDP Session** - RdpBroker establishes connection to selected target + ## Prerequisites - Node.js 18+ @@ -147,22 +165,74 @@ Fetch available RDP targets. Connect to `ws://localhost:8080/ws/rdp` -#### Client → Server Messages +The protocol follows a two-phase approach: +1. **Authentication Phase**: User authenticates and receives personalized target list +2. **Connection Phase**: User selects target and establishes RDP session -**Connect to target:** +#### Phase 1: Authentication + +**Client → Server - Authenticate:** +```json +{ + "type": "authenticate", + "username": "user@domain.com", + "password": "password123" +} +``` + +**Server → Client - Authentication Success with Targets:** +```json +{ + "type": "targets", + "targets": [ + { + "name": "Windows Server 2022", + "host": "ws2022.example.com", + "port": 3389, + "description": "Production Windows Server (user-specific)" + }, + { + "name": "Development Server", + "host": "dev.example.com", + "port": 3389, + "description": "Development environment" + } + ] +} +``` + +**Server → Client - Authentication Failed:** +```json +{ + "type": "error", + "error": "Invalid credentials" +} +``` + +#### Phase 2: Connection + +**Client → Server - Connect to Target:** ```json { "type": "connect", - "username": "user@domain.com", - "password": "password123", "target": { - "name": "Server 01", - "host": "192.168.1.10", + "name": "Windows Server 2022", + "host": "ws2022.example.com", "port": 3389 } } ``` +**Server → Client - RDP Session Ready:** +```json +{ + "type": "connected", + "target": "Windows Server 2022" +} +``` + +#### Client → Server Messages + **Mouse event:** ```json { diff --git a/web-gateway/public/js/app.js b/web-gateway/public/js/app.js index 8944ef7..a51fda9 100644 --- a/web-gateway/public/js/app.js +++ b/web-gateway/public/js/app.js @@ -72,41 +72,90 @@ class RDPWebGateway { return; } - // Store credentials temporarily - will be sent to RdpBroker + // Store credentials and authenticate via WebSocket this.currentUser = username; this.credentials = { username, password }; - // Load targets and show targets view - await this.loadTargets(); + // Authenticate and get user-specific targets from RdpBroker + await this.authenticateAndLoadTargets(); } catch (error) { console.error('Login error:', error); this.showError(errorMessage, 'Connection error. Please check your network and try again.'); - } finally { loginBtn.disabled = false; btnText.style.display = 'block'; spinner.style.display = 'none'; } } - async loadTargets() { - try { - const response = await fetch('/api/targets'); - - if (!response.ok) { - if (response.status === 503) { - const error = await response.json(); - throw new Error(error.error || 'Service unavailable'); - } - throw new Error('Failed to load targets'); - } + authenticateAndLoadTargets() { + return new Promise((resolve, reject) => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/rdp`; - const data = await response.json(); - this.showTargetsView(data.targets); - } catch (error) { - console.error('Error loading targets:', error); - // Show error in targets view - this.showTargetsView(null, error.message); - } + // Create WebSocket connection for authentication + this.ws = new WebSocket(wsUrl); + this.ws.binaryType = 'arraybuffer'; + + const timeout = setTimeout(() => { + if (this.ws) { + this.ws.close(); + reject(new Error('Authentication timeout')); + } + }, 10000); // 10 second timeout + + this.ws.onopen = () => { + console.log('WebSocket connected for authentication'); + // Send authentication request to RdpBroker + this.ws.send(JSON.stringify({ + type: 'authenticate', + username: this.credentials.username, + password: this.credentials.password + })); + }; + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + + if (message.type === 'targets') { + // Received user-specific targets from RdpBroker + clearTimeout(timeout); + console.log('Received targets from RdpBroker:', message.targets); + this.showTargetsView(message.targets); + + // Reset login button + const loginBtn = document.getElementById('loginBtn'); + const btnText = loginBtn.querySelector('.btn-text'); + const spinner = loginBtn.querySelector('.spinner'); + loginBtn.disabled = false; + btnText.style.display = 'block'; + spinner.style.display = 'none'; + + resolve(); + } else if (message.type === 'error') { + clearTimeout(timeout); + this.ws.close(); + this.ws = null; + reject(new Error(message.error || 'Authentication failed')); + } + } catch (e) { + console.error('Error parsing WebSocket message:', e); + } + }; + + this.ws.onerror = (error) => { + clearTimeout(timeout); + console.error('WebSocket error:', error); + reject(new Error('WebSocket connection failed')); + }; + + this.ws.onclose = () => { + clearTimeout(timeout); + if (this.ws) { + console.log('WebSocket closed during authentication'); + } + }; + }); } showTargetsView(targets = null, errorMsg = null) { @@ -158,27 +207,17 @@ class RDPWebGateway { } initializeRDPConnection(target) { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/rdp`; + // WebSocket already connected from authentication + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.error('WebSocket not connected'); + this.showConnectionError('Connection lost. Please login again.'); + return; + } this.canvas = document.getElementById('rdpCanvas'); this.ctx = this.canvas.getContext('2d'); - // Connect WebSocket - this.ws = new WebSocket(wsUrl); - this.ws.binaryType = 'arraybuffer'; - - this.ws.onopen = () => { - console.log('WebSocket connected'); - // Send credentials and connection request to RdpBroker - this.ws.send(JSON.stringify({ - type: 'connect', - username: this.credentials.username, - password: this.credentials.password, - target: target, - })); - }; - + // Update message handler for RDP session this.ws.onmessage = (event) => { this.handleWebSocketMessage(event); }; @@ -193,6 +232,13 @@ class RDPWebGateway { this.handleDisconnect(); }; + // Send target selection to RdpBroker + console.log('Connecting to target:', target.name); + this.ws.send(JSON.stringify({ + type: 'connect', + target: target + })); + // Setup canvas input handlers this.setupCanvasInputHandlers(); } diff --git a/web-gateway/src/rdpProxyHandler.js b/web-gateway/src/rdpProxyHandler.js index 7c71986..e675a5d 100644 --- a/web-gateway/src/rdpProxyHandler.js +++ b/web-gateway/src/rdpProxyHandler.js @@ -7,11 +7,17 @@ class RDPProxyHandler { this.rdpBrokerPort = rdpBrokerPort; this.rdpSocket = null; this.isConnected = false; + this.isAuthenticated = false; + this.dataBuffer = ''; + this.pendingTarget = null; } async handleMessage(message) { try { switch (message.type) { + case 'authenticate': + await this.handleAuthenticate(message); + break; case 'connect': await this.handleConnect(message); break; @@ -33,41 +39,28 @@ class RDPProxyHandler { } } - async handleConnect(message) { - const { username, password, target } = message; + async handleAuthenticate(message) { + const { username, password } = message; - console.log(`${new Date().toISOString()} [INFO] Connecting to RDP Broker, target: ${target?.name || 'unknown'}`); + console.log(`${new Date().toISOString()} [INFO] Authenticating user: ${username}`); try { - // Connect to RDP Broker + // Connect to RDP Broker for authentication this.rdpSocket = new net.Socket(); this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => { - console.log(`${new Date().toISOString()} [INFO] Connected to RDP Broker at ${this.rdpBrokerHost}:${this.rdpBrokerPort}`); + console.log(`${new Date().toISOString()} [INFO] Connected to RDP Broker for authentication`); this.isConnected = true; - // Send credentials to RdpBroker (it will handle authentication) - this.sendAuthToBroker(username, password); - - this.ws.send(JSON.stringify({ - type: 'connected', - target: target?.name || 'RDP Server' - })); - - // Set canvas size - this.ws.send(JSON.stringify({ - type: 'resize', - width: 1920, - height: 1080 - })); + // Send authentication request to RdpBroker + // Protocol: AUTH\nusername\npassword\n + const authMessage = `AUTH\n${username}\n${password}\n`; + this.rdpSocket.write(authMessage); }); // Handle data from RDP Broker this.rdpSocket.on('data', (data) => { - // Forward RDP data to WebSocket client - if (this.ws.readyState === 1) { // WebSocket.OPEN - this.ws.send(data); - } + this.handleBrokerData(data); }); this.rdpSocket.on('error', (error) => { @@ -79,26 +72,106 @@ class RDPProxyHandler { this.rdpSocket.on('close', () => { console.log(`${new Date().toISOString()} [INFO] RDP connection closed`); this.isConnected = false; - if (this.ws.readyState === 1) { - this.ws.close(); + if (!this.isAuthenticated && this.ws.readyState === 1) { + this.sendError('Authentication failed'); } }); } catch (error) { - console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error); - this.sendError('Failed to connect to RDP broker'); + console.error(`${new Date().toISOString()} [ERROR] Authentication error:`, error); + this.sendError('Failed to authenticate with RDP broker'); } } - sendAuthToBroker(username, password) { - // Send credentials directly to RdpBroker - // RdpBroker will handle Samba AD authentication - - if (!this.rdpSocket) return; + handleBrokerData(data) { + // Accumulate data in buffer + this.dataBuffer += data.toString(); - // Format: "Username: \nPassword: \n" - const authMessage = `${username}\n${password}\n`; - this.rdpSocket.write(authMessage); + // Check if we have complete message (ends with \n\n or specific delimiter) + if (this.dataBuffer.includes('\n\n')) { + const messages = this.dataBuffer.split('\n\n'); + this.dataBuffer = messages.pop(); // Keep incomplete part in buffer + + messages.forEach(msg => { + if (msg.trim()) { + this.processBrokerMessage(msg.trim()); + } + }); + } + + // If already authenticated and in RDP session, forward raw data to WebSocket + if (this.isAuthenticated && this.pendingTarget === null) { + if (this.ws.readyState === 1) { + this.ws.send(data); + } + } + } + + processBrokerMessage(message) { + try { + // Try to parse as JSON first (for structured messages) + const data = JSON.parse(message); + + if (data.type === 'auth_success') { + // Authentication successful, targets list received + console.log(`${new Date().toISOString()} [INFO] Authentication successful`); + this.isAuthenticated = true; + + // Send targets to client + this.ws.send(JSON.stringify({ + type: 'targets', + targets: data.targets || [] + })); + + } else if (data.type === 'auth_failed') { + // Authentication failed + console.log(`${new Date().toISOString()} [WARN] Authentication failed: ${data.message}`); + this.sendError(data.message || 'Invalid credentials'); + this.cleanup(); + + } else if (data.type === 'rdp_ready') { + // RDP session ready, start forwarding + console.log(`${new Date().toISOString()} [INFO] RDP session ready`); + this.ws.send(JSON.stringify({ + type: 'connected', + target: this.pendingTarget + })); + this.pendingTarget = null; + + } else { + console.warn(`${new Date().toISOString()} [WARN] Unknown broker message type:`, data.type); + } + } catch (e) { + // Not JSON, might be raw RDP data - ignore during auth phase + if (this.isAuthenticated) { + console.debug(`${new Date().toISOString()} [DEBUG] Non-JSON data from broker (RDP traffic)`); + } + } + } + + async handleConnect(message) { + const { target } = message; + + if (!this.isAuthenticated) { + this.sendError('Must authenticate before connecting to target'); + return; + } + + console.log(`${new Date().toISOString()} [INFO] Connecting to target: ${target?.name || 'unknown'}`); + this.pendingTarget = target?.name || 'RDP Server'; + + try { + // Send target selection to RdpBroker + // Protocol: SELECT\ntarget_name\n + const selectMessage = `SELECT\n${target?.name}\n`; + this.rdpSocket.write(selectMessage); + + // RdpBroker will respond with rdp_ready message + + } catch (error) { + console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error); + this.sendError('Failed to connect to target'); + } } handleMouseEvent(message) {