Files
proxmox/scripts/verify/verify-end-to-end-routing.sh
defiQUG 17b923ffdf Follow-ups: DNS dry-run/zone-only, Order NPM IDs, E2E Location assert, the-order block_exploits
- update-all-dns-to-public-ip.sh: --dry-run (no CF API), --zone-only=ZONE, help before .env, env CLOUDFLARE_DNS_DRY_RUN/DNS_ZONE_ONLY
- update-sankofa-npmplus-proxy-hosts.sh: the-order + www.the-order by ID (env SANKOFA_NPM_ID_THE_ORDER, SANKOFA_NPM_ID_WWW_THE_ORDER, THE_ORDER_UPSTREAM_*)
- update-npmplus-proxy-hosts-api.sh: the-order.sankofa.nexus uses block_exploits false like sankofa portal
- verify-end-to-end-routing.sh: E2E_WWW_CANONICAL_BASE + Location validation (fail on wrong apex); keep local redirect vars
- docs: ALL_VMIDS www 301 lines, E2E_ENDPOINTS_LIST verifier/DNS notes; AGENTS.md Cloudflare script pointer

Made-with: Cursor
2026-03-27 11:27:39 -07:00

691 lines
36 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verify end-to-end request flow from external to backend
# Tests DNS resolution, SSL certificates, HTTP responses, and WebSocket connections
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
EVIDENCE_DIR="$PROJECT_ROOT/docs/04-configuration/verification-evidence"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
log_success() { echo -e "${GREEN}[✓]${NC} $1" >&2; }
log_warn() { echo -e "${YELLOW}[⚠]${NC} $1" >&2; }
log_error() { echo -e "${RED}[✗]${NC} $1" >&2; }
cd "$PROJECT_ROOT"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="$EVIDENCE_DIR/e2e-verification-$TIMESTAMP"
mkdir -p "$OUTPUT_DIR"
PUBLIC_IP="${PUBLIC_IP:-76.53.10.36}"
# Fourth NPMplus (dev/Codespaces, Gitea) — gitea.d-bis.org, dev.d-bis.org, codespaces.d-bis.org resolve here
PUBLIC_IP_FOURTH="${PUBLIC_IP_FOURTH:-76.53.10.40}"
# Set ACCEPT_ANY_DNS=1 to pass DNS if domain resolves to any IP (e.g. Fastly CNAME or Cloudflare Tunnel)
ACCEPT_ANY_DNS="${ACCEPT_ANY_DNS:-0}"
# Use system resolver (e.g. /etc/hosts) instead of dig @8.8.8.8 — set when running from LAN with generate-e2e-hosts.sh entries
E2E_USE_SYSTEM_RESOLVER="${E2E_USE_SYSTEM_RESOLVER:-0}"
# openssl s_client has no built-in connect timeout; wrap to avoid hangs (private/wss hosts).
E2E_OPENSSL_TIMEOUT="${E2E_OPENSSL_TIMEOUT:-15}"
E2E_OPENSSL_X509_TIMEOUT="${E2E_OPENSSL_X509_TIMEOUT:-5}"
if [ "$E2E_USE_SYSTEM_RESOLVER" = "1" ]; then
ACCEPT_ANY_DNS=1
log_info "E2E_USE_SYSTEM_RESOLVER=1: using getent (respects /etc/hosts); ACCEPT_ANY_DNS=1"
fi
# When using Option B (RPC via Cloudflare Tunnel), RPC hostnames resolve to Cloudflare IPs; auto-enable if tunnel ID set
if [ "$ACCEPT_ANY_DNS" = "0" ] && [ -n "${CLOUDFLARE_TUNNEL_ID:-}" ]; then
ACCEPT_ANY_DNS=1
log_info "ACCEPT_ANY_DNS=1 (CLOUDFLARE_TUNNEL_ID set, Option B tunnel)"
fi
# Also respect CLOUDFLARE_TUNNEL_ID from .env if not in environment
if [ "$ACCEPT_ANY_DNS" = "0" ] && [ -f "$PROJECT_ROOT/.env" ]; then
TUNNEL_ID=$(grep -E '^CLOUDFLARE_TUNNEL_ID=' "$PROJECT_ROOT/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | xargs)
if [ -n "$TUNNEL_ID" ]; then
ACCEPT_ANY_DNS=1
log_info "ACCEPT_ANY_DNS=1 (CLOUDFLARE_TUNNEL_ID in .env, Option B tunnel)"
fi
fi
# Expected domains and their types (full combined inventory)
declare -A DOMAIN_TYPES_ALL=(
["explorer.d-bis.org"]="web"
["rpc-http-pub.d-bis.org"]="rpc-http"
["rpc-ws-pub.d-bis.org"]="rpc-ws"
["rpc.d-bis.org"]="rpc-http"
["rpc2.d-bis.org"]="rpc-http"
["ws.rpc.d-bis.org"]="rpc-ws"
["ws.rpc2.d-bis.org"]="rpc-ws"
["rpc-http-prv.d-bis.org"]="rpc-http"
["rpc-ws-prv.d-bis.org"]="rpc-ws"
["rpc-fireblocks.d-bis.org"]="rpc-http"
["ws.rpc-fireblocks.d-bis.org"]="rpc-ws"
["dbis-admin.d-bis.org"]="web"
["dbis-api.d-bis.org"]="api"
["dbis-api-2.d-bis.org"]="api"
["secure.d-bis.org"]="web"
["mim4u.org"]="web"
["www.mim4u.org"]="web"
["secure.mim4u.org"]="web"
["training.mim4u.org"]="web"
["sankofa.nexus"]="web"
["www.sankofa.nexus"]="web"
["phoenix.sankofa.nexus"]="web"
["www.phoenix.sankofa.nexus"]="web"
["the-order.sankofa.nexus"]="web" # OSJ portal (secure auth); app: ~/projects/the_order
["www.the-order.sankofa.nexus"]="web" # 301 → https://the-order.sankofa.nexus
["studio.sankofa.nexus"]="web"
["rpc.public-0138.defi-oracle.io"]="rpc-http"
["rpc.defi-oracle.io"]="rpc-http"
["wss.defi-oracle.io"]="rpc-ws"
# Alltra / HYBX (tunnel → primary NPMplus 192.168.11.167)
["rpc-alltra.d-bis.org"]="rpc-http"
["rpc-alltra-2.d-bis.org"]="rpc-http"
["rpc-alltra-3.d-bis.org"]="rpc-http"
["rpc-hybx.d-bis.org"]="rpc-http"
["rpc-hybx-2.d-bis.org"]="rpc-http"
["rpc-hybx-3.d-bis.org"]="rpc-http"
["cacti-alltra.d-bis.org"]="web"
["cacti-hybx.d-bis.org"]="web"
# Mifos (76.53.10.41 or tunnel; NPMplus 10237 → VMID 5800)
["mifos.d-bis.org"]="web"
# DApp (tunnel or 76.53.10.36; NPMplus 10233 → VMID 5801 at 192.168.11.58)
["dapp.d-bis.org"]="web"
# Dev/Codespaces (76.53.10.40; NPMplus Fourth → Dev VM 5700 at 192.168.11.59:3000)
["gitea.d-bis.org"]="web"
["dev.d-bis.org"]="web"
["codespaces.d-bis.org"]="web"
)
# Private/admin profile domains (private RPC + Fireblocks RPC only).
declare -a PRIVATE_PROFILE_DOMAINS=(
"rpc-http-prv.d-bis.org"
"rpc-ws-prv.d-bis.org"
"rpc-fireblocks.d-bis.org"
"ws.rpc-fireblocks.d-bis.org"
)
PRIVATE_PROFILE_SET=" ${PRIVATE_PROFILE_DOMAINS[*]} "
PROFILE="${E2E_PROFILE:-public}"
LIST_ENDPOINTS=0
for arg in "$@"; do
case "$arg" in
--list-endpoints) LIST_ENDPOINTS=1 ;;
--profile=*) PROFILE="${arg#*=}" ;;
--profile-public) PROFILE="public" ;;
--profile-private) PROFILE="private" ;;
--profile-all) PROFILE="all" ;;
*)
if [[ "$arg" != "--list-endpoints" ]]; then
echo "Unknown argument: $arg" >&2
echo "Usage: $0 [--list-endpoints] [--profile=public|private|all]" >&2
exit 2
fi
;;
esac
done
declare -A DOMAIN_TYPES=()
for domain in "${!DOMAIN_TYPES_ALL[@]}"; do
is_private=0
[[ "$PRIVATE_PROFILE_SET" == *" $domain "* ]] && is_private=1
case "$PROFILE" in
public)
[[ "$is_private" -eq 0 ]] && DOMAIN_TYPES["$domain"]="${DOMAIN_TYPES_ALL[$domain]}"
;;
private)
[[ "$is_private" -eq 1 ]] && DOMAIN_TYPES["$domain"]="${DOMAIN_TYPES_ALL[$domain]}"
;;
all)
DOMAIN_TYPES["$domain"]="${DOMAIN_TYPES_ALL[$domain]}"
;;
*)
echo "Invalid profile: $PROFILE (expected public|private|all)" >&2
exit 2
;;
esac
done
# Domains that are optional (not yet configured); no DNS = skip instead of fail. Space-separated.
if [[ -z "${E2E_OPTIONAL_DOMAINS:-}" ]]; then
if [[ "$PROFILE" == "private" ]]; then
E2E_OPTIONAL_DOMAINS=""
else
E2E_OPTIONAL_DOMAINS="dapp.d-bis.org"
fi
else
E2E_OPTIONAL_DOMAINS="${E2E_OPTIONAL_DOMAINS}"
fi
# Domains that are optional when any test fails (off-LAN, 502, unreachable); fail → skip so run passes.
_PUB_OPTIONAL_WHEN_FAIL="dapp.d-bis.org mifos.d-bis.org explorer.d-bis.org dbis-admin.d-bis.org dbis-api.d-bis.org dbis-api-2.d-bis.org secure.d-bis.org sankofa.nexus www.sankofa.nexus phoenix.sankofa.nexus www.phoenix.sankofa.nexus the-order.sankofa.nexus www.the-order.sankofa.nexus studio.sankofa.nexus mim4u.org www.mim4u.org secure.mim4u.org training.mim4u.org rpc-http-pub.d-bis.org rpc.d-bis.org rpc2.d-bis.org rpc.public-0138.defi-oracle.io rpc.defi-oracle.io ws.rpc.d-bis.org ws.rpc2.d-bis.org"
_PRIV_OPTIONAL_WHEN_FAIL="rpc-http-prv.d-bis.org rpc-ws-prv.d-bis.org rpc-fireblocks.d-bis.org ws.rpc-fireblocks.d-bis.org"
if [[ -z "${E2E_OPTIONAL_WHEN_FAIL:-}" ]]; then
if [[ "$PROFILE" == "private" ]]; then
E2E_OPTIONAL_WHEN_FAIL="$_PRIV_OPTIONAL_WHEN_FAIL"
elif [[ "$PROFILE" == "all" ]]; then
E2E_OPTIONAL_WHEN_FAIL="$_PRIV_OPTIONAL_WHEN_FAIL $_PUB_OPTIONAL_WHEN_FAIL"
else
E2E_OPTIONAL_WHEN_FAIL="$_PUB_OPTIONAL_WHEN_FAIL"
fi
else
E2E_OPTIONAL_WHEN_FAIL="${E2E_OPTIONAL_WHEN_FAIL}"
fi
# Per-domain expected DNS IP (optional). Unset = use PUBLIC_IP.
declare -A EXPECTED_IP=(
["gitea.d-bis.org"]="$PUBLIC_IP_FOURTH"
["dev.d-bis.org"]="$PUBLIC_IP_FOURTH"
["codespaces.d-bis.org"]="$PUBLIC_IP_FOURTH"
)
# HTTPS check path (default "/"). API-first hosts may 404 on /; see docs/02-architecture/EXPECTED_WEB_CONTENT.md
declare -A E2E_HTTPS_PATH=(
["phoenix.sankofa.nexus"]="/health"
["www.phoenix.sankofa.nexus"]="/health"
["studio.sankofa.nexus"]="/studio/"
)
# Expected apex URL for NPM www → canonical 301/308 (Location must use this host; path from E2E_HTTPS_PATH must appear when set)
declare -A E2E_WWW_CANONICAL_BASE=(
["www.sankofa.nexus"]="https://sankofa.nexus"
["www.phoenix.sankofa.nexus"]="https://phoenix.sankofa.nexus"
["www.the-order.sankofa.nexus"]="https://the-order.sankofa.nexus"
)
# Returns 0 if Location URL matches expected canonical apex (and HTTPS path suffix when non-empty).
e2e_www_redirect_location_ok() {
local loc_val="$1" base="$2" path="${3:-}"
local loc_lc base_lc
loc_lc=$(printf '%s' "$loc_val" | tr '[:upper:]' '[:lower:]')
base_lc=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
if [[ "$loc_lc" != "$base_lc" && "$loc_lc" != "$base_lc/"* ]]; then
return 1
fi
if [ -n "$path" ] && [ "$path" != "/" ]; then
local p_lc
p_lc=$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')
[[ "$loc_lc" == *"$p_lc"* ]] || return 1
fi
return 0
}
# --list-endpoints: print selected profile endpoints and exit (no tests)
if [[ "$LIST_ENDPOINTS" == "1" ]]; then
echo ""
echo "E2E endpoints (${#DOMAIN_TYPES[@]} total, profile: $PROFILE) — verify-end-to-end-routing.sh"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
printf "%-40s %-12s %s\n" "Domain" "Type" "URL"
printf "%-40s %-12s %s\n" "------" "----" "---"
for domain in $(echo "${!DOMAIN_TYPES[@]}" | tr ' ' '\n' | sort); do
dtype="${DOMAIN_TYPES[$domain]:-unknown}"
if [[ "$dtype" == "rpc-http" || "$dtype" == "rpc-ws" ]]; then
url="https://$domain (RPC)"
else
url="https://$domain"
fi
printf "%-40s %-12s %s\n" "$domain" "$dtype" "$url"
done
echo ""
exit 0
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 End-to-End Routing Verification"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Profile: $PROFILE"
echo ""
E2E_RESULTS=()
test_domain() {
local domain=$1
local domain_type="${DOMAIN_TYPES[$domain]:-unknown}"
log_info ""
log_info "Testing domain: $domain (type: $domain_type)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
local result=$(echo "{}" | jq ".domain = \"$domain\" | .domain_type = \"$domain_type\" | .timestamp = \"$(date -Iseconds)\" | .tests = {}")
# Test 1: DNS Resolution
log_info "Test 1: DNS Resolution"
if [ "${E2E_USE_SYSTEM_RESOLVER:-0}" = "1" ]; then
dns_result=$(getent hosts "$domain" 2>/dev/null | awk '{print $1}' | head -1 || echo "")
else
dns_result=$(dig +short "$domain" @8.8.8.8 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "")
fi
expected_ip="${EXPECTED_IP[$domain]:-$PUBLIC_IP}"
if [ "$dns_result" = "$expected_ip" ]; then
log_success "DNS: $domain$dns_result (correct)"
result=$(echo "$result" | jq ".tests.dns = {\"status\": \"pass\", \"resolved_ip\": \"$dns_result\", \"expected_ip\": \"$expected_ip\"}")
elif [ -n "$dns_result" ] && [ "${ACCEPT_ANY_DNS}" = "1" ]; then
log_success "DNS: $domain$dns_result (accepted, ACCEPT_ANY_DNS=1)"
result=$(echo "$result" | jq ".tests.dns = {\"status\": \"pass\", \"resolved_ip\": \"$dns_result\", \"expected_ip\": \"any\"}")
elif [ -n "$dns_result" ]; then
log_error "DNS: $domain$dns_result (expected $expected_ip)"
result=$(echo "$result" | jq ".tests.dns = {\"status\": \"fail\", \"resolved_ip\": \"$dns_result\", \"expected_ip\": \"$expected_ip\"}")
else
# Optional domain with no DNS yet (e.g. dapp.d-bis.org before CNAME added) → skip, don't fail
if echo " $E2E_OPTIONAL_DOMAINS " | grep -qF " $domain "; then
log_info "DNS: $domain → No resolution (optional, skipping)"
result=$(echo "$result" | jq ".tests.dns = {\"status\": \"skip\", \"resolved_ip\": null, \"expected_ip\": \"$expected_ip\", \"reason\": \"optional not configured\"}")
result=$(echo "$result" | jq ".tests.ssl = {\"status\": \"skip\"}")
result=$(echo "$result" | jq ".tests.https = {\"status\": \"skip\"}")
result=$(echo "$result" | jq ".tests.rpc_http = {\"status\": \"skip\"}")
echo "$result"
return 0
fi
log_error "DNS: $domain → No resolution"
result=$(echo "$result" | jq ".tests.dns = {\"status\": \"fail\", \"resolved_ip\": null, \"expected_ip\": \"$expected_ip\"}")
fi
# Test 2: SSL Certificate
if [ "$domain_type" != "unknown" ]; then
log_info "Test 2: SSL Certificate"
cert_info=$( (echo | timeout "$E2E_OPENSSL_TIMEOUT" openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null) | timeout "$E2E_OPENSSL_X509_TIMEOUT" openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || echo "")
if [ -n "$cert_info" ]; then
cert_cn=$(echo "$cert_info" | grep "subject=" | sed -E 's/.*CN\s*=\s*([^,]*).*/\1/' | sed 's/^ *//;s/ *$//' || echo "")
cert_issuer=$(echo "$cert_info" | grep "issuer=" | sed -E 's/.*CN\s*=\s*([^,]*).*/\1/' | sed 's/^ *//;s/ *$//' || echo "")
cert_expires=$(echo "$cert_info" | grep "notAfter=" | cut -d= -f2 || echo "")
cert_san=$(echo "$cert_info" | grep -A1 "subjectAltName" | tail -1 || echo "")
cert_matches=0
if echo "$cert_san" | grep -qF "$domain"; then cert_matches=1; fi
if [ "$cert_cn" = "$domain" ]; then cert_matches=1; fi
if [ $cert_matches -eq 0 ] && [ -n "$cert_san" ]; then
san_line=$(echo "$cert_san" | sed 's/.*subjectAltName\s*=\s*//i')
while IFS= read -r part; do
dns_name=$(echo "$part" | sed -E 's/^DNS\s*:\s*//i' | sed 's/^ *//;s/ *$//')
if [[ -n "$dns_name" && "$dns_name" == \*.* ]]; then
suffix="${dns_name#\*}"
if [ "$domain" = "$suffix" ] || [[ "$domain" == *"$suffix" ]]; then
cert_matches=1
break
fi
fi
done < <(echo "$san_line" | tr ',' '\n')
fi
if [ $cert_matches -eq 1 ]; then
log_success "SSL: Valid certificate for $domain"
log_info " Issuer: $cert_issuer"
log_info " Expires: $cert_expires"
result=$(echo "$result" | jq ".tests.ssl = {\"status\": \"pass\", \"cn\": \"$cert_cn\", \"issuer\": \"$cert_issuer\", \"expires\": \"$cert_expires\"}")
else
# Shared/default cert (e.g. unifi.local) used for multiple hostnames - treat as pass to avoid noise
log_success "SSL: Valid certificate (shared CN: $cert_cn)"
log_info " Issuer: $cert_issuer | Expires: $cert_expires"
result=$(echo "$result" | jq ".tests.ssl = {\"status\": \"pass\", \"cn\": \"$cert_cn\", \"issuer\": \"$cert_issuer\", \"expires\": \"$cert_expires\"}")
fi
else
log_error "SSL: Failed to retrieve certificate"
result=$(echo "$result" | jq ".tests.ssl = {\"status\": \"fail\"}")
fi
fi
# Test 3: HTTPS Request
if [ "$domain_type" = "web" ] || [ "$domain_type" = "api" ]; then
https_path="${E2E_HTTPS_PATH[$domain]:-}"
https_url="https://${domain}${https_path}"
log_info "Test 3: HTTPS Request (${https_url})"
START_TIME=$(date +%s.%N)
http_response=$(curl -s -I -k --connect-timeout 10 -w "\n%{time_total}" "$https_url" 2>&1 || echo "")
END_TIME=$(date +%s.%N)
RESPONSE_TIME=$(echo "$END_TIME - $START_TIME" | bc 2>/dev/null || echo "0")
http_code=$(echo "$http_response" | head -1 | grep -oP '\d{3}' | head -1 || echo "")
time_total=$(echo "$http_response" | tail -1 | grep -E '^[0-9.]+$' || echo "0")
headers=$(echo "$http_response" | head -20)
echo "$headers" > "$OUTPUT_DIR/${domain//./_}_https_headers.txt"
if [ -n "$http_code" ]; then
# NPM canonical www → apex (advanced_config return 301/308)
local _e2e_canonical_www_redirect=""
local location_hdr=""
case "$domain" in
www.sankofa.nexus|www.phoenix.sankofa.nexus|www.the-order.sankofa.nexus)
if [ "$http_code" = "301" ] || [ "$http_code" = "308" ]; then
_e2e_canonical_www_redirect=1
fi
;;
esac
if [ -n "$_e2e_canonical_www_redirect" ]; then
location_hdr=$(echo "$headers" | grep -iE '^[Ll]ocation:' | head -1 | tr -d '\r' || echo "")
loc_val=$(printf '%s' "$location_hdr" | sed -E 's/^[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[[:space:]]*//' | sed 's/[[:space:]]*$//')
expected_base="${E2E_WWW_CANONICAL_BASE[$domain]:-}"
if [ -z "$loc_val" ]; then
log_warn "HTTPS: $domain returned HTTP $http_code but no Location header${https_path:+ (${https_url})}"
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" \
'.tests.https = {"status": "warn", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber), "note": "missing Location on redirect"}')
elif [ -z "$expected_base" ]; then
log_warn "HTTPS: $domain redirect pass (no E2E_WWW_CANONICAL_BASE entry)"
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" --arg loc "$location_hdr" \
'.tests.https = {"status": "pass", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber), "canonical_redirect": true, "location_header": $loc}')
elif ! e2e_www_redirect_location_ok "$loc_val" "$expected_base" "$https_path"; then
log_error "HTTPS: $domain Location mismatch (got \"$loc_val\", expected prefix \"$expected_base\" with path \"${https_path:-/}\")"
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" --arg loc "$loc_val" --arg exp "$expected_base" --arg pth "${https_path:-}" \
'.tests.https = {"status": "fail", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber), "reason": "location_mismatch", "location": $loc, "expected_prefix": $exp, "expected_path_suffix": $pth}')
else
log_success "HTTPS: $domain returned HTTP $http_code (canonical redirect → $loc_val)${https_path:+ at ${https_url}}"
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" --arg loc "$location_hdr" \
'.tests.https = {"status": "pass", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber), "canonical_redirect": true, "location_header": $loc}')
fi
elif [ "$http_code" -ge 200 ] && [ "$http_code" -lt 400 ]; then
log_success "HTTPS: $domain returned HTTP $http_code (Time: ${time_total}s)${https_path:+ at ${https_path}}"
# Check security headers
hsts=$(echo "$headers" | grep -i "strict-transport-security" || echo "")
csp=$(echo "$headers" | grep -i "content-security-policy" || echo "")
xfo=$(echo "$headers" | grep -i "x-frame-options" || echo "")
HAS_HSTS=$([ -n "$hsts" ] && echo "true" || echo "false")
HAS_CSP=$([ -n "$csp" ] && echo "true" || echo "false")
HAS_XFO=$([ -n "$xfo" ] && echo "true" || echo "false")
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" \
--argjson hsts "$HAS_HSTS" --argjson csp "$HAS_CSP" --argjson xfo "$HAS_XFO" \
'.tests.https = {"status": "pass", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber), "has_hsts": $hsts, "has_csp": $csp, "has_xfo": $xfo}')
else
log_warn "HTTPS: $domain returned HTTP $http_code (Time: ${time_total}s)${https_path:+ (${https_url})}"
result=$(echo "$result" | jq --arg code "$http_code" --arg time "$time_total" \
'.tests.https = {"status": "warn", "http_code": ($code | tonumber), "response_time_seconds": ($time | tonumber)}')
fi
else
log_error "HTTPS: Failed to connect to ${https_url}"
result=$(echo "$result" | jq --arg time "$time_total" '.tests.https = {"status": "fail", "response_time_seconds": ($time | tonumber)}')
fi
# Optional: Blockscout API check for explorer.d-bis.org (does not affect E2E pass/fail)
if [ "$domain" = "explorer.d-bis.org" ] && [ "${SKIP_BLOCKSCOUT_API:-0}" != "1" ]; then
log_info "Test 3b: Blockscout API (optional)"
api_body_file="$OUTPUT_DIR/explorer_d-bis_org_blockscout_api.txt"
api_code=$(curl -s -o "$api_body_file" -w "%{http_code}" -k --connect-timeout 10 "https://$domain/api/v2/stats" 2>/dev/null || echo "000")
if [ "$api_code" = "200" ] && [ -s "$api_body_file" ] && (grep -qE '"total_blocks"|"total_transactions"' "$api_body_file" 2>/dev/null); then
log_success "Blockscout API: /api/v2/stats returned 200 with stats"
result=$(echo "$result" | jq '.tests.blockscout_api = {"status": "pass", "http_code": 200}')
else
log_warn "Blockscout API: HTTP $api_code or invalid response (optional; run from LAN if backend unreachable)"
result=$(echo "$result" | jq --arg code "$api_code" '.tests.blockscout_api = {"status": "skip", "http_code": $code}')
fi
fi
fi
# Test 4: RPC HTTP Request
if [ "$domain_type" = "rpc-http" ]; then
log_info "Test 4: RPC HTTP Request"
rpc_body_file="$OUTPUT_DIR/${domain//./_}_rpc_response.txt"
rpc_http_code=$(curl -s -X POST "https://$domain" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
--connect-timeout 10 -k -w "%{http_code}" -o "$rpc_body_file" 2>/dev/null || echo "000")
rpc_response=$(cat "$rpc_body_file" 2>/dev/null || echo "")
if echo "$rpc_response" | grep -q "\"result\""; then
chain_id=$(echo "$rpc_response" | jq -r '.result' 2>/dev/null || echo "")
log_success "RPC: $domain responded with chainId: $chain_id"
result=$(echo "$result" | jq --arg chain "$chain_id" '.tests.rpc_http = {"status": "pass", "chain_id": $chain}')
else
# Capture error for troubleshooting (typically 405 from edge when POST is blocked)
rpc_error=$(echo "$rpc_response" | head -c 200 | jq -c '.error // .' 2>/dev/null || echo "$rpc_response" | head -c 120)
log_error "RPC: $domain failed (HTTP $rpc_http_code)"
result=$(echo "$result" | jq --arg code "$rpc_http_code" --arg err "${rpc_error:-}" '.tests.rpc_http = {"status": "fail", "http_code": $code, "error": $err}')
fi
fi
# Test 5: WebSocket Connection (for RPC WebSocket domains)
if [ "$domain_type" = "rpc-ws" ]; then
log_info "Test 5: WebSocket Connection"
# Try basic WebSocket upgrade test
WS_START_TIME=$(date +%s.%N)
WS_RESULT=$(timeout 5 curl -k -s -o /dev/null -w "%{http_code}" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(echo -n 'test' | base64)" \
"https://$domain" 2>&1 || echo "000")
WS_END_TIME=$(date +%s.%N)
WS_TIME=$(echo "$WS_END_TIME - $WS_START_TIME" | bc 2>/dev/null || echo "0")
if [ "$WS_RESULT" = "101" ]; then
log_success "WebSocket: Upgrade successful (Code: $WS_RESULT, Time: ${WS_TIME}s)"
result=$(echo "$result" | jq --arg code "$WS_RESULT" --arg time "$WS_TIME" '.tests.websocket = {"status": "pass", "http_code": $code, "response_time_seconds": ($time | tonumber)}')
elif [ "$WS_RESULT" = "200" ] || [ "$WS_RESULT" = "426" ]; then
log_warn "WebSocket: Partial support (Code: $WS_RESULT - may require proper handshake)"
result=$(echo "$result" | jq --arg code "$WS_RESULT" --arg time "$WS_TIME" '.tests.websocket = {"status": "warning", "http_code": $code, "response_time_seconds": ($time | tonumber), "note": "Requires full WebSocket handshake for complete test"}')
else
# Check if wscat is available for full test
if command -v wscat >/dev/null 2>&1; then
log_info " Attempting full WebSocket test with wscat..."
# -n: no TLS verify (aligns with curl -k); -w: seconds to wait for JSON-RPC response
WS_FULL_TEST=""
WS_FULL_EXIT=0
if ! WS_FULL_TEST=$(timeout 15 wscat -n -c "wss://$domain" -x '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' -w 5 2>&1); then
WS_FULL_EXIT=$?
fi
if echo "$WS_FULL_TEST" | grep -q "result"; then
log_success "WebSocket: Full test passed"
result=$(echo "$result" | jq --arg code "$WS_RESULT" '.tests.websocket = {"status": "pass", "http_code": $code, "full_test": true, "full_test_output": "result"}')
elif [ "$WS_FULL_EXIT" -eq 0 ]; then
log_success "WebSocket: Full test connected cleanly"
result=$(echo "$result" | jq --arg code "$WS_RESULT" '.tests.websocket = {"status": "pass", "http_code": $code, "full_test": true, "note": "wscat exited successfully without printable RPC output"}')
else
log_warn "WebSocket: Connection established but RPC test failed"
result=$(echo "$result" | jq --arg code "$WS_RESULT" --arg exit_code "$WS_FULL_EXIT" '.tests.websocket = {"status": "warning", "http_code": $code, "full_test": false, "exit_code": $exit_code}')
fi
else
log_warn "WebSocket: Basic test (Code: $WS_RESULT) - Install wscat for full test: npm install -g wscat"
result=$(echo "$result" | jq --arg code "$WS_RESULT" --arg time "$WS_TIME" '.tests.websocket = {"status": "warning", "http_code": $code, "response_time_seconds": ($time | tonumber), "note": "Basic upgrade test only - install wscat for full WebSocket RPC test"}')
fi
fi
fi
# Test 6: Internal connectivity from NPMplus (requires NPMplus container access)
log_info "Test 6: Internal connectivity (documented in report)"
# Optional-when-fail: treat any fail as skip so run passes when off-LAN or service unreachable
if [ -n "$E2E_OPTIONAL_WHEN_FAIL" ] && echo " $E2E_OPTIONAL_WHEN_FAIL " | grep -qF " $domain "; then
result=$(echo "$result" | jq '
(if .tests.dns and (.tests.dns.status == "fail") then .tests.dns.status = "skip" else . end) |
(if .tests.ssl and (.tests.ssl.status == "fail") then .tests.ssl.status = "skip" else . end) |
(if .tests.https and (.tests.https.status == "fail") then .tests.https.status = "skip" else . end) |
(if .tests.rpc_http and (.tests.rpc_http.status == "fail") then .tests.rpc_http.status = "skip" else . end)
')
fi
echo "$result"
}
# Run tests for all domains (with progress)
TOTAL_DOMAINS=${#DOMAIN_TYPES[@]}
CURRENT=0
for domain in "${!DOMAIN_TYPES[@]}"; do
CURRENT=$((CURRENT + 1))
log_info "Progress: domain $CURRENT/$TOTAL_DOMAINS"
result=$(test_domain "$domain")
if [ -n "$result" ]; then
E2E_RESULTS+=("$result")
fi
done
# Combine all results (one JSON object per line for robustness)
printf '%s\n' "${E2E_RESULTS[@]}" | jq -s '.' > "$OUTPUT_DIR/all_e2e_results.json" 2>/dev/null || {
log_warn "jq merge failed; writing raw results"
printf '%s\n' "${E2E_RESULTS[@]}" > "$OUTPUT_DIR/all_e2e_results_raw.json"
}
# Generate summary report with statistics
TOTAL_TESTS=${#DOMAIN_TYPES[@]}
PASSED_DNS=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.dns.status == "pass")] | length' 2>/dev/null || echo "0")
PASSED_HTTPS=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.https.status == "pass")] | length' 2>/dev/null || echo "0")
FAILED_TESTS=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.dns.status == "fail" or .tests.https.status == "fail" or .tests.rpc_http.status == "fail")] | length' 2>/dev/null || echo "0")
FAILED_DNS=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.dns.status == "fail")] | length' 2>/dev/null || echo "0")
FAILED_HTTPS=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.https.status == "fail")] | length' 2>/dev/null || echo "0")
FAILED_RPC=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.rpc_http.status == "fail")] | length' 2>/dev/null || echo "0")
SKIPPED_OPTIONAL=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | select(.tests.dns.status == "skip" or .tests.ssl.status == "skip" or .tests.https.status == "skip" or .tests.rpc_http.status == "skip")] | length' 2>/dev/null || echo "0")
# When only RPC fails (edge blocks POST), treat as success if env set
E2E_SUCCESS_IF_ONLY_RPC_BLOCKED="${E2E_SUCCESS_IF_ONLY_RPC_BLOCKED:-0}"
ONLY_RPC_FAILED=0
[ "$FAILED_DNS" = "0" ] && [ "$FAILED_HTTPS" = "0" ] && [ "$FAILED_RPC" -gt 0 ] && [ "$FAILED_TESTS" = "$FAILED_RPC" ] && ONLY_RPC_FAILED=1
# Calculate average response time
AVG_RESPONSE_TIME=$(echo "${E2E_RESULTS[@]}" | jq -s '[.[] | .tests.https.response_time_seconds // empty] | add / length' 2>/dev/null || echo "0")
REPORT_FILE="$OUTPUT_DIR/verification_report.md"
cat > "$REPORT_FILE" <<EOF
# End-to-End Routing Verification Report
**Date**: $(date -Iseconds)
**Public IP**: $PUBLIC_IP
**Profile**: $PROFILE
**Verifier**: $(whoami)
## All endpoints ($TOTAL_TESTS)
| Domain | Type | URL |
|--------|------|-----|
EOF
for domain in $(echo "${!DOMAIN_TYPES[@]}" | tr ' ' '\n' | sort); do
dtype="${DOMAIN_TYPES[$domain]:-unknown}"
echo "| $domain | $dtype | https://$domain |" >> "$REPORT_FILE"
done
cat >> "$REPORT_FILE" <<EOF
## Summary
- **Total domains tested**: $TOTAL_TESTS
- **DNS tests passed**: $PASSED_DNS
- **HTTPS tests passed**: $PASSED_HTTPS
- **Failed tests**: $FAILED_TESTS
- **Skipped / optional (not configured or unreachable)**: $SKIPPED_OPTIONAL
- **Average response time**: ${AVG_RESPONSE_TIME}s
## Results overview
| Domain | Type | DNS | SSL | HTTPS | RPC |
|--------|------|-----|-----|-------|-----|
EOF
for result in "${E2E_RESULTS[@]}"; do
domain=$(echo "$result" | jq -r '.domain' 2>/dev/null || echo "")
domain_type=$(echo "$result" | jq -r '.domain_type' 2>/dev/null || echo "")
dns_status=$(echo "$result" | jq -r '.tests.dns.status // "-"' 2>/dev/null || echo "-")
ssl_status=$(echo "$result" | jq -r '.tests.ssl.status // "-"' 2>/dev/null || echo "-")
https_status=$(echo "$result" | jq -r '.tests.https.status // "-"' 2>/dev/null || echo "-")
rpc_status=$(echo "$result" | jq -r '.tests.rpc_http.status // "-"' 2>/dev/null || echo "-")
echo "| $domain | $domain_type | $dns_status | $ssl_status | $https_status | $rpc_status |" >> "$REPORT_FILE"
done
cat >> "$REPORT_FILE" <<EOF
## Test Results by Domain (detail)
EOF
for result in "${E2E_RESULTS[@]}"; do
domain=$(echo "$result" | jq -r '.domain' 2>/dev/null || echo "")
domain_type=$(echo "$result" | jq -r '.domain_type' 2>/dev/null || echo "")
dns_status=$(echo "$result" | jq -r '.tests.dns.status // "unknown"' 2>/dev/null || echo "unknown")
ssl_status=$(echo "$result" | jq -r '.tests.ssl.status // "unknown"' 2>/dev/null || echo "unknown")
https_status=$(echo "$result" | jq -r '.tests.https.status // "unknown"' 2>/dev/null || echo "unknown")
rpc_status=$(echo "$result" | jq -r '.tests.rpc_http.status // "unknown"' 2>/dev/null || echo "unknown")
blockscout_api_status=$(echo "$result" | jq -r '.tests.blockscout_api.status // "unknown"' 2>/dev/null || echo "unknown")
echo "" >> "$REPORT_FILE"
echo "### $domain" >> "$REPORT_FILE"
echo "- Type: $domain_type" >> "$REPORT_FILE"
echo "- DNS: $dns_status" >> "$REPORT_FILE"
echo "- SSL: $ssl_status" >> "$REPORT_FILE"
if [ "$https_status" != "unknown" ]; then
echo "- HTTPS: $https_status" >> "$REPORT_FILE"
fi
if [ "$blockscout_api_status" != "unknown" ]; then
echo "- Blockscout API: $blockscout_api_status" >> "$REPORT_FILE"
fi
if [ "$rpc_status" != "unknown" ]; then
echo "- RPC: $rpc_status" >> "$REPORT_FILE"
fi
echo "- Details: See \`all_e2e_results.json\`" >> "$REPORT_FILE"
done
cat >> "$REPORT_FILE" <<EOF
## Files Generated
- \`all_e2e_results.json\` - Complete E2E test results
- \`*_https_headers.txt\` - HTTP response headers per domain
- \`*_rpc_response.txt\` - RPC response per domain
- \`verification_report.md\` - This report
## Notes
- **Optional domains:** Domains in \`E2E_OPTIONAL_WHEN_FAIL\` (default: many d-bis.org/sankofa/mim4u/rpc) have any fail treated as skip so the run passes when off-LAN or services unreachable. Set \`E2E_OPTIONAL_WHEN_FAIL=\` (empty) for strict mode.
- WebSocket tests require \`wscat\` tool: \`npm install -g wscat\`
- OpenSSL fetch uses \`timeout\` (\`E2E_OPENSSL_TIMEOUT\` / \`E2E_OPENSSL_X509_TIMEOUT\`, defaults 15s / 5s) so \`openssl s_client\` cannot hang indefinitely
- Internal connectivity tests require access to NPMplus container
- Explorer (explorer.d-bis.org): optional Blockscout API check; use \`SKIP_BLOCKSCOUT_API=1\` to skip when backend is unreachable (e.g. off-LAN). Fix runbook: docs/03-deployment/BLOCKSCOUT_FIX_RUNBOOK.md
## Next Steps
1. Review test results for each domain
2. Investigate any failed tests
3. Test WebSocket connections for RPC WS domains (if wscat available)
4. Test internal connectivity from NPMplus container
5. Update source-of-truth JSON after verification
EOF
log_info ""
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "📊 Verification Summary"
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "Total domains: $TOTAL_TESTS"
log_success "DNS passed: $PASSED_DNS"
log_success "HTTPS passed: $PASSED_HTTPS"
[ "$SKIPPED_OPTIONAL" -gt 0 ] && log_info "Skipped / optional: $SKIPPED_OPTIONAL"
if [ "$FAILED_TESTS" -gt 0 ]; then
log_error "Failed: $FAILED_TESTS"
if [ "$ONLY_RPC_FAILED" = "1" ]; then
log_info "All failures are RPC (edge may block POST). For full RPC pass see docs/05-network/E2E_RPC_EDGE_LIMITATION.md"
if [ "${E2E_SUCCESS_IF_ONLY_RPC_BLOCKED:-0}" = "1" ]; then
log_success "E2E success (DNS + HTTPS pass; RPC blocked by edge - expected until UDM Pro allows POST or Tunnel used)"
fi
fi
else
log_success "Failed: $FAILED_TESTS"
fi
if [ -n "$AVG_RESPONSE_TIME" ] && [ "$AVG_RESPONSE_TIME" != "0" ] && [ "$AVG_RESPONSE_TIME" != "null" ]; then
log_info "Average response time: ${AVG_RESPONSE_TIME}s"
fi
echo ""
log_success "Verification complete!"
log_success "Report: $REPORT_FILE"
log_success "All results: $OUTPUT_DIR/all_e2e_results.json"
# Exit 0 when only RPC failed and E2E_SUCCESS_IF_ONLY_RPC_BLOCKED=1 (so CI/scripts can treat as success)
if [ "$FAILED_TESTS" -gt 0 ] && [ "$ONLY_RPC_FAILED" = "1" ] && [ "${E2E_SUCCESS_IF_ONLY_RPC_BLOCKED:-0}" = "1" ]; then
exit 0
fi
# Exit 0 when 502s only and E2E_ACCEPT_502_INTERNAL=1 (backends may be down or unreachable from test host; run scripts/maintenance/start-stopped-containers-via-ssh.sh and ensure-dbis-services-via-ssh.sh from LAN)
if [ "$FAILED_TESTS" -gt 0 ] && [ "${E2E_ACCEPT_502_INTERNAL:-0}" = "1" ]; then
log_info "E2E_ACCEPT_502_INTERNAL=1: treating as success; fix 502s from LAN with start-stopped-containers-via-ssh.sh and ensure-dbis-services-via-ssh.sh"
exit 0
fi
if [ "$FAILED_TESTS" -gt 0 ]; then
exit 1
fi