UNPKG

guacamole-lite

Version:

Library to create servers compatible with the Guacamole protocol. Guacamole is a web client for RDP/VNC/SSH/Telnet

292 lines (253 loc) 10.4 kB
// DOM elements const connectionScreen = document.getElementById('connection-screen'); const displayScreen = document.getElementById('display-screen'); const connectTargetRadios = document.querySelectorAll('input[name="connect-target"]'); const remoteHostDetailsDiv = document.getElementById('remote-host-details'); const connectButton = document.getElementById('connect-button'); const closeButton = document.getElementById('close-button'); const protocolRadios = document.querySelectorAll('input[name="protocol"]'); const remoteHostnameInput = document.getElementById('remote-hostname'); const remoteUsernameInput = document.getElementById('remote-username'); const remotePasswordInput = document.getElementById('remote-password'); let currentClient = null; // Store the current Guacamole client let currentKeyboard = null; // Store the keyboard handler let pasteEventListener = null; // Store the paste event listener reference // Helper function to get selected radio value function getSelectedRadioValue(name) { return document.querySelector(`input[name="${name}"]:checked`).value; } // Toggle remote host details visibility when connect target changes connectTargetRadios.forEach(radio => { radio.addEventListener('change', () => { if (getSelectedRadioValue('connect-target') === 'remote-host') { remoteHostDetailsDiv.classList.add('visible'); } else { remoteHostDetailsDiv.classList.remove('visible'); } }); }); // Connect button click handler connectButton.addEventListener('click', async () => { const target = getSelectedRadioValue('connect-target'); const protocol = getSelectedRadioValue('protocol'); let connectionSettings; // Base connection settings connectionSettings = { hostname: target === 'test-host' ? 'desktop-linux' : remoteHostnameInput.value, username: target === 'test-host' ? 'testuser' : remoteUsernameInput.value, password: target === 'test-host' ? 'Passw0rd!' : remotePasswordInput.value, }; if (protocol === 'rdp') { Object.assign(connectionSettings, { "ignore-cert": true, "security": "any", "enable-drive": true, "drive-path": "/tmp/guac-drive", "create-drive-path": true, "enable-printing": true, "audio": ["audio/L16;rate=44100"] }); } // Add VNC-specific settings if (protocol === 'vnc') { Object.assign(connectionSettings, { port: 5900, autoretry: 3, color_depth: 24, swap_red_blue: false, connect_timeout: 15 }); } const tokenObj = { connection: { type: protocol, settings: connectionSettings } }; try { // Clear previous display if any const displayDiv = document.getElementById('display'); while (displayDiv.firstChild) { displayDiv.removeChild(displayDiv.firstChild); } console.log("Generating token for:", tokenObj); console.log(`Connecting with ${protocol.toUpperCase()} to ${connectionSettings.hostname}:${connectionSettings.port || (protocol === 'rdp' ? '3389' : '5900')}`); const token = await generateGuacamoleToken(tokenObj); console.log("Token generated, initializing Guacamole..."); // Initialize Guacamole client initializeGuacamoleClient(token, protocol); } catch (error) { console.error("Failed to connect:", error); alert("Connection failed: " + error.message); // Switch back to connection screen on error displayScreen.style.display = 'none'; connectionScreen.style.display = 'flex'; } }); // Function to initialize Guacamole client function initializeGuacamoleClient(token, protocol) { // Switch to display screen before initializing to avoid UI jumping connectionScreen.style.display = 'none'; displayScreen.style.display = 'flex'; // Update display title with connection info const displayTitle = document.getElementById('display-title'); const target = getSelectedRadioValue('connect-target'); displayTitle.textContent = `Connected to: ${target === 'test-host' ? 'Test Host' : remoteHostnameInput.value} (${protocol.toUpperCase()})`; try { // Create WebSocket tunnel const tunnel = new Guacamole.WebSocketTunnel(`ws://${location.hostname}:9091/`); // Create client const client = new Guacamole.Client(tunnel); currentClient = client; // Add client display to the page const displayDiv = document.getElementById("display"); displayDiv.appendChild(client.getDisplay().getElement()); // Set up error handler client.onerror = function(error) { console.error("Guacamole error:", error); let errorMessage = error.message || "Unknown error"; // Enhanced error messages for common issues if (protocol === 'vnc' && errorMessage.includes("connect")) { errorMessage = "VNC Connection Error: Could not connect to VNC server. Please verify the host is running a VNC server on port 5900."; } else if (protocol === 'rdp' && errorMessage.includes("connect")) { errorMessage = "RDP Connection Error: Could not connect to RDP server. Please verify the host is running and accepting RDP connections."; } alert("Guacamole error: " + errorMessage); }; // Set up clipboard handler client.onclipboard = (stream, mimetype) => { let data = ''; const reader = new Guacamole.StringReader(stream); reader.ontext = text => data += text; reader.onend = () => { console.log("Clipboard data received:", data); // Update the hidden textarea and trigger copy const textarea = document.getElementById('clipboard-textarea'); if (textarea) { textarea.value = data; textarea.select(); try { const successful = document.execCommand('copy'); const msg = successful ? 'successful' : 'unsuccessful'; console.log('Copying text command was ' + msg); } catch (err) { console.error('Failed to copy text: ', err); } // Deselect the text to avoid visual artifacts window.getSelection().removeAllRanges(); } }; }; // Set up file download handler client.onfile = (stream, mimetype, filename) => { stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); const reader = new Guacamole.BlobReader(stream, mimetype); reader.onprogress = (length) => { console.log(`Downloaded ${length} bytes of ${filename}`); }; reader.onend = () => { // Automatically create a link and download the file const file = reader.getBlob(); const url = URL.createObjectURL(file); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); console.log(`File download complete: ${filename}`); }, 100); }; }; // Set up mouse const mouse = new Guacamole.Mouse(client.getDisplay().getElement()); mouse.onEach(['mousedown', 'mouseup', 'mousemove', 'mousewheel'], e => client.sendMouseState(e.state)); // Set up keyboard const keyboard = new Guacamole.Keyboard(window); keyboard.onkeydown = keysym => client.sendKeyEvent(1, keysym); keyboard.onkeyup = keysym => client.sendKeyEvent(0, keysym); currentKeyboard = keyboard; // Set up paste event listener pasteEventListener = (event) => { const text = event.clipboardData.getData('text/plain'); if (text && currentClient) { event.preventDefault(); // Prevent default paste behavior in browser // Send clipboard data to the remote session const stream = currentClient.createClipboardStream('text/plain'); const writer = new Guacamole.StringWriter(stream); writer.sendText(text); writer.sendEnd(); console.log("Sent clipboard data to remote:", text); } }; window.addEventListener('paste', pasteEventListener); // Connect to the remote desktop // Construct connection string, adding audio only if RDP let connectString = `token=${encodeURIComponent(token)}`; if (protocol === 'rdp') { connectString += `&GUAC_AUDIO=audio/L16`; } client.connect(connectString); console.log("Guacamole client initialized and connected"); } catch (error) { // Clean up any partially created resources cleanupGuacamole(); // Show error and return to connection screen console.error("Error initializing Guacamole:", error); alert("Error initializing Guacamole: " + error.message); displayScreen.style.display = 'none'; connectionScreen.style.display = 'flex'; } } // Close button click handler closeButton.addEventListener('click', () => { cleanupGuacamole(); // Switch back to connection screen displayScreen.style.display = 'none'; connectionScreen.style.display = 'flex'; }); // Function to properly clean up all Guacamole resources function cleanupGuacamole() { if (currentClient) { // Disconnect the client try { currentClient.disconnect(); } catch (e) { console.error("Error disconnecting client:", e); } currentClient = null; } // Properly detach keyboard handler if (currentKeyboard) { try { // Remove existing handlers currentKeyboard.onkeydown = null; currentKeyboard.onkeyup = null; // Reset the keyboard state completely currentKeyboard.reset(); } catch (e) { console.error("Error cleaning up keyboard:", e); } currentKeyboard = null; } // Remove paste event listener if it exists if (pasteEventListener) { window.removeEventListener('paste', pasteEventListener); pasteEventListener = null; } // Allow a brief moment for cleanup before making inputs focusable setTimeout(() => { // Re-focus on a form element to help ensure keyboard is working const firstInput = document.querySelector('#connection-form input, #connection-form select'); if (firstInput) { firstInput.focus(); } }, 100); } // Handle page unloads to clean up any active sessions window.addEventListener('beforeunload', () => { cleanupGuacamole(); });