monitoring: QR form + qrencode/zbarimg roundtrip (www.qr)
Harvest taler:// and payto:// (plus app-store links) from landings and mint JSON; validate shape; re-encode PNG and decode with zbarimg.
This commit is contained in:
parent
026f3ac04c
commit
f7856525f3
5 changed files with 529 additions and 2 deletions
|
|
@ -65,6 +65,9 @@ Each check prints **global** and **grouped** ids:
|
||||||
- **OK** green · **INFO** blue/cyan · **WARN** yellow (never red)
|
- **OK** green · **INFO** blue/cyan · **WARN** yellow (never red)
|
||||||
- **ERROR** red · **BLOCKER** magenta · **PROG** cyan bar
|
- **ERROR** red · **BLOCKER** magenta · **PROG** cyan bar
|
||||||
- Progress: auto estimate per phase, or `PROGRESS_TOTAL=N` / `PROGRESS_OFF=1`
|
- Progress: auto estimate per phase, or `PROGRESS_TOTAL=N` / `PROGRESS_OFF=1`
|
||||||
|
- **QR (urls):** harvest `taler://` / `payto://` (+ app store `data-qr-url`) from landings and
|
||||||
|
`demo-withdraw.json` / `auto-account.json` → validate form → `qrencode` PNG → `zbarimg`
|
||||||
|
decode must match. Requires packages **`qrencode`** + **`zbar-tools`**.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Every check line has a **global** run number and a **grouped** id:
|
||||||
|
|
||||||
| Area | Phase script | Groups (examples) |
|
| Area | Phase script | Groups (examples) |
|
||||||
|------|--------------|-------------------|
|
|------|--------------|-------------------|
|
||||||
| **www** | `check_urls.sh` | `exchange` `perf` `stats` `bank` `merchant` `paivana` `landing` |
|
| **www** | `check_urls.sh` | `exchange` `perf` `stats` `bank` `merchant` `paivana` `landing` `qr` |
|
||||||
| **inside** | `check_inside.sh` | `ssh` `bank` `exchange` `merchant` `caddy` `load` |
|
| **inside** | `check_inside.sh` | `ssh` `bank` `exchange` `merchant` `caddy` `load` |
|
||||||
| **versions** | `check_versions.sh` | `outside` `inside` `compare` |
|
| **versions** | `check_versions.sh` | `outside` `inside` `compare` |
|
||||||
| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` |
|
| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` |
|
||||||
|
|
@ -54,6 +54,9 @@ Numbering follows **executed** checks (early skip may shift later NN inside the
|
||||||
| **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
|
| **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
|
||||||
| **www.paivana-** | local GOA paywall front (redirect to template) |
|
| **www.paivana-** | local GOA paywall front (redirect to template) |
|
||||||
| **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; cross-links local |
|
| **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; cross-links local |
|
||||||
|
| **www.qr-** | QR payloads: harvest `taler://` / `payto://` / app `data-qr-url` from landings + mint JSON; **form** check; **qrencode → zbarimg** exact roundtrip; optional static QR images. Needs `qrencode` + `zbar-tools`. Skip: `QR_CHECK=0` |
|
||||||
|
|
||||||
|
**QR rule:** `taler://withdraw/HOST/taler-integration/UUID` (no default `:443`/`:80`); `taler://pay/` / `pay-template/`; `payto://…` shape OK; decoded PNG must equal payload. Form errors on withdraw/pay/payto are **ERROR** on local/GOA.
|
||||||
|
|
||||||
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…).
|
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…).
|
||||||
|
|
||||||
|
|
|
||||||
291
scripts/taler-monitoring/check_qr_payloads.py
Executable file
291
scripts/taler-monitoring/check_qr_payloads.py
Executable file
|
|
@ -0,0 +1,291 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Collect + validate QR-related payloads for taler-monitoring (www.qr-*).
|
||||||
|
|
||||||
|
Sources (CLI paths / URLs handled by caller writing local files):
|
||||||
|
- landing HTML (href, data-qr-url, data-qr-taler, raw text)
|
||||||
|
- demo-withdraw.json / auto-account.json (taler_withdraw_uri / qr_payload)
|
||||||
|
- optional: already-decoded lines from zbarimg (kind=decoded)
|
||||||
|
|
||||||
|
Stdout TSV: source\\tkind\\turi\\tstatus\\tdetail
|
||||||
|
kind: withdraw | pay | pay-template | refund | payto | https | other | empty
|
||||||
|
status: ok | bad
|
||||||
|
|
||||||
|
Exit 0 always (caller tallies ok/bad lines). stderr for hard parse errors only.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
|
# Schemes we care about for wallet / settlement QRs
|
||||||
|
TALER_RE = re.compile(r"taler://[^\s\"'<>]+", re.I)
|
||||||
|
PAYTO_RE = re.compile(r"payto://[^\s\"'<>]+", re.I)
|
||||||
|
HTTPS_RE = re.compile(r"https://[^\s\"'<>]+", re.I)
|
||||||
|
|
||||||
|
WITHDRAW_RE = re.compile(
|
||||||
|
r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)/?$"
|
||||||
|
)
|
||||||
|
# order pay / template — host may include path segments after
|
||||||
|
PAY_RE = re.compile(r"^taler://pay/([^/?#]+)/", re.I)
|
||||||
|
PAY_TEMPLATE_RE = re.compile(r"^taler://pay-template/([^/?#]+)/", re.I)
|
||||||
|
REFUND_RE = re.compile(r"^taler://refund/", re.I)
|
||||||
|
PAYTO_OK_RE = re.compile(r"^payto://[a-z0-9][a-z0-9+.-]*/", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def looks_like_real_uri(uri: str) -> bool:
|
||||||
|
"""Drop JS regex fragments, docs ellipses, incomplete placeholders."""
|
||||||
|
if not uri or len(uri) < 12:
|
||||||
|
return False
|
||||||
|
if any(c in uri for c in "[]{}<>\n\r\t"):
|
||||||
|
return False
|
||||||
|
if "…" in uri or "..." in uri:
|
||||||
|
return False
|
||||||
|
# incomplete withdraw stubs in docs
|
||||||
|
if re.match(r"^taler://withdraw/?$", uri, re.I):
|
||||||
|
return False
|
||||||
|
if re.match(r"^taler://pay/?$", uri, re.I):
|
||||||
|
return False
|
||||||
|
# trailing junk from HTML/JS
|
||||||
|
if uri.endswith((")", "(", "\\", "^", "*", "+", "?")):
|
||||||
|
return False
|
||||||
|
if re.search(r"[\\^$*+?]", uri):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def strip_default_port_host(host: str) -> str | None:
|
||||||
|
if not host or host.startswith(":"):
|
||||||
|
return None
|
||||||
|
if ":" in host:
|
||||||
|
h, _, p = host.rpartition(":")
|
||||||
|
if not h or not p.isdigit():
|
||||||
|
return None
|
||||||
|
if p in ("443", "80"):
|
||||||
|
return None # must be stripped for wallets
|
||||||
|
return host
|
||||||
|
|
||||||
|
|
||||||
|
def classify_and_validate(uri: str) -> tuple[str, str, str]:
|
||||||
|
"""Return (kind, status, detail)."""
|
||||||
|
uri = (uri or "").strip()
|
||||||
|
if not uri:
|
||||||
|
return "empty", "bad", "empty"
|
||||||
|
# strip accidental trailing punctuation from HTML scrape
|
||||||
|
uri = uri.rstrip(").,;]")
|
||||||
|
low = uri.lower()
|
||||||
|
|
||||||
|
if low.startswith("taler://withdraw/"):
|
||||||
|
m = WITHDRAW_RE.match(uri)
|
||||||
|
if not m:
|
||||||
|
return "withdraw", "bad", "shape need taler://withdraw/HOST/taler-integration/UUID"
|
||||||
|
host = m.group(1)
|
||||||
|
if strip_default_port_host(host) is None:
|
||||||
|
return "withdraw", "bad", "host/port invalid or default :443/:80 not stripped"
|
||||||
|
return "withdraw", "ok", f"host={host} id={m.group(2)[:12]}…"
|
||||||
|
|
||||||
|
if low.startswith("taler://pay-template/"):
|
||||||
|
m = PAY_TEMPLATE_RE.match(uri)
|
||||||
|
if not m:
|
||||||
|
return "pay-template", "bad", "need taler://pay-template/HOST/…"
|
||||||
|
host = m.group(1)
|
||||||
|
if strip_default_port_host(host) is None:
|
||||||
|
return "pay-template", "bad", "host/port invalid or default port not stripped"
|
||||||
|
return "pay-template", "ok", f"host={host}"
|
||||||
|
|
||||||
|
if low.startswith("taler://pay/"):
|
||||||
|
m = PAY_RE.match(uri)
|
||||||
|
if not m:
|
||||||
|
return "pay", "bad", "need taler://pay/HOST/…"
|
||||||
|
host = m.group(1)
|
||||||
|
if strip_default_port_host(host) is None:
|
||||||
|
return "pay", "bad", "host/port invalid or default port not stripped"
|
||||||
|
if "instances" not in uri and "/orders/" not in uri:
|
||||||
|
# still often valid private order URIs
|
||||||
|
return "pay", "ok", f"host={host}"
|
||||||
|
return "pay", "ok", f"host={host}"
|
||||||
|
|
||||||
|
if low.startswith("taler://refund/"):
|
||||||
|
if not REFUND_RE.match(uri):
|
||||||
|
return "refund", "bad", "bad refund URI"
|
||||||
|
return "refund", "ok", "refund"
|
||||||
|
|
||||||
|
if low.startswith("taler://"):
|
||||||
|
return "other", "ok", "other taler:// (accepted)"
|
||||||
|
|
||||||
|
if low.startswith("payto://"):
|
||||||
|
if not PAYTO_OK_RE.match(uri):
|
||||||
|
return "payto", "bad", "need payto://method/…"
|
||||||
|
# wallet pay QR must not be payto for GOA demo-withdraw (checked elsewhere)
|
||||||
|
return "payto", "ok", uri.split("?", 1)[0][:72]
|
||||||
|
|
||||||
|
if low.startswith("https://") or low.startswith("http://"):
|
||||||
|
p = urlparse(uri)
|
||||||
|
if p.scheme not in ("http", "https") or not p.netloc:
|
||||||
|
return "https", "bad", "bad URL"
|
||||||
|
return "https", "ok", p.netloc + (p.path or "/")[:40]
|
||||||
|
|
||||||
|
return "other", "bad", "unsupported scheme"
|
||||||
|
|
||||||
|
|
||||||
|
class LinkHarvester(HTMLParser):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.uris: list[tuple[str, str]] = [] # (source_hint, uri)
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
ad = {k.lower(): (v or "") for k, v in attrs}
|
||||||
|
for key in ("href", "src", "data-qr-url", "data-qr-taler", "data-uri", "data-payto"):
|
||||||
|
if key in ad and ad[key]:
|
||||||
|
self.uris.append((f"attr:{key}", ad[key].strip()))
|
||||||
|
for k, v in ad.items():
|
||||||
|
if k.startswith("data-") and v and (
|
||||||
|
"taler://" in v.lower() or "payto://" in v.lower() or k.endswith("qr-url")
|
||||||
|
):
|
||||||
|
self.uris.append((f"attr:{k}", v.strip()))
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if not data or "://" not in data:
|
||||||
|
return
|
||||||
|
for m in TALER_RE.finditer(data):
|
||||||
|
self.uris.append(("text", m.group(0)))
|
||||||
|
for m in PAYTO_RE.finditer(data):
|
||||||
|
self.uris.append(("text", m.group(0)))
|
||||||
|
|
||||||
|
|
||||||
|
def harvest_html(path: Path, source: str) -> list[tuple[str, str, str]]:
|
||||||
|
raw = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
h = LinkHarvester()
|
||||||
|
try:
|
||||||
|
h.feed(raw)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
out: list[tuple[str, str, str]] = []
|
||||||
|
for hint, u in h.uris:
|
||||||
|
lu = u.lower()
|
||||||
|
if lu.startswith(("taler://", "payto://", "https://", "http://")):
|
||||||
|
if looks_like_real_uri(u):
|
||||||
|
out.append((f"{source}:{hint}", u, "html"))
|
||||||
|
# raster image paths that look like QR assets (not .js)
|
||||||
|
elif re.search(r"\.(png|jpe?g|gif)(\?|$)", lu) and "qr" in lu:
|
||||||
|
out.append((f"{source}:imgpath", u, "imgpath"))
|
||||||
|
# Prefer attribute/text harvest; light regex only for complete-looking URIs in pre/code
|
||||||
|
for m in TALER_RE.finditer(raw):
|
||||||
|
u = m.group(0).rstrip(").,;]'\"")
|
||||||
|
if looks_like_real_uri(u) and (
|
||||||
|
WITHDRAW_RE.match(u)
|
||||||
|
or PAY_RE.match(u)
|
||||||
|
or PAY_TEMPLATE_RE.match(u)
|
||||||
|
or u.lower().startswith("taler://refund/")
|
||||||
|
):
|
||||||
|
out.append((f"{source}:re", u, "html"))
|
||||||
|
for m in PAYTO_RE.finditer(raw):
|
||||||
|
u = m.group(0).rstrip(").,;]'\"")
|
||||||
|
if looks_like_real_uri(u) and PAYTO_OK_RE.match(u):
|
||||||
|
out.append((f"{source}:re", u, "html"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def harvest_json(path: Path, source: str) -> list[tuple[str, str, str]]:
|
||||||
|
try:
|
||||||
|
d = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
except Exception as e:
|
||||||
|
return [(source, "", f"json-error:{e}")]
|
||||||
|
out: list[tuple[str, str, str]] = []
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return out
|
||||||
|
for key in (
|
||||||
|
"taler_withdraw_uri",
|
||||||
|
"qr_payload",
|
||||||
|
"taler_pay_uri",
|
||||||
|
"payto_uri",
|
||||||
|
"payto",
|
||||||
|
"uri",
|
||||||
|
):
|
||||||
|
v = d.get(key)
|
||||||
|
if isinstance(v, str) and v.strip():
|
||||||
|
out.append((f"{source}:{key}", v.strip(), "json"))
|
||||||
|
# nested
|
||||||
|
for k, v in d.items():
|
||||||
|
if isinstance(v, str) and (
|
||||||
|
v.startswith("taler://") or v.startswith("payto://")
|
||||||
|
):
|
||||||
|
out.append((f"{source}:{k}", v.strip(), "json"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
# args: pairs of source_label path
|
||||||
|
if len(argv) < 3 or len(argv) % 2 == 0:
|
||||||
|
print(
|
||||||
|
"usage: check_qr_payloads.py LABEL path [LABEL path ...]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
seen: set[str] = set()
|
||||||
|
rows: list[tuple[str, str, str, str, str]] = []
|
||||||
|
i = 1
|
||||||
|
while i < len(argv):
|
||||||
|
label, path_s = argv[i], argv[i + 1]
|
||||||
|
i += 2
|
||||||
|
path = Path(path_s)
|
||||||
|
if not path.is_file():
|
||||||
|
rows.append((label, "empty", "", "bad", f"missing file {path_s}"))
|
||||||
|
continue
|
||||||
|
if path.suffix.lower() in (".html", ".htm") or "html" in path.name:
|
||||||
|
items = harvest_html(path, label)
|
||||||
|
elif path.suffix.lower() == ".json" or path.name.endswith(".json"):
|
||||||
|
items = harvest_json(path, label)
|
||||||
|
else:
|
||||||
|
# plain text: one URI per line or raw
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace").strip()
|
||||||
|
items = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
items.append((label, line, "text"))
|
||||||
|
if not items and text:
|
||||||
|
items.append((label, text, "text"))
|
||||||
|
|
||||||
|
for src, uri, _origin in items:
|
||||||
|
if not uri:
|
||||||
|
continue
|
||||||
|
# skip pure fragment / relative without scheme for validate
|
||||||
|
if uri.startswith("#") or uri == "/":
|
||||||
|
continue
|
||||||
|
lu = uri.lower()
|
||||||
|
if lu.startswith("/") or "imgpath" in src:
|
||||||
|
continue
|
||||||
|
if not looks_like_real_uri(uri):
|
||||||
|
continue
|
||||||
|
# https: app-store / wallet / explicit data-qr-* only
|
||||||
|
if lu.startswith("http"):
|
||||||
|
if "data-qr" not in src and not any(
|
||||||
|
x in lu
|
||||||
|
for x in (
|
||||||
|
"play.google",
|
||||||
|
"apps.apple",
|
||||||
|
"f-droid",
|
||||||
|
"wallet.taler",
|
||||||
|
"taler.net/wallet",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if uri in seen:
|
||||||
|
continue
|
||||||
|
seen.add(uri)
|
||||||
|
kind, status, detail = classify_and_validate(uri)
|
||||||
|
rows.append((src, kind, uri, status, detail))
|
||||||
|
|
||||||
|
for src, kind, uri, status, detail in rows:
|
||||||
|
# TSV — uri may contain tabs rarely; replace
|
||||||
|
safe = uri.replace("\t", " ").replace("\n", " ")
|
||||||
|
print(f"{src}\t{kind}\t{safe}\t{status}\t{detail}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
|
|
@ -1193,4 +1193,232 @@ else
|
||||||
warn "landing merchant shop assets" "${_ma}/2 present (soft)"
|
warn "landing merchant shop assets" "${_ma}/2 present (soft)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# QR payloads — form + encode/decode roundtrip (qrencode + zbarimg)
|
||||||
|
# Collect taler:// + payto:// (+ app-store https from data-qr-url) from landings
|
||||||
|
# and bank mint JSON; re-encode PNG; decode; require exact payload match.
|
||||||
|
# Env: QR_CHECK=0 to skip; QR_ECC=M (default) for qrencode -l
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "${QR_CHECK:-1}" = "1" ] && [ "${CHECK_LANDING:-1}" = "1" ]; then
|
||||||
|
set_group qr
|
||||||
|
section "www · QR payloads (form + encode/decode · taler:// payto://)"
|
||||||
|
|
||||||
|
QR_ECC="${QR_ECC:-M}"
|
||||||
|
qr_dir="$tmp/qr-check"
|
||||||
|
mkdir -p "$qr_dir"
|
||||||
|
_qr_tools=1
|
||||||
|
if ! command -v qrencode >/dev/null 2>&1; then
|
||||||
|
warn "qr tools" "qrencode missing — form checks only (apt install qrencode)"
|
||||||
|
_qr_tools=0
|
||||||
|
fi
|
||||||
|
if ! command -v zbarimg >/dev/null 2>&1; then
|
||||||
|
warn "qr tools" "zbarimg missing — form checks only (apt install zbar-tools)"
|
||||||
|
_qr_tools=0
|
||||||
|
fi
|
||||||
|
if [ "$_qr_tools" = "1" ]; then
|
||||||
|
ok "qr tools" "qrencode + zbarimg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch sources (HTML landings + mint JSON when present)
|
||||||
|
_qr_args=()
|
||||||
|
for pair in \
|
||||||
|
"bank-intro|$BANK_PUBLIC/intro/" \
|
||||||
|
"merchant-intro|$MERCHANT_PUBLIC/intro/" \
|
||||||
|
"exchange-intro|$EXCHANGE_PUBLIC/intro/"
|
||||||
|
do
|
||||||
|
_qn="${pair%%|*}"
|
||||||
|
_qu="${pair#*|}"
|
||||||
|
_qf="$qr_dir/${_qn}.html"
|
||||||
|
_qc=$(http_body "$_qu" "$_qf" 2>/dev/null || echo 000)
|
||||||
|
if [ "$_qc" = "200" ] && [ -s "$_qf" ]; then
|
||||||
|
_qr_args+=("$_qn" "$_qf")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for pair in \
|
||||||
|
"demo-withdraw|$BANK_PUBLIC/intro/demo-withdraw.json" \
|
||||||
|
"auto-account|$BANK_PUBLIC/intro/auto-account.json"
|
||||||
|
do
|
||||||
|
_qn="${pair%%|*}"
|
||||||
|
_qu="${pair#*|}"
|
||||||
|
_qf="$qr_dir/${_qn}.json"
|
||||||
|
_qc=$(http_body "$_qu" "$_qf" 2>/dev/null || echo 000)
|
||||||
|
if [ "$_qc" = "200" ] && [ -s "$_qf" ]; then
|
||||||
|
_qr_args+=("$_qn" "$_qf")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#_qr_args[@]}" -eq 0 ]; then
|
||||||
|
warn "qr sources" "no landing HTML / mint JSON fetched — skip payload checks"
|
||||||
|
else
|
||||||
|
_qr_tsv="$qr_dir/payloads.tsv"
|
||||||
|
if ! python3 "$ROOT/check_qr_payloads.py" "${_qr_args[@]}" >"$_qr_tsv" 2>"$qr_dir/harvest.err"; then
|
||||||
|
warn "qr harvest" "collector failed · $(tr '\n' ' ' <"$qr_dir/harvest.err" | head -c 160)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_qr_n=0
|
||||||
|
_qr_form_ok=0
|
||||||
|
_qr_form_bad=0
|
||||||
|
_qr_rt_ok=0
|
||||||
|
_qr_rt_bad=0
|
||||||
|
_qr_taler=0
|
||||||
|
_qr_payto=0
|
||||||
|
_qr_bad_sample=""
|
||||||
|
_qr_i=0
|
||||||
|
|
||||||
|
while IFS=$'\t' read -r src kind uri status detail || [ -n "${src:-}" ]; do
|
||||||
|
[ -n "${src:-}" ] || continue
|
||||||
|
_qr_n=$((_qr_n + 1))
|
||||||
|
case "$kind" in
|
||||||
|
withdraw|pay|pay-template|refund|other) _qr_taler=$((_qr_taler + 1)) ;;
|
||||||
|
payto) _qr_payto=$((_qr_payto + 1)) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$status" = "ok" ]; then
|
||||||
|
_qr_form_ok=$((_qr_form_ok + 1))
|
||||||
|
else
|
||||||
|
_qr_form_bad=$((_qr_form_bad + 1))
|
||||||
|
_qr_bad_sample="${_qr_bad_sample}${_qr_bad_sample:+; }form/${kind} ${uri:0:56}"
|
||||||
|
# taler:// and payto:// form errors are hard on local/GOA
|
||||||
|
case "$kind" in
|
||||||
|
withdraw|pay|pay-template|payto)
|
||||||
|
if [ "${LOCAL_STACK:-1}" = "1" ] || [ "${EXPECT_CURRENCY:-}" = "GOA" ]; then
|
||||||
|
fail "qr form ${kind}" "${src} · ${detail} · ${uri:0:80}"
|
||||||
|
else
|
||||||
|
warn "qr form ${kind}" "${src} · ${detail} · ${uri:0:80}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "qr form ${kind}" "${src} · ${detail} · ${uri:0:80}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Encode → PNG → zbarimg (exact match)
|
||||||
|
if [ "$_qr_tools" = "1" ] && [ -n "$uri" ]; then
|
||||||
|
_qr_i=$((_qr_i + 1))
|
||||||
|
_png="$qr_dir/p-${_qr_i}.png"
|
||||||
|
# ECC: H for wallet-style long URIs, L for long https store links, else QR_ECC
|
||||||
|
_ecc="$QR_ECC"
|
||||||
|
case "$kind" in
|
||||||
|
https) _ecc=L ;;
|
||||||
|
withdraw|pay|pay-template) _ecc=M ;;
|
||||||
|
payto) _ecc=M ;;
|
||||||
|
esac
|
||||||
|
if ! qrencode -l "$_ecc" -s 4 -m 2 -o "$_png" "$uri" 2>/dev/null; then
|
||||||
|
_qr_rt_bad=$((_qr_rt_bad + 1))
|
||||||
|
fail "qr encode ${kind}" "qrencode failed · ${uri:0:72}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
_got=$(zbarimg --raw -q "$_png" 2>/dev/null | head -n1 | tr -d '\r' || true)
|
||||||
|
if [ "$_got" = "$uri" ]; then
|
||||||
|
_qr_rt_ok=$((_qr_rt_ok + 1))
|
||||||
|
else
|
||||||
|
_qr_rt_bad=$((_qr_rt_bad + 1))
|
||||||
|
_qr_bad_sample="${_qr_bad_sample}${_qr_bad_sample:+; }decode/${kind}"
|
||||||
|
fail "qr decode ${kind}" "zbar mismatch · want ${uri:0:48}… · got ${_got:0:48}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <"$_qr_tsv"
|
||||||
|
|
||||||
|
# Download any obvious static QR images from bank intro and decode (soft)
|
||||||
|
if [ "$_qr_tools" = "1" ] && [ -f "$qr_dir/bank-intro.html" ]; then
|
||||||
|
python3 - "$qr_dir/bank-intro.html" "$BANK_PUBLIC" "$qr_dir" <<'PY' >"$qr_dir/img-urls.txt" 2>/dev/null || true
|
||||||
|
import re, sys
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
html = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||||
|
base = sys.argv[2].rstrip("/") + "/"
|
||||||
|
out = sys.argv[3]
|
||||||
|
seen = set()
|
||||||
|
for m in re.finditer(r'''(?:src|href)=["']([^"']+\.(?:png|jpe?g|gif|svg))["']''', html, re.I):
|
||||||
|
u = m.group(1)
|
||||||
|
if "qr" not in u.lower() and "withdraw" not in u.lower():
|
||||||
|
continue
|
||||||
|
if u.startswith("data:"):
|
||||||
|
continue
|
||||||
|
absu = urljoin(base + "intro/", u)
|
||||||
|
if absu in seen:
|
||||||
|
continue
|
||||||
|
seen.add(absu)
|
||||||
|
print(absu)
|
||||||
|
PY
|
||||||
|
_img_n=0
|
||||||
|
_img_ok=0
|
||||||
|
while read -r _img_url; do
|
||||||
|
[ -n "$_img_url" ] || continue
|
||||||
|
_img_n=$((_img_n + 1))
|
||||||
|
_imgf="$qr_dir/img-${_img_n}.bin"
|
||||||
|
_ic=$(http_body "$_img_url" "$_imgf" 2>/dev/null || echo 000)
|
||||||
|
if [ "$_ic" != "200" ] || [ ! -s "$_imgf" ]; then
|
||||||
|
warn "qr image fetch" "HTTP ${_ic} · ${_img_url}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# zbarimg needs image format; skip non-raster
|
||||||
|
if ! file "$_imgf" 2>/dev/null | grep -qiE 'PNG|JPEG|PBM|PGM|PPM|GIF'; then
|
||||||
|
info "qr image skip" "not raster · ${_img_url}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
_dec=$(zbarimg --raw -q "$_imgf" 2>/dev/null | head -n1 | tr -d '\r' || true)
|
||||||
|
if [ -z "$_dec" ]; then
|
||||||
|
warn "qr image decode" "no barcode · ${_img_url}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
_img_ok=$((_img_ok + 1))
|
||||||
|
# validate decoded payload form
|
||||||
|
printf '%s\n' "$_dec" >"$qr_dir/img-decoded.txt"
|
||||||
|
_line=$(python3 "$ROOT/check_qr_payloads.py" "img" "$qr_dir/img-decoded.txt" 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$_line" ]; then
|
||||||
|
_st=$(printf '%s' "$_line" | cut -f4)
|
||||||
|
_kd=$(printf '%s' "$_line" | cut -f2)
|
||||||
|
if [ "$_st" = "ok" ]; then
|
||||||
|
ok "qr image ${_kd}" "${_dec:0:64}"
|
||||||
|
else
|
||||||
|
case "$_kd" in
|
||||||
|
withdraw|pay|pay-template|payto)
|
||||||
|
fail "qr image ${_kd}" "bad form · ${_dec:0:72}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "qr image ${_kd}" "bad form · ${_dec:0:72}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "qr image decode" "${_dec:0:64}"
|
||||||
|
fi
|
||||||
|
done <"$qr_dir/img-urls.txt"
|
||||||
|
if [ "$_img_n" -gt 0 ]; then
|
||||||
|
info "qr images" "fetched ${_img_n} · decoded ${_img_ok}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summaries
|
||||||
|
if [ "$_qr_n" -eq 0 ]; then
|
||||||
|
warn "qr payloads" "none found in landings/mint JSON"
|
||||||
|
else
|
||||||
|
if [ "$_qr_form_bad" -eq 0 ]; then
|
||||||
|
ok "qr form" "${_qr_form_ok}/${_qr_n} ok · taler=${_qr_taler} payto=${_qr_payto}"
|
||||||
|
else
|
||||||
|
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||||
|
fail "qr form" "${_qr_form_ok}/${_qr_n} ok · ${_qr_form_bad} bad${_qr_bad_sample:+ · $_qr_bad_sample}"
|
||||||
|
else
|
||||||
|
warn "qr form" "${_qr_form_ok}/${_qr_n} ok · ${_qr_form_bad} bad"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$_qr_tools" = "1" ]; then
|
||||||
|
if [ "$_qr_rt_bad" -eq 0 ] && [ "$_qr_rt_ok" -gt 0 ]; then
|
||||||
|
ok "qr encode/decode" "${_qr_rt_ok} roundtrips (ecc default ${QR_ECC})"
|
||||||
|
elif [ "$_qr_rt_ok" -eq 0 ] && [ "$_qr_rt_bad" -eq 0 ]; then
|
||||||
|
info "qr encode/decode" "no payloads to encode"
|
||||||
|
else
|
||||||
|
fail "qr encode/decode" "${_qr_rt_ok} ok · ${_qr_rt_bad} fail"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "${QR_CHECK:-1}" != "1" ]; then
|
||||||
|
info "qr checks" "skipped (QR_CHECK=0)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
summary
|
summary
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ Env (same meaning):
|
||||||
NO_COLOR=1 / CLICOLOR=0 disable green/yellow/red tags (default: coloured)
|
NO_COLOR=1 / CLICOLOR=0 disable green/yellow/red tags (default: coloured)
|
||||||
SKIP_SSH=1 NO_COLOR=1
|
SKIP_SSH=1 NO_COLOR=1
|
||||||
PERF_WARN_MS PERF_FAIL_MS (urls latency; default 8000 / 20000)
|
PERF_WARN_MS PERF_FAIL_MS (urls latency; default 8000 / 20000)
|
||||||
|
QR_CHECK=0 skip QR form + qrencode/zbarimg (urls phase)
|
||||||
|
QR_ECC=M qrencode ECC level (default M)
|
||||||
KOOPA_SSH KOOPA_SSH_FALLBACKS (default koopa → koopa-external)
|
KOOPA_SSH KOOPA_SSH_FALLBACKS (default koopa → koopa-external)
|
||||||
METRICS_LOAD=0 skip host/container RAM/CPU probes (e2e/ladder/inside)
|
METRICS_LOAD=0 skip host/container RAM/CPU probes (e2e/ladder/inside)
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -224,7 +226,7 @@ if [ "${PROGRESS_TOTAL:-0}" = "0" ] || [ -z "${PROGRESS_TOTAL:-}" ]; then
|
||||||
_pt=0
|
_pt=0
|
||||||
for p in "${PHASES[@]}"; do
|
for p in "${PHASES[@]}"; do
|
||||||
case "$p" in
|
case "$p" in
|
||||||
urls) _pt=$((_pt + 55)) ;;
|
urls) _pt=$((_pt + 70)) ;;
|
||||||
inside) _pt=$((_pt + 25)) ;;
|
inside) _pt=$((_pt + 25)) ;;
|
||||||
versions) _pt=$((_pt + 20)) ;;
|
versions) _pt=$((_pt + 20)) ;;
|
||||||
sanity) _pt=$((_pt + 30)) ;;
|
sanity) _pt=$((_pt + 30)) ;;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue