umbau 1.2

This commit is contained in:
2026-09-03 01:22:00 +02:00
parent 685068c21b
commit 13542163d8
3 changed files with 148 additions and 82 deletions

View File

@ -36,8 +36,25 @@ def rdp_drive_path(host_id: int, session_id: int) -> Path:
A sah/ueberschrieb/las die Dateien von B. Je-Sitzung-Pfade schliessen das A sah/ueberschrieb/las die Dateien von B. Je-Sitzung-Pfade schliessen das
aus; Aufraeumen beim Sitzungsende siehe app/rdp_proxy/ws_tunnel.py aus; Aufraeumen beim Sitzungsende siehe app/rdp_proxy/ws_tunnel.py
(`_cleanup_rdp_drive_path`). (`_cleanup_rdp_drive_path`).
Bug-Fix (Live-Test, 2./3.9.): urspruenglich als ZWEI Verzeichnis-Ebenen
unter rdp-drives/ angelegt (.../rdp-drives/{host_id}/{session_id}).
guacds RDP-Parameter "create-drive-path" legt aber nachweislich NUR das
direkte Blattverzeichnis an, keine fehlenden Elternverzeichnisse (kein
"mkdir -p") -- fuer einen Host, der noch keine Sitzung mit Dateitransfer
hatte, existierte .../rdp-drives/{host_id}/ nie, wodurch guacd mit
"Unable to create directory ... No such file or directory" abbrach. Ein
Versuch, das Elternverzeichnis stattdessen im App-Prozess selbst
anzulegen, scheiterte am echten Testserver an den Dateirechten (rdp-drives/
gehoert dem guacd-Systembenutzer, Modus 750 -- der App-Prozess hat dort
keinerlei Zugriff). Deshalb jetzt EINE flache Ebene mit host_id UND
session_id im Verzeichnisnamen selbst: weiterhin pro Sitzung eindeutig
(Befund E6 bleibt gewahrt), aber guacd muss nur noch einen einzigen neuen
Eintrag direkt unter dem bereits existierenden rdp-drives/ anlegen --
funktioniert nachweislich, siehe Docstring oben ("vorher war der Pfad NUR
je Host angelegt", also genau diese Verschachtelungstiefe).
""" """
return settings.data_dir / "rdp-drives" / str(host_id) / str(session_id) return settings.data_dir / "rdp-drives" / f"{host_id}_{session_id}"
# Der von "connect"/"select" bis zur eigentlichen Bilduebertragung genutzte # Der von "connect"/"select" bis zur eigentlichen Bilduebertragung genutzte
# StreamReader-Puffer muss groesser als der asyncio-Default (64 KiB) sein -- # StreamReader-Puffer muss groesser als der asyncio-Default (64 KiB) sein --

View File

@ -419,20 +419,18 @@ async def rdp_tunnel(
host, password.decode(), session_id=session_id, username=cred_row[1], domain=cred_row[2], host, password.decode(), session_id=session_id, username=cred_row[1], domain=cred_row[2],
client_name=f"jumphost-{user.username}", timezone=safe_timezone, client_name=f"jumphost-{user.username}", timezone=safe_timezone,
) )
if host.get("file_transfer_enabled"): # Bug-Fix (Live-Test, RDP-Teilstrecke, 2./3.9.): ein erster Versuch,
# Bug-Fix (Live-Test, RDP-Teilstrecke, 2.9.): guacd/FreeRDPs # das Elternverzeichnis hier im App-Prozess per mkdir(parents=True)
# "create-drive-path" legt nur das BLATT-Verzeichnis an, keine # anzulegen, ist am echten Testserver an den Dateirechten gescheitert
# fehlenden Elternverzeichnisse (kein "mkdir -p"). Bisher wurde # ("/var/lib/jumphost/rdp-drives" gehoert dem guacd-Systembenutzer,
# ausschliesslich data_dir/recordings_dir beim Start angelegt # Modus 750 -- der App-Prozess (Benutzer "jumphost") hat dort gar
# (siehe app/db.py) -- das Elternverzeichnis .../rdp-drives/<host_id>/ # keinen Zugriff, auch nicht zum Anlegen). Der eigentliche Fix liegt
# existierte fuer einen frischen Host nie, wodurch guacd mit # deshalb in rdp_drive_path() (siehe guacd_client.py): eine flache,
# "Unable to create directory ... No such file or directory" # aber weiterhin pro Sitzung eindeutige Pfadstruktur, die guacds
# abbrach. Deshalb hier explizit VOR dem Tunnelaufbau mit # eigenes "create-drive-path" (das nachweislich NUR das direkte
# parents=True anlegen (blockierendes Datei-I/O -> Thread, wie # Blattverzeichnis anlegt, siehe Docstring dort) ohne fehlendes
# bei _cleanup_rdp_drive_path()). # Elternverzeichnis anlegen kann -- ohne dass der App-Prozess selbst
await asyncio.to_thread( # in rdp-drives/ schreiben muesste.
rdp_drive_path(host_id, session_id).mkdir, parents=True, exist_ok=True
)
tunnel = await open_tunnel( tunnel = await open_tunnel(
guacd_host=settings.guacd_host, guacd_port=settings.guacd_port, guacd_host=settings.guacd_host, guacd_port=settings.guacd_port,
protocol="rdp", params=params, screen_width=width, screen_height=height, dpi=dpi, protocol="rdp", params=params, screen_width=width, screen_height=height, dpi=dpi,

View File

@ -16,17 +16,24 @@
* Copy & Paste: xterm.js liefert Markieren-zum-Kopieren + Strg+Umschalt+V * Copy & Paste: xterm.js liefert Markieren-zum-Kopieren + Strg+Umschalt+V
* bereits nativ. Aufgabe #11 (vom Nutzer nachtraeglich hinzugefuegt, siehe * bereits nativ. Aufgabe #11 (vom Nutzer nachtraeglich hinzugefuegt, siehe
* FORTSETZUNG_Aufgabe11.md) ergaenzt zwei konkret gewuenschte Wege: * FORTSETZUNG_Aufgabe11.md) ergaenzt zwei konkret gewuenschte Wege:
* `copyOnSelect: true` kopiert eine Markierung sofort in die * `copyOnSelect: true` versucht, eine Markierung sofort in die System-
* System-Zwischenablage (kein Strg+C/Cmd+C mehr noetig -- Strg+C bleibt * Zwischenablage zu kopieren (kein Strg+C/Cmd+C mehr noetig -- Strg+C bleibt
* ohne Markierung weiterhin SIGINT, xterm.js' Standardverhalten). Rechtsklick * ohne Markierung weiterhin SIGINT, xterm.js' Standardverhalten). Das ist
* fuegt ein: zuerst wird direkt versucht, ueber die Clipboard-API einzufuegen * aber eine Automatik OHNE direkte Benutzergeste (sie laeuft aus xterm.js'
* (funktioniert in den meisten Chromium-Browsern, da ein Rechtsklick eine * eigenem mouseup-Handler heraus) und schlaegt daher in der Praxis manchmal
* "transiente Aktivierung" ist); schlaegt das fehl (Firefox/Safari ohne * fehl, ohne dass die App das mitbekommt (Live-Test 2.9.: Markierung+Ziehen
* Berechtigung, unsicherer Kontext), erscheint ersatzweise ein winziges * hat sichtbar funktioniert, in der Zwischenablage kam aber nichts an).
* Kontextmenue mit einem "Einfuegen"-Eintrag -- der Klick darauf ist selbst * Deshalb zusaetzlich ein Rechtsklick-Kontextmenue, das PuTTYs bekannte
* eine Benutzergeste und hat dadurch nochmal bessere Chancen auf die * Konvention nachbildet: Rechtsklick MIT aktiver Markierung kopiert (direkter
* Berechtigung (siehe _wireClipboard() unten). Keine serverseitige * Versuch ueber die Clipboard-API, da ein Rechtsklick selbst eine
* Sonderbehandlung noetig, im Gegensatz zu RDP (siehe rdp.js). * "transiente Aktivierung"/Benutzergeste ist -- das ist deutlich
* zuverlaessiger als copyOnSelects impliziter Aufruf); schlaegt das fehl
* oder ist keine Markierung aktiv, faellt es auf "Einfuegen" zurueck --
* ebenfalls zuerst direkt ueber die Clipboard-API versucht, sonst ein
* winziges Kontextmenue mit "Kopieren"- bzw. "Einfuegen"-Eintrag, dessen
* Klick selbst wieder eine frische Benutzergeste ist (siehe _wireClipboard()
* unten). Keine serverseitige Sonderbehandlung noetig, im Gegensatz zu RDP
* (siehe rdp.js).
* Markieren mit gedrueckter Umschalttaste: xterm.js reicht Mausklicks an ein * Markieren mit gedrueckter Umschalttaste: xterm.js reicht Mausklicks an ein
* Vollbild-Zielprogramm mit eigener Maussteuerung (tmux, vim, htop, mc, ...) * Vollbild-Zielprogramm mit eigener Maussteuerung (tmux, vim, htop, mc, ...)
* weiter, sobald dieses per ANSI-Escape-Sequenz Maus-Tracking anfordert -- * weiter, sobald dieses per ANSI-Escape-Sequenz Maus-Tracking anfordert --
@ -187,7 +194,7 @@
this._lastErrorMessage = null; this._lastErrorMessage = null;
this._transfers = []; this._transfers = [];
this._resizeDebounce = null; this._resizeDebounce = null;
this._pasteMenuEl = null; this._clipboardMenuEl = null;
this.element = this._buildElement(); this.element = this._buildElement();
this._cacheRefs(); this._cacheRefs();
@ -442,7 +449,7 @@
this.sendResize(); this.sendResize();
this.term.focus(); this.term.focus();
} else { } else {
this._closePasteMenu(); this._closeClipboardMenu();
} }
} }
@ -475,21 +482,42 @@
}); });
} }
// --- Zwischenablage (Aufgabe #11) -------------------------------------- // --- Zwischenablage (Aufgabe #11, erweitert Live-Test 2.9.) ------------
// //
// Kopieren laeuft ueber copyOnSelect (siehe _setupTerminal()) -- hier nur // copyOnSelect (siehe _setupTerminal()) bleibt als Automatik aktiv, ist
// Einfuegen per Rechtsklick. Zwei Stufen, wie im Datei-Kopfkommentar // aber nicht zuverlaessig genug (siehe Datei-Kopfkommentar) -- deshalb
// beschrieben: direkter Versuch, dann ein winziges Kontextmenue als // zusaetzlich Rechtsklick nach PuTTY-Konvention: MIT aktiver Markierung
// Ruecksicherung, wenn der Browser die Berechtigung ohne explizite // kopieren, OHNE einfuegen. Beides zweistufig wie gehabt: direkter
// Benutzergeste verweigert. // Clipboard-API-Versuch, bei Fehlschlag ein winziges Kontextmenue als
// Ruecksicherung (dessen Klick selbst wieder eine frische Benutzergeste
// ist und dadurch bessere Chancen auf die Berechtigung hat).
_wireClipboard() { _wireClipboard() {
this.terminalContainer.addEventListener("contextmenu", (ev) => { this.terminalContainer.addEventListener("contextmenu", (ev) => {
ev.preventDefault(); ev.preventDefault();
this._attemptDirectPaste(ev.clientX, ev.clientY); if (this.term.hasSelection()) {
this._attemptDirectCopy(ev.clientX, ev.clientY);
} else {
this._attemptDirectPaste(ev.clientX, ev.clientY);
}
}); });
} }
async _attemptDirectCopy(x, y) {
const text = this.term.getSelection();
if (!navigator.clipboard || !navigator.clipboard.writeText) {
this._showCopyMenu(x, y, text);
return;
}
try {
await navigator.clipboard.writeText(text);
} catch (_) {
// Wie bei _attemptDirectPaste(): Ruecksicherung unten, deren Klick
// selbst wieder eine frische Benutzergeste ist.
this._showCopyMenu(x, y, text);
}
}
async _attemptDirectPaste(x, y) { async _attemptDirectPaste(x, y) {
if (!navigator.clipboard || !navigator.clipboard.readText) { if (!navigator.clipboard || !navigator.clipboard.readText) {
this._showPasteMenu(x, y); this._showPasteMenu(x, y);
@ -506,19 +534,22 @@
} }
} }
_closePasteMenu() { _closeClipboardMenu() {
if (this._pasteMenuEl) { if (this._clipboardMenuEl) {
this._pasteMenuEl.remove(); this._clipboardMenuEl.remove();
this._pasteMenuEl = null; this._clipboardMenuEl = null;
} }
if (this._pasteMenuCleanup) { if (this._clipboardMenuCleanup) {
this._pasteMenuCleanup(); this._clipboardMenuCleanup();
this._pasteMenuCleanup = null; this._clipboardMenuCleanup = null;
} }
} }
_showPasteMenu(x, y) { // Gemeinsamer Aufbau fuer Kopieren- und Einfuegen-Mini-Kontextmenue --
this._closePasteMenu(); // beide brauchen dieselbe Positionierung und dasselbe Schliessverhalten,
// nur Beschriftung und Klick-Aktion unterscheiden sich.
_showClipboardMenu(x, y, label, onButtonClick) {
this._closeClipboardMenu();
// CSP (style-src 'self', kein 'unsafe-inline', siehe app.css- // CSP (style-src 'self', kein 'unsafe-inline', siehe app.css-
// Kopfkommentar zu .hidden) verbietet JS-gesetzte element.style- // Kopfkommentar zu .hidden) verbietet JS-gesetzte element.style-
// Aenderungen genauso wie style="..."-Attribute -- daher KEINE // Aenderungen genauso wie style="..."-Attribute -- daher KEINE
@ -534,11 +565,50 @@
(isBottom ? "term-paste-menu-bottom" : "term-paste-menu-top") + " " + (isBottom ? "term-paste-menu-bottom" : "term-paste-menu-top") + " " +
(isRight ? "term-paste-menu-right" : "term-paste-menu-left"); (isRight ? "term-paste-menu-right" : "term-paste-menu-left");
const pasteBtn = document.createElement("button"); const btn = document.createElement("button");
pasteBtn.type = "button"; btn.type = "button";
pasteBtn.textContent = "Einfuegen"; btn.textContent = label;
pasteBtn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
this._closePasteMenu(); this._closeClipboardMenu();
await onButtonClick();
this.term.focus();
});
menu.appendChild(btn);
// Als Kind von .session-shell (nicht .session-terminal) angehaengt --
// dort mischt sich nichts mit xterm.js' eigenem DOM-Teilbaum, das
// dieses Element seit term.open() exklusiv verwaltet.
this.shellEl.appendChild(menu);
this._clipboardMenuEl = menu;
// Schliessen bei Klick daneben, Escape oder Scrollen -- Standard-
// verhalten fuer ein solches Mini-Kontextmenue (siehe auch
// workspace.js fuer dasselbe Muster bei anderen Overlays).
const onDocClick = (ev) => {
if (!menu.contains(ev.target)) this._closeClipboardMenu();
};
const onKeydown = (ev) => {
if (ev.key === "Escape") this._closeClipboardMenu();
};
const onScroll = () => this._closeClipboardMenu();
// capture:true fuer den Klick-Listener, damit er VOR einem moeglichen
// contextmenu-Listener eines anderen Elements greift; im naechsten
// Tick registriert, sonst schliesst der Rechtsklick, der das Menue
// gerade erst geoeffnet hat, es sofort wieder (derselbe Event).
window.setTimeout(() => {
document.addEventListener("click", onDocClick, true);
document.addEventListener("keydown", onKeydown);
window.addEventListener("scroll", onScroll, true);
}, 0);
this._clipboardMenuCleanup = () => {
document.removeEventListener("click", onDocClick, true);
document.removeEventListener("keydown", onKeydown);
window.removeEventListener("scroll", onScroll, true);
};
}
_showPasteMenu(x, y) {
this._showClipboardMenu(x, y, "Einfuegen", async () => {
try { try {
const text = await navigator.clipboard.readText(); const text = await navigator.clipboard.readText();
this.term.paste(text); this.term.paste(text);
@ -549,40 +619,21 @@
true true
); );
} }
this.term.focus();
}); });
menu.appendChild(pasteBtn); }
// Als Kind von .session-shell (nicht .session-terminal) angehaengt -- _showCopyMenu(x, y, text) {
// dort mischt sich nichts mit xterm.js' eigenem DOM-Teilbaum, das this._showClipboardMenu(x, y, "Kopieren", async () => {
// dieses Element seit term.open() exklusiv verwaltet. try {
this.shellEl.appendChild(menu); await navigator.clipboard.writeText(text);
this._pasteMenuEl = menu; } catch (err) {
console.error("Kopieren in die Zwischenablage nicht moeglich:", err);
// Schliessen bei Klick daneben, Escape oder Scrollen -- Standard- this._setStatus(
// verhalten fuer ein solches Mini-Kontextmenue (siehe auch "Kopieren nicht moeglich -- Browser verweigert Zwischenablage-Zugriff. Bitte Strg+Umschalt+C verwenden.",
// workspace.js fuer dasselbe Muster bei anderen Overlays). true
const onDocClick = (ev) => { );
if (!menu.contains(ev.target)) this._closePasteMenu(); }
}; });
const onKeydown = (ev) => {
if (ev.key === "Escape") this._closePasteMenu();
};
const onScroll = () => this._closePasteMenu();
// capture:true fuer den Klick-Listener, damit er VOR einem moeglichen
// contextmenu-Listener eines anderen Elements greift; im naechsten
// Tick registriert, sonst schliesst der Rechtsklick, der das Menue
// gerade erst geoeffnet hat, es sofort wieder (derselbe Event).
window.setTimeout(() => {
document.addEventListener("click", onDocClick, true);
document.addEventListener("keydown", onKeydown);
window.addEventListener("scroll", onScroll, true);
}, 0);
this._pasteMenuCleanup = () => {
document.removeEventListener("click", onDocClick, true);
document.removeEventListener("keydown", onKeydown);
window.removeEventListener("scroll", onScroll, true);
};
} }
// --- Dateitransfer-Panel --------------------------------------------- // --- Dateitransfer-Panel ---------------------------------------------
@ -752,7 +803,7 @@
window.clearInterval(this._pingTimer); window.clearInterval(this._pingTimer);
this._pingTimer = null; this._pingTimer = null;
} }
this._closePasteMenu(); this._closeClipboardMenu();
try { try {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
this.ws.close(1000, "Sitzung durch Benutzer beendet"); this.ws.close(1000, "Sitzung durch Benutzer beendet");