#!/usr/bin/env python3 """R-752 on 9202 with the LIVE catalog templates (no change yet): BookStack, Grafana, calibre-web-automated. Per app: install through the product, wait for the install hold to open, positive control, a stranger's wrong tries with the PUBLIC login name, the household's right password right after, then how long until it works again, then a wrong one still refused. One app at a time; each removed at the end.""" import sys, time import walk as w import lk APPS = sys.argv[1:] or ["bookstack", "grafana", "calibre-web"] SUB = {"bookstack": "wiki", "grafana": "grafana", "calibre-web": "books"} KEY = {"bookstack": "ADMIN_PASSWORD", "grafana": "GF_SECURITY_ADMIN_PASSWORD", "calibre-web": "ADMIN_PASSWORD"} NAME = {"bookstack": "admin@admin.com", "grafana": "admin", "calibre-web": "admin"} PROBE = {"bookstack": lk.bookstack, "grafana": lk.grafana, "calibre-web": lk.calibre} w.login() for app in APPS: sub, name, probe = SUB[app], NAME[app], PROBE[app] lk.p(f"##### {app} — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()}") lk.p("deploy:", w.deploy(app, sub)) for _ in range(120): time.sleep(5) logs = w.guest(f"docker logs --since 20m felhom-controller 2>&1 | grep -E '{app}: install hold OPENED|{app}.*deployed successfully'") if "hold OPENED" in logs or (app == "grafana" and "deployed successfully" in logs): break w.wait_app(sub, "/login", want=("200",), tries=60) pw = (w.GENERATED.get(app) or {}).get(KEY[app], "") lk.p("generated password length:", len(pw)) lk.p("positive control — right password:", probe(name, pw)) tries = 6 if app != "calibre-web" else 4 for i in range(1, tries + 1): lk.p(f"stranger wrong try {i}:", probe(name, f"wrong-{i}-{time.time()}")) lk.p("household, RIGHT password, right after:", probe(name, pw)) # calibre-web counts EVERY try (the household's too) against 3/minute and 40/day — poll it slowly every, cap = (70, 6 * 60) if app == "calibre-web" else (15, 20 * 60) waited = lk.wait_until(lambda: probe(name, pw), "ok", every, cap, "right password") lk.p(f"RESULT {app}: the right password worked again after {waited if waited is None else round(waited, 1)} min") lk.p("a wrong one after:", probe(name, "wrong-after"), "| right again:", probe(name, pw)) if app == "calibre-web": lk.p("## calibre-web: OPDS basic auth (the book-feed door) — 60 wrong tries as a stranger") res = [lk.calibre_opds(name, f"opds-wrong-{i}") for i in range(60)] lk.p("OPDS wrong tries:", {r: res.count(r) for r in set(res)}) lk.p("OPDS right password after:", lk.calibre_opds(name, pw), "| the login form, right password:", lk.calibre(name, pw)) lk.p("## calibre-web: the DAILY limit — 40 wrong tries at the 3/minute pace (~14 min), then the right password") n = 0 while n < 40: for _ in range(3): if n < 40: n += 1 r = lk.calibre(name, f"daily-{n}") if n % 10 == 0 or r not in ("wrong",): lk.p(f" wrong try {n}: {r}") time.sleep(61) lk.p("household, RIGHT password after 40:", lk.calibre(name, pw)) time.sleep(130) lk.p("household, RIGHT password 2 min later (past the minute window):", lk.calibre(name, pw)) lk.p("restart the app (the limiter's store is in memory):", w.guest("docker restart calibre-web >/dev/null && echo restarted").strip()) w.wait_app(sub, "/login", want=("200",), tries=60) lk.p("household, RIGHT password after the restart:", lk.calibre(name, pw)) if app == "grafana": lk.p("## grafana: the TRICKLE — 5 wrong, then one wrong try per minute for 8 minutes; the right password each minute") for i in range(5): lk.p(f"burst wrong {i + 1}:", probe(name, f"burst-{i}-{time.time()}")) for m in range(8): time.sleep(60) lk.p(f" minute {m + 1}: stranger wrong -> {probe(name, f'trickle-{m}')}, household right -> {probe(name, pw)}") lk.p("stop the trickle; wait for the right password:") waited = lk.wait_until(lambda: probe(name, pw), "ok", 15, 10 * 60, "right password") lk.p(f"RESULT grafana trickle: open again {waited if waited is None else round(waited, 1)} min after the last wrong try") lk.p(w.guest(f"docker logs --since 30m {app} 2>&1 | grep -i -E 'lock|block|throttl|limit' | tail -4 | cut -c1-200").strip()) w.remove(app) lk.p("removed; deployed =", w.stack(app).get("deployed"))