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

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) {