Interactive list

This commit is contained in:
Serge NOEL
2025-12-04 09:39:42 +01:00
parent cfe610c75f
commit 2080559f46
4 changed files with 608 additions and 83 deletions

336
PROTOCOL.md Normal file
View File

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

View File

@@ -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 - 🌐 **Browser-Based Access** - Connect to RDP sessions from any modern web browser
- 🔒 **Secure WebSocket** - Real-time bidirectional communication - 🔒 **Secure WebSocket** - Real-time bidirectional communication
- 🎨 **Modern UI** - Clean, responsive interface - 🎨 **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 - 📊 **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 -**Low Latency** - Optimized for performance
- ☁️ **Kubernetes Native** - Console-only logging for cloud environments - ☁️ **Kubernetes Native** - Console-only logging for cloud environments
- 🔐 **Samba AD Integration** - Authentication via RdpBroker with Samba Active Directory
## Architecture ## Architecture
@@ -19,12 +20,29 @@ HTML5 WebSocket-based gateway for accessing RDP connections through a web browse
User Browser (HTML5/WebSocket) User Browser (HTML5/WebSocket)
RDP Web Gateway (Node.js) 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 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 ## Prerequisites
- Node.js 18+ - Node.js 18+
@@ -147,22 +165,74 @@ Fetch available RDP targets.
Connect to `ws://localhost:8080/ws/rdp` 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 ```json
{ {
"type": "connect", "type": "connect",
"username": "user@domain.com",
"password": "password123",
"target": { "target": {
"name": "Server 01", "name": "Windows Server 2022",
"host": "192.168.1.10", "host": "ws2022.example.com",
"port": 3389 "port": 3389
} }
} }
``` ```
**Server → Client - RDP Session Ready:**
```json
{
"type": "connected",
"target": "Windows Server 2022"
}
```
#### Client → Server Messages
**Mouse event:** **Mouse event:**
```json ```json
{ {

View File

@@ -72,41 +72,90 @@ class RDPWebGateway {
return; return;
} }
// Store credentials temporarily - will be sent to RdpBroker // Store credentials and authenticate via WebSocket
this.currentUser = username; this.currentUser = username;
this.credentials = { username, password }; this.credentials = { username, password };
// Load targets and show targets view // Authenticate and get user-specific targets from RdpBroker
await this.loadTargets(); await this.authenticateAndLoadTargets();
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
this.showError(errorMessage, 'Connection error. Please check your network and try again.'); this.showError(errorMessage, 'Connection error. Please check your network and try again.');
} finally {
loginBtn.disabled = false; loginBtn.disabled = false;
btnText.style.display = 'block'; btnText.style.display = 'block';
spinner.style.display = 'none'; spinner.style.display = 'none';
} }
} }
async loadTargets() { authenticateAndLoadTargets() {
return new Promise((resolve, reject) => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/rdp`;
// Create WebSocket connection for authentication
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
const timeout = setTimeout(() => {
if (this.ws) {
this.ws.close();
reject(new Error('Authentication timeout'));
}
}, 10000); // 10 second timeout
this.ws.onopen = () => {
console.log('WebSocket connected for authentication');
// Send authentication request to RdpBroker
this.ws.send(JSON.stringify({
type: 'authenticate',
username: this.credentials.username,
password: this.credentials.password
}));
};
this.ws.onmessage = (event) => {
try { try {
const response = await fetch('/api/targets'); const message = JSON.parse(event.data);
if (!response.ok) { if (message.type === 'targets') {
if (response.status === 503) { // Received user-specific targets from RdpBroker
const error = await response.json(); clearTimeout(timeout);
throw new Error(error.error || 'Service unavailable'); console.log('Received targets from RdpBroker:', message.targets);
} this.showTargetsView(message.targets);
throw new Error('Failed to load targets');
}
const data = await response.json(); // Reset login button
this.showTargetsView(data.targets); const loginBtn = document.getElementById('loginBtn');
} catch (error) { const btnText = loginBtn.querySelector('.btn-text');
console.error('Error loading targets:', error); const spinner = loginBtn.querySelector('.spinner');
// Show error in targets view loginBtn.disabled = false;
this.showTargetsView(null, error.message); 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) { showTargetsView(targets = null, errorMsg = null) {
@@ -158,27 +207,17 @@ class RDPWebGateway {
} }
initializeRDPConnection(target) { initializeRDPConnection(target) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // WebSocket already connected from authentication
const wsUrl = `${protocol}//${window.location.host}/ws/rdp`; 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.canvas = document.getElementById('rdpCanvas');
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
// Connect WebSocket // Update message handler for RDP session
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,
}));
};
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
this.handleWebSocketMessage(event); this.handleWebSocketMessage(event);
}; };
@@ -193,6 +232,13 @@ class RDPWebGateway {
this.handleDisconnect(); 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 // Setup canvas input handlers
this.setupCanvasInputHandlers(); this.setupCanvasInputHandlers();
} }

View File

@@ -7,11 +7,17 @@ class RDPProxyHandler {
this.rdpBrokerPort = rdpBrokerPort; this.rdpBrokerPort = rdpBrokerPort;
this.rdpSocket = null; this.rdpSocket = null;
this.isConnected = false; this.isConnected = false;
this.isAuthenticated = false;
this.dataBuffer = '';
this.pendingTarget = null;
} }
async handleMessage(message) { async handleMessage(message) {
try { try {
switch (message.type) { switch (message.type) {
case 'authenticate':
await this.handleAuthenticate(message);
break;
case 'connect': case 'connect':
await this.handleConnect(message); await this.handleConnect(message);
break; break;
@@ -33,41 +39,28 @@ class RDPProxyHandler {
} }
} }
async handleConnect(message) { async handleAuthenticate(message) {
const { username, password, target } = 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 { try {
// Connect to RDP Broker // Connect to RDP Broker for authentication
this.rdpSocket = new net.Socket(); this.rdpSocket = new net.Socket();
this.rdpSocket.connect(this.rdpBrokerPort, this.rdpBrokerHost, () => { 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; this.isConnected = true;
// Send credentials to RdpBroker (it will handle authentication) // Send authentication request to RdpBroker
this.sendAuthToBroker(username, password); // Protocol: AUTH\nusername\npassword\n
const authMessage = `AUTH\n${username}\n${password}\n`;
this.ws.send(JSON.stringify({ this.rdpSocket.write(authMessage);
type: 'connected',
target: target?.name || 'RDP Server'
}));
// Set canvas size
this.ws.send(JSON.stringify({
type: 'resize',
width: 1920,
height: 1080
}));
}); });
// Handle data from RDP Broker // Handle data from RDP Broker
this.rdpSocket.on('data', (data) => { this.rdpSocket.on('data', (data) => {
// Forward RDP data to WebSocket client this.handleBrokerData(data);
if (this.ws.readyState === 1) { // WebSocket.OPEN
this.ws.send(data);
}
}); });
this.rdpSocket.on('error', (error) => { this.rdpSocket.on('error', (error) => {
@@ -79,28 +72,108 @@ class RDPProxyHandler {
this.rdpSocket.on('close', () => { this.rdpSocket.on('close', () => {
console.log(`${new Date().toISOString()} [INFO] RDP connection closed`); console.log(`${new Date().toISOString()} [INFO] RDP connection closed`);
this.isConnected = false; this.isConnected = false;
if (this.ws.readyState === 1) { if (!this.isAuthenticated && this.ws.readyState === 1) {
this.ws.close(); this.sendError('Authentication failed');
} }
}); });
} catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Authentication error:`, error);
this.sendError('Failed to authenticate with RDP broker');
}
}
handleBrokerData(data) {
// Accumulate data in buffer
this.dataBuffer += data.toString();
// Check if we have complete message (ends with \n\n or specific delimiter)
if (this.dataBuffer.includes('\n\n')) {
const messages = this.dataBuffer.split('\n\n');
this.dataBuffer = messages.pop(); // Keep incomplete part in buffer
messages.forEach(msg => {
if (msg.trim()) {
this.processBrokerMessage(msg.trim());
}
});
}
// 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) { } catch (error) {
console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error); console.error(`${new Date().toISOString()} [ERROR] Connection error:`, error);
this.sendError('Failed to connect to RDP broker'); this.sendError('Failed to connect to target');
} }
} }
sendAuthToBroker(username, password) {
// Send credentials directly to RdpBroker
// RdpBroker will handle Samba AD authentication
if (!this.rdpSocket) return;
// Format: "Username: <username>\nPassword: <password>\n"
const authMessage = `${username}\n${password}\n`;
this.rdpSocket.write(authMessage);
}
handleMouseEvent(message) { handleMouseEvent(message) {
if (!this.isConnected || !this.rdpSocket) return; if (!this.isConnected || !this.rdpSocket) return;