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,