Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34baa67be | ||
|
|
491724beb5 | ||
|
|
b54b87ca6f |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.15"
|
version = "2.8.18"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -374,13 +374,13 @@ def detail(device_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
||||||
"""Der Bildschirmzugriff dieses Clients -- frisch gefragt, nicht geraten.
|
"""Der Bildschirmzugriff dieses Clients -- aus dem, was bekannt ist.
|
||||||
|
|
||||||
Die Kacheln der Statusuebersicht nehmen den Stand aus dem letzten
|
Diese Seite fragt das Geraet **nicht**. Erreichbarkeit kommt aus dem
|
||||||
Ueberwachungsdurchlauf; hier geht es um **ein** Geraet, also fragt die Seite
|
Ueberwachungsdurchlauf, die Anmeldeverfahren aus der letzten
|
||||||
selbst nach (ein halbe Sekunde Zeitlimit, und nur wenn der Client gerade
|
ausdruecklichen Pruefung. Der Grund steht in :func:`vnc.zugang_gemerkt`:
|
||||||
erreichbar ist). Das Ergebnis wird gleich festgehalten -- dann stimmt auch
|
jede Messung ist eine angefangene Protokollverhandlung, und ein RealVNC
|
||||||
die Kachel, ohne auf den naechsten Durchlauf zu warten.
|
Server zaehlt so etwas mit.
|
||||||
"""
|
"""
|
||||||
extension = core()
|
extension = core()
|
||||||
if not current_user.has("device_vnc.execute"):
|
if not current_user.has("device_vnc.execute"):
|
||||||
@@ -388,12 +388,7 @@ def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
|||||||
if not extension.settings.get(conn, "vnc_enabled"):
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
return None
|
return None
|
||||||
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
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")
|
return vnc.zugang_gemerkt(device, port=port)
|
||||||
zugang = vnc.zugang(device, port=port, pruefen=online)
|
|
||||||
if online:
|
|
||||||
with transaction():
|
|
||||||
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
|
||||||
return zugang
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<int:device_id>/vnc")
|
@bp.get("/<int:device_id>/vnc")
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import Blueprint, abort, render_template
|
from flask import Blueprint, abort, flash, redirect, render_template, url_for
|
||||||
from tesm_core import audit
|
from tesm_core import audit
|
||||||
from tesm_core.auth.models import current_user, permission_required, reauth_required
|
from tesm_core.auth.models import current_user, permission_required, reauth_required
|
||||||
from tesm_core.auth.service import client_ip, reauth_is_fresh
|
from tesm_core.auth.service import client_ip, reauth_is_fresh
|
||||||
@@ -71,11 +71,13 @@ def _port(conn: Any) -> int:
|
|||||||
def screen(device_id: int):
|
def screen(device_id: int):
|
||||||
"""Die Seite mit der Leinwand.
|
"""Die Seite mit der Leinwand.
|
||||||
|
|
||||||
Sie misst vor dem Verbinden, **wie** der VNC-Server sich authentifizieren
|
Sie beruehrt das Geraet nicht: die Sitzung selbst ist der einzige Anlass,
|
||||||
will. Bietet er nur Verfahren an, die der Browser-Client nicht kennt, steht
|
dort anzuklopfen. Was ueber die Anmeldeverfahren bekannt ist, steht in der
|
||||||
das als Satz auf der Seite -- mit der Zeile, die das auf dem Geraet aendert.
|
Datenbank -- aus der letzten ausdruecklichen Pruefung (Knopf "Verfahren
|
||||||
Der Unterschied zur schwarzen Flaeche ist der ganze Punkt: wer nicht weiss,
|
pruefen"). Dass diese Seite frueher selbst gemessen hat, war ein Fehler:
|
||||||
woran es liegt, sucht den Fehler bei TESM.
|
jede Messung ist eine angefangene und abgebrochene Protokollverhandlung,
|
||||||
|
und ein RealVNC Server zaehlt so etwas -- das Oeffnen der Seite hat der
|
||||||
|
Sitzung also selbst im Weg gestanden.
|
||||||
"""
|
"""
|
||||||
conn = db()
|
conn = db()
|
||||||
device = _geraet(conn, device_id)
|
device = _geraet(conn, device_id)
|
||||||
@@ -85,7 +87,7 @@ def screen(device_id: int):
|
|||||||
if not port:
|
if not port:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
zugang = vnc.zugang(device, port=port, pruefen=str(device.get("state") or "") == "online")
|
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||||
benutzer, passwort = "", ""
|
benutzer, passwort = "", ""
|
||||||
darf_geheimnis = bool(current_user.has("credentials.secrets"))
|
darf_geheimnis = bool(current_user.has("credentials.secrets"))
|
||||||
if device.get("credential_id") and darf_geheimnis:
|
if device.get("credential_id") and darf_geheimnis:
|
||||||
@@ -122,6 +124,43 @@ def screen(device_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/monitoring/clients/<int:device_id>/screen/probe")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
@reauth_required
|
||||||
|
def probe(device_id: int):
|
||||||
|
"""Fragt das Geraet ausdruecklich, wie es sich authentifizieren will.
|
||||||
|
|
||||||
|
Ein Knopf und keine Nebenwirkung des Seitenaufbaus: die Messung bricht eine
|
||||||
|
Protokollverhandlung ab, und RealVNC zaehlt das. Sie gehoert in die Hand
|
||||||
|
dessen, der gerade sucht -- nicht in jeden Aufruf.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
port = _port(conn)
|
||||||
|
if not port:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
from ..services import monitor
|
||||||
|
|
||||||
|
zugang = vnc.zugang(device, port=port)
|
||||||
|
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)
|
||||||
|
if zugang.absage:
|
||||||
|
flash(f"Der Server antwortet: „{zugang.absage}“", "warning")
|
||||||
|
elif zugang.verfahren:
|
||||||
|
flash(
|
||||||
|
"Angebotene Anmeldeverfahren: "
|
||||||
|
+ ", ".join(vnc.verfahren_name(v) for v in zugang.verfahren),
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(f"{zugang.ziel}:{port} hat nicht geantwortet.", "danger")
|
||||||
|
return redirect(url_for("screen.screen", device_id=device_id))
|
||||||
|
|
||||||
|
|
||||||
def init_app(app) -> None: # type: ignore[no-untyped-def]
|
def init_app(app) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Meldet Seite und -- falls verfuegbar -- die WebSocket-Strecke an."""
|
"""Meldet Seite und -- falls verfuegbar -- die WebSocket-Strecke an."""
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|||||||
@@ -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]
|
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
"""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=10, name="vnc-erreichbarkeit", sql=V10),
|
||||||
Migration(version=11, name="vnc-ziel", sql=V11),
|
Migration(version=11, name="vnc-ziel", sql=V11),
|
||||||
Migration(version=12, name="vnc-verfahren", sql=V12),
|
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.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,
|
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_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
|
FROM devices d
|
||||||
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
|
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
|
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
|
duration_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
def merke_vnc(
|
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = "") -> None:
|
||||||
conn: sqlite3.Connection,
|
|
||||||
device_id: int,
|
|
||||||
offen: bool,
|
|
||||||
ziel: str = "",
|
|
||||||
verfahren: tuple[int, ...] = (),
|
|
||||||
) -> None:
|
|
||||||
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
||||||
|
|
||||||
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
||||||
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
||||||
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
||||||
geschrieben, von dem, der ohnehin gefragt hat.
|
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(
|
conn.execute(
|
||||||
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=?, vnc_types=? "
|
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? WHERE device_id=?",
|
||||||
"WHERE device_id=?",
|
(1 if offen else 0, _now(), ziel, device_id),
|
||||||
(
|
)
|
||||||
1 if offen else 0,
|
|
||||||
_now(),
|
|
||||||
ziel,
|
def merke_verfahren(
|
||||||
",".join(str(v) for v in verfahren),
|
conn: sqlite3.Connection, device_id: int, verfahren: tuple[int, ...], absage: str = ""
|
||||||
device_id,
|
) -> 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
|
erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert
|
||||||
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
||||||
je Aufruf, und nur fuer Geraete, die ohnehin gerade antworten.
|
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
|
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
|
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
||||||
# seinen Knopf nicht verlieren.
|
# seinen Knopf nicht verlieren.
|
||||||
return device, ergebnis, None
|
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:
|
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
|
||||||
results = list(pool.map(pruefen, devices))
|
results = list(pool.map(pruefen, devices))
|
||||||
@@ -587,13 +597,7 @@ def run_sweep(
|
|||||||
report.checked += 1
|
report.checked += 1
|
||||||
previous, current = record_status(conn, int(device["id"]), result)
|
previous, current = record_status(conn, int(device["id"]), result)
|
||||||
if vnc_offen is not None:
|
if vnc_offen is not None:
|
||||||
merke_vnc(
|
merke_vnc(conn, int(device["id"]), vnc_offen.erreichbar, vnc_offen.ziel)
|
||||||
conn,
|
|
||||||
int(device["id"]),
|
|
||||||
vnc_offen.erreichbar,
|
|
||||||
vnc_offen.ziel,
|
|
||||||
vnc_offen.verfahren,
|
|
||||||
)
|
|
||||||
if result.ok:
|
if result.ok:
|
||||||
report.online += 1
|
report.online += 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ class Zugang:
|
|||||||
#: gemessen). Daraus entsteht der Satz, warum eine Sitzung im Browser
|
#: gemessen). Daraus entsteht der Satz, warum eine Sitzung im Browser
|
||||||
#: gehen wird oder nicht.
|
#: gehen wird oder nicht.
|
||||||
verfahren: tuple[int, ...] = ()
|
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
|
@property
|
||||||
def browsertauglich(self) -> bool:
|
def browsertauglich(self) -> bool:
|
||||||
@@ -72,6 +75,14 @@ class Zugang:
|
|||||||
"""Warum eine Sitzung im Browser nicht zustande kaeme -- im Klartext."""
|
"""Warum eine Sitzung im Browser nicht zustande kaeme -- im Klartext."""
|
||||||
if not self.erreichbar:
|
if not self.erreichbar:
|
||||||
return f"Auf {self.ziel}:{self.port} antwortet kein VNC-Server."
|
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:
|
if not self.verfahren:
|
||||||
return ""
|
return ""
|
||||||
if self.browsertauglich:
|
if self.browsertauglich:
|
||||||
@@ -181,7 +192,36 @@ def verfahren_name(nummer: int) -> str:
|
|||||||
return str(nummer)
|
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.
|
"""Fragt den Server, **wie** er sich authentifizieren will.
|
||||||
|
|
||||||
Der Anfang einer VNC-Verbindung ist fuer jeden Server derselbe: er nennt
|
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
|
Das ist die Angabe, an der eine Bildschirmsitzung im Browser scheitert oder
|
||||||
gelingt, und sie hier zu messen ist der Unterschied zwischen einer
|
gelingt, und sie hier zu messen ist der Unterschied zwischen einer
|
||||||
schwarzen Flaeche und dem Satz "dieser Server bietet nur Verfahren an, die
|
schwarzen Flaeche und dem Satz "dieser Server bietet nur Verfahren an, die
|
||||||
der Browser nicht kennt". Gibt ``(Fassung, Verfahren)`` zurueck; bei einem
|
der Browser nicht kennt".
|
||||||
Fehler ``("", ())``.
|
|
||||||
|
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
|
import socket
|
||||||
|
|
||||||
@@ -202,7 +246,7 @@ def sicherheitsverfahren(host: str, port: int = STANDARD_PORT) -> tuple[str, tup
|
|||||||
sock.settimeout(ZEITLIMIT)
|
sock.settimeout(ZEITLIMIT)
|
||||||
gruss = sock.recv(12)
|
gruss = sock.recv(12)
|
||||||
if not gruss.startswith(b"RFB "):
|
if not gruss.startswith(b"RFB "):
|
||||||
return "", ()
|
return "", (), ""
|
||||||
fassung = gruss[4:11].decode("ascii", "replace")
|
fassung = gruss[4:11].decode("ascii", "replace")
|
||||||
# Wir antworten mit 3.8 -- der Fassung, die auch der Browser
|
# Wir antworten mit 3.8 -- der Fassung, die auch der Browser
|
||||||
# spricht. Ein Server, der mehr kann, faellt darauf zurueck und
|
# spricht. Ein Server, der mehr kann, faellt darauf zurueck und
|
||||||
@@ -225,26 +269,45 @@ def sicherheitsverfahren(host: str, port: int = STANDARD_PORT) -> tuple[str, tup
|
|||||||
break
|
break
|
||||||
rohwert += stueck
|
rohwert += stueck
|
||||||
if len(rohwert) < 4:
|
if len(rohwert) < 4:
|
||||||
return fassung, ()
|
return fassung, (), ""
|
||||||
bestimmt = int.from_bytes(rohwert, "big")
|
bestimmt = int.from_bytes(rohwert, "big")
|
||||||
return fassung, () if bestimmt == 0 else (bestimmt,)
|
if bestimmt == 0:
|
||||||
|
return fassung, (), _absage(sock)
|
||||||
|
return fassung, (bestimmt,), ""
|
||||||
|
|
||||||
anzahl = sock.recv(1)
|
anzahl = sock.recv(1)
|
||||||
if not anzahl:
|
if not anzahl:
|
||||||
return fassung, ()
|
return fassung, (), ""
|
||||||
wie_viele = anzahl[0]
|
wie_viele = anzahl[0]
|
||||||
if wie_viele == 0:
|
if wie_viele == 0:
|
||||||
# Null heisst: der Server lehnt ab und schickt den Grund.
|
# Null heisst: der Server lehnt ab und schickt den Grund.
|
||||||
return fassung, ()
|
return fassung, (), _absage(sock)
|
||||||
liste = b""
|
liste = b""
|
||||||
while len(liste) < wie_viele:
|
while len(liste) < wie_viele:
|
||||||
stueck = sock.recv(wie_viele - len(liste))
|
stueck = sock.recv(wie_viele - len(liste))
|
||||||
if not stueck:
|
if not stueck:
|
||||||
break
|
break
|
||||||
liste += stueck
|
liste += stueck
|
||||||
return fassung, tuple(liste)
|
return fassung, tuple(liste), ""
|
||||||
except OSError:
|
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:
|
def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True) -> Zugang:
|
||||||
@@ -252,8 +315,42 @@ def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: boo
|
|||||||
host = ziel(device)
|
host = ziel(device)
|
||||||
if not pruefen:
|
if not pruefen:
|
||||||
return Zugang(ziel=host, port=port, erreichbar=False)
|
return Zugang(ziel=host, port=port, erreichbar=False)
|
||||||
fassung, verfahren = sicherheitsverfahren(host, port)
|
fassung, verfahren, absage = sicherheitsverfahren(host, port)
|
||||||
return Zugang(ziel=host, port=port, erreichbar=bool(fassung), verfahren=verfahren)
|
return Zugang(
|
||||||
|
ziel=host,
|
||||||
|
port=port,
|
||||||
|
erreichbar=bool(fassung),
|
||||||
|
verfahren=verfahren,
|
||||||
|
absage=absage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def zugang_gemerkt(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> Zugang:
|
||||||
|
"""Der Zugang, wie er in der Datenbank steht -- **ohne** das Geraet zu fragen.
|
||||||
|
|
||||||
|
Frueher hat diese Funktion selbst gemessen, wenn die letzte Messung alt war.
|
||||||
|
Das ist gestrichen, und zwar aus Erfahrung: jede Messung ist eine
|
||||||
|
angefangene und abgebrochene Protokollverhandlung, und ein RealVNC Server
|
||||||
|
zaehlt so etwas. Wer eine Bildschirmseite oeffnet, klopfte damit zweimal an
|
||||||
|
-- einmal zum Messen, einmal fuer die Sitzung --, und das zweite Klopfen
|
||||||
|
fiel dem ersten zum Opfer.
|
||||||
|
|
||||||
|
Jetzt beruehrt das Oeffnen einer Seite das Geraet gar nicht mehr. Die
|
||||||
|
Erreichbarkeit kommt aus dem Ueberwachungsdurchlauf (ein blosser
|
||||||
|
Verbindungsaufbau), die Anmeldeverfahren aus der letzten **ausdruecklichen**
|
||||||
|
Pruefung -- der Knopf dafuer steht auf der Seite und wird gedrueckt, wenn
|
||||||
|
etwas nicht geht.
|
||||||
|
"""
|
||||||
|
gespeichert = str(device.get("vnc_types") or "")
|
||||||
|
return Zugang(
|
||||||
|
ziel=ziel(device),
|
||||||
|
port=port,
|
||||||
|
erreichbar=bool(device.get("vnc_open")),
|
||||||
|
verfahren=tuple(
|
||||||
|
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
||||||
|
),
|
||||||
|
absage=str(device.get("vnc_reason") or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verbindungsdatei(
|
def verbindungsdatei(
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="card__title">
|
<div class="card__title">
|
||||||
{{ icon('monitor', size=17) }} {{ device.name }}
|
{{ 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') }}
|
{% if zugang.browsertauglich %}{{ pill('Browser-Sitzung möglich', 'online') }}
|
||||||
{% else %}{{ pill('Verfahren nicht unterstützt', 'warning') }}{% endif %}
|
{% else %}{{ pill('Verfahren nicht unterstützt', 'warning') }}{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -39,8 +40,11 @@
|
|||||||
Tastatur und Maus gehen an das Gerät, sobald das Bild steht. Die Zwischenablage
|
Tastatur und Maus gehen an das Gerät, sobald das Bild steht. Die Zwischenablage
|
||||||
bleibt getrennt.
|
bleibt getrennt.
|
||||||
{% if zugang.verfahren %}
|
{% if zugang.verfahren %}
|
||||||
Angebotene Anmeldeverfahren:
|
Zuletzt angebotene Anmeldeverfahren:
|
||||||
{% for nummer in zugang.verfahren %}{{ verfahren_name(nummer) }}{% if not loop.last %}, {% endif %}{% endfor %}.
|
{% for nummer in zugang.verfahren %}{{ verfahren_name(nummer) }}{% if not loop.last %}, {% endif %}{% endfor %}.
|
||||||
|
{% else %}
|
||||||
|
Welche Anmeldeverfahren das Gerät anbietet, ist nicht bekannt – die Seite
|
||||||
|
fragt es nicht von sich aus.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,6 +52,17 @@
|
|||||||
<button class="btn btn--sm" type="button" id="screen-cad" hidden
|
<button class="btn btn--sm" type="button" id="screen-cad" hidden
|
||||||
title="Strg+Alt+Entf an das Gerät senden">Strg+Alt+Entf</button>
|
title="Strg+Alt+Entf an das Gerät senden">Strg+Alt+Entf</button>
|
||||||
<button class="btn btn--sm" type="button" id="screen-disconnect" hidden>Trennen</button>
|
<button class="btn btn--sm" type="button" id="screen-disconnect" hidden>Trennen</button>
|
||||||
|
{# Ausdrücklich ein Knopf und keine Nebenwirkung des Seitenaufbaus: die
|
||||||
|
Prüfung bricht eine Protokollverhandlung ab, und RealVNC zählt das.
|
||||||
|
Wer sucht, drückt ihn; wer nur arbeiten will, merkt nichts davon. #}
|
||||||
|
<form method="post" data-no-guard
|
||||||
|
action="{{ url_for('screen.probe', device_id=device.id) }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--sm btn--ghost" type="submit"
|
||||||
|
title="Fragt das Gerät, welche Anmeldeverfahren es anbietet">
|
||||||
|
Verfahren prüfen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<a class="btn btn--sm btn--ghost"
|
<a class="btn btn--sm btn--ghost"
|
||||||
href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||||
{{ icon('download', size=14) }} Verbindungsdatei
|
{{ icon('download', size=14) }} Verbindungsdatei
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ HTTPS=0
|
|||||||
HTTPS_PORT=""
|
HTTPS_PORT=""
|
||||||
DOMAIN=""
|
DOMAIN=""
|
||||||
ACME_EMAIL=""
|
ACME_EMAIL=""
|
||||||
|
ERZWINGEN=0
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && 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; }
|
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||||
info() { printf ' %s\n' "$*"; }
|
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.
|
#: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist.
|
||||||
SICHERUNG_STEHT=0
|
SICHERUNG_STEHT=0
|
||||||
INSTALL_FERTIG=0
|
INSTALL_FERTIG=0
|
||||||
@@ -129,6 +163,7 @@ Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
|||||||
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
||||||
--skip-packages Keine Systempakete installieren
|
--skip-packages Keine Systempakete installieren
|
||||||
--yes Rueckfragen ueberspringen
|
--yes Rueckfragen ueberspringen
|
||||||
|
--force Auch dann einrichten, wenn dieselbe Fassung schon laeuft
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +179,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
||||||
--skip-packages) SKIP_PACKAGES=1; shift ;;
|
--skip-packages) SKIP_PACKAGES=1; shift ;;
|
||||||
--yes|-y) ASSUME_YES=1; shift ;;
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
--force) ERZWINGEN=1; shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) fail "unbekannte Option: $1" ;;
|
*) fail "unbekannte Option: $1" ;;
|
||||||
esac
|
esac
|
||||||
@@ -261,6 +297,23 @@ FAILED_DIR="/srv/$NAME-failed-$TIMESTAMP"
|
|||||||
IS_UPDATE=0
|
IS_UPDATE=0
|
||||||
[[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1
|
[[ -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"
|
step "TESM-Installation"
|
||||||
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||||
info "Ziel : $ROOT"
|
info "Ziel : $ROOT"
|
||||||
@@ -278,6 +331,7 @@ else
|
|||||||
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
||||||
fi
|
fi
|
||||||
info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)"
|
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
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
read -r -p "Fortfahren? [j/N] " answer
|
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 "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
||||||
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
||||||
echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&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
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +175,32 @@ release_holen() {
|
|||||||
|
|
||||||
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
|
# --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.
|
# bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht.
|
||||||
OHNE_RUECKFRAGE=0
|
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
|
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,
|
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||||
eine **benannte Instanz** zu aktualisieren:
|
eine **benannte Instanz** zu aktualisieren:
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.7"
|
version = "2.8.9"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -11,25 +11,33 @@
|
|||||||
|
|
||||||
import RFB from "../vendor/novnc/core/rfb.js";
|
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]);
|
|
||||||
|
|
||||||
// Drei Eingriffe in private Methoden der mitgelieferten Bibliothek. Bewusst
|
// Drei Eingriffe in private Methoden der mitgelieferten Bibliothek. Bewusst
|
||||||
// hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC laesst
|
// hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC laesst
|
||||||
// sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt eine
|
// sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt eine
|
||||||
// Methode weg, greift der jeweilige Eingriff nicht mehr, und die Sitzung
|
// Methode weg, entfaellt nur die Auskunft, nicht die Sitzung.
|
||||||
// laeuft wieder wie ohne ihn. Kein stiller Bruch.
|
//
|
||||||
|
// 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;
|
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;
|
||||||
// Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
|
// Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
|
||||||
// "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
|
// "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
|
||||||
// funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden, also
|
// funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden, also
|
||||||
@@ -79,6 +87,9 @@ function starten(wurzel) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let rfb = null;
|
let rfb = null;
|
||||||
|
let versuche = 0;
|
||||||
|
let stand = false;
|
||||||
|
let letzterZugang = {};
|
||||||
|
|
||||||
function melden(text, art = "") {
|
function melden(text, art = "") {
|
||||||
zeile.textContent = text;
|
zeile.textContent = text;
|
||||||
@@ -93,9 +104,35 @@ function starten(wurzel) {
|
|||||||
if (rfb && rfb._rfbAuthScheme > 0) {
|
if (rfb && rfb._rfbAuthScheme > 0) {
|
||||||
teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`);
|
teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`);
|
||||||
}
|
}
|
||||||
|
if (gescheitert.size) {
|
||||||
|
teile.push(`erfolglos: ${[...gescheitert].join(", ")}`);
|
||||||
|
}
|
||||||
return teile.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
|
// Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene
|
||||||
// Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die
|
// Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die
|
||||||
// Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM
|
// Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM
|
||||||
@@ -122,22 +159,17 @@ function starten(wurzel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function verbinden(zugang) {
|
function verbinden(zugang) {
|
||||||
melden("Verbindung wird aufgebaut …");
|
melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …");
|
||||||
letzterGrund = "";
|
letzterGrund = "";
|
||||||
letzterCode = 0;
|
letzterCode = 0;
|
||||||
|
letzterZugang = zugang;
|
||||||
|
versuche += 1;
|
||||||
|
|
||||||
// Vorauswahl nur, wenn wir nichts anzubieten haben: dann zaehlt der Weg,
|
// Was abgerissen ist, wird nicht noch einmal gewaehlt. Sonst faende noVNC
|
||||||
// der ohne Zugangsdaten auskommt -- sofern der Server ihn anbietet.
|
// dasselbe Verfahren wieder zuerst -- und der zweite Anlauf waere eine
|
||||||
const hatZugangsdaten = Boolean(zugang && (zugang.password || zugang.username));
|
// Wiederholung des ersten.
|
||||||
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) {
|
RFB.prototype._isSupportedSecurityType = function (typ) {
|
||||||
if (!hatZugangsdaten && ohnePasswortMoeglich) {
|
if (gescheitert.has(typ)) return false;
|
||||||
return OHNE_ZUGANGSDATEN.has(typ);
|
|
||||||
}
|
|
||||||
return istUnterstuetzt.call(this, typ);
|
return istUnterstuetzt.call(this, typ);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,6 +185,7 @@ function starten(wurzel) {
|
|||||||
rfb.focusOnClick = true;
|
rfb.focusOnClick = true;
|
||||||
|
|
||||||
rfb.addEventListener("connect", () => {
|
rfb.addEventListener("connect", () => {
|
||||||
|
stand = true;
|
||||||
melden(`Verbunden mit ${wurzel.dataset.ziel}`, "ok");
|
melden(`Verbunden mit ${wurzel.dataset.ziel}`, "ok");
|
||||||
abfrage.hidden = true;
|
abfrage.hidden = true;
|
||||||
leinwand.hidden = false;
|
leinwand.hidden = false;
|
||||||
@@ -166,6 +199,8 @@ function starten(wurzel) {
|
|||||||
knopfStrg.hidden = true;
|
knopfStrg.hidden = true;
|
||||||
if (ereignis.detail && ereignis.detail.clean) {
|
if (ereignis.detail && ereignis.detail.clean) {
|
||||||
melden("Sitzung beendet.");
|
melden("Sitzung beendet.");
|
||||||
|
} else if (!stand && naechsterVersuch()) {
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
const erklaerung = grund();
|
const erklaerung = grund();
|
||||||
melden(
|
melden(
|
||||||
|
|||||||
+19
-8
@@ -21,14 +21,25 @@ Datei -- mit Grund und Fundstelle, damit ein Wechsel auf eine neuere Fassung
|
|||||||
nicht stillschweigend darueberlaeuft.
|
nicht stillschweigend darueberlaeuft.
|
||||||
|
|
||||||
**Was TESM von aussen umbiegt** (also *nicht* in diesen Dateien steht, sondern
|
**Was TESM von aussen umbiegt** (also *nicht* in diesen Dateien steht, sondern
|
||||||
in `js/screen.js`): `RFB.prototype._isSupportedSecurityType` wird ueberschrieben,
|
in `js/screen.js`):
|
||||||
solange keine Zugangsdaten vorliegen und der Server ein Verfahren ohne
|
|
||||||
Anmeldung anbietet. Grund: noVNC nimmt "das erste, das ich kann, in der
|
* `RFB.prototype._fail` und `_socketClose` -- nur, um den Grund eines
|
||||||
Reihenfolge des Servers"; ein RealVNC Server nennt RA2ne vor "ohne
|
Abbruchs zu erfahren. noVNC meldet ihn sonst ausschliesslich in die
|
||||||
Authentifizierung", und der Browser liefe in eine Passwortabfrage, obwohl
|
Entwicklerkonsole, und `console.error` mitzulesen traegt nicht: die
|
||||||
daneben der Weg offensteht, den auch der RealVNC Viewer nimmt. Faellt die
|
Bibliothek bindet es beim Laden.
|
||||||
Methode in einer neueren Fassung weg, greift die Vorauswahl nicht mehr -- die
|
* `RFB.prototype._isSupportedSecurityType` -- **erst nachdem** ein Verfahren
|
||||||
Sitzung laeuft dann wie ohne sie, es bricht nichts.
|
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`,
|
**Was noVNC an Sicherheitsverfahren spricht** (`core/rfb.js`,
|
||||||
`_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES
|
`_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES
|
||||||
|
|||||||
Reference in new issue
Block a user