464 lines
16 KiB
JavaScript
464 lines
16 KiB
JavaScript
class RDPWebGateway {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.currentUser = null;
|
|
this.currentTarget = null;
|
|
this.credentials = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Login form
|
|
const loginForm = document.getElementById('loginForm');
|
|
if (loginForm) {
|
|
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
|
|
}
|
|
|
|
// Logout button
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => this.handleLogout());
|
|
}
|
|
|
|
// Disconnect button
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
if (disconnectBtn) {
|
|
disconnectBtn.addEventListener('click', () => this.handleDisconnect());
|
|
}
|
|
|
|
// Fullscreen button
|
|
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
|
if (fullscreenBtn) {
|
|
fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
|
|
}
|
|
|
|
// Ctrl+Alt+Del button
|
|
const ctrlAltDelBtn = document.getElementById('ctrlAltDelBtn');
|
|
if (ctrlAltDelBtn) {
|
|
ctrlAltDelBtn.addEventListener('click', () => this.sendCtrlAltDel());
|
|
}
|
|
}
|
|
|
|
async handleLogin(e) {
|
|
e.preventDefault();
|
|
|
|
const username = document.getElementById('username').value;
|
|
const password = document.getElementById('password').value;
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
const btnText = loginBtn.querySelector('.btn-text');
|
|
const spinner = loginBtn.querySelector('.spinner');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
|
|
// Show loading state
|
|
loginBtn.disabled = true;
|
|
btnText.style.display = 'none';
|
|
spinner.style.display = 'block';
|
|
errorMessage.style.display = 'none';
|
|
|
|
try {
|
|
// Check if RdpBroker service is available
|
|
const statusResponse = await fetch('/api/broker-status');
|
|
const statusData = await statusResponse.json();
|
|
|
|
if (!statusData.available) {
|
|
this.showError(errorMessage, 'RDP service is currently unavailable. Please contact your administrator.');
|
|
return;
|
|
}
|
|
|
|
// Store credentials and authenticate via WebSocket
|
|
this.currentUser = username;
|
|
this.credentials = { username, password };
|
|
|
|
// Authenticate and get user-specific targets from RdpBroker
|
|
await this.authenticateAndLoadTargets();
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
// Show specific error message if available
|
|
const errorMsg = error.message || 'Connection error. Please check your network and try again.';
|
|
this.showError(errorMessage, errorMsg);
|
|
loginBtn.disabled = false;
|
|
btnText.style.display = 'block';
|
|
spinner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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) {
|
|
document.getElementById('loginCard').style.display = 'none';
|
|
document.getElementById('targetsCard').style.display = 'block';
|
|
document.getElementById('rdpViewer').style.display = 'none';
|
|
document.getElementById('currentUser').textContent = this.currentUser;
|
|
|
|
if (errorMsg) {
|
|
const targetsList = document.getElementById('targetsList');
|
|
targetsList.innerHTML = `
|
|
<div style="text-align: center; padding: 20px;">
|
|
<p style="color: var(--error-color); margin-bottom: 10px;">⚠️ ${this.escapeHtml(errorMsg)}</p>
|
|
<button onclick="location.reload()" class="btn btn-secondary">Retry</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
this.displayTargets(targets);
|
|
}
|
|
|
|
displayTargets(targets) {
|
|
const targetsList = document.getElementById('targetsList');
|
|
targetsList.innerHTML = '';
|
|
|
|
if (!targets || targets.length === 0) {
|
|
targetsList.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No remote desktops available</p>';
|
|
return;
|
|
}
|
|
|
|
targets.forEach(target => {
|
|
const targetItem = document.createElement('div');
|
|
targetItem.className = 'target-item';
|
|
targetItem.innerHTML = `
|
|
<div class="target-name">${this.escapeHtml(target.name)}</div>
|
|
<div class="target-description">${this.escapeHtml(target.description)}</div>
|
|
<div class="target-host">${this.escapeHtml(target.host)}:${target.port}</div>
|
|
`;
|
|
targetItem.addEventListener('click', () => this.connectToTarget(target));
|
|
targetsList.appendChild(targetItem);
|
|
});
|
|
}
|
|
|
|
async connectToTarget(target) {
|
|
this.currentTarget = target;
|
|
this.showRDPViewer();
|
|
this.initializeRDPConnection(target);
|
|
}
|
|
|
|
initializeRDPConnection(target) {
|
|
// 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');
|
|
|
|
// Update message handler for RDP session
|
|
this.ws.onmessage = (event) => {
|
|
this.handleWebSocketMessage(event);
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
this.showConnectionError('Connection error occurred');
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.log('WebSocket closed');
|
|
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();
|
|
}
|
|
|
|
handleWebSocketMessage(event) {
|
|
try {
|
|
const message = JSON.parse(event.data);
|
|
|
|
switch (message.type) {
|
|
case 'connected':
|
|
document.getElementById('loadingOverlay').style.display = 'none';
|
|
document.getElementById('connectionInfo').textContent =
|
|
`Connected to ${this.currentTarget.name}`;
|
|
console.log('RDP connection established');
|
|
break;
|
|
|
|
case 'screen':
|
|
// Render screen update (PNG image data)
|
|
this.renderScreenUpdate(message);
|
|
break;
|
|
|
|
case 'disconnected':
|
|
this.showConnectionError('RDP connection closed');
|
|
break;
|
|
|
|
case 'error':
|
|
this.showConnectionError(message.error);
|
|
break;
|
|
|
|
default:
|
|
console.warn('Unknown message type:', message.type);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling WebSocket message:', error);
|
|
}
|
|
}
|
|
|
|
renderScreenUpdate(update) {
|
|
try {
|
|
// Decode base64 PNG image
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
// Draw image to canvas at specified position
|
|
this.ctx.drawImage(img, update.x, update.y, update.width, update.height);
|
|
};
|
|
img.onerror = (error) => {
|
|
console.error('Failed to load screen update image:', error);
|
|
};
|
|
img.src = 'data:image/png;base64,' + update.data;
|
|
} catch (error) {
|
|
console.error('Error rendering screen update:', error);
|
|
}
|
|
}
|
|
|
|
setupCanvasInputHandlers() {
|
|
const canvas = this.canvas;
|
|
|
|
// Mouse events
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
this.sendMouseEvent('move', e);
|
|
});
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
this.sendMouseEvent('down', e);
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', (e) => {
|
|
this.sendMouseEvent('up', e);
|
|
});
|
|
|
|
canvas.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
this.sendMouseEvent('wheel', e);
|
|
});
|
|
|
|
// Keyboard events
|
|
window.addEventListener('keydown', (e) => {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
e.preventDefault();
|
|
this.sendKeyEvent('down', e);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keyup', (e) => {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
e.preventDefault();
|
|
this.sendKeyEvent('up', e);
|
|
}
|
|
});
|
|
|
|
// Prevent context menu
|
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
}
|
|
|
|
sendMouseEvent(type, event) {
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = Math.floor((event.clientX - rect.left) * (this.canvas.width / rect.width));
|
|
const y = Math.floor((event.clientY - rect.top) * (this.canvas.height / rect.height));
|
|
|
|
// Map button: 0=left, 1=middle, 2=right
|
|
let button = 0;
|
|
if (type === 'down' || type === 'up') {
|
|
button = event.button;
|
|
}
|
|
|
|
this.ws.send(JSON.stringify({
|
|
type: 'mouse',
|
|
action: type,
|
|
x: x,
|
|
y: y,
|
|
button: button
|
|
}));
|
|
}
|
|
|
|
sendKeyEvent(type, event) {
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
this.ws.send(JSON.stringify({
|
|
type: 'keyboard',
|
|
action: type,
|
|
code: event.keyCode || event.which
|
|
}));
|
|
}
|
|
keyCode: event.keyCode,
|
|
ctrlKey: event.ctrlKey,
|
|
altKey: event.altKey,
|
|
shiftKey: event.shiftKey,
|
|
}));
|
|
}
|
|
|
|
sendCtrlAltDel() {
|
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
this.ws.send(JSON.stringify({
|
|
type: 'special',
|
|
action: 'ctrl-alt-del',
|
|
}));
|
|
}
|
|
|
|
toggleFullscreen() {
|
|
const viewer = document.getElementById('rdpViewer');
|
|
|
|
if (!document.fullscreenElement) {
|
|
viewer.requestFullscreen().catch(err => {
|
|
console.error('Error attempting to enable fullscreen:', err);
|
|
});
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
handleDisconnect() {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
this.currentTarget = null;
|
|
this.showTargetsView();
|
|
}
|
|
|
|
handleLogout() {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
this.currentUser = null;
|
|
this.currentTarget = null;
|
|
this.credentials = null;
|
|
this.showLoginView();
|
|
}
|
|
|
|
showLoginView() {
|
|
document.getElementById('loginCard').style.display = 'block';
|
|
document.getElementById('targetsCard').style.display = 'none';
|
|
document.getElementById('rdpViewer').style.display = 'none';
|
|
document.getElementById('username').value = '';
|
|
document.getElementById('password').value = '';
|
|
}
|
|
|
|
showRDPViewer() {
|
|
document.getElementById('loginCard').style.display = 'none';
|
|
document.getElementById('targetsCard').style.display = 'none';
|
|
document.getElementById('rdpViewer').style.display = 'block';
|
|
document.getElementById('loadingOverlay').style.display = 'flex';
|
|
}
|
|
|
|
showError(element, message) {
|
|
element.textContent = message;
|
|
element.style.display = 'block';
|
|
}
|
|
|
|
showConnectionError(message) {
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
overlay.innerHTML = `
|
|
<div style="text-align: center;">
|
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="32" cy="32" r="30" stroke="#e74c3c" stroke-width="4" fill="none"/>
|
|
<path d="M32 16v20M32 44v4" stroke="#e74c3c" stroke-width="4" stroke-linecap="round"/>
|
|
</svg>
|
|
<h3 style="color: white; margin-top: 16px;">Connection Failed</h3>
|
|
<p style="color: rgba(255,255,255,0.8); margin-top: 8px;">${this.escapeHtml(message)}</p>
|
|
<button class="btn btn-primary" onclick="rdpGateway.handleDisconnect()" style="margin-top: 20px;">
|
|
Back to Desktop Selection
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize the app
|
|
const rdpGateway = new RDPWebGateway();
|
|
// v1.4
|