refactor!: unify into one compose, drop airgap/mirror/dnsmasq/avahi/blessingskin
Collapse the override-file matrix into a single docker-compose.yml holding the whole stack: drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma. Bring-up is now just `docker compose up -d --build`. Removed features (dead-ends for this deployment): - airgap / full asset mirror (mirror-airgap.sh, mirror-mods.sh, 20-mirror.caddy, caddy local-CA export, /ca.crt) - dnsmasq + avahi/mDNS + dns-records.sh + mdns-host-setup.sh + ENABLE_MDNS; DNS is now handled outside this repo (HOST_LAN_IP dropped) - Blessing Skin auth variant (compose + 00-core-blessingskin.caddy + BS_* env) - host-nginx-static variant (docker-compose.nginx.yml, nginx/ulicraft.conf.tmpl) - build-stack.sh and its run modes (online/airgap/core) Production ingress is the only path now: host nginx terminates TLS (LE certs) in front of caddy on a localhost-only port; caddy routes every vhost by Host header. Launcher downloads move mirror/launcher -> launcher/. Docs: README, deploy skill, and plan/ rewritten to match; obsolete plan docs (dnsmasq, blessing-skin, assets-mirror, full-airgap-mirror, dns-and-run-modes) deleted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-stack.sh — orchestrate the Ulicraft stack: offline prep then bring-up.
|
||||
#
|
||||
# Phases:
|
||||
# preflight always — verify .env + required tools
|
||||
# --prep ONLINE — render configs, build landing, mirror mods + air-gap,
|
||||
# fetch launcher, pre-bake the server volume
|
||||
# --up OFFLINE — docker compose up -d the full stack
|
||||
# (no flag) preflight + print what each phase would do, then exit
|
||||
#
|
||||
# Cautious by design: halts on the first missing var, missing tool, or failed
|
||||
# step. Each sub-script is itself responsible for its own integrity checks.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
COMPOSE=(docker compose)
|
||||
|
||||
# ---- helpers ---------------------------------------------------------------
|
||||
log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
need_tool() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }
|
||||
have_script() { [ -x "tooling/$1" ] || die "missing/not-executable: tooling/$1"; }
|
||||
|
||||
# ---- preflight -------------------------------------------------------------
|
||||
preflight() {
|
||||
log "preflight"
|
||||
[ -f .env ] || die ".env not found (copy .env.example and fill it)"
|
||||
# shellcheck disable=SC1091
|
||||
set -a; . ./.env; set +a
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}"
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset in .env}"
|
||||
: "${RCON_PASSWORD:?RCON_PASSWORD unset in .env}"
|
||||
|
||||
need_tool docker
|
||||
${COMPOSE[@]} version >/dev/null 2>&1 || die "docker compose plugin not available"
|
||||
need_tool envsubst
|
||||
need_tool jq
|
||||
need_tool curl
|
||||
need_tool sha1sum
|
||||
command -v pnpm >/dev/null 2>&1 || warn "pnpm not found — landing build will be skipped if absent"
|
||||
|
||||
echo " BASE_DOMAIN=${BASE_DOMAIN} HOST_LAN_IP=${HOST_LAN_IP}"
|
||||
echo " preflight ok"
|
||||
}
|
||||
|
||||
# ---- online prep -----------------------------------------------------------
|
||||
prep() {
|
||||
log "PHASE prep (requires internet)"
|
||||
|
||||
log "render configs"
|
||||
have_script render-config.sh
|
||||
tooling/render-config.sh
|
||||
|
||||
log "build landing page → www/"
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
( cd landing && pnpm install --frozen-lockfile && BASE_DOMAIN="${BASE_DOMAIN}" pnpm run build )
|
||||
else
|
||||
warn "pnpm absent — skipping landing build"
|
||||
fi
|
||||
|
||||
log "mirror mod jars (offline pack)"
|
||||
have_script mirror-mods.sh
|
||||
tooling/mirror-mods.sh
|
||||
|
||||
log "mirror full air-gap upstream (meta/libraries/assets/client.jar)"
|
||||
have_script mirror-airgap.sh
|
||||
tooling/mirror-airgap.sh
|
||||
|
||||
log "fetch launcher releases"
|
||||
have_script fetch-launcher.sh
|
||||
tooling/fetch-launcher.sh
|
||||
|
||||
log "fetch authlib-injector.jar (server JVM agent → drasl)"
|
||||
if [ -f runtime/authlib-injector.jar ]; then
|
||||
echo " runtime/authlib-injector.jar present — skipping"
|
||||
else
|
||||
ai_url="$(curl -fsSL https://api.github.com/repos/yushijinhun/authlib-injector/releases/latest \
|
||||
| jq -r '.assets[] | select(.name|test("authlib-injector-[0-9].*\\.jar$")) | .browser_download_url' | head -1)"
|
||||
[ -n "$ai_url" ] && [ "$ai_url" != "null" ] || die "could not resolve authlib-injector release asset"
|
||||
curl -fSL --retry 3 -o runtime/authlib-injector.jar "$ai_url"
|
||||
echo " downloaded $(basename "$ai_url")"
|
||||
fi
|
||||
|
||||
if [ "${ENABLE_MDNS:-false}" = true ]; then
|
||||
log "build avahi mDNS image (cache it for offline use)"
|
||||
"${COMPOSE[@]}" -f docker-compose.yml -f docker-compose.avahi.yml build avahi
|
||||
fi
|
||||
|
||||
log "pre-bake server volume (TYPE=NEOFORGE install, one-shot, online)"
|
||||
# Bring minecraft up once so itzg installs NeoForge + mods into the volume,
|
||||
# then stop it. The party then runs offline against the baked volume.
|
||||
# caddy.yml so the pack (pack.${BASE_DOMAIN}) resolves + is served during bake.
|
||||
"${COMPOSE[@]}" -f docker-compose.yml -f docker-compose.caddy.yml up -d minecraft
|
||||
warn "watch logs until 'Done' then re-run with --up; or automate a readiness probe"
|
||||
}
|
||||
|
||||
# ---- bring-up --------------------------------------------------------------
|
||||
# Modes (DNS is YOUR party server — see tooling/dns-records.sh for the records):
|
||||
# online base + landing/launcher; internet present, no mirror
|
||||
# airgap online + full upstream mirror (tls internal :443) for no-internet
|
||||
# core base only (no landing, no mirror) — debugging
|
||||
up() {
|
||||
local mode="${1:-airgap}"
|
||||
# build-stack is the caddy (LAN/air-gap) deployer; caddy lives in its own file
|
||||
# now (the base stack can also run behind the host's nginx — see
|
||||
# docker-compose.nginx.yml). Always include caddy here.
|
||||
local -a files=(-f docker-compose.yml -f docker-compose.caddy.yml)
|
||||
case "$mode" in
|
||||
core) ;;
|
||||
online) files+=(-f docker-compose.static.yml) ;;
|
||||
airgap) files+=(-f docker-compose.static.yml -f docker-compose.mirror.yml) ;;
|
||||
*) die "unknown up mode: '$mode' (use online|airgap|core)" ;;
|
||||
esac
|
||||
# Optional mDNS (.local) responder — see ENABLE_MDNS in .env.
|
||||
if [ "${ENABLE_MDNS:-false}" = true ]; then
|
||||
files+=(-f docker-compose.avahi.yml)
|
||||
case "$BASE_DOMAIN" in
|
||||
*.local) ;;
|
||||
*) warn "ENABLE_MDNS=true but BASE_DOMAIN=$BASE_DOMAIN is not *.local — mDNS won't resolve it" ;;
|
||||
esac
|
||||
pgrep -x avahi-daemon >/dev/null 2>&1 \
|
||||
|| warn "host avahi-daemon not detected — the avahi container publishes through it; start it (e.g. 'systemctl start avahi-daemon')"
|
||||
[ -S /run/dbus/system_bus_socket ] \
|
||||
|| warn "host D-Bus socket /run/dbus/system_bus_socket missing — avahi publish will fail"
|
||||
local ifc; ifc=$(ip -o link show 2>/dev/null | wc -l)
|
||||
[ "${ifc:-0}" -gt 6 ] \
|
||||
&& warn "host has ${ifc} network interfaces — set 'allow-interfaces=<lan-nic>' in /etc/avahi/avahi-daemon.conf, else avahi self-collides (Local name collision)"
|
||||
fi
|
||||
log "PHASE up (mode=$mode${ENABLE_MDNS:+, mdns=$ENABLE_MDNS})"
|
||||
have_script render-config.sh
|
||||
tooling/render-config.sh # cheap; ensures configs match current .env
|
||||
|
||||
# Static mode mounts ./caddy/caddy-root-ca.crt as a file at /srv/ca-pub/ca.crt.
|
||||
# Pre-create it (empty) so Docker doesn't make a directory at that path; the
|
||||
# real cert is exported below once Caddy's local CA exists.
|
||||
case "$mode" in
|
||||
online|airgap) [ -e caddy/caddy-root-ca.crt ] || : > caddy/caddy-root-ca.crt ;;
|
||||
esac
|
||||
|
||||
"${COMPOSE[@]}" "${files[@]}" up -d
|
||||
|
||||
# Mirror/full provision Caddy's local CA — export it so guests can trust the
|
||||
# HTTPS asset mirror (served at http://${BASE_DOMAIN}/ca.crt in static/full).
|
||||
case "$mode" in
|
||||
airgap)
|
||||
local i
|
||||
for i in 1 2 3 4 5; do
|
||||
docker exec caddy cat /data/caddy/pki/authorities/local/root.crt \
|
||||
> caddy/caddy-root-ca.crt 2>/dev/null && break
|
||||
sleep 1
|
||||
done
|
||||
[ -s caddy/caddy-root-ca.crt ] \
|
||||
&& echo " exported Caddy CA → caddy/caddy-root-ca.crt (served at /ca.crt)" \
|
||||
|| warn "Caddy CA not ready; export later: docker exec caddy cat /data/caddy/pki/authorities/local/root.crt > caddy/caddy-root-ca.crt"
|
||||
;;
|
||||
esac
|
||||
|
||||
log "stack up (mode=$mode). apex: http://${BASE_DOMAIN} auth: http://auth.${BASE_DOMAIN}"
|
||||
echo
|
||||
log "Add these to your party DNS (mode=$mode):"
|
||||
[ -x tooling/dns-records.sh ] && tooling/dns-records.sh "$mode" | sed -n '/## Plain/,/^$/p'
|
||||
}
|
||||
|
||||
# ---- dispatch --------------------------------------------------------------
|
||||
preflight
|
||||
case "${1:-}" in
|
||||
--prep) prep ;;
|
||||
--up) up "${2:-airgap}" ;;
|
||||
"") cat <<EOF
|
||||
|
||||
Nothing run. Choose a phase:
|
||||
$0 --prep # ONLINE, once: render, build landing, mirror, fetch, pre-bake
|
||||
$0 --up [mode] # bring the stack up; mode = online|airgap|core (default airgap)
|
||||
# online = landing + launcher; internet present, no mirror
|
||||
# airgap = online + full upstream mirror (:443), no internet
|
||||
# core = base only (debugging)
|
||||
|
||||
DNS: configure your party DNS with the records from tooling/dns-records.sh [mode].
|
||||
mDNS (.local) alternative: set ENABLE_MDNS=true + BASE_DOMAIN=*.local in .env.
|
||||
EOF
|
||||
;;
|
||||
*) die "unknown arg: $1 (use --prep or --up [online|airgap|core])" ;;
|
||||
esac
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# dns-records.sh — print the DNS records to configure on YOUR party DNS server.
|
||||
#
|
||||
# Usage: tooling/dns-records.sh [online|airgap] (default: airgap)
|
||||
# online service names only — your DNS forwards everything else to the internet
|
||||
# airgap service names + the spoofed upstream hosts (Mojang/launcher/NeoForge),
|
||||
# so guest laptops fetch game files from the LAN mirror with no internet
|
||||
#
|
||||
# All names point to HOST_LAN_IP. Reads BASE_DOMAIN / HOST_LAN_IP from .env.
|
||||
# Prints several formats; use whichever your DNS UI wants.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset (set in .env)}"
|
||||
|
||||
mode="${1:-airgap}"
|
||||
|
||||
# Stack service names (always).
|
||||
services=(
|
||||
"${BASE_DOMAIN}"
|
||||
"auth.${BASE_DOMAIN}"
|
||||
"packwiz.${BASE_DOMAIN}"
|
||||
"mc.${BASE_DOMAIN}"
|
||||
)
|
||||
|
||||
# Upstream hosts spoofed onto the LAN mirror (AIRGAP only). Keep in sync with
|
||||
# caddy/conf.d/20-mirror.caddy and docker-compose.mirror.yml.
|
||||
spoof=(
|
||||
"meta.prismlauncher.org"
|
||||
"piston-meta.mojang.com"
|
||||
"piston-data.mojang.com"
|
||||
"libraries.minecraft.net"
|
||||
"maven.neoforged.net"
|
||||
"resources.download.minecraft.net"
|
||||
)
|
||||
|
||||
names=("${services[@]}")
|
||||
case "$mode" in
|
||||
online) ;;
|
||||
airgap) names+=("${spoof[@]}") ;;
|
||||
*) echo "usage: $0 [online|airgap] [hosts]" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Marker so /etc/hosts edits can be applied/removed cleanly.
|
||||
MARK="ulicraft ${mode}"
|
||||
|
||||
# `hosts` format: print ONLY the /etc/hosts block (marker-wrapped) so it can be
|
||||
# piped straight into /etc/hosts to simulate the party DNS. See README.
|
||||
if [ "${2:-}" = hosts ]; then
|
||||
echo "# >>> ${MARK} >>>"
|
||||
for n in "${names[@]}"; do printf '%s %s\n' "$HOST_LAN_IP" "$n"; done
|
||||
echo "# <<< ${MARK} <<<"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "# DNS records for mode=${mode} -> ${HOST_LAN_IP}"
|
||||
echo "# Configure these on your party DNS server (all A records to the host)."
|
||||
echo
|
||||
|
||||
echo "## Plain (name -> ip)"
|
||||
for n in "${names[@]}"; do printf '%-36s %s\n' "$n" "$HOST_LAN_IP"; done
|
||||
echo
|
||||
|
||||
echo "## hosts file (/etc/hosts style)"
|
||||
for n in "${names[@]}"; do printf '%s %s\n' "$HOST_LAN_IP" "$n"; done
|
||||
echo
|
||||
|
||||
echo "## BIND zone (A records)"
|
||||
for n in "${names[@]}"; do printf '%-36s IN A %s\n' "${n}." "$HOST_LAN_IP"; done
|
||||
echo
|
||||
|
||||
echo "## dnsmasq (address=)"
|
||||
echo "address=/${BASE_DOMAIN}/${HOST_LAN_IP} # wildcard covers all *.${BASE_DOMAIN}"
|
||||
if [ "$mode" = airgap ]; then
|
||||
for n in "${spoof[@]}"; do printf 'address=/%s/%s\n' "$n" "$HOST_LAN_IP"; done
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "## Minecraft (no port needed)"
|
||||
echo "# Clients connect to mc.${BASE_DOMAIN} (defaults to :25565)."
|
||||
echo "# Optional SRV if your DNS supports it:"
|
||||
echo "_minecraft._tcp.mc.${BASE_DOMAIN}. IN SRV 0 0 25565 mc.${BASE_DOMAIN}."
|
||||
@@ -3,7 +3,7 @@
|
||||
# offline LAN hosting, then create stable-name symlinks for the landing page.
|
||||
#
|
||||
# Guests use FjordLauncherUnlocked (Prism/PolyMC fork with authlib-injector
|
||||
# built in). We mirror every release asset under mirror/launcher/<tag>/ and
|
||||
# built in). We mirror every release asset under launcher/<tag>/ and
|
||||
# maintain a `latest` symlink → <tag>. Caddy serves this at /launcher/latest/…
|
||||
#
|
||||
# The landing page (landing/src/data/site.ts) links to FIXED filenames so the
|
||||
@@ -16,11 +16,11 @@
|
||||
# Cautious by design: halts on any error, and if a stable-name category matches
|
||||
# 0 or >1 candidate it prints the candidates and exits non-zero rather than
|
||||
# guessing. Idempotent — re-runs overwrite symlinks and re-download safely.
|
||||
# mirror/ is gitignored — only this script is tracked.
|
||||
# launcher/ is gitignored — only this script is tracked.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="hero-persson/FjordLauncherUnlocked"
|
||||
DEST_ROOT="$(dirname "$0")/../mirror/launcher"
|
||||
DEST_ROOT="$(dirname "$0")/../launcher"
|
||||
API="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
command -v curl >/dev/null || { echo "curl required" >&2; exit 1; }
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mdns-host-setup.sh — pin the HOST avahi-daemon to the LAN interface so it
|
||||
# stops self-colliding across docker bridges ("Local name collision"), which
|
||||
# is what blocks the mDNS (.local) path on a host with many interfaces.
|
||||
#
|
||||
# Detects the LAN NIC from HOST_LAN_IP (.env), sets allow-interfaces=<nic> in
|
||||
# /etc/avahi/avahi-daemon.conf (idempotent, backs up first), restarts avahi.
|
||||
# Re-execs itself with sudo if not root. Run: tooling/mdns-host-setup.sh
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset (set in .env)}"
|
||||
|
||||
nic="$(ip -o -4 addr show | awk -v ip="$HOST_LAN_IP" 'index($0, " "ip"/"){print $2; exit}')"
|
||||
[ -n "$nic" ] || { echo "ERROR: no interface found with IP $HOST_LAN_IP" >&2; exit 1; }
|
||||
echo "LAN interface for ${HOST_LAN_IP}: ${nic}"
|
||||
|
||||
conf=/etc/avahi/avahi-daemon.conf
|
||||
[ -f "$conf" ] || { echo "ERROR: $conf not found — is avahi-daemon installed?" >&2; exit 1; }
|
||||
|
||||
# Need root to edit the conf + restart the service.
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "escalating with sudo…"
|
||||
exec sudo "$0" "$@"
|
||||
fi
|
||||
|
||||
# Already pinned to this NIC? nothing to do.
|
||||
if grep -qE "^allow-interfaces=${nic}([,[:space:]]|$)" "$conf"; then
|
||||
echo "already set: allow-interfaces=${nic}"
|
||||
else
|
||||
cp -a "$conf" "${conf}.bak.$(date +%s)"
|
||||
if grep -qE '^allow-interfaces=' "$conf"; then
|
||||
sed -i -E "s/^allow-interfaces=.*/allow-interfaces=${nic}/" "$conf"
|
||||
elif grep -qE '^#\s*allow-interfaces=' "$conf"; then
|
||||
sed -i -E "s/^#\s*allow-interfaces=.*/allow-interfaces=${nic}/" "$conf"
|
||||
else
|
||||
# insert right after the [server] section header
|
||||
sed -i -E "/^\[server\]/a allow-interfaces=${nic}" "$conf"
|
||||
fi
|
||||
echo "set allow-interfaces=${nic} (backup: ${conf}.bak.*)"
|
||||
fi
|
||||
|
||||
echo "restarting avahi-daemon…"
|
||||
systemctl restart avahi-daemon
|
||||
sleep 1
|
||||
systemctl is-active --quiet avahi-daemon && echo "avahi-daemon active" || { echo "ERROR: avahi-daemon not active" >&2; exit 1; }
|
||||
grep -E '^allow-interfaces=' "$conf"
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mirror-airgap.sh — populate the byte-exact upstream mirror for full client air-gap.
|
||||
#
|
||||
# Architecture (see plan/11-full-airgap-mirror.md): dnsmasq spoofs the real
|
||||
# upstream hostnames -> our host; Caddy serves byte-exact path mirrors over HTTPS
|
||||
# with `tls internal`. The launcher is UNMODIFIED — it requests the real Mojang
|
||||
# URLs and DNS resolves them to us. Mirror files live at:
|
||||
# mirror/upstream/<host>/<path> (== the real URL path, byte-for-byte)
|
||||
# `mirror/` is gitignored.
|
||||
#
|
||||
# ── What this script does DETERMINISTICALLY (no proxy needed) ────────────────
|
||||
# Everything reachable purely from the Mojang piston metadata graph for a given
|
||||
# MC version:
|
||||
# 1. version_manifest_v2.json (piston-meta.mojang.com)
|
||||
# 2. the per-version <id>.json (piston-meta.mojang.com)
|
||||
# 3. client.jar (piston-data.mojang.com) — sha1 verified
|
||||
# 4. every library artifact (libraries.minecraft.net / url-derived host) — sha1 verified
|
||||
# 5. the asset index (piston-meta / piston-data, real path) — sha1 verified
|
||||
# 6. every asset object (resources.download.minecraft.net/<2hex>/<hash>) — sha1 verified, idempotent
|
||||
# All downloads go to a .partial file, are sha1-checked, then atomically moved.
|
||||
# Any fetch failure or sha1 mismatch HALTS the script (set -euo pipefail).
|
||||
#
|
||||
# ── What this script CANNOT do here — the MANUAL PROXY-CAPTURE step ──────────
|
||||
# Two upstream surfaces are NOT enumerable from the piston graph and require the
|
||||
# online proxy-capture procedure documented in plan/11-full-airgap-mirror.md
|
||||
# ("Capture (the actual work)"):
|
||||
#
|
||||
# * meta.prismlauncher.org — Prism-format component metadata (the launcher's
|
||||
# own index of MC / LWJGL / NeoForge components).
|
||||
# Layout is launcher-specific (/v1/...), versioned,
|
||||
# and only knowable by observing what the launcher
|
||||
# actually requests.
|
||||
# * maven.neoforged.net — NeoForge installer + the NeoForge-processed
|
||||
# libraries (and possibly extra maven mirrors the
|
||||
# NeoForge install pulls). The exact artifact set
|
||||
# depends on the launcher/installer run.
|
||||
#
|
||||
# For those: launch the instance once online behind a logging forward/reverse
|
||||
# proxy (mitmproxy or a logging Caddy), collect every absolute URL hit, then
|
||||
# fetch each into mirror/upstream/<host>/<path> preserving the path exactly and
|
||||
# verifying sha1 where the source provides it. This script prints a MANUAL STEP
|
||||
# notice at the end as a reminder.
|
||||
#
|
||||
# Requires: curl, jq, sha1sum.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
|
||||
# --- env (optional) --------------------------------------------------------
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
|
||||
MC_VERSION="${MC_VERSION:-1.21.1}"
|
||||
NEOFORGE_VERSION="${NEOFORGE_VERSION:-21.1.209}" # informational; NeoForge libs come from capture
|
||||
|
||||
# --- required tools --------------------------------------------------------
|
||||
for tool in curl jq sha1sum; do
|
||||
command -v "$tool" >/dev/null 2>&1 || { echo "error: '$tool' is required but not found in PATH" >&2; exit 1; }
|
||||
done
|
||||
|
||||
MIRROR_ROOT="mirror/upstream"
|
||||
|
||||
MANIFEST_HOST="piston-meta.mojang.com"
|
||||
MANIFEST_PATH="mc/game/version_manifest_v2.json"
|
||||
MANIFEST_URL="https://${MANIFEST_HOST}/${MANIFEST_PATH}"
|
||||
RESOURCES_HOST="resources.download.minecraft.net"
|
||||
|
||||
echo "mirror-airgap: MC_VERSION=${MC_VERSION} NEOFORGE_VERSION=${NEOFORGE_VERSION} (NeoForge handled by capture)"
|
||||
echo "mirror root: ${MIRROR_ROOT}/"
|
||||
|
||||
# --- helpers ---------------------------------------------------------------
|
||||
sha1_of() { sha1sum "$1" | awk '{print $1}'; }
|
||||
|
||||
# host + path from an absolute https?:// URL (path has no leading slash).
|
||||
url_host() { printf '%s' "$1" | sed -E 's#^[a-zA-Z]+://([^/]+)/.*$#\1#'; }
|
||||
url_path() { printf '%s' "$1" | sed -E 's#^[a-zA-Z]+://[^/]+/##'; }
|
||||
|
||||
# Download $url into mirror/upstream/<host>/<path>, verifying sha1 ($want_sha1,
|
||||
# may be empty to skip verification). Idempotent: a present file whose sha1
|
||||
# already matches is skipped. Halts on any failure or mismatch.
|
||||
# Echoes the destination path on stdout (last line) for callers that need it.
|
||||
fetch_to_mirror() { # $1=url $2=want_sha1(optional)
|
||||
local url="$1" want="${2:-}"
|
||||
local host path dest tmp got
|
||||
host="$(url_host "$url")"
|
||||
path="$(url_path "$url")"
|
||||
[ -n "$host" ] || { echo "error: could not parse host from url: $url" >&2; exit 1; }
|
||||
[ -n "$path" ] || { echo "error: could not parse path from url: $url" >&2; exit 1; }
|
||||
dest="${MIRROR_ROOT}/${host}/${path}"
|
||||
|
||||
if [ -f "$dest" ]; then
|
||||
if [ -n "$want" ]; then
|
||||
got="$(sha1_of "$dest")"
|
||||
if [ "$got" = "$want" ]; then
|
||||
printf '%s\n' "$dest"
|
||||
return 0
|
||||
fi
|
||||
echo " re-fetch ${host}/${path} (local sha1 mismatch)" >&2
|
||||
else
|
||||
# no sha1 to check (e.g. manifest/version json) — re-fetch to stay fresh
|
||||
:
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
tmp="${dest}.partial"
|
||||
if ! curl -fSL --retry 3 -o "$tmp" "$url"; then
|
||||
rm -f "$tmp"
|
||||
echo "error: download failed: $url" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$want" ]; then
|
||||
got="$(sha1_of "$tmp")"
|
||||
if [ "$got" != "$want" ]; then
|
||||
rm -f "$tmp"
|
||||
echo "error: sha1 mismatch for $url" >&2
|
||||
echo " expected: $want" >&2
|
||||
echo " got: $got" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mv -f "$tmp" "$dest"
|
||||
printf '%s\n' "$dest"
|
||||
}
|
||||
|
||||
# --- 1. version manifest ---------------------------------------------------
|
||||
echo "[1/6] version manifest"
|
||||
manifest_dest="$(fetch_to_mirror "$MANIFEST_URL" "" | tail -n1)"
|
||||
|
||||
version_url="$(jq -r --arg v "$MC_VERSION" '.versions[] | select(.id == $v) | .url' "$manifest_dest")"
|
||||
[ -n "$version_url" ] && [ "$version_url" != "null" ] || {
|
||||
echo "error: MC version '$MC_VERSION' not found in version manifest" >&2; exit 1; }
|
||||
echo " version json: $version_url"
|
||||
|
||||
# --- 2. per-version json ---------------------------------------------------
|
||||
echo "[2/6] version json"
|
||||
version_dest="$(fetch_to_mirror "$version_url" "" | tail -n1)"
|
||||
|
||||
# --- 3. client.jar ---------------------------------------------------------
|
||||
echo "[3/6] client.jar"
|
||||
client_url="$(jq -r '.downloads.client.url' "$version_dest")"
|
||||
client_sha1="$(jq -r '.downloads.client.sha1' "$version_dest")"
|
||||
[ -n "$client_url" ] && [ "$client_url" != "null" ] || { echo "error: no .downloads.client.url in version json" >&2; exit 1; }
|
||||
fetch_to_mirror "$client_url" "$client_sha1" >/dev/null
|
||||
echo " ok client.jar (sha1 verified)"
|
||||
|
||||
# --- 4. libraries ----------------------------------------------------------
|
||||
echo "[4/6] libraries"
|
||||
lib_count="$(jq '[.libraries[] | select(.downloads.artifact != null)] | length' "$version_dest")"
|
||||
echo " $lib_count library artifact(s)"
|
||||
# Emit "url<TAB>sha1" per artifact; host+path derived from the url itself so
|
||||
# whatever maven host appears (libraries.minecraft.net or others) is mirrored.
|
||||
jq -r '.libraries[] | select(.downloads.artifact != null)
|
||||
| "\(.downloads.artifact.url)\t\(.downloads.artifact.sha1)"' "$version_dest" \
|
||||
| while IFS=$'\t' read -r lurl lsha1; do
|
||||
[ -n "$lurl" ] || continue
|
||||
fetch_to_mirror "$lurl" "$lsha1" >/dev/null
|
||||
done
|
||||
echo " ok libraries (sha1 verified)"
|
||||
|
||||
# --- 5. asset index --------------------------------------------------------
|
||||
echo "[5/6] asset index"
|
||||
ai_url="$(jq -r '.assetIndex.url' "$version_dest")"
|
||||
ai_sha1="$(jq -r '.assetIndex.sha1' "$version_dest")"
|
||||
ai_id="$(jq -r '.assetIndex.id' "$version_dest")"
|
||||
[ -n "$ai_url" ] && [ "$ai_url" != "null" ] || { echo "error: no .assetIndex.url in version json" >&2; exit 1; }
|
||||
echo " asset index id=$ai_id"
|
||||
ai_dest="$(fetch_to_mirror "$ai_url" "$ai_sha1" | tail -n1)"
|
||||
|
||||
# --- 6. asset objects ------------------------------------------------------
|
||||
echo "[6/6] asset objects (the bulk — this is the large/slow part)"
|
||||
obj_total="$(jq '.objects | length' "$ai_dest")"
|
||||
echo " $obj_total object(s) -> ${RESOURCES_HOST}/<2hex>/<hash>"
|
||||
n=0
|
||||
# Each object: hash -> served at resources.download.minecraft.net/<first2>/<hash>;
|
||||
# sha1 == the hash itself. Idempotent: fetch_to_mirror skips valid existing files.
|
||||
jq -r '.objects[].hash' "$ai_dest" \
|
||||
| while read -r hash; do
|
||||
[ -n "$hash" ] || continue
|
||||
n=$((n + 1))
|
||||
prefix="${hash:0:2}"
|
||||
obj_url="https://${RESOURCES_HOST}/${prefix}/${hash}"
|
||||
fetch_to_mirror "$obj_url" "$hash" >/dev/null
|
||||
done
|
||||
echo " ok asset objects (sha1 verified, idempotent)"
|
||||
|
||||
# --- manual step notice ----------------------------------------------------
|
||||
cat >&2 <<'NOTICE'
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
MANUAL STEP REQUIRED — proxy-capture (see plan/11-full-airgap-mirror.md)
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
This script mirrored the deterministic Mojang piston graph only:
|
||||
piston-meta.mojang.com, piston-data.mojang.com, libraries.minecraft.net,
|
||||
resources.download.minecraft.net.
|
||||
|
||||
The following are NOT enumerable from the piston graph and were NOT fetched:
|
||||
* meta.prismlauncher.org — Prism-format component metadata (/v1/...)
|
||||
* maven.neoforged.net — NeoForge installer + processed libraries
|
||||
(plus any extra maven mirrors the install pulls)
|
||||
|
||||
To complete the air-gap mirror, run the capture procedure from
|
||||
plan/11-full-airgap-mirror.md ("Capture (the actual work)"):
|
||||
1. Launch the instance ONCE online behind a logging forward/reverse proxy
|
||||
(mitmproxy or a logging Caddy). Collect every absolute URL requested.
|
||||
2. Fetch each into mirror/upstream/<host>/<path>, preserving the path
|
||||
exactly; verify sha1 where the source json provides it.
|
||||
3. Offline re-test on a clean machine (internet cut, LAN DNS = dnsmasq,
|
||||
Caddy root CA trusted). Any 404 -> a missing URL -> add it to the fetch.
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
NOTICE
|
||||
|
||||
echo "deterministic mirror complete for MC ${MC_VERSION}."
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mirror-mods.sh — vendor mod jars locally + rewrite packwiz download URLs.
|
||||
#
|
||||
# For offline (air-gapped) LAN play, packwiz .pw.toml files point at the
|
||||
# Modrinth/CF CDN. This script downloads each jar into pack/mods-files/,
|
||||
# rewrites the [download] url to the local Caddy mirror, then runs
|
||||
# `packwiz refresh` so index.toml/hashes match the rewritten state.
|
||||
#
|
||||
# Cautious by design: halts on download failure or hash mismatch, idempotent
|
||||
# (skips already-vendored valid jars), and no-ops gracefully with zero mods.
|
||||
# pack/mods-files/ is gitignored — only the .pw.toml metadata is tracked.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.." # repo root
|
||||
|
||||
# --- env -------------------------------------------------------------------
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
||||
|
||||
# --- packwiz binary --------------------------------------------------------
|
||||
PACKWIZ="/home/oier/go/bin/packwiz"
|
||||
[ -x "$PACKWIZ" ] || PACKWIZ="$(command -v packwiz || true)"
|
||||
[ -n "$PACKWIZ" ] || { echo "error: packwiz not found (looked at /home/oier/go/bin/packwiz and PATH)" >&2; exit 1; }
|
||||
|
||||
PACK_DIR="pack"
|
||||
MODS_META_DIR="$PACK_DIR/mods"
|
||||
MODS_FILES_DIR="$PACK_DIR/mods-files"
|
||||
MIRROR_BASE="http://packwiz.${BASE_DOMAIN}/mods-files"
|
||||
|
||||
[ -f "$PACK_DIR/pack.toml" ] || { echo "error: $PACK_DIR/pack.toml not found — run packwiz init first" >&2; exit 1; }
|
||||
|
||||
# --- collect .pw.toml files ------------------------------------------------
|
||||
shopt -s nullglob
|
||||
metas=("$MODS_META_DIR"/*.pw.toml)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "${#metas[@]}" -eq 0 ]; then
|
||||
echo "no mods in $MODS_META_DIR/ — nothing to mirror (no-op)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$MODS_FILES_DIR"
|
||||
|
||||
# --- helpers ---------------------------------------------------------------
|
||||
# Read a top-level scalar TOML key (filename) from a .pw.toml.
|
||||
toml_top_value() { # $1=file $2=key
|
||||
sed -n "s/^[[:space:]]*$2[[:space:]]*=[[:space:]]*\"\(.*\)\"[[:space:]]*\$/\1/p" "$1" | head -n1
|
||||
}
|
||||
|
||||
# Read a key from the [download] table only.
|
||||
toml_download_value() { # $1=file $2=key
|
||||
awk -v key="$2" '
|
||||
/^\[/ { in_dl = ($0 ~ /^\[download\]/) ; next }
|
||||
in_dl {
|
||||
line=$0
|
||||
sub(/^[ \t]*/, "", line)
|
||||
if (line ~ "^" key "[ \t]*=") {
|
||||
sub("^" key "[ \t]*=[ \t]*", "", line)
|
||||
gsub(/^"|"[ \t]*$/, "", line)
|
||||
print line
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
|
||||
changed=0
|
||||
echo "mirroring ${#metas[@]} mod(s) → $MIRROR_BASE"
|
||||
|
||||
for meta in "${metas[@]}"; do
|
||||
filename="$(toml_top_value "$meta" filename)"
|
||||
url="$(toml_download_value "$meta" url)"
|
||||
hash_format="$(toml_download_value "$meta" hash-format)"
|
||||
want_hash="$(toml_download_value "$meta" hash)"
|
||||
|
||||
[ -n "$filename" ] || { echo "error: no filename in $meta" >&2; exit 1; }
|
||||
[ -n "$url" ] || { echo "error: no [download] url in $meta" >&2; exit 1; }
|
||||
|
||||
dest="$MODS_FILES_DIR/$filename"
|
||||
already_local="$MIRROR_BASE/$filename"
|
||||
|
||||
# If the .pw.toml already points at the mirror and the jar exists & verifies,
|
||||
# skip (idempotent re-run).
|
||||
if [ "$url" = "$already_local" ] && [ -f "$dest" ]; then
|
||||
if [ "${hash_format:-sha256}" = "sha256" ] && [ -n "$want_hash" ]; then
|
||||
if [ "$(sha256_of "$dest")" = "$want_hash" ]; then
|
||||
echo " skip $filename (already vendored, hash ok)"
|
||||
continue
|
||||
fi
|
||||
echo " re-fetch $filename (local hash mismatch)"
|
||||
else
|
||||
echo " skip $filename (already vendored)"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Download (from current url — CDN on first run). Use a temp file so a failed
|
||||
# download never leaves a partial jar behind.
|
||||
echo " fetch $filename"
|
||||
tmp="$dest.partial"
|
||||
if ! curl -fSL --retry 3 -o "$tmp" "$url"; then
|
||||
rm -f "$tmp"
|
||||
echo "error: download failed for $filename ($url)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify against the recorded hash before trusting the file.
|
||||
if [ "${hash_format:-sha256}" = "sha256" ] && [ -n "$want_hash" ]; then
|
||||
got_hash="$(sha256_of "$tmp")"
|
||||
if [ "$got_hash" != "$want_hash" ]; then
|
||||
rm -f "$tmp"
|
||||
echo "error: hash mismatch for $filename" >&2
|
||||
echo " expected: $want_hash" >&2
|
||||
echo " got: $got_hash" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mv -f "$tmp" "$dest"
|
||||
|
||||
# Rewrite the [download] url to the local mirror if not already.
|
||||
if [ "$url" != "$already_local" ]; then
|
||||
esc_old="$(printf '%s' "$url" | sed -e 's/[\/&]/\\&/g')"
|
||||
esc_new="$(printf '%s' "$already_local" | sed -e 's/[\/&]/\\&/g')"
|
||||
sed -i "s/^\([[:space:]]*url[[:space:]]*=[[:space:]]*\)\"$esc_old\"/\1\"$esc_new\"/" "$meta"
|
||||
echo " url → $already_local"
|
||||
changed=1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- refresh so index.toml + hashes reflect the rewritten metadata ---------
|
||||
echo "running packwiz refresh ..."
|
||||
( cd "$PACK_DIR" && "$PACKWIZ" refresh )
|
||||
|
||||
echo "done. mirrored ${#metas[@]} mod(s)$( [ "$changed" -eq 1 ] && echo ', URLs rewritten to local mirror' )."
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-config.sh — turn *.tmpl files into real configs via envsubst.
|
||||
# Substitutes ONLY ${BASE_DOMAIN} and ${HOST_LAN_IP} so literal $-syntax that
|
||||
# belongs to the target file (dnsmasq, caddy, toml) is left untouched.
|
||||
# Substitutes ONLY ${BASE_DOMAIN} so literal $-syntax that belongs to the target
|
||||
# file (caddy, toml) is left untouched.
|
||||
# Halts on any unset var or missing template (cautious: no half-rendered output).
|
||||
set -euo pipefail
|
||||
|
||||
@@ -9,17 +9,15 @@ cd "$(dirname "$0")/.." # repo root
|
||||
|
||||
[ -f .env ] && { set -a; . ./.env; set +a; }
|
||||
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
||||
: "${HOST_LAN_IP:?HOST_LAN_IP unset (set in .env)}"
|
||||
|
||||
render() { # $1=template $2=output
|
||||
[ -f "$1" ] || { echo "missing template: $1" >&2; exit 1; }
|
||||
envsubst '${BASE_DOMAIN} ${HOST_LAN_IP}' < "$1" > "$2"
|
||||
envsubst '${BASE_DOMAIN}' < "$1" > "$2"
|
||||
echo "rendered $2"
|
||||
}
|
||||
|
||||
render drasl/config/config.toml.tmpl drasl/config/config.toml
|
||||
render nmsr/config.toml.tmpl nmsr/config.toml
|
||||
render dnsmasq/dnsmasq.conf.tmpl dnsmasq/dnsmasq.conf
|
||||
|
||||
# pack templates (mods/*.pw.toml.tmpl, CustomSkinLoader, …) + packwiz index.
|
||||
# Delegated to render-pack.sh (BASE_DOMAIN only — single source of pack render).
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-nginx.sh — render an nginx vhost template with the .env values.
|
||||
# render-nginx.sh — render the nginx vhost template with the .env values.
|
||||
#
|
||||
# Substitutes $BASE_DOMAIN, $APP_DIR, $CADDY_HTTP_PORT into a template and
|
||||
# prints the result to stdout, or installs it with --install (writes to
|
||||
# sites-available, symlinks sites-enabled, tests, reloads nginx).
|
||||
# Substitutes $BASE_DOMAIN, $APP_DIR, $CADDY_HTTP_PORT into
|
||||
# nginx/ulicraft-caddy.conf.tmpl (the host nginx that terminates TLS in front of
|
||||
# caddy) and prints the result to stdout, or installs it with --install (writes
|
||||
# to sites-available, symlinks sites-enabled, tests, reloads nginx).
|
||||
#
|
||||
# Usage:
|
||||
# tooling/render-nginx.sh # render caddy-front template -> stdout
|
||||
# tooling/render-nginx.sh # render -> stdout
|
||||
# tooling/render-nginx.sh --install # render + install + reload nginx
|
||||
# TEMPLATE=nginx/ulicraft.conf.tmpl tooling/render-nginx.sh # the static variant
|
||||
#
|
||||
# .env vars used:
|
||||
# BASE_DOMAIN required
|
||||
# CADDY_HTTP_PORT caddy-front template only (localhost port nginx proxies to)
|
||||
# CADDY_HTTP_PORT localhost port nginx reverse-proxies to (caddy)
|
||||
# optional:
|
||||
# APP_DIR host path to this repo (cert files live under APP_DIR/certs)
|
||||
# — defaults to the repo root (this checkout)
|
||||
# TEMPLATE template to render (default nginx/ulicraft-caddy.conf.tmpl)
|
||||
# SITE_NAME installed filename (default ulicraft.conf)
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# ${BASE_DOMAIN}, then rebuild the packwiz index.
|
||||
#
|
||||
# Pack templates are domain-dependent build output (download URLs, skin API
|
||||
# roots, …). This renderer needs ONLY ${BASE_DOMAIN} — no dnsmasq HOST_LAN_IP —
|
||||
# so it can run while curating mods before the full deploy config exists.
|
||||
# roots, …). This renderer needs ONLY ${BASE_DOMAIN}, so it can run while
|
||||
# curating mods before the full deploy config exists.
|
||||
# Both tooling/render-config.sh (deploy) and tooling/add-custom-mod.sh call it.
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
Reference in New Issue
Block a user