#!/usr/bin/env bash # Shared helpers. Sourced by every demo script. AS=http://localhost:9000 RS=http://localhost:8090 # Kill by main class, never by a pattern that could match this script's own command line. # `pkill -f spring-boot` matches the shell running it and takes the shell with it. kill_app() { local mainclass="$1" port="${2:-}" for p in $(ps -eo pid,cmd | grep "[${mainclass:0:1}]${mainclass:1}" | awk '{print $1}'); do kill -9 "$p" 2>/dev/null || true done # Then wait for the port to actually close. `ss -lptn` often reports the socket with no # PID, so a port-based kill can silently do nothing while the old process keeps serving - # which looks exactly like your config change having had no effect. Waiting for the # listener to disappear is the only reliable signal that the restart is real. if [ -n "$port" ]; then for _ in $(seq 1 30); do curl -s -o /dev/null --max-time 1 "http://localhost:$port/" || return 0 sleep 1 done echo "WARNING: something is still listening on :$port after kill_app $mainclass" >&2 return 1 fi sleep 1 } wait_for() { local url="$1" tries="${2:-90}" for _ in $(seq 1 "$tries"); do if curl -s -o /dev/null --max-time 2 "$url"; then return 0; fi sleep 1 done echo "timed out waiting for $url - check the app log" >&2 return 1 } hr() { printf '%s\n' "------------------------------------------------------------------"; } section() { echo; hr; echo "== $*"; hr; } # Decode a JWS payload without verifying it. Debug only - never do this to decide anything. jwt_payload() { python3 - "$1" <<'PY' import base64, json, sys part = sys.argv[1].split('.')[1] part += '=' * (-len(part) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2, sort_keys=True)) PY } jwt_header() { python3 - "$1" <<'PY' import base64, json, sys part = sys.argv[1].split('.')[0] part += '=' * (-len(part) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2, sort_keys=True)) PY } # Pull a hidden input's value out of a page. Attribute order is not fixed - Spring # Security's default login page renders name before value, Thymeleaf renders value before # name - so a naive grep for name="x" value="y" works on one and silently returns empty on # the other. Empty CSRF token, HTTP 403, and an hour lost. form_value() { python3 - "$1" "$2" <<'PY' import re, sys html = open(sys.argv[1], encoding='utf-8', errors='replace').read() want = sys.argv[2] for tag in re.findall(r']*>', html, re.I): attrs = dict((m.group(1).lower(), m.group(2)) for m in re.finditer(r'([\w:-]+)\s*=\s*"([^"]*)"', tag)) if attrs.get('name') == want: print(attrs.get('value', '')) break PY } # Every value of a repeated input (the scope checkboxes on the consent page). form_values() { python3 - "$1" "$2" <<'PY' import re, sys html = open(sys.argv[1], encoding='utf-8', errors='replace').read() want = sys.argv[2] for tag in re.findall(r']*>', html, re.I): attrs = dict((m.group(1).lower(), m.group(2)) for m in re.finditer(r'([\w:-]+)\s*=\s*"([^"]*)"', tag)) if attrs.get('name') == want and 'disabled' not in tag.lower(): print(attrs.get('value', '')) PY }