#!/usr/bin/env bash # Outside-in public HTTPS checks (no SSH). set -euo pipefail ROOT=$(cd "$(dirname "$0")" && pwd) # shellcheck source=lib.sh source "$ROOT/lib.sh" # Area www-### — public HTTPS (outside-in) set_area www section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT check_url() { local label="$1" expect="$2" url="$3" local code code=$(http_code "$url") case ",$expect," in *",$code,"*) ok "$label $url" ;; *) fail "$label $url" "got $code want $expect" ;; esac } # Soft check: OK on expect, WARN on foreign stack if down, ERROR on local stack check_url_soft() { local label="$1" expect="$2" url="$3" local code code=$(http_code "$url") case ",$expect," in *",$code,"*) ok "$label $url" ;; *) if [ "${LOCAL_STACK:-1}" = "0" ]; then warn "$label $url" "got $code (optional on remote domain)" else fail "$label $url" "got $code want $expect" fi ;; esac } expect_currency() { local label="$1" file="$2" want="${EXPECT_CURRENCY:-}" local cur cur=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("currency",""))' "$file" 2>/dev/null || true) if [ -z "$want" ]; then info "$label currency" "${cur:-?}" return fi if [ "$cur" = "$want" ]; then ok "$label currency=$want" else fail "$label currency" "got ${cur:-?} want $want" fi } # Legal docs: /terms and /privacy must be HTTP 200 with a real document body # (not empty, not "not configured", not JSON API error). # $1=label $2=url $3=optional needle regex (case-insensitive) for local stack check_legal_doc() { local label="$1" url="$2" needle="${3:-}" local f code soft soft=0 [ "${LOCAL_STACK:-1}" = "0" ] && soft=1 f=$(mktemp) code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" \ -H "Accept: text/html,text/markdown,text/plain,*/*" \ -o "$f" -w '%{http_code}' "$url" 2>/dev/null || echo 000) if [ "$code" != "200" ]; then rm -f "$f" if [ "$soft" = "1" ]; then warn "$label" "HTTP $code — $url" else fail "$label" "HTTP $code — $url" fi return fi if [ ! -s "$f" ]; then rm -f "$f" fail "$label" "empty body — $url" return fi # merchant returns plain "not configured" when PRIVACY_ETAG missing if grep -qiE '^(not configured)\s*$' "$f" 2>/dev/null \ || grep -qiE '"code"\s*:\s*21' "$f" 2>/dev/null; then rm -f "$f" fail "$label" "not configured / API error — $url" return fi if [ -n "$needle" ] && [ "${LOCAL_STACK:-1}" = "1" ]; then if ! grep -qiE "$needle" "$f" 2>/dev/null; then warn "$label content" "missing /$needle/ — $url" rm -f "$f" return fi fi ok "$label" "HTTP 200 · $(wc -c <"$f" | tr -d ' ') bytes" rm -f "$f" } # Shared shape for bank mint JSON (demo-withdraw + auto-account). # Args: $1=json file $2=auto|demo # stdout (ok): one detail line; for demo, optional "\twithdrawal_id" suffix. # stderr (fail): short reason. Exit 0/1. validate_bank_withdraw_json() { python3 - "$1" "$2" <<'PY' import json, re, sys from urllib.parse import urlparse def check_withdraw_uri(wuri: str): """HOST or HOST:non-default-port; default :443/:80 must be stripped (wallet fix).""" m = re.match( r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)$", wuri or "", ) if not m: print( "need taler://withdraw/HOST/taler-integration/ID:", (wuri or "")[:120], file=sys.stderr, ) return None host = m.group(1) if not host or host.startswith(":"): print("bad withdraw host:", host, file=sys.stderr) return None if ":" in host: h, _, p = host.rpartition(":") if not h or not p.isdigit(): print("bad withdraw host:port:", host, file=sys.stderr) return None if p in ("443", "80"): print("withdraw must strip default port :%s:" % p, host, file=sys.stderr) return None return host, m.group(2), wuri path, mode = sys.argv[1], sys.argv[2] d = json.load(open(path)) wuri = d.get("taler_withdraw_uri") or ( d.get("qr_payload") if mode == "auto" else "" ) or "" if mode == "auto": if not d.get("ok"): print("ok!=true", file=sys.stderr) sys.exit(1) if d.get("payto_uri"): print("payto_uri must not be present", file=sys.stderr) sys.exit(1) if not check_withdraw_uri(wuri): sys.exit(1) webui = d.get("login_url") or d.get("webui") or d.get("account_url") or "" u = urlparse(webui) if u.scheme not in ("http", "https") or "webui" not in (u.path or ""): print("login webui missing:", webui[:80], file=sys.stderr) sys.exit(1) print("%s · %s" % (d.get("username", ""), wuri[:72])) elif mode == "demo": if not d.get("ok", True) and "taler_withdraw_uri" not in d: print("not ok", file=sys.stderr) sys.exit(1) if not check_withdraw_uri(wuri): sys.exit(1) print("%s\t%s" % (wuri[:80], d.get("withdrawal_id") or "")) else: print("bad mode:", mode, file=sys.stderr) sys.exit(2) sys.exit(0) PY } # --- exchange (core; always required) --- www-001 … check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config" code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json") if [ "$code" = "200" ]; then expect_currency "exchange" "$tmp/ec.json" # currency_specification.alt_unit_names (wallet codec) if json_has_alt_unit_names "$tmp/ec.json" "${EXPECT_CURRENCY:-}" >/tmp/alt-ex.$$ 2>&1; then ok "exchange /config alt_unit_names" "$(tr '\n' '; ' "$PERF_TSV" # Measure one URL: require HTTP expect (default 200), report time_total in ms. # $1=label $2=url $3=optional expected codes (default 200) check_perf() { local label="$1" url="$2" expect="${3:-200}" local out code t_s ms out=$(curl -skS --max-redirs 3 -L -m "${PERF_CURL_TIMEOUT:-25}" \ -o /dev/null -w '%{http_code} %{time_total}' "$url" 2>/dev/null || echo "000 0") code=$(printf '%s' "$out" | awk '{print $1}') t_s=$(printf '%s' "$out" | awk '{print $2}') ms=$(awk -v t="${t_s:-0}" 'BEGIN{ ms=(t+0)*1000 if (ms>0 && ms<1) ms=1 printf "%d", int(ms+0.5) }') # Always record sample for rollup (label, ms, http, url) printf '%s\t%s\t%s\t%s\n' "$label" "$ms" "$code" "$url" >>"$PERF_TSV" case ",$expect," in *",$code,"*) if [ "$ms" -ge "${PERF_FAIL_MS}" ] 2>/dev/null; then fail "$label" "HTTP $code · ${ms} ms ≥ fail ${PERF_FAIL_MS} ms · $url" elif [ "$ms" -ge "${PERF_WARN_MS}" ] 2>/dev/null; then warn "$label" "HTTP $code · ${ms} ms ≥ warn ${PERF_WARN_MS} ms · $url" else ok "$label" "HTTP $code · ${ms} ms · $url" fi ;; *) if [ "${LOCAL_STACK:-1}" = "1" ]; then fail "$label" "HTTP $code want $expect · ${ms} ms · $url" else warn "$label" "HTTP $code want $expect · ${ms} ms · $url" fi ;; esac } # Bank first (wallet-critical paths before UI chrome) check_perf "perf bank /taler-integration/config" "$BANK_PUBLIC/taler-integration/config" check_perf "perf bank /config" "$BANK_PUBLIC/config" if [ "${CHECK_LANDING:-1}" = "1" ]; then check_perf "perf bank /intro/" "$BANK_PUBLIC/intro/" check_perf "perf bank /intro/stats.json" "$BANK_PUBLIC/intro/stats.json" 200 fi check_perf "perf bank /webui/" "$BANK_PUBLIC/webui/" 200,301,302 # Exchange check_perf "perf exchange /config" "$EXCHANGE_PUBLIC/config" check_perf "perf exchange /keys" "$EXCHANGE_PUBLIC/keys" if [ "${CHECK_LANDING:-1}" = "1" ]; then check_perf "perf exchange /intro/" "$EXCHANGE_PUBLIC/intro/" fi # Merchant check_perf "perf merchant /config" "$MERCHANT_PUBLIC/config" check_perf "perf merchant /webui/" "$MERCHANT_PUBLIC/webui/" 200,301,302 if [ "${CHECK_LANDING:-1}" = "1" ]; then check_perf "perf merchant /intro/" "$MERCHANT_PUBLIC/intro/" fi info "perf note" "RTT measured from this host (outside-in)" # --------------------------------------------------------------------------- # Landing load stats (stats.json written *inside* containers, usually public). # Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from # the container. Report loadavg + RSS + in-container probe ms — one line each. # --------------------------------------------------------------------------- : "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m _fetch_landing_stats_json() { # $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok local name="$1" base="$2" local f="$tmp/stats-${name}.json" local code ctr path rm -f "$f" code=$(http_body "${base}/intro/stats.json" "$f" 2>/dev/null || echo 000) if [ "$code" = "200" ] && [ -s "$f" ]; then return 0 fi # Optional inside-container fallback (no public path or empty) if [ "${LOCAL_STACK:-0}" = "1" ] && [ "${SKIP_SSH:-0}" != "1" ] && koopa_ssh_ok 2>/dev/null; then case "$name" in bank) ctr=taler-hacktivism-bank; path=/var/www/bank-landing/stats.json ;; exchange) ctr=taler-hacktivism-exchange-ansible; path=/var/www/exchange-landing/stats.json ;; merchant) ctr=taler-hacktivism; path=/var/www/merchant-landing/stats.json ;; *) return 1 ;; esac if koopa_ssh_run 12 "podman exec ${ctr} cat ${path} 2>/dev/null" >"$f" 2>/dev/null \ && [ -s "$f" ]; then return 0 fi fi return 1 } report_landing_load_stats() { local name="$1" base="$2" local f="$tmp/stats-${name}.json" local line age_s if ! _fetch_landing_stats_json "$name" "$base"; then if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then warn "perf ${name} landing-stats" "stats.json not available (public or SSH)" fi return fi line=$(python3 - "$f" "${STATS_STALE_SECS}" <<'PY' 2>/dev/null || true import json, sys, time path, stale = sys.argv[1], int(sys.argv[2]) try: d = json.load(open(path)) except Exception as e: print("ERR parse:" + str(e)[:80]) sys.exit(0) if not d.get("ok", True): print("ERR ok=false " + str(d.get("error") or "")[:80]) sys.exit(0) p = d.get("performance") or {} if not isinstance(p, dict) or not p: print("ERR no performance block") sys.exit(0) mem = p.get("memory") if isinstance(p.get("memory"), dict) else {} rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?" load = p.get("loadavg") or "?" # latency fields differ by site bits = [] for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"): if k in p and p[k] is not None: bits.append("%s=%sms" % (k.replace("_ms", ""), p[k])) lat = " ".join(bits) if bits else "latency=?" gen = d.get("generated_at_human") or d.get("generated_at") or "?" # staleness age = "" try: gu = d.get("generated_at_unix") if gu is not None: age_s = int(time.time()) - int(gu) age = " age=%ss" % age_s if age_s > stale: print("STALE loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) sys.exit(0) except Exception: pass print("OK loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age)) PY ) case "$line" in OK\ *) ok "perf ${name} landing-stats" "${line#OK }" ;; STALE\ *) warn "perf ${name} landing-stats" "stale · ${line#STALE }" ;; ERR\ *) warn "perf ${name} landing-stats" "${line#ERR }" ;; *) warn "perf ${name} landing-stats" "unreadable stats.json" ;; esac } if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then section "www · performance · landing load stats (stats.json · in-container probes)" report_landing_load_stats "bank" "$BANK_PUBLIC" report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC" report_landing_load_stats "merchant" "$MERCHANT_PUBLIC" info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed" fi # Rollup + optional JSON for metrics_print_overall if [ -s "$PERF_TSV" ]; then PERF_JSON="$tmp/perf-summary.json" perf_line=$(python3 - "$PERF_TSV" "$PERF_JSON" "$PERF_WARN_MS" "$PERF_FAIL_MS" <<'PY' import json, sys rows = [] for line in open(sys.argv[1]): parts = line.rstrip("\n").split("\t") if len(parts) < 3: continue label, ms_s, code = parts[0], parts[1], parts[2] try: ms = int(float(ms_s)) except Exception: continue rows.append({"label": label, "ms": ms, "http": code}) vals = sorted(r["ms"] for r in rows) n = len(vals) if n == 0: print("n=0") json.dump({"n": 0}, open(sys.argv[2], "w")) raise SystemExit(0) def pct(p): if n == 1: return vals[0] i = min(n - 1, max(0, int(round((p / 100.0) * (n - 1))))) return vals[i] avg = int(round(sum(vals) / n)) warn_ms = int(sys.argv[3]) fail_ms = int(sys.argv[4]) slow = [r for r in rows if r["ms"] >= warn_ms] rep = { "n": n, "min_ms": vals[0], "p50_ms": pct(50), "avg_ms": avg, "max_ms": vals[-1], "warn_ms": warn_ms, "fail_ms": fail_ms, "slow": [{"label": r["label"], "ms": r["ms"]} for r in slow], "samples": rows, } # metrics_print_overall expects named buckets with n/min/p50/avg/max out = { "www_public_https": { "n": n, "min_ms": vals[0], "p50_ms": pct(50), "avg_ms": avg, "max_ms": vals[-1], }, **{r["label"].replace(" ", "_"): {"n": 1, "min_ms": r["ms"], "p50_ms": r["ms"], "avg_ms": r["ms"], "max_ms": r["ms"]} for r in rows}, } json.dump(out, open(sys.argv[2], "w"), indent=2) extra = "" if slow: extra = " slow: " + ", ".join("%s=%dms" % (r["label"].replace("perf ", ""), r["ms"]) for r in slow) print( "n=%d min=%dms p50=%dms avg=%dms max=%dms (warn≥%dms fail≥%dms)%s" % (n, vals[0], pct(50), avg, vals[-1], warn_ms, fail_ms, extra) ) PY ) info "perf summary" "$perf_line" # Keep a copy if METRICS_DIR is set (e2e/ladder overall stats) if [ -n "${METRICS_DIR:-}" ] && [ -d "${METRICS_DIR}" ]; then cp -f "$PERF_JSON" "${METRICS_DIR}/perf-summary.json" 2>/dev/null || true fi fi info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}" # Terms + privacy (legal docs) check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy" check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration" # trailing slash: 200 or redirect to bare path code=$(http_code "$EXCHANGE_PUBLIC/terms/") case "$code" in 200|301|302) ok "exchange /terms/" "HTTP $code" ;; *) if [ "${LOCAL_STACK:-1}" = "1" ]; then warn "exchange /terms/" "HTTP $code" else warn "exchange /terms/" "HTTP $code" fi ;; esac # --- bank --- if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "bank /config" 200 "$BANK_PUBLIC/config" else check_url "bank /config" 200 "$BANK_PUBLIC/config" fi code=$(http_body "$BANK_PUBLIC/config" "$tmp/bc.json") if [ "$code" = "200" ]; then expect_currency "bank" "$tmp/bc.json" if json_has_alt_unit_names "$tmp/bc.json" >/tmp/alt-bank.$$ 2>&1; then ok "bank /config alt_unit_names" "$(tr '\n' '; ' "$tmp/aa-val"); then ok "bank /intro/auto-account.json" "$aa_detail" else fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))" fi ;; 405|501|404|502|503|000) fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)" ;; *) fail "bank /intro/auto-account.json" "HTTP $aa_code want 200" ;; esac fi # Bank legal docs (landing nginx via Caddy /terms* /privacy* or /intro/*) check_legal_doc "bank /terms" "$BANK_PUBLIC/terms" "terms|GOA|exploration|bank|FADP|revDSG" # Prefer /privacy; fall back to /intro/privacy.html for older deploys code=$(http_code "$BANK_PUBLIC/privacy") if [ "$code" = "200" ]; then check_legal_doc "bank /privacy" "$BANK_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|bank" else if [ "${LOCAL_STACK:-1}" = "1" ]; then check_legal_doc "bank /privacy (or /intro/privacy.html)" \ "$BANK_PUBLIC/intro/privacy.html" "privacy|FADP|revDSG|data|GOA|bank" # still report bare /privacy failure for local warn "bank /privacy" "HTTP $code — prefer Caddy handle /privacy* → landing" else check_url_soft "bank /privacy" 200 "$BANK_PUBLIC/privacy" fi fi # --- merchant --- if [ "${LOCAL_STACK:-1}" = "0" ]; then check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config" else check_url "merchant /config" 200 "$MERCHANT_PUBLIC/config" fi code=$(http_body "$MERCHANT_PUBLIC/config" "$tmp/mc.json") if [ "$code" = "200" ]; then want="${EXPECT_CURRENCY:-}" if python3 - "$tmp/mc.json" "$want" <<'PY' import json,sys try: d=json.load(open(sys.argv[1])) except Exception: sys.exit(2) want=sys.argv[2] if not want: sys.exit(0) curs=list((d.get("currencies") or {}).keys()) ex=d.get("exchanges") or [] ok = want in curs or any((e.get("currency") if isinstance(e,dict) else None)==want for e in ex) if not ok and isinstance(d.get("currency"), str): ok = d["currency"]==want sys.exit(0 if ok else 1) PY then ok "merchant /config (${want:-currency} ok)" else ec=$? if [ "$ec" = "2" ]; then warn "merchant /config" "non-JSON body" elif [ -n "$want" ]; then fail "merchant /config currency" "want $want" else info "merchant /config" "ok" fi fi # merchant-local currency maps (GOA + CHF, …) if json_has_alt_unit_names "$tmp/mc.json" >/tmp/alt-mer.$$ 2>&1; then ok "merchant /config currencies alt_unit_names" "$(tr '\n' '; ' /dev/null || true) pcode=$(printf '%s' "$hdr" | awk 'BEGIN{c="000"} /^HTTP/{c=$2} END{print c}') loc=$(printf '%s' "$hdr" | awk 'BEGIN{IGNORECASE=1} /^location:/{sub(/\r$/,""); sub(/^location:[[:space:]]*/,""); print; exit}') case "$pcode" in 301|302|303|307|308) if printf '%s' "$loc" | grep -qiE 'paivana|templates|well-known'; then ok "paivana /" "HTTP $pcode → template flow" else ok "paivana /" "HTTP $pcode redirect" fi info "paivana Location" "${loc:0:140}" ;; 200) warn "paivana /" "HTTP 200 (expected paywall redirect to template)" ;; *) warn "paivana /" "HTTP ${pcode:-000} — ${PAIVANA_PUBLIC}/ (e2e pay may still work via template)" ;; esac fi # --------------------------------------------------------------------------- # Landing pages: every HTTPS link exposed on bank / merchant / exchange intros # + required static assets + bank withdraw mint (taler://withdraw only) # Skipped when CHECK_LANDING=0 (e.g. taler-ops.ch — no GOA-style landings). # --------------------------------------------------------------------------- if [ "${CHECK_LANDING:-1}" != "1" ]; then section "www · landing pages" info "landing checks" "skipped (CHECK_LANDING=0 · stack has no public /intro landings)" summary exit 0 fi section "www · landing exposed links · bank / merchant / exchange" # Probe one URL: print code to stdout (200 after following redirects counts as 200). # Sets _landing_code. Exit 0 if OK (200 or redirect→200), 1 otherwise. _landing_probe() { local url="$1" local code code=$(http_code "$url") case "$code" in 200) _landing_code=200; return 0 ;; 301|302|303|307|308) code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000) _landing_code="$code" [ "$code" = "200" ] && return 0 return 1 ;; *) _landing_code="$code" return 1 ;; esac } # Known-good landing static paths — one report line via caller aggregate, or # soft=1 single warn. Returns 0 if ok. check_landing_asset() { local label="$1" url="$2" soft="${3:-0}" if _landing_probe "$url"; then return 0 fi if [ "$soft" = "1" ]; then warn "$label" "HTTP ${_landing_code:-?} — $url" else fail "$label" "HTTP ${_landing_code:-?} — $url" fi return 1 } # Soft external: never ERROR; used only for failures in aggregated external probe. check_external_soft() { local label="$1" url="$2" local code code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000) case "$code" in 200|204|301|302|303|307|308) return 0 ;; *) warn "$label" "HTTP $code · $url"; return 1 ;; esac } # Parse one landing HTML: collect absolute https + root-relative href/src; # resolve against base; classify own-stack vs external. # Writes lists: $1.own $1.ext (one URL per line) extract_landing_urls() { local base="$1" html="$2" out_prefix="$3" python3 - "$base" "$html" "$out_prefix" <<'PY' import re, sys from urllib.parse import urljoin, urlparse base, html_path, out = sys.argv[1], sys.argv[2], sys.argv[3] html = open(html_path, encoding="utf-8", errors="replace").read() base = base.rstrip("/") + "/" parsed_base = urlparse(base) # Hard-check only the three public Taler hosts for this stack (not git.* etc.) own_hosts = { (parsed_base.hostname or "").lower(), "bank.hacktivism.ch", "exchange.hacktivism.ch", "taler.hacktivism.ch", } # include configured public hosts when domain differs (demo / ops) for envu in ( __import__("os").environ.get("BANK_PUBLIC", ""), __import__("os").environ.get("EXCHANGE_PUBLIC", ""), __import__("os").environ.get("MERCHANT_PUBLIC", ""), ): h = urlparse(envu).hostname if envu else None if h: own_hosts.add(h.lower()) raw = set() for m in re.finditer( r'''(?:href|src|content)=["']([^"'#]+)["']''', html, re.I ): raw.add(m.group(1).strip()) # bare absolute URLs in scripts (fetch, template strings) for m in re.finditer(r'''https://[^\s"'<>\\]+''', html): u = m.group(0).rstrip("\\).,;'\"") # strip trailing punctuation leftovers while u and u[-1] in ".,);]}\"'": u = u[:-1] if u.startswith("https://"): raw.add(u) own, ext = set(), set() skip_prefix = ("data:", "javascript:", "mailto:", "taler://", "blob:") skip_exact = {"website", "summary_large_image", "image/png", "en_US"} for r in raw: if not r or r in skip_exact: continue if r.startswith(skip_prefix): continue # meta content noise if re.fullmatch(r"\d+", r) or r.startswith("width="): continue if " " in r and not r.startswith("http"): continue if r.startswith("//"): absu = "https:" + r elif r.startswith("http://") or r.startswith("https://"): absu = r elif r.startswith("/"): absu = urljoin(base, r) else: # relative asset if "/" in r or r.endswith((".js", ".css", ".png", ".svg", ".html", ".json", ".uri")): absu = urljoin(base + "intro/", r) else: continue # drop query-only noise / anchors already stripped p = urlparse(absu) if p.scheme not in ("http", "https"): continue # normalize: drop fragment absu = absu.split("#", 1)[0] host = (p.hostname or "").lower() # og image query ok if host in own_hosts: own.add(absu) else: ext.add(absu) open(out + ".own", "w").write("\n".join(sorted(own)) + ("\n" if own else "")) open(out + ".ext", "w").write("\n".join(sorted(ext)) + ("\n" if ext else "")) print(f"own={len(own)} ext={len(ext)}") PY } check_one_landing() { local name="$1" base="$2" local html="$tmp/landing-${name}.html" local pref="$tmp/urls-${name}" local code n own_n ext_n local a_ok=0 a_fail=0 a_soft=0 local own_ok=0 own_fail=0 local ext_ok=0 ext_fail=0 local fail_sample="" soft_sample="" code=$(http_body "${base}/intro/" "$html") if [ "$code" != "200" ]; then if [ "${LOCAL_STACK:-1}" = "1" ]; then fail "landing ${name}" "/intro/ HTTP $code — skip assets/links" else warn "landing ${name}" "/intro/ HTTP $code — skip assets/links" fi return fi # Required static assets (hard: qrcode + og; soft: qr-logo) for url in \ "${base}/intro/qrcode.min.js" \ "${base}/intro/og-goa-shop.png" do if _landing_probe "$url"; then a_ok=$((a_ok + 1)) else a_fail=$((a_fail + 1)) fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url" fi done if _landing_probe "${base}/intro/qr-logo.png"; then a_ok=$((a_ok + 1)) else a_soft=$((a_soft + 1)) soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} qr-logo.png" fi n=$(extract_landing_urls "$base" "$html" "$pref" 2>/dev/null || echo "own=0 ext=0") own_n=0 ext_n=0 [ -f "${pref}.own" ] && own_n=$(grep -c . "${pref}.own" 2>/dev/null || echo 0) [ -f "${pref}.ext" ] && ext_n=$(grep -c . "${pref}.ext" 2>/dev/null || echo 0) # strip newlines from grep -c edge cases own_n=${own_n//[^0-9]/} ext_n=${ext_n//[^0-9]/} own_n=${own_n:-0} ext_n=${ext_n:-0} # Probe own-stack URLs — count only; list failures if [ -f "${pref}.own" ]; then while IFS= read -r u; do [ -n "$u" ] || continue if _landing_probe "$u"; then own_ok=$((own_ok + 1)) else own_fail=$((own_fail + 1)) # keep a few samples (max ~3) if [ "$own_fail" -le 3 ]; then fail_sample="${fail_sample}${fail_sample:+; }own HTTP ${_landing_code} $u" fi fi done < "${pref}.own" fi # External: soft counts if [ -f "${pref}.ext" ]; then while IFS= read -r u; do [ -n "$u" ] || continue code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$u" 2>/dev/null || echo 000) case "$code" in 200|204|301|302|303|307|308) ext_ok=$((ext_ok + 1)) ;; *) ext_fail=$((ext_fail + 1)) if [ "$ext_fail" -le 3 ]; then soft_sample="${soft_sample}${soft_sample:+; }ext HTTP $code $u" fi ;; esac done < "${pref}.ext" fi # One primary line per landing local detail detail="/intro $(wc -c <"$html" | tr -d ' ')B · assets ${a_ok}/$((a_ok + a_fail + a_soft)) · own-links ${own_ok}/${own_n} · external ${ext_ok}/${ext_n}" if [ "$a_fail" -gt 0 ] || [ "$own_fail" -gt 0 ] || [ "${own_n:-0}" -lt 1 ]; then if [ "${LOCAL_STACK:-1}" = "1" ]; then fail "landing ${name}" "$detail${fail_sample:+ · $fail_sample}" else warn "landing ${name}" "$detail${fail_sample:+ · $fail_sample}" fi else ok "landing ${name}" "$detail" fi if [ "$a_soft" -gt 0 ] || [ "$ext_fail" -gt 0 ]; then warn "landing ${name} soft" "${soft_sample:-soft issues}" fi } check_one_landing "bank" "$BANK_PUBLIC" check_one_landing "merchant" "$MERCHANT_PUBLIC" check_one_landing "exchange" "$EXCHANGE_PUBLIC" # Cross-links: one line if [ "${LOCAL_STACK:-1}" = "1" ]; then _cx_ok=0 _cx_fail=0 _cx_detail="" for pair in \ "bank→merchant|$MERCHANT_PUBLIC/intro/" \ "bank→exchange|$EXCHANGE_PUBLIC/intro/" \ "merchant→bank|$BANK_PUBLIC/intro/" \ "exchange→bank|$BANK_PUBLIC/intro/" do _cx_name="${pair%%|*}" _cx_url="${pair#*|}" if _landing_probe "$_cx_url"; then _cx_ok=$((_cx_ok + 1)) else _cx_fail=$((_cx_fail + 1)) _cx_detail="${_cx_detail}${_cx_detail:+; }${_cx_name} HTTP ${_landing_code}" fi done if [ "$_cx_fail" -eq 0 ]; then ok "landing cross-links" "${_cx_ok}/4 intros reachable" else fail "landing cross-links" "${_cx_ok}/4 ok · ${_cx_detail}" fi fi # Bank withdraw mint + shop assets — compact if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then _ba_ok=0 _ba_soft=0 _ba_msg="" if _landing_probe "$BANK_PUBLIC/intro/shop-pay.js"; then _ba_ok=$((_ba_ok + 1)); else _ba_soft=$((_ba_soft + 1)); _ba_msg="${_ba_msg}shop-pay.js; "; fi if _landing_probe "$BANK_PUBLIC/intro/shop-pay.css"; then _ba_ok=$((_ba_ok + 1)); else _ba_soft=$((_ba_soft + 1)); _ba_msg="${_ba_msg}shop-pay.css; "; fi dw_code=$(http_body "$BANK_PUBLIC/intro/demo-withdraw.json" "$tmp/dw.json") case "$dw_code" in 200) if dw_out=$(validate_bank_withdraw_json "$tmp/dw.json" demo 2>"$tmp/dw-val"); then dw_detail=${dw_out%%$'\t'*} wid=${dw_out#*$'\t'} [ "$wid" = "$dw_out" ] && wid= ok "bank /intro/demo-withdraw.json" "$dw_detail" if [ -n "$wid" ]; then if _landing_probe "$BANK_PUBLIC/taler-integration/withdrawal-operation/${wid}"; then _ba_ok=$((_ba_ok + 1)) else fail "landing bank withdraw-op" "HTTP ${_landing_code} · id=$wid" fi fi ok "landing bank withdraw/shop" "demo-withdraw + shop assets ok (${_ba_ok} checks)" else fail "landing bank demo-withdraw" "invalid taler://withdraw shape" fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))" fi ;; 405|501|404|502|503|000) fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)" ;; *) if [ "${LOCAL_STACK:-1}" = "1" ]; then fail "landing bank demo-withdraw" "HTTP $dw_code want 200" else warn "landing bank demo-withdraw" "HTTP $dw_code" fi ;; esac if [ "$_ba_soft" -gt 0 ]; then warn "landing bank shop assets" "${_ba_msg}soft-missing" fi fi # Merchant shop assets — one soft line _ma=0 _landing_probe "$MERCHANT_PUBLIC/intro/shop-pay.js" && _ma=$((_ma + 1)) _landing_probe "$MERCHANT_PUBLIC/intro/shop-pay.css" && _ma=$((_ma + 1)) if [ "$_ma" -eq 2 ]; then ok "landing merchant shop assets" "shop-pay.js + .css" else warn "landing merchant shop assets" "${_ma}/2 present (soft)" fi summary