Automatischer Push: 19 Datei(en) geändert
- .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]>
This commit is contained in:
1 parent
371a1f5d2a
commit
395642c480
19 files changed
+1636
-32
No files matched your search
@@ -0,0 +1,152 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user