/* * RDP-Session-Client: guacamole-common-js <-> /ws/rdp/{host_id}. * * Vollbild: native Browser-Fullscreen-API (Konzept 4.3). * Copy & Paste: bidirektionale Synchronisation ueber Guacamole.Client * onclipboard-Event (Ziel -> Browser) und den `paste`-Browser-Event * (Browser -> Ziel, sendet eine "clipboard"-Instruktion). Wird serverseitig * zusaetzlich blockiert, wenn fuer den Host clipboard_enabled=false ist * (siehe app/rdp_proxy/ws_tunnel.py) -- das UI blendet den Hinweis dann ein. */ (() => { "use strict"; const shell = document.getElementById("session-shell"); const hostId = shell.dataset.hostId; const statusEl = document.getElementById("status"); const displayDiv = document.getElementById("rdp-display"); const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; const width = Math.round(window.innerWidth); const height = Math.round(window.innerHeight - 40); const dpi = Math.round(window.devicePixelRatio * 96) || 96; // WICHTIG: die Verbindungsparameter gehoeren NICHT in die Tunnel-URL. // Guacamole.WebSocketTunnel.connect(data) baut die Socket-URL selbst als // `tunnelURL + "?" + data` zusammen. Standen die Parameter schon in der // URL, entstand daraus `...?width=1280&height=800&dpi=96?undefined` -- der // letzte Query-Parameter war damit kein gueltiger Integer mehr, FastAPI // wies den WebSocket noch vor dem Routenhandler ab (HTTP 422) und im // Verbindungslog des Servers tauchte kein einziger Eintrag auf. const tunnelUrl = `${proto}//${window.location.host}/ws/rdp/${hostId}`; const connectParams = `width=${width}&height=${height}&dpi=${dpi}`; const tunnel = new Guacamole.WebSocketTunnel(tunnelUrl); const client = new Guacamole.Client(tunnel); displayDiv.appendChild(client.getDisplay().getElement()); client.onstatechange = (state) => { // 0=idle,1=connecting,2=waiting,3=connected,4=disconnecting,5=disconnected const labels = ["Idle", "Verbinde ...", "Warte auf Server ...", "Verbunden", "Trenne ...", "Getrennt"]; statusEl.textContent = labels[state] || `Status ${state}`; }; const showError = (err) => { // Der Server gibt den Abbruchgrund als WebSocket-Close-Reason mit // (siehe _reject() in app/rdp_proxy/ws_tunnel.py); guacamole-common-js // reicht ihn als Guacamole.Status.message hierher durch. statusEl.textContent = "Fehler: " + ((err && err.message) || "unbekannt"); statusEl.classList.add("error"); // Troubleshooting: Statuscode + Rohobjekt zusaetzlich in die // Browser-Konsole, fuer den Fall, dass die Klartextmeldung allein nicht // reicht (z.B. guacd/FreeRDP-interne Codes). console.error("RDP-Sitzung beendet/Fehler:", err); }; client.onerror = showError; tunnel.onerror = showError; client.onclipboard = (stream, mimetype) => { if (!mimetype.startsWith("text/")) return; const reader = new Guacamole.StringReader(stream); let data = ""; reader.ontext = (text) => { data += text; }; reader.onend = () => { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(data).catch(() => {}); } }; }; client.connect(connectParams); window.addEventListener("beforeunload", () => client.disconnect()); const mouse = new Guacamole.Mouse(client.getDisplay().getElement()); mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = (mouseState) => { client.sendMouseState(mouseState); }; const keyboard = new Guacamole.Keyboard(document); keyboard.onkeydown = (keysym) => client.sendKeyEvent(1, keysym); keyboard.onkeyup = (keysym) => client.sendKeyEvent(0, keysym); document.addEventListener("paste", (ev) => { const text = (ev.clipboardData || window.clipboardData).getData("text"); if (!text) return; const stream = client.createClipboardStream("text/plain"); const writer = new Guacamole.StringWriter(stream); writer.sendText(text); writer.sendEnd(); }); document.getElementById("fullscreen-btn").addEventListener("click", () => { if (!document.fullscreenElement) { shell.requestFullscreen().catch(() => {}); } else { document.exitFullscreen(); } }); // Strg+Alt+Entf: der Browser faengt diese Kombination selbst ab (Windows // reserviert sie systemweit), sie kommt also nie als normales Tastatur- // Event beim RDP-Ziel an. Abhilfe wie im offiziellen Guacamole-Client: // die drei Tasten einzeln als synthetische Key-Events senden (X11- // Keysyms: Ctrl=0xFFE3, Alt=0xFFE9, Delete=0xFFFF), erst alle drei // herunter- dann in umgekehrter Reihenfolge wieder hochdruecken. document.getElementById("ctrlaltdel-btn").addEventListener("click", () => { const keys = [0xffe3, 0xffe9, 0xffff]; for (const keysym of keys) client.sendKeyEvent(1, keysym); for (const keysym of keys.slice().reverse()) client.sendKeyEvent(0, keysym); }); document.getElementById("exit-btn").addEventListener("click", () => { statusEl.textContent = "Sitzung wird beendet ..."; statusEl.classList.remove("error"); try { client.disconnect(); } catch (_) { /* bereits getrennt */ } window.close(); // Siehe terminal.js fuer die ausfuehrliche Begruendung: window.close() // schliesst den Tab nur unter bestimmten Voraussetzungen lautlos -- // deshalb zusaetzlich immer ein Fallback zum Dashboard. window.setTimeout(() => { window.location.href = "/dashboard"; }, 300); }); })();