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,