- .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/plan-kopie/welle_vergleich.py - .vault-tools/plan-kopie/welle_vergleich_treiber.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/angriffe.py - .vault-tools/trading-plattform-pruefung/browserlauf.py - .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py - .vault-tools/trading-plattform-pruefung/c_eigenlauf.py - .vault-tools/trading-plattform-pruefung/d_eigenlauf.py - .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py - .vault-tools/trading-plattform-pruefung/ntfy_probe_server.py - … und 7 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
153 lines
7.5 KiB
Python
153 lines
7.5 KiB
Python
"""Eigener Lauf zu Paket C: Ein entfernter Benutzer verliert Sitzung und offenen Strom, der andere bleibt.
|
|
|
|
Koordinator-Werkzeug, nicht fuers Repo. Frage (Herkunft: Anforderung Paket C vom 2026-09-18, C1, und die
|
|
Plankritik): Endet mit dem Entfernen eines Benutzers aus der Zugangsdatei wirklich alles, was er offen hat --
|
|
die HTTP-Sitzung (401), der schreibende Weg zum Killswitch (401) und der schon offene Uebersichtsstrom
|
|
(Schliessen mit 1008) --, waehrend ein zweiter Benutzer ungestoert weiterarbeitet? Und antwortet das Backend
|
|
auf eine halb geschriebene Zugangsdatei mit einer Ablehnung statt mit einem 500?
|
|
Gegenstand ist das Probe-Backend aus start_probe_zugang.py (Attrappe, Wegwerf-Register, Wegwerf-Zugang),
|
|
gestartet mit zwei-benutzer. Entfernt wird ueber das Skript des Projekts, nicht von Hand.
|
|
Was es nicht sieht: einen Browser (dafuer browserlauf_zugang.py) und den echten Broker.
|
|
Regressionsnachweis: --sabotage laesst das Entfernen aus; die Pruefungen auf 401 und 1008 muessen dann rot enden.
|
|
|
|
Aufruf (WSL, aus dem Codebaum): python c_eigenlauf.py <basis-url> <ablage> [--sabotage]
|
|
Exit 0 ohne Befund, 2 mit Befund, 3 wenn eine Voraussetzung fehlt.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
from websockets.exceptions import ConnectionClosed
|
|
from websockets.sync.client import connect
|
|
|
|
befunde: list[str] = []
|
|
|
|
|
|
def pruefe(name: str, bedingung: bool, einzelheit: str = "") -> None:
|
|
print(f" {'ok ' if bedingung else 'BEFUND'} {name}: {einzelheit}")
|
|
if not bedingung:
|
|
befunde.append(name)
|
|
|
|
|
|
def totp(geheimnis: str, zeit_s: float) -> str:
|
|
schluessel = base64.b32decode(geheimnis.upper() + "=" * (-len(geheimnis) % 8))
|
|
mac = hmac.new(schluessel, struct.pack(">Q", int(zeit_s // 30)), hashlib.sha1).digest()
|
|
versatz = mac[-1] & 0x0F
|
|
zahl = struct.unpack(">I", mac[versatz : versatz + 4])[0] & 0x7FFFFFFF
|
|
return f"{zahl % 1_000_000:06d}"
|
|
|
|
|
|
def anmelden(basis: str, zugang: dict) -> requests.Session:
|
|
sitzung = requests.Session()
|
|
antwort = sitzung.post(
|
|
f"{basis}/api/anmeldung",
|
|
json={"benutzer": zugang["benutzer"], "passwort": zugang["passwort"], "code": totp(zugang["geheimnis"], time.time())},
|
|
headers={"Origin": basis},
|
|
timeout=15,
|
|
)
|
|
pruefe(f"Anmeldung {zugang['benutzer']}", antwort.status_code == 200, f"HTTP {antwort.status_code}")
|
|
return sitzung
|
|
|
|
|
|
def cookie(sitzung: requests.Session) -> str:
|
|
return "; ".join(f"{c.name}={c.value}" for c in sitzung.cookies)
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 3:
|
|
print(__doc__)
|
|
return 3
|
|
basis, ablage = sys.argv[1].rstrip("/"), Path(sys.argv[2])
|
|
sabotage = "--sabotage" in sys.argv[3:]
|
|
if totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 59) != "287082":
|
|
print("VORAUSSETZUNG FEHLT: der eigene TOTP-Nachbau trifft den RFC-Vektor nicht")
|
|
return 3
|
|
try:
|
|
erster = json.loads((ablage / "zugang.json").read_text(encoding="utf-8"))
|
|
zweiter = json.loads((ablage / "zugang2.json").read_text(encoding="utf-8"))
|
|
except OSError:
|
|
print("VORAUSSETZUNG FEHLT: zugang.json und zugang2.json in der Ablage (Backend mit zwei-benutzer starten)")
|
|
return 3
|
|
zugang_datei = ablage / "zugang.toml"
|
|
ws_url = basis.replace("http", "ws", 1) + "/ws/uebersicht"
|
|
|
|
print("== Beide melden sich an, beide Stroeme laufen")
|
|
s1, s2 = anmelden(basis, erster), anmelden(basis, zweiter)
|
|
with connect(ws_url, additional_headers={"Cookie": cookie(s1)}, origin=basis, open_timeout=5) as ws1, connect(
|
|
ws_url, additional_headers={"Cookie": cookie(s2)}, origin=basis, open_timeout=5
|
|
) as ws2:
|
|
pruefe("Strom des ersten liefert", "zeit" in str(ws1.recv(timeout=10)), "erster Takt da")
|
|
pruefe("Strom des zweiten liefert", "zeit" in str(ws2.recv(timeout=10)), "erster Takt da")
|
|
|
|
print("== Der erste Benutzer wird ueber das Skript entfernt")
|
|
if sabotage:
|
|
print(" sabotage: das Entfernen faellt aus")
|
|
else:
|
|
lauf = subprocess.run(
|
|
[sys.executable, "scripts/zugang.py", "--datei", str(zugang_datei), "entfernen", erster["benutzer"]],
|
|
input=erster["benutzer"] + "\n", capture_output=True, text=True, timeout=60,
|
|
)
|
|
pruefe("Skript entfernt mit Exit 0", lauf.returncode == 0, lauf.stdout.strip().splitlines()[-1][:120] if lauf.stdout.strip() else lauf.stderr[:120])
|
|
liste = subprocess.run(
|
|
[sys.executable, "scripts/zugang.py", "--datei", str(zugang_datei), "liste"], capture_output=True, text=True, timeout=60
|
|
)
|
|
pruefe("liste nennt nur noch den zweiten", zweiter["benutzer"] in liste.stdout and erster["benutzer"] not in liste.stdout, liste.stdout.strip().replace("\n", " | ")[:160])
|
|
pruefe("liste zeigt kein Geheimnis", zweiter["geheimnis"] not in liste.stdout and "scrypt" not in liste.stdout.lower(), f"{len(liste.stdout)} Zeichen")
|
|
|
|
print("== Der erste ist draussen")
|
|
antwort = s1.get(f"{basis}/api/uebersicht", timeout=15)
|
|
pruefe("GET /api/uebersicht 401", antwort.status_code == 401, f"HTTP {antwort.status_code}")
|
|
antwort = s1.post(f"{basis}/api/killswitch/anhalten", json={"grund": "probe"}, headers={"Origin": basis}, timeout=15)
|
|
pruefe("POST /api/killswitch/anhalten 401", antwort.status_code == 401, f"HTTP {antwort.status_code}")
|
|
antwort = s1.get(f"{basis}/api/sitzung", timeout=15)
|
|
pruefe("/api/sitzung meldet nicht angemeldet", antwort.status_code == 200 and antwort.json().get("angemeldet") is False, antwort.text[:100])
|
|
beginn = time.monotonic()
|
|
geschlossen, takte = None, 0
|
|
try:
|
|
while time.monotonic() - beginn < 12:
|
|
ws1.recv(timeout=6)
|
|
takte += 1
|
|
except ConnectionClosed as ausnahme:
|
|
geschlossen = ausnahme.rcvd.code if ausnahme.rcvd else None
|
|
except TimeoutError:
|
|
pass
|
|
pruefe("offener Strom des ersten schliesst mit 1008", geschlossen == 1008, f"Code {geschlossen}, davor noch {takte} Takt(e), nach {time.monotonic() - beginn:.1f} s")
|
|
pruefe("hoechstens ein Takt rutscht noch durch", takte <= 1, f"{takte} Takt(e)")
|
|
|
|
print("== Der zweite arbeitet weiter")
|
|
antwort = s2.get(f"{basis}/api/uebersicht", timeout=15)
|
|
pruefe("GET /api/uebersicht des zweiten 200", antwort.status_code == 200, f"HTTP {antwort.status_code}")
|
|
try:
|
|
pruefe("Strom des zweiten liefert weiter", "zeit" in str(ws2.recv(timeout=10)), "Takt da")
|
|
except Exception as ausnahme: # noqa: BLE001 - jeder Abbruch hier ist der Befund
|
|
pruefe("Strom des zweiten liefert weiter", False, repr(ausnahme)[:120])
|
|
|
|
print("== Halb geschriebene Zugangsdatei")
|
|
original = zugang_datei.read_bytes()
|
|
try:
|
|
zugang_datei.write_text('[benutzer.halb]\npasswort = "x"\n', encoding="utf-8")
|
|
time.sleep(0.05)
|
|
antwort = s2.get(f"{basis}/api/uebersicht", timeout=15)
|
|
pruefe("Ablehnung statt 500", antwort.status_code in (401, 503), f"HTTP {antwort.status_code}")
|
|
finally:
|
|
zugang_datei.write_bytes(original)
|
|
time.sleep(0.05)
|
|
antwort = s2.get(f"{basis}/api/uebersicht", timeout=15)
|
|
# Seit der Nachbesserung vom 2026-09-18: Eine unlesbare Datei sperrt nur aus, solange sie unlesbar ist.
|
|
pruefe("nach dem Zuruecklegen gilt die Sitzung des zweiten wieder", antwort.status_code == 200, f"HTTP {antwort.status_code}")
|
|
|
|
print(f"{len(befunde)} Befund(e).")
|
|
return 2 if befunde else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|