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
- 🔒 **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
{

View File

@@ -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();
}

View File

@@ -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: <username>\nPassword: <password>\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) {