feat(bootstrap): verify Paper and Velocity jars against Fill's digest
This commit is contained in:
7 files changed
+168
-9
No files matched your search
+29
-8
@@ -1200,7 +1200,7 @@ game_stack_source() {
|
||||
# MC_VERSION is read off Limbo's CI artifact name (Limbo-<limbo-ver>-<mc-ver>.jar), which
|
||||
# is the only place the pairing is published.
|
||||
resolve_game_jars() {
|
||||
local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta file base rest
|
||||
local ci="https://ci.loohpjames.com/job/Limbo/lastSuccessfulBuild" meta file base rest paper
|
||||
log "resolving the newest LOOHP/Limbo CI build"
|
||||
# Fetch first, filter second: `curl | grep | head` dies of SIGPIPE under `set -o pipefail`
|
||||
# the moment head closes the pipe early. Same shape everywhere below.
|
||||
@@ -1221,8 +1221,10 @@ resolve_game_jars() {
|
||||
# PaperMC Fill v3. The old api.papermc.io v2 has returned HTTP 410 since 2026-07-01 and
|
||||
# is never coming back; Fill wants a descriptive User-Agent.
|
||||
log "resolving the newest Paper ${MC_VERSION} build"
|
||||
PAPER_JAR_URL="$(papermc_latest_jar paper "$MC_VERSION")" \
|
||||
|| die "could not resolve a Paper build for Minecraft ${MC_VERSION} (the login gate pins this protocol; the build likely exists — Fill upstream is down or flapping)"
|
||||
paper="$(papermc_latest_jar paper "$MC_VERSION")" \
|
||||
|| die "could not resolve a Paper build for Minecraft ${MC_VERSION} (the login gate pins this protocol; the build likely exists — Fill upstream is down, flapping, or no longer content-addressed)"
|
||||
PAPER_JAR_URL="${paper% *}"
|
||||
PAPER_JAR_SHA256="${paper##* }"
|
||||
# LuckPerms is not version-matched to MC_VERSION the way Paper is: it ships one
|
||||
# current Bukkit build that supports the whole supported Minecraft range, so there is
|
||||
# no per-version endpoint to ask.
|
||||
@@ -1250,20 +1252,29 @@ luckperms_latest_jar() {
|
||||
printf '%s\n' "$url"
|
||||
}
|
||||
|
||||
# papermc_latest_jar prints the download URL of the newest build of <project> <version>.
|
||||
# papermc_latest_jar prints "<url> <sha256>" for the newest build of <project> <version>.
|
||||
# --retry rides out Fill's transient gateway errors (502/503/504 are in curl's retry
|
||||
# set): a single blip must not abort the whole bootstrap claiming the build is missing.
|
||||
# Plain --retry only, deliberately: --retry-connrefused needs curl 7.52+, which the yum
|
||||
# (el7) path does not have, and it would only add ECONNREFUSED to an already-covered set.
|
||||
# The digest is not fished out of the JSON separately: Fill's download URLs are
|
||||
# content-addressed (/v1/objects/<sha256>/<name>.jar), so the path segment names the
|
||||
# bytes the URL serves and both halves come from the same grep of the same response. A
|
||||
# URL without that shape fails the resolve rather than waving the download through
|
||||
# unchecked.
|
||||
papermc_latest_jar() {
|
||||
local project="$1" version="$2" json urls url
|
||||
local project="$1" version="$2" json urls url sha
|
||||
json="$(curl -fsSL --retry 5 --retry-delay 2 \
|
||||
-A "felis-bootstrap (+https://github.com/MliroLirrorsIngenuity/Felis)" \
|
||||
"https://fill.papermc.io/v3/projects/${project}/versions/${version}/builds/latest")" || return 1
|
||||
urls="$(printf '%s' "$json" | grep -o 'https://fill-data\.papermc\.io/[^"]*\.jar' || true)"
|
||||
url="${urls%%$'\n'*}"
|
||||
[ -n "$url" ] || return 1
|
||||
printf '%s\n' "$url"
|
||||
sha="${url#*/objects/}"
|
||||
sha="${sha%%/*}"
|
||||
case "$sha" in *[!0-9a-f]*|"") return 1 ;; esac
|
||||
[ "${#sha}" -eq 64 ] || return 1
|
||||
printf '%s %s\n' "$url" "$sha"
|
||||
}
|
||||
|
||||
build_game_stack() {
|
||||
@@ -1281,6 +1292,7 @@ build_game_stack() {
|
||||
log "building ${FELIS_LOBBY_IMAGE} (Paper ${MC_VERSION} + felis-paper /menu + LuckPerms)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/lobby/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
--build-arg LUCKPERMS_JAR_URL="$LUCKPERMS_JAR_URL" \
|
||||
-t "$FELIS_LOBBY_IMAGE" "$GAME_STACK_DIR"
|
||||
|
||||
@@ -1289,6 +1301,7 @@ build_game_stack() {
|
||||
log "building ${FELIS_PAPER_IMAGE} (plain Paper ${MC_VERSION}, forwarding via the operator initContainer)"
|
||||
docker build -f "${GAME_STACK_DIR}/deploy/paper/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
-t "$FELIS_PAPER_IMAGE" "$GAME_STACK_DIR"
|
||||
|
||||
local img
|
||||
@@ -1486,7 +1499,7 @@ install_jre() {
|
||||
|
||||
install_velocity() {
|
||||
install_jre
|
||||
local url tmp have want
|
||||
local url tmp have want resolved
|
||||
prepare_velocity_layout
|
||||
if [ -n "$FELIS_VELOCITY_FORK_JAR" ]; then
|
||||
[ -f "$FELIS_VELOCITY_FORK_JAR" ] \
|
||||
@@ -1514,12 +1527,20 @@ install_velocity() {
|
||||
atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root
|
||||
else
|
||||
log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build"
|
||||
url="$(papermc_latest_jar velocity "$FELIS_VELOCITY_VERSION")" \
|
||||
resolved="$(papermc_latest_jar velocity "$FELIS_VELOCITY_VERSION")" \
|
||||
|| die "no Velocity build for ${FELIS_VELOCITY_VERSION} (override with FELIS_VELOCITY_VERSION)"
|
||||
url="${resolved% *}"
|
||||
want="${resolved##* }"
|
||||
log "downloading Velocity ${FELIS_VELOCITY_VERSION}"
|
||||
tmp="$(mktemp "${VELOCITY_DIR}/.velocity.jar.XXXXXX")"
|
||||
remember_temp "$tmp"
|
||||
curl -fsSL "$url" -o "$tmp" || die "failed to download Velocity: ${url}"
|
||||
# The same gate the Via plugins and the fork jar pass: this jar is the proxy every
|
||||
# player connects through, and Fill already promised its digest in the URL — a
|
||||
# truncated or tampered download becomes a refusal here, not a proxy that won't boot.
|
||||
have="$(sha256sum <"$tmp" | cut -d' ' -f1)"
|
||||
[ "$have" = "$want" ] \
|
||||
|| die "Velocity ${FELIS_VELOCITY_VERSION} checksum mismatch: got ${have}, expected ${want}"
|
||||
atomic_install_file "$tmp" "${VELOCITY_DIR}/velocity.jar" 0644 root root
|
||||
fi
|
||||
|
||||
|
||||
@@ -61,6 +61,77 @@ expect "an uppercase digest is the same digest" "LOG: installing the Felis-Legac
|
||||
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"
|
||||
|
||||
# --- papermc_latest_jar answers "url sha256" from one response --------------------------
|
||||
# Fill's download URLs are content-addressed (/v1/objects/<sha256>/<name>.jar), and the
|
||||
# resolver's contract is to hand both halves back from the same grep — or refuse a URL
|
||||
# that carries no digest, rather than wave the download through unchecked. Run under
|
||||
# bash, not sh: bootstrap.sh is bash and the function uses $'\n'.
|
||||
|
||||
fn="$(awk '/^papermc_latest_jar\(\)/,/^}/' "$BS")"
|
||||
[ -n "$fn" ] || { echo "FAIL: no papermc_latest_jar in $BS"; exit 1; }
|
||||
[ "$(printf '%s\n' "$fn" | wc -l)" -lt 30 ] \
|
||||
|| { echo "FAIL: the extracted papermc_latest_jar is not just the function -- did its closing brace move?"; exit 1; }
|
||||
|
||||
rsha=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
|
||||
run_resolver() { # canned-fill-response
|
||||
CANNED="$1" bash -c '
|
||||
curl() { printf "%s" "$CANNED"; }
|
||||
'"$fn"'
|
||||
if out="$(papermc_latest_jar velocity 3.5.1)"; then
|
||||
printf "RESOLVED %s\n" "$out"
|
||||
else
|
||||
printf "REFUSED\n"
|
||||
fi
|
||||
'
|
||||
}
|
||||
|
||||
out="$(run_resolver "{\"url\":\"https://fill-data.papermc.io/v1/objects/${rsha}/velocity-3.5.1-615.jar\"}")"
|
||||
expect "the resolver pairs the url with its own digest" \
|
||||
"RESOLVED https://fill-data.papermc.io/v1/objects/${rsha}/velocity-3.5.1-615.jar ${rsha}" "$out"
|
||||
|
||||
out="$(run_resolver '{"url":"https://fill-data.papermc.io/mirror/velocity-3.5.1-615.jar"}')"
|
||||
expect "a URL that carries no digest is refused" "REFUSED" "$out"
|
||||
|
||||
# --- the resolved-Velocity digest gate --------------------------------------------------
|
||||
# The download must hash to what the content-addressed URL promised, BEFORE
|
||||
# atomic_install_file — the same refusal the Via plugins and the fork jar already get.
|
||||
|
||||
# The end pattern spells ${VELOCITY_DIR} with dots: escaped braces are literal in gawk
|
||||
# and mawk but undefined in POSIX awk, and CI's awk is whatever ubuntu ships.
|
||||
vblock="$(awk '/log "resolving the newest Velocity/,/atomic_install_file "\$tmp" "\$.VELOCITY_DIR.\/velocity\.jar"/' "$BS")"
|
||||
[ -n "$vblock" ] || { echo "FAIL: no resolved-Velocity install block found in $BS"; exit 1; }
|
||||
[ "$(printf '%s\n' "$vblock" | wc -l)" -lt 30 ] \
|
||||
|| { echo "FAIL: the extracted block is not the velocity install -- did its last line move?"; exit 1; }
|
||||
|
||||
vdir="$(mktemp -d)"
|
||||
trap 'rm -f "$jar"; rm -rf "$vdir"' EXIT
|
||||
vwant="$(printf 'stand-in velocity build\n' | sha256sum | cut -d' ' -f1)"
|
||||
|
||||
run_velocity_install() { # digest-the-resolver-reports
|
||||
WANT="$1" VELOCITY_DIR="$vdir" FELIS_VELOCITY_VERSION=3.5.1 bash -c '
|
||||
die() { printf "DIE: %s\n" "$*"; exit 1; }
|
||||
log() { printf "LOG: %s\n" "$*"; }
|
||||
remember_temp() { :; }
|
||||
papermc_latest_jar() {
|
||||
printf "%s %s\n" "https://fill-data.papermc.io/v1/objects/${WANT}/velocity-3.5.1-615.jar" "$WANT"
|
||||
}
|
||||
curl() { while [ "$#" -gt 1 ] && [ "$1" != "-o" ]; do shift; done; printf "stand-in velocity build\n" > "$2"; }
|
||||
atomic_install_file() { printf "INSTALL: %s\n" "$2"; }
|
||||
'"$vblock"
|
||||
}
|
||||
|
||||
out="$(run_velocity_install deadbeef)"
|
||||
expect "a download that does not hash to the promised digest is refused" \
|
||||
"DIE: Velocity 3.5.1 checksum mismatch: got ${vwant}, expected deadbeef" "$out"
|
||||
case "$out" in
|
||||
*INSTALL:*) echo "FAIL a refused download must not reach atomic_install_file"; fails=$((fails + 1)) ;;
|
||||
*) echo "PASS a refused download is not installed" ;;
|
||||
esac
|
||||
|
||||
out="$(run_velocity_install "$vwant")"
|
||||
expect "the matching download installs" "INSTALL: ${vdir}/velocity.jar" "$out"
|
||||
|
||||
# ---------------------------------------------------------------------------------------
|
||||
if [ "$fails" -eq 0 ]; then
|
||||
echo "ALL PASS"
|
||||
|
||||
+18
-1
@@ -13,7 +13,8 @@
|
||||
# 1. Prebuilt tars at deploy/images/felis-limbo.tar + felis-lobby.tar (imported as-is).
|
||||
# 2. Otherwise built on this host with docker, resolving the LOOHP/Limbo CI jar and
|
||||
# the latest stable Paper jar automatically. Override any of:
|
||||
# LIMBO_JAR_URL LIMBO_SCHEM_URL LIMBO_VERSION PAPER_JAR_URL PAPER_MC_VERSION
|
||||
# LIMBO_JAR_URL LIMBO_SCHEM_URL LIMBO_VERSION PAPER_JAR_URL PAPER_JAR_SHA256
|
||||
# PAPER_MC_VERSION
|
||||
#
|
||||
# Toggles: SKIP_BOOTSTRAP=1 (base already up), SKIP_SETUP=1 (stop before the TUI).
|
||||
set -Eeuo pipefail
|
||||
@@ -73,9 +74,24 @@ else
|
||||
: "${PAPER_MC_VERSION:=1.21.8}"
|
||||
: "${PAPER_JAR_URL:=$(curl -fsSL --max-time 30 "https://fill.papermc.io/v3/projects/paper/versions/${PAPER_MC_VERSION}/builds/latest" | grep -oE 'https://fill-data\.papermc\.io/[^"]+\.jar' | head -1)}"
|
||||
[ -n "$PAPER_JAR_URL" ] || die "could not resolve the Paper jar; set PAPER_JAR_URL"
|
||||
# Both Dockerfiles require the jar's digest. The fill-data URL is content-addressed
|
||||
# (the objects/ path segment IS the sha256), so it is derived rather than asked for;
|
||||
# a mirror override carries no such segment and must bring its own digest.
|
||||
if [ -z "${PAPER_JAR_SHA256:-}" ]; then
|
||||
sha="${PAPER_JAR_URL#*/objects/}"
|
||||
sha="${sha%%/*}"
|
||||
case "$sha" in
|
||||
*[!0-9a-f]*|"") sha="" ;;
|
||||
esac
|
||||
if [ "${#sha}" -ne 64 ]; then
|
||||
die "cannot derive the Paper jar sha256 from PAPER_JAR_URL (not a content-addressed fill-data URL); set PAPER_JAR_SHA256"
|
||||
fi
|
||||
PAPER_JAR_SHA256="$sha"
|
||||
fi
|
||||
log "building $LOBBY_IMAGE (Paper $PAPER_MC_VERSION)"
|
||||
docker build -f "$SRC_DIR/deploy/lobby/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
-t "$LOBBY_IMAGE" "$SRC_DIR"
|
||||
docker save "$LOBBY_IMAGE" | "$K3S" ctr images import -
|
||||
|
||||
@@ -84,6 +100,7 @@ else
|
||||
log "building $PAPER_IMAGE (plain Paper $PAPER_MC_VERSION, forwarding via the operator initContainer)"
|
||||
docker build -f "$SRC_DIR/deploy/paper/Dockerfile" \
|
||||
--build-arg PAPER_JAR_URL="$PAPER_JAR_URL" \
|
||||
--build-arg PAPER_JAR_SHA256="$PAPER_JAR_SHA256" \
|
||||
-t "$PAPER_IMAGE" "$SRC_DIR"
|
||||
docker save "$PAPER_IMAGE" | "$K3S" ctr images import -
|
||||
fi
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# Fill v3 API — api.papermc.io v2 has returned HTTP 410 since 2026-07-01):
|
||||
# docker build -f deploy/lobby/Dockerfile \
|
||||
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-26.2-<build>.jar \
|
||||
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
|
||||
# --build-arg LUCKPERMS_JAR_URL="$(curl -fsSL https://metadata.luckperms.net/data/all \
|
||||
# | grep -o 'https://download.luckperms.net/[^"]*/bukkit/loader/[^"]*\.jar')" \
|
||||
# -t felis-lobby:demo .
|
||||
@@ -42,6 +43,10 @@ RUN cd plugins/paper \
|
||||
# also runs the plugin's Java-21 bytecode, so only the runtime moves.
|
||||
FROM eclipse-temurin:25-jre
|
||||
ARG PAPER_JAR_URL
|
||||
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
|
||||
# that shape at build time. Checking the digest after the download turns a truncated or
|
||||
# tampered fetch into a failed build instead of a lobby booted on the wrong bytes.
|
||||
ARG PAPER_JAR_SHA256
|
||||
# LuckPerms is required, not optional: the panel's whole permission surface
|
||||
# (internal/api/handlers_access.go) issues `lp user ...` over RCON, so a lobby built
|
||||
# without it answers every grant with "Unknown command" — a failure the operator only
|
||||
@@ -55,12 +60,16 @@ RUN set -eu; \
|
||||
if [ -z "${PAPER_JAR_URL:-}" ]; then \
|
||||
echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \
|
||||
fi; \
|
||||
if [ -z "${PAPER_JAR_SHA256:-}" ]; then \
|
||||
echo "ERROR: --build-arg PAPER_JAR_SHA256=<paper jar sha256> is required" >&2; exit 1; \
|
||||
fi; \
|
||||
if [ -z "${LUCKPERMS_JAR_URL:-}" ]; then \
|
||||
echo "ERROR: --build-arg LUCKPERMS_JAR_URL=<luckperms bukkit jar> is required" >&2; exit 1; \
|
||||
fi; \
|
||||
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
|
||||
mkdir -p /paper/plugins; \
|
||||
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
|
||||
echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c; \
|
||||
curl -fSL "$LUCKPERMS_JAR_URL" -o /paper/plugins/LuckPerms.jar; \
|
||||
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
|
||||
echo "eula=true" > /paper/eula.txt
|
||||
|
||||
@@ -29,6 +29,7 @@ this at every layer:
|
||||
```
|
||||
docker build -f deploy/lobby/Dockerfile \
|
||||
--build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
|
||||
--build-arg PAPER_JAR_SHA256=<sha256 of that jar> \
|
||||
-t felis-lobby:demo .
|
||||
docker save felis-lobby:demo | sudo k3s ctr images import -
|
||||
# felis.toml → [velocity] lobby_image = "felis-lobby:demo"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# API — the SAME url the lobby build resolves, so this reuses it and adds no new dependency):
|
||||
# docker build -f deploy/paper/Dockerfile \
|
||||
# --build-arg PAPER_JAR_URL=https://fill-data.papermc.io/v1/objects/<sha>/paper-<ver>-<build>.jar \
|
||||
# --build-arg PAPER_JAR_SHA256=<that same sha — the objects/ path segment> \
|
||||
# -t felis-paper:demo .
|
||||
# docker save felis-paper:demo | sudo k3s ctr images import -
|
||||
# # felis.toml → recommended via 0019_recommended_paper.sql (no [velocity] key points here)
|
||||
@@ -30,13 +31,21 @@
|
||||
# to boot on anything older.
|
||||
FROM eclipse-temurin:25-jre
|
||||
ARG PAPER_JAR_URL
|
||||
# Required alongside the URL: Fill's URLs are content-addressed, but nothing enforces
|
||||
# that shape at build time. Checking the digest after the download turns a truncated or
|
||||
# tampered fetch into a failed build instead of a server booted on the wrong bytes.
|
||||
ARG PAPER_JAR_SHA256
|
||||
RUN set -eu; \
|
||||
if [ -z "${PAPER_JAR_URL:-}" ]; then \
|
||||
echo "ERROR: --build-arg PAPER_JAR_URL=<paper jar> is required" >&2; exit 1; \
|
||||
fi; \
|
||||
if [ -z "${PAPER_JAR_SHA256:-}" ]; then \
|
||||
echo "ERROR: --build-arg PAPER_JAR_SHA256=<paper jar sha256> is required" >&2; exit 1; \
|
||||
fi; \
|
||||
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates; \
|
||||
mkdir -p /paper; \
|
||||
curl -fSL "$PAPER_JAR_URL" -o /paper/paper.jar; \
|
||||
echo "$PAPER_JAR_SHA256 /paper/paper.jar" | sha256sum -c; \
|
||||
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
||||
COPY deploy/paper/entrypoint.sh /usr/local/bin/felis-entrypoint.sh
|
||||
|
||||
|
||||
Reference in new issue
Block a user