Interactive list
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user