FELIS_VELOCITY_FORK_JAR replaces the proxy every player connects through, and the only thing checked about it was that the path pointed at a readable file. A truncated copy, a stale build left at the same path, or the two-patch jar where the three-patch one was meant all installed silently. It now requires FELIS_VELOCITY_FORK_JAR_SHA256 and refuses on a mismatch, hashing stdin rather than the path for the reason install_via_plugins already documents: sha256sum escapes its output line for a filename carrying a backslash or newline, and the leading "\" that adds fails every comparison. The absent-digest refusal prints the jar's actual hash, so the first run after a deliberate rebuild is one copy-paste rather than an investigation. The comparison ignores case and internal spaces. The fork is built on a developer machine, which is usually Windows, and nothing there prints a digest the way sha256sum does: Get-FileHash returns uppercase and certutil has shipped the bytes space-separated. Comparing raw would refuse two of the three spellings of the correct answer and word the refusal as tampering. No digest is hardcoded, which is the half of the request this does not deliver. The fork is built from Felis-Legacy and has never been reproduced on a second machine, so a constant here would pin one machine's output rather than the fork. The comment that previously asserted the build "is not byte-reproducible" is gone too -- it was stated more confidently than the evidence supports. The fork jars on disk carry Gradle's constant 1980-02-01 entry timestamps, so the usual reason a jar differs between builds is already absent; that is not proof it reproduces, and neither claim should sit in the script unmeasured. This is deliberately not a supply-chain signature and the comment says so: an operator who can write the jar can write the digest. What it buys is that a path stops being an identity, and that every later re-run re-checks the same build. Scope: the fork jar only. The else branch still curls stock Velocity from PaperMC with no verification at all, and that is the branch a default install takes. The digest is already in hand there -- Fill v3 returns checksums.sha256 and its download URL is content-addressed on that same value -- and papermc_latest_jar discards it. Left alone rather than widened into this change. deploy/bootstrap_test.sh covers the gate's two refusals, its happy path, and the two Windows digest spellings. Each case extracts the block under test out of bootstrap.sh with awk and runs it with die/log stubbed, rather than transcribing it -- a transcribed copy passes forever after someone edits the original. The extraction is length-bounded: awk runs an unmatched end pattern to EOF, which would quietly feed the rest of bootstrap.sh to the shell under test. bootstrap.sh itself cannot run here; it wants root, a package manager and k3s. A `shell` CI job runs that plus a syntax check over every tracked script. The syntax step dispatches on each file's shebang instead of running `sh -n` across the board. The blanket form looks fine and is a false green: on a developer machine `sh` is usually bash and accepts everything, while the runner's `sh` is dash. Verified against the real thing rather than an approximation -- inside ubuntu:24.04, where /bin/sh is /usr/bin/dash, the dispatching loop passes all six scripts and the blanket loop dies at bootstrap.sh:191 on the first of its 14 arrays. Refs: Felis-Legacy #19
71 lines
3.0 KiB
Bash
71 lines
3.0 KiB
Bash
#!/bin/sh
|
|
# Checks for deploy/bootstrap.sh. Run it as: sh deploy/bootstrap_test.sh
|
|
#
|
|
# The script it tests cannot be run here — it wants root, a package manager, k3s and the
|
|
# network — so each case extracts the block it is about out of bootstrap.sh verbatim and runs
|
|
# that with die/log stubbed. Extraction rather than a transcribed copy is the point: a copy
|
|
# passes forever after someone edits the original.
|
|
set -u
|
|
|
|
BS="${1:-$(dirname "$0")/bootstrap.sh}"
|
|
[ -f "$BS" ] || { echo "no such script: $BS"; exit 1; }
|
|
fails=0
|
|
|
|
expect() { # label needle haystack
|
|
case "$3" in
|
|
*"$2"*) echo "PASS $1" ;;
|
|
*) echo "FAIL $1: expected <$2> in:"; echo "$3"; fails=$((fails + 1)) ;;
|
|
esac
|
|
}
|
|
|
|
# --- the FELIS_VELOCITY_FORK_JAR digest gate -------------------------------------------
|
|
# This jar becomes the proxy every player connects through, so the interesting cases are the
|
|
# two refusals, not the happy path.
|
|
|
|
gate="$(awk '/have="\$\(sha256sum <"\$FELIS_VELOCITY_FORK_JAR"/,/^ log "installing the Felis-Legacy/' "$BS")"
|
|
[ -n "$gate" ] || { echo "FAIL: no fork-jar digest gate found in $BS"; exit 1; }
|
|
# awk runs an unmatched end pattern to EOF, which would quietly pipe the rest of bootstrap.sh
|
|
# into the `sh -c` below. The emptiness check above only catches a broken start pattern.
|
|
[ "$(printf '%s\n' "$gate" | wc -l)" -lt 40 ] \
|
|
|| { echo "FAIL: the extracted block is not the gate -- did its last line move?"; exit 1; }
|
|
|
|
jar="$(mktemp)"
|
|
trap 'rm -f "$jar"' EXIT
|
|
printf 'stand-in for a fork build\n' > "$jar"
|
|
want="$(sha256sum <"$jar" | cut -d' ' -f1)"
|
|
|
|
run_gate() { # digest
|
|
FELIS_VELOCITY_FORK_JAR="$jar" FELIS_VELOCITY_FORK_JAR_SHA256="$1" sh -c '
|
|
die() { printf "DIE: %s\n" "$*"; exit 1; }
|
|
log() { printf "LOG: %s\n" "$*"; }
|
|
'"$gate"
|
|
}
|
|
|
|
out="$(run_gate '')"
|
|
expect "fork jar without a digest is refused" "DIE: FELIS_VELOCITY_FORK_JAR_SHA256 is required" "$out"
|
|
expect "the refusal names the jar's real digest" "$want" "$out"
|
|
|
|
out="$(run_gate 'deadbeef')"
|
|
expect "a mismatched digest is refused" "checksum mismatch: got ${want}, expected deadbeef" "$out"
|
|
|
|
out="$(run_gate "$want")"
|
|
expect "the matching digest installs" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
|
expect "the install line records the digest" "(sha256 ${want})" "$out"
|
|
|
|
# The build host is usually Windows, where nothing prints the digest the way sha256sum does:
|
|
# Get-FileHash returns uppercase and certutil has shipped the bytes space-separated. Feeding
|
|
# the gate its own output can never catch that -- these two cases are the operator's paste.
|
|
out="$(run_gate "$(printf '%s' "$want" | tr 'a-z' 'A-Z')")"
|
|
expect "an uppercase digest is the same digest" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
|
|
|
out="$(run_gate "$(printf '%s' "$want" | sed 's/../& /g')")"
|
|
expect "a space-separated digest is the same digest" "LOG: installing the Felis-Legacy Velocity fork" "$out"
|
|
|
|
# ---------------------------------------------------------------------------------------
|
|
if [ "$fails" -eq 0 ]; then
|
|
echo "ALL PASS"
|
|
else
|
|
echo "$fails FAILED"
|
|
fi
|
|
exit "$fails"
|