Files
Rust-Link/egg/install.sh
wtclaude fe7b2822c5
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m58s
feat(egg): install every plugin file the release lists, helpers included (D182)
Rust-Plugins now releases a ZoneManager helper, RunicGatewayZones.cs, beside
the bridge (docs/modules/rust/PLAN_FIXES.md D181, D182), installed by default.
The egg copied only RunicGateway.cs out of the tarball.

It now takes every .cs the plugin manifest lists in `files`, checks each
against its own sha256 before anything is placed (the bridge's own checksum
was never checked by the egg before), and refuses a name that is not a plain
<Name>.cs. A release older than helpers lists only the bridge and installs
exactly what it did before. Exercised in an Alpine shell against both, a
tampered helper, a missing one, `../evil.cs` and `evil.dll`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 21:25:35 -05:00

192 lines
11 KiB
Bash
Executable File

#!/bin/bash
# Rust + the Runic Gateway bridge — the egg's install script.
#
# Egg 18 "Rust Autowipe"'s script, unchanged down to its wipe, plus two things
# (docs/modules/rust/PLAN.md §34.2.6):
#
# 1. THE WIPE GUARD. `rm -rf ${REMOVE_FILES}` runs with rust-link/ moved out of
# the server root, so no list an operator types — wildcards included — can
# reach the bridge's store or its token.
# 2. THE BRIDGE. The sidecar, its launcher and the plugin, from a published
# bundle, each checked against the bundle's sha256 before anything is
# placed. RUNICGATEWAY_BUNDLE pins a bundle; blank takes the current one
# (D151). This runs at install and reinstall only — never at boot, so a
# restart cannot change the protocol under a website that has not moved.
#
# Server Files: /mnt/server
# Image to install with is 'ghcr.io/ptero-eggs/installers:debian' (jq, curl,
# sha256sum and tar; no python3).
##
#
# Variables
# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.
# WINDOWS_INSTALL - if it's a windows server you want to install set to 1
# SRCDS_APPID - steam app id found here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
# SRCDS_BETAID - beta branch of a steam app. Leave blank to install normal branch
# SRCDS_BETAPASS - password for a beta branch should one be required during private or closed testing phases.. Leave blank for no password.
# INSTALL_FLAGS - Any additional SteamCMD flags to pass during install.. Keep in mind that steamcmd auto update process in the docker image might overwrite or ignore these when it performs update on server boot.
# AUTO_UPDATE - Adding this variable to the egg allows disabling or enabling automated updates on boot. Boolean value. 0 to disable and 1 to enable.
#
##
## just in case someone removed the defaults.
if [[ "${STEAM_USER}" == "" ]] || [[ "${STEAM_PASS}" == "" ]]; then
echo -e "steam user is not set.\n"
echo -e "Using anonymous user.\n"
STEAM_USER=anonymous
STEAM_PASS=""
STEAM_AUTH=""
else
echo -e "user set to ${STEAM_USER}"
fi
## download and install steamcmd
cd /tmp
mkdir -p /mnt/server/steamcmd
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
cd /mnt/server/steamcmd
# SteamCMD fails otherwise for some reason, even running as root.
# This is changed at the end of the install process anyways.
chown -R root:root /mnt
export HOME=/mnt/server
## install game using steamcmd
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} $( [[ -z ${SRCDS_BETAID} ]] || printf %s "-beta ${SRCDS_BETAID}" ) $( [[ -z ${SRCDS_BETAPASS} ]] || printf %s "-betapassword ${SRCDS_BETAPASS}" ) ${INSTALL_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
## set up 32 bit libraries
mkdir -p /mnt/server/.steam/sdk32
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
## set up 64 bit libraries
mkdir -p /mnt/server/.steam/sdk64
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
## ── The wipe, with rust-link/ held outside the server root ─────────────────
# The store keeps all-time rollups across wipes (R12) and the token is what the
# website holds; a swept store is the failure that looks like success. Moved to
# /tmp — outside /mnt/server, so no path in REMOVE_FILES can name it — and put
# back straight after, before anything else can fail.
if [ "${REGEN_SERVER}" == "1" ]; then
cd /mnt/server/
RG_KEEP=/tmp/runicgateway-rust-link.keep
rm -rf "${RG_KEEP}"
if [ -d rust-link ]; then mv rust-link "${RG_KEEP}"; fi
rm -rf ${REMOVE_FILES}
if [ -d "${RG_KEEP}" ]; then rm -rf rust-link; mv "${RG_KEEP}" rust-link; fi
fi
if [ $WORLD_SEED == "0" ]; then
if [ ! -f /mnt/server/seed.txt ]; then
rm -sf /mnt/server/seed.txt
fi
cat /dev/urandom | tr -dc '1-9' | fold -w 5 | head -n 1 > /mnt/server/seed.txt
fi
## ── The Runic Gateway bridge ─────────────────────────────────────────────────
# After the wipe, so REMOVE_FILES can never delete the plugin this just placed.
rg_install() {
set -euo pipefail
# Overridable only for testing against a mock: the panel passes a container
# just the variables an egg declares, and this one is not declared.
local api="${RUNICGATEWAY_BUNDLE_API:-https://gitea.whitlocktech.com/api/v1/repos/RunicGateway/installer/contents/v2/rust}"
local work=/tmp/runicgateway
local plugins
case "${FRAMEWORK:-vanilla}" in
oxide) plugins=/mnt/server/oxide/plugins ;;
carbon) plugins=/mnt/server/carbon/plugins ;;
*)
# Not a failure (§34.4): failing would leave the operator without a game
# server over a bridge they may not want yet. The startup skips the
# launcher when it is absent, so the server boots exactly as egg 18's.
echo "Runic Gateway: FRAMEWORK=${FRAMEWORK:-vanilla} - the bridge needs Oxide or Carbon, so nothing of it was installed."
return 0
;;
esac
rm -rf "${work}"; mkdir -p "${work}"
local doc="current.json"
if [ -n "${RUNICGATEWAY_BUNDLE:-}" ]; then doc="bundle-${RUNICGATEWAY_BUNDLE}.json"; fi
echo "Runic Gateway: resolving bundle ${doc}"
# The contents API, not /raw/: raw reads are CDN-cached for hours, which would
# hand a reinstall right after a release the bundle from before it.
curl -fsSL "${api}/${doc}?ref=bundles" | jq -r '.content' | base64 -d > "${work}/bundle.json" \
|| { echo "Runic Gateway: could not fetch ${doc} - is RUNICGATEWAY_BUNDLE a published bundle?"; return 1; }
jq -e '.schema == 2 and .game == "rust"' "${work}/bundle.json" >/dev/null \
|| { echo "Runic Gateway: ${doc} is not a schema-2 Rust bundle"; return 1; }
local tag protocol
tag="$(jq -r '.bundle' "${work}/bundle.json")"
protocol="$(jq -r '.protocol' "${work}/bundle.json")"
echo "Runic Gateway: bundle ${tag}, protocol ${protocol}"
# Everything is fetched and checked BEFORE anything is placed: a half-updated
# pair is a sidecar and a plugin speaking two protocols.
fetch() { # <jq path to an asset> <local name>
local name url sha
name="$(jq -r "$1.name" "${work}/bundle.json")"
url="$(jq -r "$1.url" "${work}/bundle.json")"
sha="$(jq -r "$1.sha256" "${work}/bundle.json")"
curl -fsSL -o "${work}/$2" "${url}" || { echo "Runic Gateway: could not download ${name}"; return 1; }
echo "${sha} ${work}/$2" | sha256sum -c --quiet - \
|| { echo "Runic Gateway: ${name} does not match the bundle's sha256 - refusing it"; return 1; }
}
fetch '.sidecar.assets["linux-x86_64"]' rust-link-sidecar
fetch '.sidecar.launcher' with-sidecar.sh
fetch '.payload.asset' plugin.tar.gz
tar -xzf "${work}/plugin.tar.gz" -C "${work}"
local manifest="${work}/runicgateway-rust-plugin/manifest.json"
[ -f "${manifest}" ] || { echo "Runic Gateway: the plugin tarball has no manifest.json"; return 1; }
[ "$(jq -r '.protocol' "${manifest}")" = "${protocol}" ] \
|| { echo "Runic Gateway: the plugin declares protocol $(jq -r '.protocol' "${manifest}"), the bundle ${protocol} - refusing the pair"; return 1; }
mkdir -p /mnt/server/rust-link "${plugins}"
install -m 755 "${work}/rust-link-sidecar" /mnt/server/rust-link/rust-link-sidecar
install -m 755 "${work}/with-sidecar.sh" /mnt/server/rust-link/with-sidecar.sh
# Every .cs the release's manifest lists: the bridge, and the helpers shipped
# beside it (docs/modules/rust/PLAN_FIXES.md D182 - today RunicGatewayZones.cs,
# the ZoneManager helper), each checked against its own sha256 first. A name
# is written into the plugins directory, so only a plain <Name>.cs is taken.
local file sha
for file in $(jq -r '.files | keys[]' "${manifest}"); do
case "${file}" in
*[!A-Za-z0-9_.]* | .* | *..* | *[!s] ) echo "Runic Gateway: the plugin manifest lists ${file}, which is not a plugin file - refusing it"; return 1 ;;
esac
[ "${file%.cs}" != "${file}" ] || { echo "Runic Gateway: the plugin manifest lists ${file}, which is not a .cs file - refusing it"; return 1; }
[ -f "${work}/runicgateway-rust-plugin/${file}" ] || { echo "Runic Gateway: the plugin manifest lists ${file} but the tarball has none"; return 1; }
sha="$(jq -r --arg f "${file}" '.files[$f]' "${manifest}")"
echo "${sha} ${work}/runicgateway-rust-plugin/${file}" | sha256sum -c --quiet - \
|| { echo "Runic Gateway: ${file} does not match the plugin manifest's sha256 - refusing it"; return 1; }
done
[ -f "${work}/runicgateway-rust-plugin/RunicGateway.cs" ] || { echo "Runic Gateway: the plugin tarball has no RunicGateway.cs"; return 1; }
for file in $(jq -r '.files | keys[]' "${manifest}"); do
install -m 644 "${work}/runicgateway-rust-plugin/${file}" "${plugins}/${file}"
done
# What is installed, readable from the panel's file manager.
jq --arg framework "${FRAMEWORK}" --arg installed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{ bundle, protocol, framework: $framework, installed: $installed,
sidecar: { tag: .sidecar.tag }, plugin: { tag: .payload.tag, commit: .payload.commit } }' \
"${work}/bundle.json" > /mnt/server/rust-link/bundle.json
echo "Runic Gateway: installed sidecar $(jq -r '.sidecar.tag' "${work}/bundle.json") and plugin $(jq -r '.payload.tag' "${work}/bundle.json") (${FRAMEWORK})"
echo "Runic Gateway: add this server under Admin -> Rust -> Servers; the console prints its URL and, on first boot, its token."
}
# In a subshell so `set -e` inside cannot leak into the rest of this script, and
# so a failure fails the install with its reason rather than leaving a half pair.
# NOT `( rg_install ) || …`: a subshell in a condition runs with errexit OFF, and
# an unchecked failed step inside it would sail on to "installed".
( rg_install )
if [ $? -ne 0 ]; then
echo "Runic Gateway: the bridge was NOT installed (see above)."
exit 1
fi
## install end
echo "-----------------------------------------"
echo "Installation completed..."
echo "-----------------------------------------"