Version 2.8.17: keine Vorauswahl, keine Messung im Seitenaufbau
This commit is contained in:
1 parent
b54b87ca6f
commit
5f67e92298
7 files changed
+99
-110
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.16"
|
version = "2.8.17"
|
||||||
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,15 +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, 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
|
|
||||||
|
|
||||||
|
|
||||||
@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,17 +87,7 @@ def screen(device_id: int):
|
|||||||
if not port:
|
if not port:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
zugang, frisch = vnc.zugang_gemerkt(
|
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||||
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 = "", ""
|
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:
|
||||||
@@ -132,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)
|
||||||
|
|||||||
@@ -325,56 +325,32 @@ def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: boo
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#: So lange gilt eine Messung der Anmeldeverfahren als frisch.
|
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.
|
||||||
#: 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
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
def zugang_gemerkt(
|
Jetzt beruehrt das Oeffnen einer Seite das Geraet gar nicht mehr. Die
|
||||||
device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True
|
Erreichbarkeit kommt aus dem Ueberwachungsdurchlauf (ein blosser
|
||||||
) -> tuple[Zugang, bool]:
|
Verbindungsaufbau), die Anmeldeverfahren aus der letzten **ausdruecklichen**
|
||||||
"""Der Zugang -- aus der letzten Messung, wenn die frisch genug ist.
|
Pruefung -- der Knopf dafuer steht auf der Seite und wird gedrueckt, wenn
|
||||||
|
etwas nicht geht.
|
||||||
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 "")
|
gespeichert = str(device.get("vnc_types") or "")
|
||||||
verfahren = tuple(
|
return Zugang(
|
||||||
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
ziel=ziel(device),
|
||||||
)
|
|
||||||
return (
|
|
||||||
Zugang(
|
|
||||||
ziel=host,
|
|
||||||
port=port,
|
port=port,
|
||||||
erreichbar=bool(device.get("vnc_open")),
|
erreichbar=bool(device.get("vnc_open")),
|
||||||
verfahren=verfahren,
|
verfahren=tuple(
|
||||||
absage=str(device.get("vnc_reason") or ""),
|
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
||||||
),
|
),
|
||||||
False,
|
absage=str(device.get("vnc_reason") or ""),
|
||||||
)
|
)
|
||||||
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(
|
def verbindungsdatei(
|
||||||
|
|||||||
@@ -40,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>
|
||||||
@@ -49,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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.7"
|
version = "2.8.8"
|
||||||
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,18 @@
|
|||||||
|
|
||||||
import RFB from "../vendor/novnc/core/rfb.js";
|
import RFB from "../vendor/novnc/core/rfb.js";
|
||||||
|
|
||||||
// Sicherheitsverfahren, die ohne Zugangsdaten auskommen. Bietet der Server
|
// Zwei Eingriffe in private Methoden der mitgelieferten Bibliothek -- beide
|
||||||
// eines davon an und haben wir keine, nehmen wir es -- auch wenn der Server
|
// nur, um zu *erfahren*, was schiefging; keiner greift in die Verhandlung ein.
|
||||||
// ein anderes zuerst nennt.
|
// 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.
|
||||||
//
|
//
|
||||||
// Warum das noetig ist: noVNC waehlt "das erste, das ich kann, in der
|
// Es stand hier einmal ein dritter, der die Wahl des Sicherheitsverfahrens
|
||||||
// Reihenfolge des Servers". Ein RealVNC Server nennt RA2ne vor "ohne
|
// umbog: ohne hinterlegte Zugangsdaten sollte ein Verfahren ohne Anmeldung
|
||||||
// Authentifizierung"; der Browser liefe also in eine Passwortabfrage, obwohl
|
// gewinnen, weil der Server RA2ne zuerst nennt. Gut gemeint und falsch --
|
||||||
// daneben ein Weg offensteht, den auch der RealVNC Viewer nimmt. Wer ein
|
// genau dieser Griff hat eine Sitzung, die lief, zum Abbruch gebracht: der
|
||||||
// Passwort hinterlegt hat, behaelt die Reihenfolge des Servers -- dann ist
|
// RealVNC Server bietet "ohne Authentifizierung" zwar an, legt bei dieser Wahl
|
||||||
// der angemeldete Weg der bessere.
|
// aber auf. Die Reihenfolge des Servers ist seine Entscheidung, nicht unsere.
|
||||||
const OHNE_ZUGANGSDATEN = new Set([1]);
|
|
||||||
|
|
||||||
// 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
|
// 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
|
||||||
@@ -126,21 +119,6 @@ function starten(wurzel) {
|
|||||||
letzterGrund = "";
|
letzterGrund = "";
|
||||||
letzterCode = 0;
|
letzterCode = 0;
|
||||||
|
|
||||||
// Vorauswahl nur, wenn wir nichts anzubieten haben: dann zaehlt der Weg,
|
|
||||||
// der ohne Zugangsdaten auskommt -- sofern der Server ihn anbietet.
|
|
||||||
const hatZugangsdaten = Boolean(zugang && (zugang.password || zugang.username));
|
|
||||||
const angeboten = (wurzel.dataset.verfahren || "")
|
|
||||||
.split(",")
|
|
||||||
.map((wert) => parseInt(wert, 10))
|
|
||||||
.filter((wert) => !Number.isNaN(wert));
|
|
||||||
const ohnePasswortMoeglich = angeboten.some((wert) => OHNE_ZUGANGSDATEN.has(wert));
|
|
||||||
RFB.prototype._isSupportedSecurityType = function (typ) {
|
|
||||||
if (!hatZugangsdaten && ohnePasswortMoeglich) {
|
|
||||||
return OHNE_ZUGANGSDATEN.has(typ);
|
|
||||||
}
|
|
||||||
return istUnterstuetzt.call(this, typ);
|
|
||||||
};
|
|
||||||
|
|
||||||
rfb = new RFB(leinwand, adresse, {
|
rfb = new RFB(leinwand, adresse, {
|
||||||
credentials: zugang,
|
credentials: zugang,
|
||||||
// Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des
|
// Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des
|
||||||
|
|||||||
Reference in new issue
Block a user