Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b54b87ca6f | ||
|
|
270ba455a8 |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.8.14"
|
||||
version = "2.8.16"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -389,10 +389,13 @@ def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
||||
return None
|
||||
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
||||
online = str(device.get("state") or "") == "online" and not device.get("deleted_at")
|
||||
zugang = vnc.zugang(device, port=port, pruefen=online)
|
||||
if online:
|
||||
zugang, frisch = vnc.zugang_gemerkt(device, port=port, pruefen=online)
|
||||
if frisch:
|
||||
with transaction():
|
||||
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
||||
monitor.merke_verfahren(
|
||||
conn, int(device["id"]), zugang.verfahren, zugang.absage
|
||||
)
|
||||
return zugang
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +85,17 @@ def screen(device_id: int):
|
||||
if not port:
|
||||
abort(404)
|
||||
|
||||
zugang = vnc.zugang(device, port=port, pruefen=str(device.get("state") or "") == "online")
|
||||
zugang, frisch = vnc.zugang_gemerkt(
|
||||
device, port=port, pruefen=str(device.get("state") or "") == "online"
|
||||
)
|
||||
if frisch:
|
||||
from ..services import monitor
|
||||
|
||||
with core().database.transaction(conn):
|
||||
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
||||
monitor.merke_verfahren(
|
||||
conn, int(device["id"]), zugang.verfahren, zugang.absage
|
||||
)
|
||||
benutzer, passwort = "", ""
|
||||
darf_geheimnis = bool(current_user.has("credentials.secrets"))
|
||||
if device.get("credential_id") and darf_geheimnis:
|
||||
@@ -138,6 +148,26 @@ def init_app(app) -> None: # type: ignore[no-untyped-def]
|
||||
def screen_socket(ws, device_id: int): # type: ignore[no-untyped-def]
|
||||
_strecke(ws, device_id)
|
||||
|
||||
@sock.route("/ws/probe")
|
||||
def probe_socket(ws): # type: ignore[no-untyped-def]
|
||||
"""Sagt "da", und mehr nicht.
|
||||
|
||||
Wozu: bricht eine Bildschirmsitzung ab, gibt es zwei sehr verschiedene
|
||||
Ursachen -- der VNC-Server mag nicht, oder die WebSocket-Verbindung
|
||||
kommt gar nicht erst durch (ein Proxy zwischen Browser und TESM, der
|
||||
"Upgrade" nicht weiterreicht, ist der haeufigste Fall). Von aussen
|
||||
sehen beide gleich aus: ein Abbruch ohne Grund. Diese Probe trennt
|
||||
sie, ohne dass jemand ein Protokoll lesen muss.
|
||||
"""
|
||||
if not current_user.is_authenticated:
|
||||
ws.close(1008, "Nicht angemeldet.")
|
||||
return
|
||||
try:
|
||||
ws.send("da")
|
||||
ws.receive(timeout=2)
|
||||
except Exception: # noqa: BLE001 - der Browser ist weg, das ist keine Stoerung
|
||||
pass
|
||||
|
||||
|
||||
def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||
"""Bytes hin, Bytes her -- und sonst nichts.
|
||||
|
||||
@@ -526,6 +526,19 @@ ALTER TABLE device_status ADD COLUMN vnc_types TEXT NOT NULL DEFAULT '';
|
||||
"""
|
||||
|
||||
|
||||
# Version 13 ----------------------------------------------------------------
|
||||
#
|
||||
# Die Absage eines VNC-Servers und der Zeitpunkt der Verfahrensmessung. Beide
|
||||
# gehoeren getrennt vom Ueberwachungsdurchlauf gefuehrt: der fragt nur noch,
|
||||
# ob der Port annimmt (eine abgebrochene Protokollverhandlung sperrt bei
|
||||
# RealVNC die eigene Adresse aus), die Verfahren misst die Seite -- und auch
|
||||
# die nur, wenn die letzte Messung alt ist.
|
||||
V13 = """
|
||||
ALTER TABLE device_status ADD COLUMN vnc_reason TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE device_status ADD COLUMN vnc_types_at TEXT;
|
||||
"""
|
||||
|
||||
|
||||
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
||||
|
||||
@@ -571,4 +584,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
|
||||
Migration(version=11, name="vnc-ziel", sql=V11),
|
||||
Migration(version=12, name="vnc-verfahren", sql=V12),
|
||||
Migration(version=13, name="vnc-absage", sql=V13),
|
||||
)
|
||||
@@ -544,7 +544,8 @@ def list_devices(
|
||||
st.last_ok_at, st.last_change_at, st.last_restart_at, st.restart_count,
|
||||
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at,
|
||||
COALESCE(st.vnc_target, '') AS vnc_target,
|
||||
COALESCE(st.vnc_types, '') AS vnc_types
|
||||
COALESCE(st.vnc_types, '') AS vnc_types,
|
||||
COALESCE(st.vnc_reason, '') AS vnc_reason, st.vnc_types_at
|
||||
FROM devices d
|
||||
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
|
||||
LEFT JOIN credentials c ON c.id = d.credential_id AND c.deleted_at IS NULL
|
||||
|
||||
@@ -507,30 +507,36 @@ class SweepReport:
|
||||
duration_ms: int = 0
|
||||
|
||||
|
||||
def merke_vnc(
|
||||
conn: sqlite3.Connection,
|
||||
device_id: int,
|
||||
offen: bool,
|
||||
ziel: str = "",
|
||||
verfahren: tuple[int, ...] = (),
|
||||
) -> None:
|
||||
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = "") -> None:
|
||||
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
||||
|
||||
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
||||
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
||||
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
||||
geschrieben, von dem, der ohnehin gefragt hat.
|
||||
|
||||
Die Anmeldeverfahren bleiben unberuehrt: sie kommen aus einer anderen
|
||||
Quelle (:func:`merke_verfahren`) und haetten hier nur den Wert "nicht
|
||||
gefragt" -- der wuerde eine gute Messung ueberschreiben.
|
||||
"""
|
||||
conn.execute(
|
||||
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=?, vnc_types=? "
|
||||
"WHERE device_id=?",
|
||||
(
|
||||
1 if offen else 0,
|
||||
_now(),
|
||||
ziel,
|
||||
",".join(str(v) for v in verfahren),
|
||||
device_id,
|
||||
),
|
||||
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? WHERE device_id=?",
|
||||
(1 if offen else 0, _now(), ziel, device_id),
|
||||
)
|
||||
|
||||
|
||||
def merke_verfahren(
|
||||
conn: sqlite3.Connection, device_id: int, verfahren: tuple[int, ...], absage: str = ""
|
||||
) -> None:
|
||||
"""Haelt fest, wie sich der VNC-Server authentifizieren will.
|
||||
|
||||
Mit Zeitstempel, denn diese Messung kostet eine angefangene
|
||||
Protokollverhandlung -- und die soll sich nicht bei jedem Seitenaufbau
|
||||
wiederholen.
|
||||
"""
|
||||
conn.execute(
|
||||
"UPDATE device_status SET vnc_types=?, vnc_reason=?, vnc_types_at=? WHERE device_id=?",
|
||||
(",".join(str(v) for v in verfahren), absage, _now(), device_id),
|
||||
)
|
||||
|
||||
|
||||
@@ -553,6 +559,10 @@ def run_sweep(
|
||||
erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert
|
||||
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
||||
je Aufruf, und nur fuer Geraete, die ohnehin gerade antworten.
|
||||
|
||||
**Nur die Frage nach dem Port**, kein Wort Protokoll: ein RealVNC Server
|
||||
zaehlt abgebrochene Verhandlungen und sperrt die Adresse eine Weile aus.
|
||||
Welche Anmeldeverfahren er anbietet, misst die Seite, die es braucht.
|
||||
"""
|
||||
from . import vnc as vnc_dienst # spaet: vnc benutzt tcp_reachable von hier
|
||||
|
||||
@@ -575,7 +585,7 @@ def run_sweep(
|
||||
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
||||
# seinen Knopf nicht verlieren.
|
||||
return device, ergebnis, None
|
||||
return device, ergebnis, vnc_dienst.zugang(device, port=vnc_port)
|
||||
return device, ergebnis, vnc_dienst.zugang_kurz(device, port=vnc_port)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
|
||||
results = list(pool.map(pruefen, devices))
|
||||
@@ -587,13 +597,7 @@ def run_sweep(
|
||||
report.checked += 1
|
||||
previous, current = record_status(conn, int(device["id"]), result)
|
||||
if vnc_offen is not None:
|
||||
merke_vnc(
|
||||
conn,
|
||||
int(device["id"]),
|
||||
vnc_offen.erreichbar,
|
||||
vnc_offen.ziel,
|
||||
vnc_offen.verfahren,
|
||||
)
|
||||
merke_vnc(conn, int(device["id"]), vnc_offen.erreichbar, vnc_offen.ziel)
|
||||
if result.ok:
|
||||
report.online += 1
|
||||
else:
|
||||
|
||||
@@ -61,6 +61,9 @@ class Zugang:
|
||||
#: gemessen). Daraus entsteht der Satz, warum eine Sitzung im Browser
|
||||
#: gehen wird oder nicht.
|
||||
verfahren: tuple[int, ...] = ()
|
||||
#: Der Text, mit dem der Server die Verbindung abgelehnt hat -- er nennt
|
||||
#: dann kein Verfahren, sondern eine 0 und einen Grund.
|
||||
absage: str = ""
|
||||
|
||||
@property
|
||||
def browsertauglich(self) -> bool:
|
||||
@@ -72,6 +75,14 @@ class Zugang:
|
||||
"""Warum eine Sitzung im Browser nicht zustande kaeme -- im Klartext."""
|
||||
if not self.erreichbar:
|
||||
return f"Auf {self.ziel}:{self.port} antwortet kein VNC-Server."
|
||||
if self.absage:
|
||||
return (
|
||||
f"Der VNC-Server auf {self.ziel} weist die Verbindung ab: "
|
||||
f"„{self.absage}“. Das ist seine Auskunft, nicht unsere – "
|
||||
"typisch sind eine Sperre nach zu vielen Fehlversuchen "
|
||||
"(RealVNC: BlacklistTimeout), eine Adressfilterung oder eine "
|
||||
"belegte Sitzung."
|
||||
)
|
||||
if not self.verfahren:
|
||||
return ""
|
||||
if self.browsertauglich:
|
||||
@@ -181,7 +192,36 @@ def verfahren_name(nummer: int) -> str:
|
||||
return str(nummer)
|
||||
|
||||
|
||||
def sicherheitsverfahren(host: str, port: int = STANDARD_PORT) -> tuple[str, tuple[int, ...]]:
|
||||
def _absage(sock: Any) -> str:
|
||||
"""Der Grund, den ein Server einer abgelehnten Verbindung mitgibt.
|
||||
|
||||
Auf eine 0 -- ob als Anzahl (ab 3.7) oder als bestimmtes Verfahren (3.3) --
|
||||
folgt eine Laenge und ein Text. Genau dieser Satz ist die Auskunft, die
|
||||
sonst niemand bekommt: "zu viele Fehlversuche", "Verbindung von dieser
|
||||
Adresse nicht erlaubt", "kein gemeinsames Verfahren". Ohne ihn steht da
|
||||
nur, dass es nicht geht.
|
||||
"""
|
||||
try:
|
||||
roh = sock.recv(4)
|
||||
if len(roh) < 4:
|
||||
return ""
|
||||
laenge = int.from_bytes(roh, "big")
|
||||
if not 0 < laenge <= 1024:
|
||||
return ""
|
||||
text = b""
|
||||
while len(text) < laenge:
|
||||
stueck = sock.recv(laenge - len(text))
|
||||
if not stueck:
|
||||
break
|
||||
text += stueck
|
||||
return text.decode("utf-8", "replace").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def sicherheitsverfahren(
|
||||
host: str, port: int = STANDARD_PORT
|
||||
) -> tuple[str, tuple[int, ...], str]:
|
||||
"""Fragt den Server, **wie** er sich authentifizieren will.
|
||||
|
||||
Der Anfang einer VNC-Verbindung ist fuer jeden Server derselbe: er nennt
|
||||
@@ -192,8 +232,12 @@ def sicherheitsverfahren(host: str, port: int = STANDARD_PORT) -> tuple[str, tup
|
||||
Das ist die Angabe, an der eine Bildschirmsitzung im Browser scheitert oder
|
||||
gelingt, und sie hier zu messen ist der Unterschied zwischen einer
|
||||
schwarzen Flaeche und dem Satz "dieser Server bietet nur Verfahren an, die
|
||||
der Browser nicht kennt". Gibt ``(Fassung, Verfahren)`` zurueck; bei einem
|
||||
Fehler ``("", ())``.
|
||||
der Browser nicht kennt".
|
||||
|
||||
Gibt ``(Fassung, Verfahren, Absage)`` zurueck. Die Absage ist der Text, den
|
||||
ein Server mitschickt, wenn er die Verbindung **ablehnt** -- er meldet dann
|
||||
kein Verfahren, sondern eine 0 und einen Grund. Bei einem Fehler
|
||||
``("", (), "")``.
|
||||
"""
|
||||
import socket
|
||||
|
||||
@@ -202,28 +246,68 @@ def sicherheitsverfahren(host: str, port: int = STANDARD_PORT) -> tuple[str, tup
|
||||
sock.settimeout(ZEITLIMIT)
|
||||
gruss = sock.recv(12)
|
||||
if not gruss.startswith(b"RFB "):
|
||||
return "", ()
|
||||
return "", (), ""
|
||||
fassung = gruss[4:11].decode("ascii", "replace")
|
||||
# Wir antworten mit 3.8 -- der Fassung, die auch der Browser
|
||||
# spricht. Ein Server, der mehr kann, faellt darauf zurueck und
|
||||
# nennt trotzdem seine Verfahren.
|
||||
sock.sendall(b"RFB 003.008" + bytes([10]))
|
||||
# Antworten wird mit *seiner* Fassung, hoechstens 3.8. Wer einem
|
||||
# 3.3-Server mit 3.8 antwortet, bekommt keine Liste, sondern eine
|
||||
# 32-Bit-Zahl -- und liest sie als "keine Verfahren".
|
||||
eigene = "003.003" if fassung == "003.003" else "003.008"
|
||||
sock.sendall(b"RFB " + eigene.encode("ascii") + bytes([10]))
|
||||
|
||||
if eigene == "003.003":
|
||||
# RFB 3.3: der Server bestimmt allein und schickt eine
|
||||
# 32-Bit-Zahl. Das ist kein Sonderfall aus der Vorzeit --
|
||||
# RealVNC faellt darauf zurueck, sobald keine Anmeldung
|
||||
# verlangt wird.
|
||||
rohwert = b""
|
||||
while len(rohwert) < 4:
|
||||
stueck = sock.recv(4 - len(rohwert))
|
||||
if not stueck:
|
||||
break
|
||||
rohwert += stueck
|
||||
if len(rohwert) < 4:
|
||||
return fassung, (), ""
|
||||
bestimmt = int.from_bytes(rohwert, "big")
|
||||
if bestimmt == 0:
|
||||
return fassung, (), _absage(sock)
|
||||
return fassung, (bestimmt,), ""
|
||||
|
||||
anzahl = sock.recv(1)
|
||||
if not anzahl:
|
||||
return fassung, ()
|
||||
return fassung, (), ""
|
||||
wie_viele = anzahl[0]
|
||||
if wie_viele == 0:
|
||||
# Null heisst: der Server lehnt ab und schickt den Grund.
|
||||
return fassung, ()
|
||||
return fassung, (), _absage(sock)
|
||||
liste = b""
|
||||
while len(liste) < wie_viele:
|
||||
stueck = sock.recv(wie_viele - len(liste))
|
||||
if not stueck:
|
||||
break
|
||||
liste += stueck
|
||||
return fassung, tuple(liste)
|
||||
return fassung, tuple(liste), ""
|
||||
except OSError:
|
||||
return "", ()
|
||||
return "", (), ""
|
||||
|
||||
|
||||
def zugang_kurz(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> Zugang:
|
||||
"""Nur die Frage, ob dort jemand annimmt -- ohne ein Wort Protokoll.
|
||||
|
||||
Das ist die Fassung fuer den Ueberwachungsdurchlauf. Er laeuft alle paar
|
||||
Minuten ueber jedes Geraet, und eine angefangene und sofort wieder
|
||||
abgebrochene Protokollverhandlung ist nicht nur unhoeflich: ein RealVNC
|
||||
Server zaehlt abgebrochene Versuche und sperrt die Adresse eine Weile aus
|
||||
(``BlacklistTimeout``). Eine Ueberwachung, die ihr eigenes Ziel aussperrt,
|
||||
waere ein schoener Eigentor -- also fragt sie nur, ob der Port annimmt.
|
||||
|
||||
Welche Anmeldeverfahren der Server kann, misst erst die Seite, die es
|
||||
wirklich braucht -- und auch die nicht oefter als noetig.
|
||||
"""
|
||||
host = ziel(device)
|
||||
return Zugang(ziel=host, port=port, erreichbar=erreichbar(host, port))
|
||||
|
||||
|
||||
def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True) -> Zugang:
|
||||
@@ -231,8 +315,66 @@ def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: boo
|
||||
host = ziel(device)
|
||||
if not pruefen:
|
||||
return Zugang(ziel=host, port=port, erreichbar=False)
|
||||
fassung, verfahren = sicherheitsverfahren(host, port)
|
||||
return Zugang(ziel=host, port=port, erreichbar=bool(fassung), verfahren=verfahren)
|
||||
fassung, verfahren, absage = sicherheitsverfahren(host, port)
|
||||
return Zugang(
|
||||
ziel=host,
|
||||
port=port,
|
||||
erreichbar=bool(fassung),
|
||||
verfahren=verfahren,
|
||||
absage=absage,
|
||||
)
|
||||
|
||||
|
||||
#: So lange gilt eine Messung der Anmeldeverfahren als frisch.
|
||||
#:
|
||||
#: Sie kostet eine angefangene Protokollverhandlung, und ein RealVNC Server
|
||||
#: zaehlt abgebrochene Versuche. Wer eine Seite dreimal neu laedt, soll dem
|
||||
#: Geraet nicht dreimal als Fehlversuch erscheinen.
|
||||
MESSUNG_GILT = 300.0
|
||||
|
||||
|
||||
def zugang_gemerkt(
|
||||
device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True
|
||||
) -> tuple[Zugang, bool]:
|
||||
"""Der Zugang -- aus der letzten Messung, wenn die frisch genug ist.
|
||||
|
||||
Gibt ``(Zugang, frisch_gemessen)`` zurueck; das zweite sagt dem Aufrufer,
|
||||
ob er das Ergebnis festhalten muss.
|
||||
"""
|
||||
host = ziel(device)
|
||||
if not pruefen:
|
||||
return Zugang(ziel=host, port=port, erreichbar=False), False
|
||||
|
||||
gemessen_am = str(device.get("vnc_types_at") or "")
|
||||
if gemessen_am and _juenger_als(gemessen_am, MESSUNG_GILT):
|
||||
gespeichert = str(device.get("vnc_types") or "")
|
||||
verfahren = tuple(
|
||||
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
||||
)
|
||||
return (
|
||||
Zugang(
|
||||
ziel=host,
|
||||
port=port,
|
||||
erreichbar=bool(device.get("vnc_open")),
|
||||
verfahren=verfahren,
|
||||
absage=str(device.get("vnc_reason") or ""),
|
||||
),
|
||||
False,
|
||||
)
|
||||
return zugang(device, port=port), True
|
||||
|
||||
|
||||
def _juenger_als(zeitpunkt: str, sekunden: float) -> bool:
|
||||
"""Ist dieser ISO-Zeitstempel juenger als die Frist?"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
try:
|
||||
moment = datetime.fromisoformat(zeitpunkt)
|
||||
except ValueError:
|
||||
return False
|
||||
if moment.tzinfo is None:
|
||||
moment = moment.replace(tzinfo=timezone.utc)
|
||||
return (datetime.now(timezone.utc) - moment).total_seconds() < sekunden
|
||||
|
||||
|
||||
def verbindungsdatei(
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
<div class="flex-1">
|
||||
<div class="card__title">
|
||||
{{ icon('monitor', size=17) }} {{ device.name }}
|
||||
{% if zugang.verfahren %}
|
||||
{% if zugang.absage %}{{ pill('Verbindung abgelehnt', 'warning') }}
|
||||
{% elif zugang.verfahren %}
|
||||
{% if zugang.browsertauglich %}{{ pill('Browser-Sitzung möglich', 'online') }}
|
||||
{% else %}{{ pill('Verfahren nicht unterstützt', 'warning') }}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.8.6"
|
||||
version = "2.8.7"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -23,13 +23,38 @@ import RFB from "../vendor/novnc/core/rfb.js";
|
||||
// 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.
|
||||
// Drei Eingriffe in private Methoden der mitgelieferten Bibliothek. Bewusst
|
||||
// hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC laesst
|
||||
// sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt eine
|
||||
// Methode weg, greift der jeweilige Eingriff nicht mehr, und die Sitzung
|
||||
// laeuft wieder wie ohne ihn. Kein stiller Bruch.
|
||||
const istUnterstuetzt = RFB.prototype._isSupportedSecurityType;
|
||||
|
||||
// Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
|
||||
// "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
|
||||
// funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden, also
|
||||
// bevor eine Seite etwas daran aendern koennte. (Genau daran ist der erste
|
||||
// Versuch gescheitert -- die Meldung blieb leer.)
|
||||
let letzterGrund = "";
|
||||
const originalFail = RFB.prototype._fail;
|
||||
RFB.prototype._fail = function (details) {
|
||||
letzterGrund = String(details);
|
||||
return originalFail.call(this, details);
|
||||
};
|
||||
|
||||
// Und der Abschluss der Leitung selbst: der Schliesscode sagt, ob TESM
|
||||
// abgewiesen hat (1008), ob es ein Serverfehler war (1011) -- oder ob die
|
||||
// Verbindung nie zustande kam (1006).
|
||||
let letzterCode = 0;
|
||||
const originalSocketClose = RFB.prototype._socketClose;
|
||||
RFB.prototype._socketClose = function (ereignis) {
|
||||
letzterCode = (ereignis && ereignis.code) || 0;
|
||||
if (ereignis && ereignis.reason) {
|
||||
letzterGrund = letzterGrund || String(ereignis.reason);
|
||||
}
|
||||
return originalSocketClose.call(this, ereignis);
|
||||
};
|
||||
|
||||
const wurzel = document.getElementById("screen");
|
||||
if (wurzel) {
|
||||
starten(wurzel);
|
||||
@@ -54,32 +79,16 @@ 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 (letzterGrund) {
|
||||
teile.push(letzterGrund.replace(/^Failed (when|while) \w+: /, ""));
|
||||
}
|
||||
if (rfb && rfb._rfbAuthScheme > 0) {
|
||||
teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`);
|
||||
@@ -87,9 +96,35 @@ function starten(wurzel) {
|
||||
return teile.join(" · ");
|
||||
}
|
||||
|
||||
// Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene
|
||||
// Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die
|
||||
// Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM
|
||||
// "Upgrade" nicht durchreicht. Diese Probe trennt sie.
|
||||
function streckeTesten() {
|
||||
return new Promise((fertig) => {
|
||||
let beantwortet = false;
|
||||
const pruefling = new WebSocket(`${schema}//${window.location.host}/ws/probe`);
|
||||
const schliessen = (ergebnis) => {
|
||||
if (beantwortet) return;
|
||||
beantwortet = true;
|
||||
try {
|
||||
pruefling.close();
|
||||
} catch (fehler) {
|
||||
/* schon zu */
|
||||
}
|
||||
fertig(ergebnis);
|
||||
};
|
||||
pruefling.onmessage = () => schliessen(true);
|
||||
pruefling.onerror = () => schliessen(false);
|
||||
pruefling.onclose = () => schliessen(false);
|
||||
window.setTimeout(() => schliessen(false), 4000);
|
||||
});
|
||||
}
|
||||
|
||||
function verbinden(zugang) {
|
||||
melden("Verbindung wird aufgebaut …");
|
||||
letzterFehler = "";
|
||||
letzterGrund = "";
|
||||
letzterCode = 0;
|
||||
|
||||
// Vorauswahl nur, wenn wir nichts anzubieten haben: dann zaehlt der Weg,
|
||||
// der ohne Zugangsdaten auskommt -- sofern der Server ihn anbietet.
|
||||
@@ -136,10 +171,28 @@ function starten(wurzel) {
|
||||
melden(
|
||||
erklaerung
|
||||
? `Verbindung abgebrochen – ${erklaerung}`
|
||||
: "Verbindung abgebrochen, ohne dass der Client einen Grund nennt. " +
|
||||
"Antwortet der VNC-Server noch?",
|
||||
: "Verbindung abgebrochen. Wird geprüft, woran es liegt …",
|
||||
"fehler",
|
||||
);
|
||||
// 1006 heisst: die Leitung ging zu, ohne dass die Gegenseite einen
|
||||
// Grund geschickt hat -- typisch, wenn die WebSocket-Verbindung gar
|
||||
// nicht erst zustande kam. Dann sagt die Probe, wer schweigt.
|
||||
if (letzterCode === 1006 || !erklaerung) {
|
||||
streckeTesten().then((steht) => {
|
||||
melden(
|
||||
steht
|
||||
? `${erklaerung || "Verbindung abgebrochen"} – die WebSocket-Strecke zu ` +
|
||||
"TESM selbst steht (Probe erfolgreich), der Abbruch kommt also von " +
|
||||
"der VNC-Seite."
|
||||
: "Die WebSocket-Verbindung zu TESM kommt nicht zustande – auch eine " +
|
||||
"einfache Probe scheitert. Ein Proxy zwischen Browser und TESM muss " +
|
||||
"die Kopfzeilen Upgrade und Connection durchreichen (nginx: " +
|
||||
"location /ws/ mit proxy_set_header Upgrade $http_upgrade). Dasselbe " +
|
||||
"gilt für die Browser-Konsole.",
|
||||
"fehler",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
rfb = null;
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user