133 lines
6.5 KiB
Bash
133 lines
6.5 KiB
Bash
#!/usr/bin/env bash
|
|
# Actual packaged HTTP lifecycle; synthetic-only isolated storage and identities.
|
|
set -euo pipefail
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
|
|
image=${PODSTEADR_FIXTURE_IMAGE:?Immutable local candidate image ID required}
|
|
[[ "$image" =~ ^(sha256:)?[a-f0-9]{64}$ ]] || exit 2
|
|
bootstrap=${PODSTEADR_KEY_BOOTSTRAP:-0}
|
|
[[ "$bootstrap" == 0 || "$bootstrap" == 1 ]] || exit 2
|
|
name="archy-podsteadr-contract-$$"
|
|
volume="$name-data"
|
|
work=$(mktemp -d)
|
|
cleanup() {
|
|
result=$?; trap - EXIT
|
|
for container in "$name" "$name-key" "$name-invalid" "$name-bootstrap"; do
|
|
if podman container exists "$container"; then
|
|
timeout 15 podman rm -f --time 0 "$container" >/dev/null 2>&1 || true
|
|
fi
|
|
if podman container exists "$container"; then result=1; fi
|
|
done
|
|
podman volume rm -f "$volume" >/dev/null 2>&1 || true
|
|
if podman volume exists "$volume"; then result=1; fi
|
|
rm -rf -- "$work"
|
|
exit "$result"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
[[ $(podman image inspect "$image" --format '{{.Config.User}}') == 1000:1000 ]]
|
|
common=(--pull=never --network=none --pid=private --ipc=private --http-proxy=false
|
|
--read-only --cap-drop=all --security-opt=no-new-privileges
|
|
--cpus=1 --memory=512m --memory-swap=512m --pids-limit=128 --timeout=180
|
|
--tmpfs /tmp:rw,notmpcopyup,nosuid,nodev,size=32m)
|
|
keys=$(podman run --rm --name "$name-key" "${common[@]}" --entrypoint node "$image" --input-type=module -e \
|
|
'import {getPublicKey} from "nostr-tools/pure";console.log([1,3].map(n=>getPublicKey(new Uint8Array(32).fill(n))).join(" "))')
|
|
read -r original replacement <<< "$keys"
|
|
[[ "$original" =~ ^[a-f0-9]{64}$ && "$replacement" =~ ^[a-f0-9]{64}$ ]]
|
|
podman volume create "$volume" >/dev/null
|
|
expected_server_key=''
|
|
if [[ "$bootstrap" == 1 ]]; then
|
|
# Only this owned synthetic volume is initialized; no live bind or secret export.
|
|
podman run --rm --name "$name-bootstrap" "${common[@]}" \
|
|
--volume "$volume:/data:U" --entrypoint node "$image" \
|
|
dist/provision-server-key.js /data > "$work/bootstrap.json"
|
|
podman run --rm --name "$name-bootstrap" "${common[@]}" \
|
|
--volume "$volume:/data:ro" --entrypoint node "$image" \
|
|
dist/provision-server-key.js /data > "$work/bootstrap-repeat.json"
|
|
cmp "$work/bootstrap.json" "$work/bootstrap-repeat.json"
|
|
expected_server_key=$(python3 - "$work/bootstrap.json" <<'PY'
|
|
import json,re,sys
|
|
value=json.load(open(sys.argv[1]))
|
|
assert set(value)=={'version','publicKey'} and value['version']==1
|
|
assert re.fullmatch('[a-f0-9]{64}',value['publicKey'])
|
|
print(value['publicKey'])
|
|
PY
|
|
)
|
|
fi
|
|
start() {
|
|
podman run -d --name "$name" "${common[@]}" \
|
|
--volume "$volume:/data:U" \
|
|
--volume "$repo/tests/apps/podsteadr/protocol.mjs:/app/qualification-protocol.mjs:ro" \
|
|
--env "PODSTEADR_ADMIN_PUBKEYS=$1" --env PUBLIC_URL=http://127.0.0.1:8095 \
|
|
--env NOSTR_RELAYS= --env CASHU_MINT_URL_DEFAULT=http://127.0.0.1:9996 \
|
|
--env MEDIAMTX_API_URL=http://127.0.0.1:9997 \
|
|
--env BLOSSOM_URL_DEFAULT=http://127.0.0.1:8098 "$image" >/dev/null
|
|
for _ in $(seq 1 30); do
|
|
if timeout 3 podman exec "$name" node -e 'fetch("http://127.0.0.1:8095/api/health").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))' >/dev/null 2>&1; then
|
|
podman healthcheck run "$name" >/dev/null
|
|
if [[ -n "$expected_server_key" ]]; then
|
|
podman exec --env "EXPECTED_KEY=$expected_server_key" "$name" node --input-type=module -e '
|
|
const response=await fetch("http://127.0.0.1:8095/api/settings/public");
|
|
if(!response.ok || (await response.json()).serverPubkey!==process.env.EXPECTED_KEY) process.exit(1);
|
|
'
|
|
fi
|
|
return
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
podman logs "$name" >&2
|
|
return 1
|
|
}
|
|
start "$original"
|
|
timeout 45 podman exec --env PODSTEADR_FIXTURE_PHASE=seed "$name" node /app/qualification-protocol.mjs
|
|
podman stop --time 5 "$name" >/dev/null
|
|
[[ $(podman inspect "$name" --format '{{.State.ExitCode}}') == 0 ]] || {
|
|
echo 'Packaged server did not close cleanly on SIGTERM' >&2; exit 1;
|
|
}
|
|
podman rm "$name" >/dev/null
|
|
start "$replacement"
|
|
timeout 45 podman exec --env PODSTEADR_FIXTURE_PHASE=verify-preserved "$name" node /app/qualification-protocol.mjs
|
|
# Also qualify abrupt process loss, without deleting or repairing the database.
|
|
podman kill --signal KILL "$name" >/dev/null
|
|
podman rm "$name" >/dev/null
|
|
start "$replacement"
|
|
# The final verify phase deliberately logs out the saved cookie. Defer that
|
|
# logout until after the additional missing-key/restoration check in this mode.
|
|
final_phase=verify
|
|
if [[ "$bootstrap" == 1 ]]; then final_phase=verify-preserved; fi
|
|
timeout 45 podman exec --env "PODSTEADR_FIXTURE_PHASE=$final_phase" "$name" node /app/qualification-protocol.mjs
|
|
if [[ "$bootstrap" == 1 ]]; then
|
|
podman stop --time 5 "$name" >/dev/null
|
|
[[ $(podman inspect "$name" --format '{{.State.ExitCode}}') == 0 ]]
|
|
podman rm "$name" >/dev/null
|
|
for phase in hold verify-restore; do
|
|
if [[ "$phase" == verify-restore ]]; then
|
|
if timeout 15 podman run --rm --name "$name-invalid" "${common[@]}" \
|
|
--volume "$volume:/data" --env "PODSTEADR_ADMIN_PUBKEYS=$replacement" \
|
|
--env PUBLIC_URL=http://127.0.0.1:8095 --env NOSTR_RELAYS= "$image" > "$work/key-loss.log" 2>&1; then
|
|
echo 'Missing established key unexpectedly started or regenerated' >&2; exit 1
|
|
else
|
|
status=$?
|
|
[[ "$status" == 1 ]] || { echo 'Missing-key check did not prove application refusal' >&2; exit 1; }
|
|
grep -q 'Server identity unavailable' "$work/key-loss.log"
|
|
fi
|
|
fi
|
|
podman run --rm --name "$name-bootstrap" "${common[@]}" \
|
|
--volume "$volume:/data" \
|
|
--volume "$repo/tests/apps/podsteadr/key-loss.mjs:/app/qualification-key-loss.mjs:ro" \
|
|
--entrypoint node "$image" /app/qualification-key-loss.mjs "$phase"
|
|
done
|
|
start "$replacement"
|
|
timeout 45 podman exec --env PODSTEADR_FIXTURE_PHASE=verify "$name" node /app/qualification-protocol.mjs
|
|
fi
|
|
# No default administrator binding may silently become first-login admin.
|
|
if timeout 15 podman run --rm --name "$name-invalid" "${common[@]}" "$image" >"$work/invalid.log" 2>&1; then
|
|
echo 'Missing identity binding unexpectedly started' >&2; exit 1
|
|
else
|
|
status=$?
|
|
[[ "$status" == 1 ]] || { echo 'Invalid-binding check did not prove application refusal' >&2; exit 1; }
|
|
grep -q 'PODSTEADR_ADMIN_PUBKEYS' "$work/invalid.log"
|
|
fi
|
|
if [[ "$bootstrap" == 1 ]]; then echo 'PASS key-only bootstrap, readonly repeat and same public identity across normal/crash recreation'; fi
|
|
echo 'PASS packaged HTTP recreation/preservation; not manager install/update/uninstall, actual media, financial or provenance acceptance.'
|