diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 504ce3f..2c370a3 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.13" +version = "2.8.14" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/devices.py b/apps/tesm/src/tesm/blueprints/devices.py index e7f7bcd..eae2e55 100644 --- a/apps/tesm/src/tesm/blueprints/devices.py +++ b/apps/tesm/src/tesm/blueprints/devices.py @@ -424,7 +424,11 @@ def vnc_connection(device_id: int): f"Verbindungsdatei für {ziel}:{port}", ) return Response( - vnc.verbindungsdatei(device, port=port), + vnc.verbindungsdatei( + device, + port=port, + warnung_aus=bool(extension.settings.get(conn, "vnc_warn_unencrypted_off")), + ), mimetype="application/octet-stream", headers={ "Content-Disposition": f'attachment; filename="{vnc.dateiname(device)}"', diff --git a/apps/tesm/src/tesm/blueprints/settings.py b/apps/tesm/src/tesm/blueprints/settings.py index b678021..0794709 100644 --- a/apps/tesm/src/tesm/blueprints/settings.py +++ b/apps/tesm/src/tesm/blueprints/settings.py @@ -105,6 +105,7 @@ def system(): "dashboard_public", "vnc_enabled", "vnc_port", + "vnc_warn_unencrypted_off", "terminal_switches_enabled", "terminal_clients_enabled", "terminal_idle_timeout", @@ -147,6 +148,7 @@ def system_save(): "dashboard_public", "vnc_enabled", "vnc_port", + "vnc_warn_unencrypted_off", "terminal_switches_enabled", "terminal_clients_enabled", "terminal_idle_timeout", diff --git a/apps/tesm/src/tesm/services/vnc.py b/apps/tesm/src/tesm/services/vnc.py index 80fb42b..21598a8 100644 --- a/apps/tesm/src/tesm/services/vnc.py +++ b/apps/tesm/src/tesm/services/vnc.py @@ -235,13 +235,24 @@ def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: boo return Zugang(ziel=host, port=port, erreichbar=bool(fassung), verfahren=verfahren) -def verbindungsdatei(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> str: +def verbindungsdatei( + device: Mapping[str, Any], *, port: int = STANDARD_PORT, warnung_aus: bool = False +) -> str: """Der Inhalt der ``.vnc``-Datei -- drei Zeilen, keine vierte. ``ConnMethod=tcp`` ist die direkte Verbindung (nicht ueber den Cloud-Dienst von RealVNC), ``FriendlyName`` der Name, den der Viewer anzeigt, ``Host`` das Ziel. Ein abweichender Port gehoert in dieser Datei an den Host, mit Doppelpunkt -- einen eigenen Schluessel dafuer gibt es nicht. + + ``warnung_aus`` haengt eine vierte Zeile an: ``WarnUnencrypted=False``. Sie + betrifft **den Viewer**, nicht den Server -- er zeigt sonst bei jeder + Verbindung zu einem Geraet ohne Streckenverschluesselung einen roten + Hinweis, den man wegklicken muss. Wer ``Encryption=AlwaysOff`` auf dem + Geraet gesetzt hat (damit der Browser ueberhaupt verbinden kann), sieht + diesen Hinweis fortan bei jedem Start. Er bleibt in der Vorgabe stehen: + eine unverschluesselte Strecke soll man einmal bewusst hinnehmen, nicht + stillschweigend. Abschaltbar unter Verwaltung -> System. """ host = ziel(device) if port != STANDARD_PORT: @@ -251,6 +262,8 @@ def verbindungsdatei(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> f"FriendlyName={str(device.get('name') or host).strip()}", f"Host={host}", ] + if warnung_aus: + zeilen.append("WarnUnencrypted=False") return ZEILE.join(zeilen) + ZEILE diff --git a/apps/tesm/src/tesm/settings_spec.py b/apps/tesm/src/tesm/settings_spec.py index 3676f9d..a8bef06 100644 --- a/apps/tesm/src/tesm/settings_spec.py +++ b/apps/tesm/src/tesm/settings_spec.py @@ -66,6 +66,16 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = ( minimum=1, maximum=65535, ), + SettingSpec( + "vnc_warn_unencrypted_off", + False, + "bool", + "Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken", + hint="Setzt WarnUnencrypted=False in die Verbindungsdatei. Betrifft nur den " + "RealVNC Viewer auf dem Arbeitsplatz, nicht die Verbindung selbst: die bleibt " + "unverschlüsselt, wenn das Gerät es so anbietet.", + section="monitoring", + ), # -- DHCP --------------------------------------------------------------- # SettingSpec("dhcp_domain", "local", "str", "Domainname", section="dhcp"), SettingSpec( diff --git a/apps/tesm/src/tesm/templates/tesm/screen.html b/apps/tesm/src/tesm/templates/tesm/screen.html index a2cdfe4..eb7d377 100644 --- a/apps/tesm/src/tesm/templates/tesm/screen.html +++ b/apps/tesm/src/tesm/templates/tesm/screen.html @@ -74,6 +74,7 @@
diff --git a/apps/tesm/src/tesm/templates/tesm/settings_system.html b/apps/tesm/src/tesm/templates/tesm/settings_system.html index c5b9f6a..d994575 100644 --- a/apps/tesm/src/tesm/templates/tesm/settings_system.html +++ b/apps/tesm/src/tesm/templates/tesm/settings_system.html @@ -53,6 +53,10 @@ {{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number', hint_text=specs['vnc_port'].hint) }}
+ {{ checkbox('vnc_warn_unencrypted_off', + 'Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken', + checked=values.vnc_warn_unencrypted_off, + hint_text=specs['vnc_warn_unencrypted_off'].hint) }} diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index 065c534..cb0018a 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.8.5" +version = "2.8.6" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js index d44971a..de12c51 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/screen.js @@ -11,6 +11,25 @@ import RFB from "../vendor/novnc/core/rfb.js"; +// Sicherheitsverfahren, die ohne Zugangsdaten auskommen. Bietet der Server +// eines davon an und haben wir keine, nehmen wir es -- auch wenn der Server +// ein anderes zuerst nennt. +// +// Warum das noetig ist: noVNC waehlt "das erste, das ich kann, in der +// Reihenfolge des Servers". Ein RealVNC Server nennt RA2ne vor "ohne +// Authentifizierung"; der Browser liefe also in eine Passwortabfrage, obwohl +// daneben ein Weg offensteht, den auch der RealVNC Viewer nimmt. Wer ein +// Passwort hinterlegt hat, behaelt die Reihenfolge des Servers -- dann ist +// der angemeldete Weg der bessere. +const OHNE_ZUGANGSDATEN = new Set([1]); + +// Der Eingriff sitzt in einer privaten Methode der mitgelieferten Bibliothek. +// Das ist bewusst hier und nicht in der Bibliothek selbst: eine neue Fassung +// von noVNC laesst sich dann austauschen, ohne eine Aenderung nachzupflegen -- +// faellt die Methode weg, greift die Vorauswahl nicht mehr, und die Sitzung +// laeuft wieder wie ohne sie. Kein stiller Bruch. +const istUnterstuetzt = RFB.prototype._isSupportedSecurityType; + const wurzel = document.getElementById("screen"); if (wurzel) { starten(wurzel); @@ -35,14 +54,58 @@ function starten(wurzel) { ); let rfb = null; + let letzterFehler = ""; function melden(text, art = "") { zeile.textContent = text; zeile.className = `screen__status${art ? " screen__status--" + art : ""}`; } + // noVNC meldet den *Grund* eines Fehlschlags ausschliesslich ins Protokoll + // der Entwicklerkonsole -- das Ereignis "disconnect" trägt ihn nicht. Ohne + // diesen Griff bliebe der Seite nur "Verbindung abgebrochen", und damit + // steht der Anwender genau dort, wo er ohne die ganze Anzeige auch stuende. + // Darum wird die Fehlerausgabe waehrend der Sitzung mitgelesen -- und + // hinterher wieder losgelassen. + const echteAusgabe = console.error.bind(console); + console.error = (...teile) => { + letzterFehler = teile.map((teil) => String(teil)).join(" "); + echteAusgabe(...teile); + }; + window.addEventListener("beforeunload", () => { + console.error = echteAusgabe; + }); + + function grund() { + const teile = []; + if (letzterFehler) { + teile.push(letzterFehler.replace(/^Failed (when|while) \w+: /, "")); + } + if (rfb && rfb._rfbAuthScheme > 0) { + teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`); + } + return teile.join(" · "); + } + function verbinden(zugang) { melden("Verbindung wird aufgebaut …"); + letzterFehler = ""; + + // Vorauswahl nur, wenn wir nichts anzubieten haben: dann zaehlt der Weg, + // der ohne Zugangsdaten auskommt -- sofern der Server ihn anbietet. + const hatZugangsdaten = Boolean(zugang && (zugang.password || zugang.username)); + const angeboten = (wurzel.dataset.verfahren || "") + .split(",") + .map((wert) => parseInt(wert, 10)) + .filter((wert) => !Number.isNaN(wert)); + const ohnePasswortMoeglich = angeboten.some((wert) => OHNE_ZUGANGSDATEN.has(wert)); + RFB.prototype._isSupportedSecurityType = function (typ) { + if (!hatZugangsdaten && ohnePasswortMoeglich) { + return OHNE_ZUGANGSDATEN.has(typ); + } + return istUnterstuetzt.call(this, typ); + }; + rfb = new RFB(leinwand, adresse, { credentials: zugang, // Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des @@ -69,9 +132,12 @@ function starten(wurzel) { if (ereignis.detail && ereignis.detail.clean) { melden("Sitzung beendet."); } else { + const erklaerung = grund(); melden( - "Verbindung abgebrochen. Antwortet der VNC-Server noch, und lässt der " + - "Arbeitsplatz die Verbindung zu?", + erklaerung + ? `Verbindung abgebrochen – ${erklaerung}` + : "Verbindung abgebrochen, ohne dass der Client einen Grund nennt. " + + "Antwortet der VNC-Server noch?", "fehler", ); } diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md b/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md index 4d761e7..1e8193d 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/vendor/novnc/HERKUNFT.md @@ -20,6 +20,16 @@ der einen Bildschirm fernsteuert. Datei -- mit Grund und Fundstelle, damit ein Wechsel auf eine neuere Fassung nicht stillschweigend darueberlaeuft. +**Was TESM von aussen umbiegt** (also *nicht* in diesen Dateien steht, sondern +in `js/screen.js`): `RFB.prototype._isSupportedSecurityType` wird ueberschrieben, +solange keine Zugangsdaten vorliegen und der Server ein Verfahren ohne +Anmeldung anbietet. Grund: noVNC nimmt "das erste, das ich kann, in der +Reihenfolge des Servers"; ein RealVNC Server nennt RA2ne vor "ohne +Authentifizierung", und der Browser liefe in eine Passwortabfrage, obwohl +daneben der Weg offensteht, den auch der RealVNC Viewer nimmt. Faellt die +Methode in einer neueren Fassung weg, greift die Vorauswahl nicht mehr -- die +Sitzung laeuft dann wie ohne sie, es bricht nichts. + **Was noVNC an Sicherheitsverfahren spricht** (`core/rfb.js`, `_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES ohne Streckenverschluesselung), `16` Tight, `19` VeNCrypt, `22` xvp, `30` ARD,