"""Gegenmessung zu Paket S5 gegen eine laufende Probe-Instanz: dieselben Angriffe, die der Sicherheitspruefer gemessen hat. Koordinator-Pruefcode (WSL). Gemessen wird gegen 127.0.0.1:; die Instanz laeuft aus einer Kopie mit Broker-Attrappe, Wegwerf-Register und Wegwerf-Zugang. Nichts davon beruehrt den Betrieb. Aufruf: python s5_echtlauf.py """ import base64 import hashlib import hmac import http.client import json import os import struct import subprocess import sys import threading import time from concurrent.futures import ThreadPoolExecutor from pathlib import Path import requests PORT = int(sys.argv[1]) ABLAGE = Path(sys.argv[2]) CODE = Path(sys.argv[3]) BASIS = f"http://127.0.0.1:{PORT}" KOPF = {"Origin": BASIS} z = json.loads((ABLAGE / "zugang.json").read_text(encoding="utf-8")) BEN, PW, GEH = z["benutzer"], z["passwort"], z["geheimnis"] befunde = [] def code(versatz=0): s = base64.b32decode(GEH) mac = hmac.new(s, struct.pack(">Q", int(time.time()) // 30 + versatz), hashlib.sha1).digest() v = mac[-1] & 0x0F return f"{(struct.unpack('>I', mac[v:v + 4])[0] & 0x7FFFFFFF) % 1_000_000:06d}" def melde_an(): s = requests.Session() s.trust_env = False a = s.post(f"{BASIS}/api/anmeldung", json={"benutzer": BEN, "passwort": PW, "code": code()}, headers=KOPF, timeout=60) return s, a.status_code def naechster_schritt(): time.sleep(30.5 - (time.time() % 30)) def server_pid(): aus = subprocess.run(["pgrep", "-f", "start_probe_w.py"], capture_output=True, text=True).stdout.split() return int(aus[0]) if aus else None def speicher_mib(pid): werte = {} for zeile in Path(f"/proc/{pid}/status").read_text().splitlines(): if zeile.startswith(("VmRSS", "VmHWM")): name, wert = zeile.split(":") werte[name] = int(wert.split()[0]) / 1024 return werte def pruefe(text, ok): print((" OK " if ok else " BEFUND ") + text) if not ok: befunde.append(text) pid = server_pid() print(f"Server-PID {pid}, Speicher zu Beginn {speicher_mib(pid)}") print("== 1: 422 spiegelt die Eingabe nicht mehr") lang = "P" * 300 a = requests.post(f"{BASIS}/api/anmeldung", json={"benutzer": BEN, "passwort": lang, "code": "123456"}, headers=KOPF, timeout=10) pruefe(f"HTTP {a.status_code}, Passwort im Koerper: {lang in a.text}", a.status_code == 422 and lang not in a.text) print(f" Koerper: {a.text[:200]}") print("== 2: Rumpf 512 MiB mit content-length, unangemeldet") vor = speicher_mib(pid) t0 = time.perf_counter() verbindung = http.client.HTTPConnection("127.0.0.1", PORT, timeout=60) verbindung.putrequest("POST", "/api/anmeldung") verbindung.putheader("Origin", BASIS) verbindung.putheader("Content-Type", "application/json") verbindung.putheader("Content-Length", str(512 * 1024 * 1024)) verbindung.endheaders() gesendet = 0 status = None try: block = b"x" * (1024 * 1024) for _ in range(512): verbindung.send(block) gesendet += len(block) except OSError as ausnahme: print(f" Senden brach nach {gesendet / 1024 / 1024:.0f} MiB ab: {type(ausnahme).__name__}") try: antwort = verbindung.getresponse() status = antwort.status antwort.read() except Exception as ausnahme: # noqa: BLE001 print(f" Antwort nicht lesbar: {type(ausnahme).__name__}") verbindung.close() nach = speicher_mib(pid) print(f" Status {status}, {time.perf_counter() - t0:.2f} s, gesendet {gesendet / 1024 / 1024:.0f} MiB, Speicher {vor} -> {nach}") pruefe("413 und Spitzenspeicher waechst um weniger als 50 MiB", status == 413 and nach["VmHWM"] - vor["VmHWM"] < 50) print("== 3: Rumpf ohne content-length (chunked), unangemeldet") vor = speicher_mib(pid) t0 = time.perf_counter() verbindung = http.client.HTTPConnection("127.0.0.1", PORT, timeout=60) verbindung.putrequest("POST", "/api/anmeldung") verbindung.putheader("Origin", BASIS) verbindung.putheader("Content-Type", "application/json") verbindung.putheader("Transfer-Encoding", "chunked") verbindung.endheaders() gesendet = 0 status = None try: block = b"y" * (256 * 1024) for _ in range(800): verbindung.send(hex(len(block))[2:].encode() + b"\r\n" + block + b"\r\n") gesendet += len(block) verbindung.send(b"0\r\n\r\n") except OSError as ausnahme: print(f" Senden brach nach {gesendet / 1024 / 1024:.1f} MiB ab: {type(ausnahme).__name__}") try: antwort = verbindung.getresponse() status = antwort.status antwort.read() except Exception as ausnahme: # noqa: BLE001 print(f" Antwort nicht lesbar: {type(ausnahme).__name__}") verbindung.close() nach = speicher_mib(pid) print(f" Status {status}, {time.perf_counter() - t0:.2f} s, gesendet {gesendet / 1024 / 1024:.1f} MiB, Speicher {vor} -> {nach}") pruefe("413 (oder Abbruch der Verbindung) und Spitzenspeicher waechst um weniger als 50 MiB", status in (413, None) and nach["VmHWM"] - vor["VmHWM"] < 50) print("== 4: Killswitch unter 40 parallelen teuren Kerzenabrufen") sitz, st = melde_an() print(f" Anmeldung HTTP {st}") cookie = "; ".join(f"{c.name}={c.value}" for c in sitz.cookies) ds = sitz.get(f"{BASIS}/api/markt/datensaetze", timeout=120).json().get("datensaetze", []) intraday = [d for d in ds if d.get("intraday")] if not intraday: print(" kein Intraday-Datensatz im Katalog -- Lastmessung nicht moeglich") befunde.append("Lastmessung nicht moeglich") else: bt = intraday[0]["bar_type"] teuer = f"{BASIS}/api/markt/kerzen?bar_type={bt}&aufloesung_min=60&anzahl=3000" t0 = time.perf_counter() einzel = requests.get(teuer, headers={"Cookie": cookie}, timeout=600) print(f" ein einzelner teurer Abruf: HTTP {einzel.status_code}, {time.perf_counter() - t0:.1f} s") t0 = time.perf_counter() sitz.post(f"{BASIS}/api/killswitch/anhalten", json={"grund": "grundlinie"}, headers=KOPF, timeout=60) print(f" Grundlinie POST /api/killswitch/anhalten ohne Last: {time.perf_counter() - t0:.2f} s") laeuft = [True] messung = {"gesund": [], "anhalten": []} def miss(): while laeuft[0]: t = time.perf_counter() try: requests.get(f"{BASIS}/api/gesund", timeout=300) messung["gesund"].append(time.perf_counter() - t) except Exception: # noqa: BLE001 messung["gesund"].append(-1.0) t = time.perf_counter() try: r = sitz.post(f"{BASIS}/api/killswitch/anhalten", json={"grund": "lastprobe"}, headers=KOPF, timeout=300) messung["anhalten"].append((time.perf_counter() - t, r.status_code)) except Exception: # noqa: BLE001 messung["anhalten"].append((-1.0, 0)) time.sleep(0.5) faden = threading.Thread(target=miss, daemon=True) faden.start() t0 = time.perf_counter() with ThreadPoolExecutor(max_workers=40) as pool: codes = list(pool.map(lambda _: requests.get(teuer, headers={"Cookie": cookie}, timeout=900).status_code, range(40))) dauer = time.perf_counter() - t0 laeuft[0] = False faden.join(timeout=310) zeiten = [w for w, _ in messung["anhalten"]] print(f" 40 Kerzenabrufe: {dauer:.1f} s, Status {sorted(set(codes))}") print(f" anhalten waehrend der Last: n={len(zeiten)}, max {max(zeiten):.2f} s, Status {sorted({s for _, s in messung['anhalten']})}") print(f" gesund waehrend der Last: n={len(messung['gesund'])}, max {max(messung['gesund']):.2f} s") pruefe("Killswitch antwortet unter Last in unter 5 s (vorher gemessen: 118 s)", 0 <= max(zeiten) < 5.0) print("== 5: ueberlanger Grund landet nicht im Register") db = ABLAGE / "registry.db" vorher = db.stat().st_size a = sitz.post(f"{BASIS}/api/killswitch/anhalten", json={"grund": "G" * (1024 * 1024)}, headers=KOPF, timeout=120) print(f" grund mit 1 MiB -> HTTP {a.status_code}, registry.db {vorher} -> {db.stat().st_size} Bytes") pruefe("abgelehnt (413 oder 422), Register waechst nicht", a.status_code in (413, 422) and db.stat().st_size - vorher < 65536) print("== 6: Passwortwechsel beendet die Sitzung") pruefe(f"Sitzung gilt vor dem Wechsel (HTTP {sitz.get(f'{BASIS}/api/uebersicht', timeout=60).status_code})", True) sys.path.insert(0, str(CODE)) os.chdir(CODE) from trading.dashboard.zugang import Benutzer, lies_benutzer, schreibe_benutzer, setze_passwort # noqa: E402 bestand = lies_benutzer(ABLAGE / "zugang.toml") bestand[BEN] = Benutzer(name=BEN, passwort=setze_passwort("ein-neues-passwort-2026"), totp_geheimnis=bestand[BEN].totp_geheimnis) schreibe_benutzer(ABLAGE / "zugang.toml", bestand) time.sleep(1.0) danach = sitz.get(f"{BASIS}/api/uebersicht", timeout=60).status_code pruefe(f"alte Sitzung nach dem Passwortwechsel: HTTP {danach}", danach == 401) halt = sitz.post(f"{BASIS}/api/killswitch/anhalten", json={"grund": "nach-wechsel"}, headers=KOPF, timeout=60).status_code pruefe(f"Killswitch mit der alten Sitzung: HTTP {halt}", halt == 401) print(f"Speicher am Ende {speicher_mib(pid)}") print(f"{len(befunde)} Befund(e): {befunde}") sys.exit(2 if befunde else 0)