Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a528e9f3b2 | ||
|
|
3b3ce96678 | ||
|
|
42316ba137 | ||
|
|
d34baa67be |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.8.17"
|
||||
version = "2.8.21"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -411,12 +411,14 @@ def vnc_connection(device_id: int):
|
||||
ziel = vnc.ziel(device)
|
||||
if not ziel:
|
||||
abort(404)
|
||||
with transaction():
|
||||
_audit(
|
||||
conn,
|
||||
"device.vnc_opened",
|
||||
str(device["name"]),
|
||||
f"Verbindungsdatei für {ziel}:{port}",
|
||||
# Kein Eintrag in der Aenderungskette: eine Verbindungsdatei ist derselbe
|
||||
# Vorgang wie die Sitzung im Browser, nur fuer einen Viewer auf dem
|
||||
# Arbeitsplatz -- und sie aendert nichts. Wer zusieht, steht im laufenden
|
||||
# Protokoll; das Aenderungsprotokoll bleibt die Liste der Eingriffe.
|
||||
live = core().registry.get("live_log")
|
||||
if live is not None:
|
||||
live.info(
|
||||
"vnc", f"{current_user.username} lädt die Verbindungsdatei für {device['name']}"
|
||||
)
|
||||
return Response(
|
||||
vnc.verbindungsdatei(
|
||||
|
||||
@@ -50,6 +50,10 @@ VERBINDUNGSLIMIT = 6.0
|
||||
#: bis ein Schub weitergereicht wird.
|
||||
BLOCK = 32 * 1024
|
||||
|
||||
#: Wie oft nachgesehen wird, ob die Sitzung noch etwas tut. Kurz genug, dass
|
||||
#: das Trennen puenktlich ist, lang genug, dass das Nachsehen nichts kostet.
|
||||
TAKT = 1.0
|
||||
|
||||
|
||||
def _geraet(conn: Any, device_id: int) -> dict[str, Any] | None:
|
||||
device = inventory.get_device(conn, device_id)
|
||||
@@ -231,37 +235,91 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||
except OSError as exc:
|
||||
ws.close(1011, f"{host}:{port} antwortet nicht ({exc}).")
|
||||
return
|
||||
# Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht.
|
||||
#
|
||||
# ``create_connection`` setzt es an den Socket, und dort gilt es fuer jede
|
||||
# weitere Leseoperation. Ein Bildschirm, auf dem sich gerade nichts tut,
|
||||
# schickt sekundenlang nichts; nach sechs Sekunden Stille brach das Lesen
|
||||
# mit einer Zeitueberschreitung ab, und die Sitzung war weg. Genau so
|
||||
# beobachtet: Bild steht, nach fuenf bis zehn Sekunden Schluss.
|
||||
#
|
||||
# Stattdessen ein kurzer Takt: das Lesen laeuft immer wieder in ein
|
||||
# Zeitlimit, und *das* ist kein Abbruch, sondern der Anlass nachzusehen,
|
||||
# ob die Sitzung ueberhaupt noch etwas tut.
|
||||
gegenstelle.settimeout(TAKT)
|
||||
leerlauf = int(extension.settings.get(conn, "vnc_idle_timeout") or 0)
|
||||
|
||||
# Ins **laufende** Protokoll, nicht in die Aenderungskette: eine
|
||||
# Bildschirmsitzung aendert nichts an der Anlage. Das Aenderungsprotokoll
|
||||
# soll beantworten, wer was umgestellt hat -- wer wann zugesehen hat,
|
||||
# gehoert nicht in dieselbe Liste und macht sie in einem Betrieb mit
|
||||
# ein paar Dutzend Geraeten unlesbar. Sichtbar ist die Sitzung trotzdem,
|
||||
# solange sie laeuft.
|
||||
begonnen = time.monotonic()
|
||||
with extension.database.transaction(conn):
|
||||
audit.log(
|
||||
conn,
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
action="device.screen_opened",
|
||||
target=str(device["name"]),
|
||||
detail=f"Bildschirmsitzung zu {host}:{port}",
|
||||
ip=client_ip(),
|
||||
severity="warning",
|
||||
)
|
||||
live = extension.registry.get("live_log")
|
||||
if live is not None:
|
||||
live.info("vnc", f"{current_user.username} öffnet den Bildschirm von {device['name']}")
|
||||
|
||||
laeuft = threading.Event()
|
||||
laeuft.set()
|
||||
#: Wann zuletzt etwas floss -- in *irgendeine* Richtung. Eine Liste, weil
|
||||
#: beide Richtungen daran schreiben und eine Zuweisung in Python nicht in
|
||||
#: die andere hineinlaufen kann.
|
||||
zuletzt = [time.monotonic()]
|
||||
#: Grund des Endes, falls wir selbst getrennt haben.
|
||||
schluss = [""]
|
||||
#: Kam ueberhaupt schon ein Byte vom Geraet?
|
||||
gehoert = [False]
|
||||
|
||||
def vom_geraet() -> None:
|
||||
"""Alles, was der VNC-Server schickt, geht als Binaerrahmen weiter."""
|
||||
try:
|
||||
while laeuft.is_set():
|
||||
daten = gegenstelle.recv(BLOCK)
|
||||
try:
|
||||
daten = gegenstelle.recv(BLOCK)
|
||||
except socket.timeout:
|
||||
# Kein Fehler, nur Stille -- und die bedeutet zweierlei,
|
||||
# je nachdem, ob die Sitzung schon stand.
|
||||
#
|
||||
# **Vor dem ersten Byte** ist Stille ein Befund: der Port
|
||||
# nimmt an, aber es kommt nichts. Genau so verhaelt sich
|
||||
# ein RealVNC Server, der die Adresse gerade aussperrt. Wer
|
||||
# das nicht erfaehrt, sieht ewig "Verbindung wird
|
||||
# aufgebaut" -- und sucht den Fehler bei uns.
|
||||
if not gehoert[0] and time.monotonic() - begonnen > VERBINDUNGSLIMIT:
|
||||
schluss[0] = (
|
||||
f"{host}:{port} nimmt die Verbindung an, schickt aber nichts. "
|
||||
"So verhält sich ein VNC-Server, der die Adresse gerade "
|
||||
"aussperrt (RealVNC: BlacklistTimeout) – ein Neustart des "
|
||||
"VNC-Dienstes auf dem Gerät räumt das ab."
|
||||
)
|
||||
break
|
||||
# **Danach** ist Stille der Normalfall: ein Bildschirm, auf
|
||||
# dem sich nichts tut, schickt nichts.
|
||||
if gehoert[0] and leerlauf and time.monotonic() - zuletzt[0] > leerlauf:
|
||||
schluss[0] = (
|
||||
f"Sitzung nach {leerlauf} Sekunden ohne Bewegung getrennt"
|
||||
)
|
||||
break
|
||||
continue
|
||||
if not daten:
|
||||
if not gehoert[0]:
|
||||
schluss[0] = (
|
||||
f"{host}:{port} hat die Verbindung sofort wieder geschlossen."
|
||||
)
|
||||
break
|
||||
gehoert[0] = True
|
||||
zuletzt[0] = time.monotonic()
|
||||
ws.send(daten)
|
||||
except Exception: # noqa: BLE001 - Abbruch ist der Normalfall, nicht die Stoerung
|
||||
pass
|
||||
finally:
|
||||
laeuft.clear()
|
||||
try:
|
||||
ws.close()
|
||||
if schluss[0]:
|
||||
ws.close(1000 if gehoert[0] else 1011, schluss[0])
|
||||
else:
|
||||
ws.close()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
@@ -278,6 +336,7 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||
# waere entweder ein Irrtum oder ein Versuch -- beides endet
|
||||
# hier und nicht auf dem Geraet.
|
||||
continue
|
||||
zuletzt[0] = time.monotonic()
|
||||
gegenstelle.sendall(nachricht)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.debug("Bildschirmsitzung beendet (%s)", device["name"], exc_info=True)
|
||||
@@ -289,14 +348,9 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||
pass
|
||||
gegenstelle.close()
|
||||
dauer = int(time.monotonic() - begonnen)
|
||||
with extension.database.session() as frisch, extension.database.transaction(frisch):
|
||||
audit.log(
|
||||
frisch,
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
action="device.screen_closed",
|
||||
target=str(device["name"]),
|
||||
detail=f"Dauer {dauer}s",
|
||||
ip=client_ip(),
|
||||
severity="info",
|
||||
if live is not None:
|
||||
live.info(
|
||||
"vnc",
|
||||
f"Bildschirmsitzung {device['name']} beendet ({dauer}s)"
|
||||
+ (f" – {schluss[0]}" if schluss[0] else ""),
|
||||
)
|
||||
@@ -105,6 +105,7 @@ def system():
|
||||
"dashboard_public",
|
||||
"vnc_enabled",
|
||||
"vnc_port",
|
||||
"vnc_idle_timeout",
|
||||
"vnc_warn_unencrypted_off",
|
||||
"terminal_switches_enabled",
|
||||
"terminal_clients_enabled",
|
||||
@@ -148,6 +149,7 @@ def system_save():
|
||||
"dashboard_public",
|
||||
"vnc_enabled",
|
||||
"vnc_port",
|
||||
"vnc_idle_timeout",
|
||||
"vnc_warn_unencrypted_off",
|
||||
"terminal_switches_enabled",
|
||||
"terminal_clients_enabled",
|
||||
|
||||
@@ -66,6 +66,18 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = (
|
||||
minimum=1,
|
||||
maximum=65535,
|
||||
),
|
||||
SettingSpec(
|
||||
"vnc_idle_timeout",
|
||||
300,
|
||||
"int",
|
||||
"Bildschirmsitzung bei Untätigkeit trennen nach (Sekunden)",
|
||||
hint="Gezählt wird, wenn in **keine** Richtung mehr etwas fließt: der Bildschirm "
|
||||
"ändert sich nicht und es kommt keine Eingabe. Eine Sitzung, die jemand offen "
|
||||
"vergisst, hält sonst die Verbindung zum Gerät. 0 schaltet die Trennung ab.",
|
||||
section="monitoring",
|
||||
minimum=0,
|
||||
maximum=86400,
|
||||
),
|
||||
SettingSpec(
|
||||
"vnc_warn_unencrypted_off",
|
||||
False,
|
||||
|
||||
@@ -46,17 +46,6 @@
|
||||
{{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar',
|
||||
checked=values.dashboard_public,
|
||||
hint_text=specs['dashboard_public'].hint) }}
|
||||
{{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten',
|
||||
checked=values.vnc_enabled,
|
||||
hint_text=specs['vnc_enabled'].hint) }}
|
||||
<div class="form-grid">
|
||||
{{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number',
|
||||
hint_text=specs['vnc_port'].hint) }}
|
||||
</div>
|
||||
{{ 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) }}
|
||||
</div>
|
||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||
</form>
|
||||
@@ -117,6 +106,39 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
<div class="card__title">{{ icon('monitor', size=17) }} Bildschirm (VNC)</div>
|
||||
<div class="card__sub">
|
||||
Dieselben Regeln wie bei der Konsole: eigenes Recht, frische
|
||||
Passwortbestätigung, getrennte Zwischenablage. Der Knopf erscheint nur, wo
|
||||
ein VNC-Server antwortet – das prüft der Überwachungsdurchlauf mit.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('settings.system_save') }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body stack">
|
||||
{{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten',
|
||||
checked=values.vnc_enabled,
|
||||
hint_text=specs['vnc_enabled'].hint) }}
|
||||
<div class="form-grid">
|
||||
{{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number',
|
||||
hint_text=specs['vnc_port'].hint) }}
|
||||
{{ field('vnc_idle_timeout', 'Trennen bei Untätigkeit (Sekunden)',
|
||||
value=values.vnc_idle_timeout, type='number',
|
||||
hint_text=specs['vnc_idle_timeout'].hint) }}
|
||||
</div>
|
||||
{{ 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) }}
|
||||
</div>
|
||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -39,6 +39,7 @@ HTTPS=0
|
||||
HTTPS_PORT=""
|
||||
DOMAIN=""
|
||||
ACME_EMAIL=""
|
||||
ERZWINGEN=0
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -49,6 +50,39 @@ warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*"; }
|
||||
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
|
||||
# -- Welche Fassung liegt hier, und welche laeuft schon? ---------------------
|
||||
#
|
||||
# Ein Installationslauf auf dieselbe Fassung macht nichts besser: er baut die
|
||||
# Python-Umgebung neu, kopiert dieselben Dateien, laedt nginx neu und startet
|
||||
# den Dienst -- mit der einzigen Wirkung, dass die Anwendung ein paar Sekunden
|
||||
# weg ist. Wer "update.sh" in einen Zeitplan haengt, bekaeme das taeglich.
|
||||
#
|
||||
# Verglichen wird die Anwendung **und** der Kern: die beiden Nummern laufen
|
||||
# absichtlich auseinander (siehe pyproject.toml), und eine Auslieferung kann
|
||||
# denselben Anwendungsstand mit neuem Kern enthalten.
|
||||
|
||||
#: Die Nummer aus einer pyproject.toml. Die erste Zeile "version = ..." gilt --
|
||||
#: weitere Vorkommen stehen in Abhaengigkeiten und gehen uns nichts an.
|
||||
paket_version() {
|
||||
sed -n 's/^version = "\([^"]*\)"/\1/p' "$1" 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
#: Was in dieser Installation laeuft: "<anwendung> <kern>", sonst leer.
|
||||
laufende_version() {
|
||||
local wurzel="$1" modul="$2"
|
||||
[[ -x "$wurzel/venv/bin/python" ]] || return 0
|
||||
"$wurzel/venv/bin/python" -c \
|
||||
"import $modul, tesm_core; print($modul.__version__, tesm_core.__version__)" \
|
||||
2>/dev/null || true
|
||||
}
|
||||
|
||||
#: Ist das, was hier liegt, schon installiert? Eine eigene Funktion, weil genau
|
||||
#: diese Frage sich pruefen lassen muss -- ohne einen Server aufzusetzen.
|
||||
gleiche_version() {
|
||||
local laeuft="$1" liegt="$2"
|
||||
[[ -n "$laeuft" && "$laeuft" == "$liegt" ]]
|
||||
}
|
||||
|
||||
#: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist.
|
||||
SICHERUNG_STEHT=0
|
||||
INSTALL_FERTIG=0
|
||||
@@ -129,6 +163,7 @@ Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
||||
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
||||
--skip-packages Keine Systempakete installieren
|
||||
--yes Rueckfragen ueberspringen
|
||||
--force Auch dann einrichten, wenn dieselbe Fassung schon laeuft
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -144,6 +179,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
||||
--skip-packages) SKIP_PACKAGES=1; shift ;;
|
||||
--yes|-y) ASSUME_YES=1; shift ;;
|
||||
--force) ERZWINGEN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unbekannte Option: $1" ;;
|
||||
esac
|
||||
@@ -261,6 +297,23 @@ FAILED_DIR="/srv/$NAME-failed-$TIMESTAMP"
|
||||
IS_UPDATE=0
|
||||
[[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1
|
||||
|
||||
# Laeuft hier schon genau das, was hier liegt? Dann ist Schluss, bevor
|
||||
# irgendetwas angefasst wird -- und zwar mit Ausstiegscode 0: es ist nichts
|
||||
# schiefgegangen, es war nur nichts zu tun.
|
||||
MODUL="tesm"
|
||||
[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license"
|
||||
LIEGT="$(paket_version "$REPO_DIR/apps/$APP/pyproject.toml") $(
|
||||
paket_version "$REPO_DIR/packages/tesm-core/pyproject.toml")"
|
||||
LAEUFT="$(laufende_version "$ROOT" "$MODUL")"
|
||||
if [[ $IS_UPDATE -eq 1 && $ERZWINGEN -eq 0 ]] && gleiche_version "$LAEUFT" "$LIEGT"; then
|
||||
step "Nichts zu tun"
|
||||
info "$APP ${LIEGT%% *} (tesm-core ${LIEGT##* }) laeuft hier bereits."
|
||||
info "Ein zweiter Durchlauf wuerde nur die Umgebung neu bauen und den"
|
||||
info "Dienst neu starten -- ohne dass sich etwas aendert."
|
||||
info "Trotzdem neu einrichten: --force"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
step "TESM-Installation"
|
||||
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||
info "Ziel : $ROOT"
|
||||
@@ -278,6 +331,7 @@ else
|
||||
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
||||
fi
|
||||
info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)"
|
||||
info "Fassung : ${LIEGT%% *} (tesm-core ${LIEGT##* })${LAEUFT:+, bisher ${LAEUFT%% *}}"
|
||||
|
||||
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||
read -r -p "Fortfahren? [j/N] " answer
|
||||
|
||||
@@ -33,6 +33,8 @@ EXTRA=("$@")
|
||||
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
||||
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
||||
echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2
|
||||
echo "Laeuft die Fassung schon, endet der Aufruf ohne Arbeit; --force setzt" >&2
|
||||
echo "sie trotzdem neu auf." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
@@ -173,6 +175,32 @@ release_holen() {
|
||||
|
||||
release_holen
|
||||
|
||||
# Schon installiert? Dann gar nicht erst fragen.
|
||||
#
|
||||
# Dieselbe Pruefung steht in install.sh und gilt dort endgueltig (sie
|
||||
# vergleicht auch den Kern). Hier steht sie noch einmal, weil ein Update, das
|
||||
# ohne Wirkung bleibt, gar nicht erst nach einer Bestaetigung fragen soll --
|
||||
# und weil "es lief durch" sonst wie eine Aktualisierung aussieht.
|
||||
ERZWINGEN=0
|
||||
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
|
||||
[[ "${EXTRA[i]}" == "--force" ]] && ERZWINGEN=1
|
||||
done
|
||||
INSTALLATION="$APP"
|
||||
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
|
||||
[[ "${EXTRA[i]}" == "--instance" ]] && INSTALLATION="$APP-${EXTRA[i + 1]:-}"
|
||||
done
|
||||
MODUL="tesm"
|
||||
[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license"
|
||||
if [[ $ERZWINGEN -eq 0 && -x "/srv/$INSTALLATION/venv/bin/python" ]]; then
|
||||
LAEUFT="$("/srv/$INSTALLATION/venv/bin/python" -c \
|
||||
"import $MODUL; print($MODUL.__version__)" 2>/dev/null || true)"
|
||||
if [[ -n "$LAEUFT" && "v$LAEUFT" == "$TAG" ]]; then
|
||||
echo "==> $APP $LAEUFT laeuft hier bereits -- nichts zu tun."
|
||||
echo " Trotzdem neu einrichten: --force"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es
|
||||
# bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht.
|
||||
OHNE_RUECKFRAGE=0
|
||||
|
||||
@@ -120,6 +120,14 @@ sudo bash /srv/tesm/deploy/update.sh tesm
|
||||
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
|
||||
```
|
||||
|
||||
**Läuft die Fassung schon, passiert nichts.** `update.sh` vergleicht den
|
||||
aufgelösten Tag mit dem, was installiert ist, und endet mit `0`, bevor es eine
|
||||
Rückfrage stellt; `install.sh` prüft es noch einmal genauer (Anwendung **und**
|
||||
Kern) und bricht vor dem ersten Eingriff ab. Ein zweiter Lauf auf dieselbe
|
||||
Fassung baut sonst die Python-Umgebung neu, kopiert dieselben Dateien und
|
||||
startet den Dienst neu -- ein paar Sekunden Ausfall ohne jede Wirkung. Wer das
|
||||
trotzdem will (eine beschädigte Installation etwa), hängt `--force` an.
|
||||
|
||||
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||
eine **benannte Instanz** zu aktualisieren:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.8.8"
|
||||
version = "2.8.11"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -11,18 +11,42 @@
|
||||
|
||||
import RFB from "../vendor/novnc/core/rfb.js";
|
||||
|
||||
// Zwei Eingriffe in private Methoden der mitgelieferten Bibliothek -- beide
|
||||
// nur, um zu *erfahren*, was schiefging; keiner greift in die Verhandlung ein.
|
||||
// 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, entfaellt nur die Auskunft, nicht die Sitzung.
|
||||
// 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, entfaellt nur die Auskunft, nicht die Sitzung.
|
||||
//
|
||||
// Es stand hier einmal ein dritter, der die Wahl des Sicherheitsverfahrens
|
||||
// umbog: ohne hinterlegte Zugangsdaten sollte ein Verfahren ohne Anmeldung
|
||||
// gewinnen, weil der Server RA2ne zuerst nennt. Gut gemeint und falsch --
|
||||
// genau dieser Griff hat eine Sitzung, die lief, zum Abbruch gebracht: der
|
||||
// RealVNC Server bietet "ohne Authentifizierung" zwar an, legt bei dieser Wahl
|
||||
// aber auf. Die Reihenfolge des Servers ist seine Entscheidung, nicht unsere.
|
||||
// Zwei davon holen nur Auskunft (_fail, _socketClose). Der dritte greift in
|
||||
// die Wahl des Sicherheitsverfahrens ein -- aber erst, **nachdem** eines
|
||||
// nachweislich nicht getragen hat.
|
||||
//
|
||||
// Die Vorgeschichte gehoert dazu, damit niemand den Schritt zurueckgeht: hier
|
||||
// stand einmal eine Vorauswahl, die ohne hinterlegte Zugangsdaten ein
|
||||
// Verfahren ohne Anmeldung erzwang, weil RealVNC RA2ne zuerst nennt. Das war
|
||||
// geraten und falsch. Was jetzt hier steht, ist das Gegenteil einer
|
||||
// Vermutung: der Server nennt seine Reihenfolge, die gilt -- und wenn seine
|
||||
// erste Wahl die Verbindung abreissen laesst, ist das ein Befund, auf den man
|
||||
// antworten darf. Gemessen am Geraet: RealVNC bietet 6 (RA2ne) an, waehlt man
|
||||
// es, legt es auf; daneben liegt 1 (ohne Authentifizierung), und darueber
|
||||
// laeuft auch der Viewer.
|
||||
const istUnterstuetzt = RFB.prototype._isSupportedSecurityType;
|
||||
|
||||
//: Verfahren, die in dieser Sitzung schon abgerissen sind.
|
||||
const gescheitert = new Set();
|
||||
|
||||
//: Hoechstens ein zweiter Anlauf. Jeder Versuch ist eine Verbindung, und ein
|
||||
//: RealVNC Server zaehlt abgebrochene Versuche mit -- wer dreimal anklopft,
|
||||
//: sperrt sich aus.
|
||||
const HOECHSTENS = 2;
|
||||
|
||||
//: So lange darf "wird aufgebaut" stehen, bevor die Seite selbst nachsieht.
|
||||
//:
|
||||
//: Der Server hat eigene Grenzen und meldet sich, wenn das Geraet schweigt --
|
||||
//: aber nur, wenn die Leitung dorthin ueberhaupt steht. Kommt die
|
||||
//: WebSocket-Verbindung gar nicht erst zustande, wartet noVNC ohne jedes
|
||||
//: Ereignis, und die Seite stuende stumm bis zum Seitenwechsel. Genau so
|
||||
//: gemeldet: "Verbindung wird aufgebaut ..." und sonst nichts.
|
||||
const AUFBAU_GRENZE = 12000;
|
||||
// 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
|
||||
@@ -72,6 +96,10 @@ function starten(wurzel) {
|
||||
);
|
||||
|
||||
let rfb = null;
|
||||
let aufbauUhr = 0;
|
||||
let versuche = 0;
|
||||
let stand = false;
|
||||
let letzterZugang = {};
|
||||
|
||||
function melden(text, art = "") {
|
||||
zeile.textContent = text;
|
||||
@@ -86,9 +114,35 @@ function starten(wurzel) {
|
||||
if (rfb && rfb._rfbAuthScheme > 0) {
|
||||
teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`);
|
||||
}
|
||||
if (gescheitert.size) {
|
||||
teile.push(`erfolglos: ${[...gescheitert].join(", ")}`);
|
||||
}
|
||||
return teile.join(" · ");
|
||||
}
|
||||
|
||||
// Hat das gewählte Verfahren die Verbindung abreissen lassen? Dann ist das
|
||||
// ein Befund und keine Vermutung -- der naechste Anlauf laesst es aus.
|
||||
//
|
||||
// Nur solange nie ein Bild stand: bricht eine **laufende** Sitzung ab, liegt
|
||||
// es nicht am Anmeldeverfahren, und ein stiller Neuaufbau wuerde die Ursache
|
||||
// verdecken.
|
||||
function naechsterVersuch() {
|
||||
const verfahren = rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0;
|
||||
if (!verfahren || versuche >= HOECHSTENS || gescheitert.has(verfahren)) {
|
||||
return false;
|
||||
}
|
||||
gescheitert.add(verfahren);
|
||||
melden(
|
||||
`Verfahren ${verfahren} hat die Verbindung nicht getragen – nächster Anlauf ` +
|
||||
"mit dem nächsten, das der Server anbietet …",
|
||||
"warnung",
|
||||
);
|
||||
const zugang = letzterZugang;
|
||||
rfb = null;
|
||||
window.setTimeout(() => verbinden(zugang), 400);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -115,9 +169,38 @@ function starten(wurzel) {
|
||||
}
|
||||
|
||||
function verbinden(zugang) {
|
||||
melden("Verbindung wird aufgebaut …");
|
||||
melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …");
|
||||
letzterGrund = "";
|
||||
letzterCode = 0;
|
||||
letzterZugang = zugang;
|
||||
versuche += 1;
|
||||
window.clearTimeout(aufbauUhr);
|
||||
aufbauUhr = window.setTimeout(() => {
|
||||
// Weder verbunden noch abgebrochen noch nach Zugangsdaten gefragt --
|
||||
// dann haengt es an der Leitung, nicht am VNC-Server.
|
||||
melden("Es kommt keine Antwort. Wird geprüft, woran es liegt …", "warnung");
|
||||
streckeTesten().then((steht) => {
|
||||
melden(
|
||||
steht
|
||||
? "Die WebSocket-Strecke zu TESM steht, aber die Sitzung kommt nicht " +
|
||||
"zustande. Antwortet der VNC-Server auf dem Gerät noch? Ein Neustart " +
|
||||
"des VNC-Dienstes räumt eine Sperre ab."
|
||||
: "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).",
|
||||
"fehler",
|
||||
);
|
||||
});
|
||||
}, AUFBAU_GRENZE);
|
||||
|
||||
// Was abgerissen ist, wird nicht noch einmal gewaehlt. Sonst faende noVNC
|
||||
// dasselbe Verfahren wieder zuerst -- und der zweite Anlauf waere eine
|
||||
// Wiederholung des ersten.
|
||||
RFB.prototype._isSupportedSecurityType = function (typ) {
|
||||
if (gescheitert.has(typ)) return false;
|
||||
return istUnterstuetzt.call(this, typ);
|
||||
};
|
||||
|
||||
rfb = new RFB(leinwand, adresse, {
|
||||
credentials: zugang,
|
||||
@@ -131,6 +214,8 @@ function starten(wurzel) {
|
||||
rfb.focusOnClick = true;
|
||||
|
||||
rfb.addEventListener("connect", () => {
|
||||
window.clearTimeout(aufbauUhr);
|
||||
stand = true;
|
||||
melden(`Verbunden mit ${wurzel.dataset.ziel}`, "ok");
|
||||
abfrage.hidden = true;
|
||||
leinwand.hidden = false;
|
||||
@@ -139,11 +224,17 @@ function starten(wurzel) {
|
||||
});
|
||||
|
||||
rfb.addEventListener("disconnect", (ereignis) => {
|
||||
window.clearTimeout(aufbauUhr);
|
||||
leinwand.hidden = true;
|
||||
knopfTrennen.hidden = true;
|
||||
knopfStrg.hidden = true;
|
||||
if (ereignis.detail && ereignis.detail.clean) {
|
||||
melden("Sitzung beendet.");
|
||||
// Beim sauberen Schluss steht der Grund im Schliessrahmen -- etwa,
|
||||
// dass TESM wegen Untätigkeit getrennt hat. Ohne ihn stuende da
|
||||
// "beendet", und niemand wuesste, warum das Bild weg ist.
|
||||
melden(letzterGrund ? `Sitzung beendet – ${letzterGrund}` : "Sitzung beendet.");
|
||||
} else if (!stand && naechsterVersuch()) {
|
||||
return;
|
||||
} else {
|
||||
const erklaerung = grund();
|
||||
melden(
|
||||
@@ -176,6 +267,7 @@ function starten(wurzel) {
|
||||
});
|
||||
|
||||
rfb.addEventListener("credentialsrequired", (ereignis) => {
|
||||
window.clearTimeout(aufbauUhr);
|
||||
const gefragt = ereignis.detail.types || ["password"];
|
||||
abfrage.hidden = false;
|
||||
feldBenutzer.closest("label").hidden = !gefragt.includes("username");
|
||||
@@ -184,6 +276,7 @@ function starten(wurzel) {
|
||||
});
|
||||
|
||||
rfb.addEventListener("securityfailure", (ereignis) => {
|
||||
window.clearTimeout(aufbauUhr);
|
||||
melden(
|
||||
`Anmeldung abgelehnt: ${ereignis.detail.reason || "kein Grund genannt"}`,
|
||||
"fehler",
|
||||
|
||||
+19
-8
@@ -21,14 +21,25 @@ 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.
|
||||
in `js/screen.js`):
|
||||
|
||||
* `RFB.prototype._fail` und `_socketClose` -- nur, um den Grund eines
|
||||
Abbruchs zu erfahren. noVNC meldet ihn sonst ausschliesslich in die
|
||||
Entwicklerkonsole, und `console.error` mitzulesen traegt nicht: die
|
||||
Bibliothek bindet es beim Laden.
|
||||
* `RFB.prototype._isSupportedSecurityType` -- **erst nachdem** ein Verfahren
|
||||
die Verbindung nachweislich hat abreissen lassen. Dann wird es fuer den
|
||||
zweiten Anlauf ausgelassen, damit noVNC nicht wieder dasselbe zuerst findet.
|
||||
Hoechstens zwei Anlaeufe, denn jeder ist eine Verbindung, und ein RealVNC
|
||||
Server zaehlt abgebrochene mit.
|
||||
|
||||
Hier stand einmal das Gegenteil: eine **Vorauswahl**, die ohne hinterlegte
|
||||
Zugangsdaten ein Verfahren ohne Anmeldung erzwang. Das war geraten und hat
|
||||
eine laufende Sitzung gebrochen. Der Unterschied ist nicht der Code, sondern
|
||||
der Anlass: eine Vermutung vorher gegen einen Befund hinterher.
|
||||
|
||||
Faellt eine dieser Methoden in einer neueren Fassung weg, entfaellt nur die
|
||||
Auskunft bzw. der zweite Anlauf -- die Sitzung laeuft wie ohne sie.
|
||||
|
||||
**Was noVNC an Sicherheitsverfahren spricht** (`core/rfb.js`,
|
||||
`_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES
|
||||
|
||||
Reference in new issue
Block a user