181 lines
5.1 KiB
Bash
Executable File
181 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CLIENT_HOST="${CLIENT_HOST:-k_client}"
|
|
CA_FILE="${CA_FILE:-/home/user/chromecard/tls/phase2/ca.crt}"
|
|
PROXY_URL="${PROXY_URL:-https://127.0.0.1:9771}"
|
|
USERNAME="${USERNAME:-alice}"
|
|
REQUESTS="${REQUESTS:-20}"
|
|
PARALLELISM="${PARALLELISM:-8}"
|
|
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-8}"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: phase5_chain_regression.sh [options]
|
|
|
|
Runs the Phase 5 split-VM regression from the host by executing the client-side
|
|
flow inside k_client over SSH.
|
|
|
|
Options:
|
|
--client-host HOST SSH host alias for k_client (default: k_client)
|
|
--ca-file PATH CA bundle path inside k_client
|
|
--proxy-url URL Proxy URL visible from k_client
|
|
--username NAME Username for session login
|
|
--requests N Number of counter requests to issue
|
|
--parallelism N Number of concurrent workers
|
|
--connect-timeout SEC SSH connect timeout
|
|
-h, --help Show this help text
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--client-host)
|
|
CLIENT_HOST="$2"
|
|
shift 2
|
|
;;
|
|
--ca-file)
|
|
CA_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--proxy-url)
|
|
PROXY_URL="$2"
|
|
shift 2
|
|
;;
|
|
--username)
|
|
USERNAME="$2"
|
|
shift 2
|
|
;;
|
|
--requests)
|
|
REQUESTS="$2"
|
|
shift 2
|
|
;;
|
|
--parallelism)
|
|
PARALLELISM="$2"
|
|
shift 2
|
|
;;
|
|
--connect-timeout)
|
|
CONNECT_TIMEOUT="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
ssh \
|
|
-o BatchMode=yes \
|
|
-o StrictHostKeyChecking=accept-new \
|
|
-o ConnectTimeout="${CONNECT_TIMEOUT}" \
|
|
"${CLIENT_HOST}" \
|
|
env \
|
|
CA_FILE="${CA_FILE}" \
|
|
PROXY_URL="${PROXY_URL}" \
|
|
USERNAME="${USERNAME}" \
|
|
REQUESTS="${REQUESTS}" \
|
|
PARALLELISM="${PARALLELISM}" \
|
|
python3 - <<'PY'
|
|
import concurrent.futures
|
|
import json
|
|
import os
|
|
import ssl
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
ca_file = os.environ["CA_FILE"]
|
|
proxy_url = os.environ["PROXY_URL"].rstrip("/")
|
|
username = os.environ["USERNAME"]
|
|
requests = int(os.environ["REQUESTS"])
|
|
parallelism = int(os.environ["PARALLELISM"])
|
|
|
|
if requests < 1:
|
|
raise SystemExit("REQUESTS must be >= 1")
|
|
if parallelism < 1:
|
|
raise SystemExit("PARALLELISM must be >= 1")
|
|
|
|
ctx = ssl.create_default_context(cafile=ca_file)
|
|
|
|
def post_json(path: str, payload: dict | None = None, token: str | None = None):
|
|
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
|
headers = {}
|
|
if payload is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
req = urllib.request.Request(
|
|
f"{proxy_url}{path}",
|
|
data=data,
|
|
headers=headers,
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
|
|
return resp.status, json.loads(resp.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8")
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
payload = {"ok": False, "error": body}
|
|
return exc.code, payload
|
|
|
|
status, login = post_json("/session/login", {"username": username})
|
|
if status != 200 or "session_token" not in login:
|
|
print(json.dumps({"ok": False, "stage": "login", "status": status, "response": login}))
|
|
raise SystemExit(1)
|
|
|
|
token = login["session_token"]
|
|
values = []
|
|
|
|
def fetch_one(_: int) -> int:
|
|
status, payload = post_json("/resource/counter", {}, token=token)
|
|
if status != 200:
|
|
raise RuntimeError(json.dumps({"status": status, "response": payload}))
|
|
return int(payload["upstream"]["value"])
|
|
|
|
try:
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=parallelism) as pool:
|
|
for value in pool.map(fetch_one, range(requests)):
|
|
values.append(value)
|
|
|
|
status_resp, session = post_json("/session/status", {}, token=token)
|
|
logout_status, logout = post_json("/session/logout", {}, token=token)
|
|
invalid_status, invalid = post_json("/resource/counter", {}, token=token)
|
|
except Exception as exc:
|
|
try:
|
|
post_json("/session/logout", {}, token=token)
|
|
finally:
|
|
raise SystemExit(str(exc))
|
|
|
|
sorted_values = sorted(values)
|
|
expected = list(range(sorted_values[0], sorted_values[-1] + 1)) if sorted_values else []
|
|
|
|
summary = {
|
|
"ok": True,
|
|
"username": username,
|
|
"proxy_url": proxy_url,
|
|
"requests": requests,
|
|
"parallelism": parallelism,
|
|
"unique": len(set(values)) == len(values),
|
|
"gap_free": sorted_values == expected,
|
|
"min": min(sorted_values) if sorted_values else None,
|
|
"max": max(sorted_values) if sorted_values else None,
|
|
"values": sorted_values,
|
|
"login": login,
|
|
"session_status": {"status": status_resp, "response": session},
|
|
"logout": {"status": logout_status, "response": logout},
|
|
"post_logout": {"status": invalid_status, "response": invalid},
|
|
}
|
|
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
if not summary["unique"] or not summary["gap_free"] or logout_status != 200 or invalid_status != 401:
|
|
raise SystemExit(1)
|
|
PY
|