92a60c62bd
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
152 lines
5.3 KiB
Python
152 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Login probes for R-752 on 9202 — each one a fresh, cookie-less visitor through traefik (a STRANGER unless it holds
|
|
the right password). Every probe returns one word: ok / wrong / locked / other:<detail>. EVIDENCE, NOT PRODUCT."""
|
|
import json, os, re, subprocess, tempfile, time
|
|
from datetime import datetime, timezone
|
|
import walk as w
|
|
|
|
|
|
def now():
|
|
return datetime.now(timezone.utc).strftime("%H:%M:%S")
|
|
|
|
|
|
def p(*a):
|
|
print(now(), *a, flush=True)
|
|
|
|
|
|
def curl(sub, path, *extra, data=None, jar=None, timeout=20):
|
|
args = ["curl", "-sk", "--max-time", str(timeout), "-D", "-", "-H", f"Host: {sub}.{w.DOMAIN}"]
|
|
if jar:
|
|
args += ["-b", jar, "-c", jar]
|
|
args += list(extra)
|
|
if data is not None:
|
|
args += ["--data-raw", data]
|
|
args.append(f"{w.BASE}{path}")
|
|
r = subprocess.run(args, capture_output=True, text=True)
|
|
return r.stdout or ""
|
|
|
|
|
|
def status(out):
|
|
m = re.findall(r"(?m)^HTTP/\S+ (\d{3})", out)
|
|
return m[-1] if m else "000"
|
|
|
|
|
|
def location(out):
|
|
m = re.findall(r"(?im)^location:\s*(\S+)", out)
|
|
return m[-1] if m else ""
|
|
|
|
|
|
def form_token(html, name):
|
|
m = re.search(r'name="%s"\s+value="([^"]+)"' % name, html) or re.search(r'value="([^"]+)"\s+name="%s"' % name, html)
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
def enc(**kw):
|
|
from urllib.parse import urlencode
|
|
return urlencode(kw)
|
|
|
|
|
|
# ---- BookStack: the web form (Laravel CSRF _token), key email|ip, 5 tries / 60 s ----------------------------------
|
|
def bookstack(email, pw, sub="wiki"):
|
|
jar = tempfile.mktemp()
|
|
try:
|
|
page = curl(sub, "/login", jar=jar)
|
|
tok = form_token(page, "_token")
|
|
out = curl(sub, "/login", "-H", "Content-Type: application/x-www-form-urlencoded", jar=jar,
|
|
data=enc(_token=tok, email=email, password=pw))
|
|
loc = location(out)
|
|
if status(out) == "302" and not loc.rstrip("/").endswith("/login"):
|
|
return "ok"
|
|
back = curl(sub, "/login", jar=jar)
|
|
if re.search(r"(?i)too many|throttle|t[uú]l sok", back):
|
|
return "locked"
|
|
if status(out) in ("302", "422", "200"):
|
|
return "wrong"
|
|
return "other:" + status(out)
|
|
finally:
|
|
try:
|
|
os.unlink(jar)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
# ---- Grafana: POST /login JSON ----------------------------------------------------------------------------------
|
|
def grafana(user, pw, sub="grafana"):
|
|
out = curl(sub, "/login", "-H", "Content-Type: application/json", data=json.dumps({"user": user, "password": pw}))
|
|
body = out.split("\r\n\r\n")[-1]
|
|
if status(out) == "200":
|
|
return "ok"
|
|
if re.search(r"(?i)temporarily blocked|too many", body):
|
|
return "locked"
|
|
if status(out) in ("401", "400"):
|
|
return "wrong"
|
|
return "other:" + status(out) + " " + body[:80]
|
|
|
|
|
|
# ---- calibre-web-automated: the web form (Flask-WTF csrf_token), and OPDS basic auth -----------------------------
|
|
def calibre(user, pw, sub="books"):
|
|
jar = tempfile.mktemp()
|
|
try:
|
|
page = curl(sub, "/login", jar=jar)
|
|
tok = form_token(page, "csrf_token")
|
|
out = curl(sub, "/login", "-H", "Content-Type: application/x-www-form-urlencoded", jar=jar,
|
|
data=enc(csrf_token=tok, username=user, password=pw, next="/"))
|
|
if status(out) == "302" and "/login" not in location(out):
|
|
return "ok"
|
|
if re.search(r"(?i)wait one minute|v[aá]rj", out):
|
|
return "locked"
|
|
if re.search(r"(?i)wrong username|hib[aá]s", out) or status(out) == "200":
|
|
return "wrong"
|
|
return "other:" + status(out)
|
|
finally:
|
|
try:
|
|
os.unlink(jar)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def calibre_opds(user, pw, sub="books"):
|
|
out = curl(sub, "/opds", "-u", f"{user}:{pw}")
|
|
c = status(out)
|
|
return {"200": "ok", "401": "wrong", "429": "locked"}.get(c, "other:" + c)
|
|
|
|
|
|
# ---- wger: the web login form (Django CSRF), as the catalog's own fixture logs in; django-axes on authenticate() ----
|
|
def wger(user, pw, sub="fitness"):
|
|
jar = tempfile.mktemp()
|
|
try:
|
|
o = f"https://{sub}.{w.DOMAIN}"
|
|
hdr = ["-H", f"Origin: {o}", "-H", f"Referer: {o}/en/user/login"]
|
|
page = curl(sub, "/en/user/login", *hdr, jar=jar)
|
|
m = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page)
|
|
if not m:
|
|
return "other:no-csrf " + status(page)
|
|
out = curl(sub, "/en/user/login", *hdr, "-H", "Content-Type: application/x-www-form-urlencoded", jar=jar,
|
|
data=enc(csrfmiddlewaretoken=m.group(1), login=user, password=pw))
|
|
c = status(out)
|
|
jt = open(jar).read() if os.path.exists(jar) else ""
|
|
if c == "302" and "sessionid" in jt:
|
|
return "ok"
|
|
if c in ("429", "403") or re.search(r"(?i)account locked|too many", out):
|
|
return "locked"
|
|
if c == "200":
|
|
return "wrong"
|
|
return "other:" + c
|
|
finally:
|
|
try:
|
|
os.unlink(jar)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def wait_until(fn, want, every, cap_s, label):
|
|
"""Poll fn() every `every` s until it answers `want`; returns minutes waited (None if never)."""
|
|
t0 = time.time()
|
|
while time.time() - t0 < cap_s:
|
|
time.sleep(every)
|
|
r = fn()
|
|
p(f" +{(time.time() - t0) / 60:.1f} min {label}: {r}")
|
|
if r == want:
|
|
return (time.time() - t0) / 60
|
|
return None
|