footprint_update_needed only ever came from comparing the installed marker against a channel manifest, so it could not fire on a git or local install — they have no channel to ask. Those are exactly the installs whose code tree is synced by hand, i.e. the ones most able to drift, and the drift was silent: the helpers in /usr/local/lib/libreportal could sit behind the code that calls them with nothing reporting it. That is how this box ended up running a crowdsec helper with no bouncer-traefik-rotate action while the tool that needs it shipped. init.sh is what bakes the marker, so the install tree's own init.sh is authoritative for every mode. lpInstallTreeFootprintVersion reads it and lpFootprintStale compares. Wired into both non-release branches of the WebUI status generator, and into the local branch of the interactive update check, which is where a local operator actually looks. Fails safe: a tree older than the marker, or a missing init.sh, reports current rather than warning — verified alongside the real stale case. Also gives webuiSystemUpdateCheck the self-reload guard webuiGenerateAppsToolsConfig already documents. The WebUI task service sources these once at startup, so without it an edited generator keeps writing the old JSON from memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
9.2 KiB
Bash
173 lines
9.2 KiB
Bash
#!/bin/bash
|
|
#
|
|
# LibrePortal source-fetch helpers for the RUNTIME paths (update / reinstall /
|
|
# reset). They place a fresh copy of the code at $script_dir (the install tree,
|
|
# which holds ONLY code — configs/logs live in the separate system tree, so the
|
|
# install tree can be replaced wholesale).
|
|
#
|
|
# Three modes, keyed off CFG_INSTALL_MODE:
|
|
# release download a versioned, checksum-verified tarball over HTTPS (default)
|
|
# git clone (handled by the existing git_* helpers / initGIT)
|
|
# local copy from a local folder (existing copyFilesFromLocal)
|
|
#
|
|
# init.sh's initGIT does the FRESH-install fetch inline (it is self-contained for
|
|
# the bare /root reinstall copy); these functions are the sourced runtime twins.
|
|
|
|
# Release host + channel: env wins (testing), then config, then the public default.
|
|
lpReleaseBaseUrl() { echo "${LP_RELEASE_BASE_URL:-${CFG_RELEASE_BASE_URL:-https://get.libreportal.org}}"; }
|
|
lpReleaseChannel() { echo "${LP_RELEASE_CHANNEL:-${CFG_RELEASE_CHANNEL:-stable}}"; }
|
|
|
|
# Downloader + checksum tool detection (curl|wget, sha256sum|shasum).
|
|
_lpFetchTool() { command -v curl >/dev/null 2>&1 && { echo curl; return; }; command -v wget >/dev/null 2>&1 && { echo wget; return; }; echo ""; }
|
|
_lpDownload() { # _lpDownload <url> <outfile|->
|
|
local tool; tool="$(_lpFetchTool)"
|
|
case "$tool" in
|
|
curl) [[ "$2" == "-" ]] && curl -fsSL "$1" || curl -fsSL "$1" -o "$2" ;;
|
|
wget) [[ "$2" == "-" ]] && wget -qO- "$1" || wget -qO "$2" "$1" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
_lpSha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1; fi; }
|
|
_lpJsonStr() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/'; }
|
|
_lpJsonNum() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*[0-9]+" | head -1 | grep -oE '[0-9]+$'; }
|
|
# JSON-escape a scalar for safe inline embedding (backslash first, then quote).
|
|
# Used by the jq-free fallbacks (history fallback, artifact applied-records).
|
|
_lpJsonEsc() { local s="${1//\\/\\\\}"; s="${s//\"/\\\"}"; printf '%s' "$s"; }
|
|
|
|
# Verify a downloaded file against a detached minisig using the ROOT-OWNED
|
|
# footprint public key (/usr/local/lib/libreportal/libreportal.pub). The key is
|
|
# in the footprint so the manager can't swap it to bless a forgery. Trust posture
|
|
# matches lpVerifyInstall (verify.sh): once the key is REAL (not the REPLACE_ME
|
|
# placeholder) a valid signature is MANDATORY; an unactivated/dev build skips it.
|
|
# This is the single trust anchor shared by the release fetch (lpFetchRelease)
|
|
# and the artifact-index fetch (lpFetchIndex, source/artifacts.sh).
|
|
# Args: <file> <sigfile>
|
|
# Returns 0 = OK to proceed (signature verified, OR signing not activated);
|
|
# 1 = REFUSE (key real but minisign missing / sig missing / sig invalid).
|
|
# Echoes "verified" or "unsigned" for the caller's audit line.
|
|
lpVerifyMinisig() {
|
|
local file="$1" sig="$2"
|
|
local pub="/usr/local/lib/libreportal/libreportal.pub"
|
|
if [[ ! -f "$pub" ]] || grep -q REPLACE_ME "$pub" 2>/dev/null; then
|
|
echo unsigned; return 0
|
|
fi
|
|
if ! command -v minisign >/dev/null 2>&1; then
|
|
isError "lpVerifyMinisig: minisign required to verify '$file' but not installed"; return 1
|
|
fi
|
|
if [[ ! -f "$sig" ]]; then
|
|
isError "lpVerifyMinisig: signature for '$file' missing — refusing"; return 1
|
|
fi
|
|
if ! minisign -Vm "$file" -p "$pub" -x "$sig" >/dev/null 2>&1; then
|
|
isError "lpVerifyMinisig: SIGNATURE INVALID for '$file' — refusing"; return 1
|
|
fi
|
|
echo verified; return 0
|
|
}
|
|
|
|
# Root-owned-footprint version: the one INSTALLED on this box (marker written by
|
|
# init.sh) vs the one the channel's latest release ships (manifest). When the
|
|
# latter is greater, a plain manager-run update can't apply it (it can't rewrite
|
|
# the root-owned helpers/wrapper/unit) — a root re-install is required.
|
|
lpInstalledFootprintVersion() { local v; v=$(cat /usr/local/lib/libreportal/.footprint_version 2>/dev/null | tr -d ' \t\n\r'); echo "${v:-0}"; }
|
|
|
|
# The footprint version the CODE IN THE INSTALL TREE would bake if init.sh were
|
|
# re-run. This is the only comparison available to a git or local install: those
|
|
# have no channel to ask, so lpReleaseLatestFootprint cannot answer and the
|
|
# staleness never surfaced — the root helpers silently stayed behind the code
|
|
# that expects them. Reading init.sh directly works for every mode, because
|
|
# init.sh is what bakes the marker in the first place.
|
|
lpInstallTreeFootprintVersion() {
|
|
local f="${script_dir%/}/init.sh" v=""
|
|
[[ -f "$f" ]] && v=$(grep -m1 -oE '^footprint_version=[0-9]+' "$f" 2>/dev/null | cut -d= -f2)
|
|
echo "${v:-0}"
|
|
}
|
|
|
|
# True when what is baked into /usr/local/lib/libreportal is older than what the
|
|
# install tree carries, i.e. a root re-install (`sudo <install>/init.sh`) is owed.
|
|
lpFootprintStale() {
|
|
local installed tree
|
|
installed=$(lpInstalledFootprintVersion)
|
|
tree=$(lpInstallTreeFootprintVersion)
|
|
[[ "${tree:-0}" -gt "${installed:-0}" ]]
|
|
}
|
|
lpReleaseLatestFootprint() {
|
|
local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1
|
|
local f; f=$(_lpJsonNum "$m" footprint_version); echo "${f:-0}"
|
|
}
|
|
|
|
# Resolve the latest published version of the configured channel (empty on failure).
|
|
lpReleaseLatestVersion() {
|
|
local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1
|
|
_lpJsonStr "$m" version
|
|
}
|
|
|
|
# Download + verify + extract a release into $script_dir. Arg: optional version
|
|
# (else the channel's latest). Returns non-zero (and leaves $script_dir untouched)
|
|
# on any download/checksum failure — callers must not proceed on failure.
|
|
lpFetchRelease() {
|
|
local want_ver="${1:-}" base channel manifest tarname want_sha tmp tar got
|
|
base="$(lpReleaseBaseUrl)"; channel="$(lpReleaseChannel)"
|
|
[[ -n "$(_lpFetchTool)" ]] || { isError "lpFetchRelease: need curl or wget"; return 1; }
|
|
|
|
if [[ -z "$want_ver" ]]; then
|
|
manifest="$(_lpDownload "$base/$channel/latest.json" - 2>/dev/null)" || { isError "lpFetchRelease: cannot fetch channel manifest"; return 1; }
|
|
want_ver="$(_lpJsonStr "$manifest" version)"
|
|
tarname="$(_lpJsonStr "$manifest" url)"
|
|
want_sha="$(_lpJsonStr "$manifest" sha256)"
|
|
fi
|
|
[[ -n "$want_ver" ]] || { isError "lpFetchRelease: could not determine version"; return 1; }
|
|
[[ -n "$tarname" ]] || tarname="libreportal-${want_ver}.tar.gz"
|
|
|
|
tmp="$(mktemp -d)"; tar="$tmp/$tarname"
|
|
if ! _lpDownload "$base/$channel/$tarname" "$tar"; then isError "lpFetchRelease: download failed ($tarname)"; rm -rf "$tmp"; return 1; fi
|
|
if [[ -z "$want_sha" ]]; then
|
|
_lpDownload "$base/$channel/$tarname.sha256" "$tar.sha256" 2>/dev/null && want_sha="$(cut -d' ' -f1 < "$tar.sha256")"
|
|
fi
|
|
[[ -n "$want_sha" ]] || { isError "lpFetchRelease: no checksum available — refusing"; rm -rf "$tmp"; return 1; }
|
|
got="$(_lpSha256 "$tar")"
|
|
if [[ "$got" != "$want_sha" ]]; then isError "lpFetchRelease: CHECKSUM MISMATCH ($tarname) — refusing"; rm -rf "$tmp"; return 1; fi
|
|
|
|
# Signature: verify against the root-owned footprint key (mandatory once the
|
|
# key is real; skipped only for an unsigned/dev build). Shared trust anchor
|
|
# with the artifact-index path — see lpVerifyMinisig above. Fetch the .minisig
|
|
# first (best-effort) so the verifier can find it; the verifier itself decides
|
|
# whether a missing/invalid signature is fatal.
|
|
_lpDownload "$base/$channel/$tarname.minisig" "$tar.minisig" 2>/dev/null || true
|
|
local sigstate
|
|
if ! sigstate="$(lpVerifyMinisig "$tar" "$tar.minisig")"; then rm -rf "$tmp"; return 1; fi
|
|
[[ "$sigstate" == "verified" ]] && isNotice "Release signature verified."
|
|
|
|
# Replace the install tree (code only; configs/logs are in the system tree).
|
|
runInstallOp rm -rf "$script_dir"
|
|
runInstallOp mkdir -p "$script_dir"
|
|
if ! runInstallOp tar xzf "$tar" -C "$script_dir" --strip-components=1; then
|
|
isError "lpFetchRelease: extract failed"; rm -rf "$tmp"; return 1
|
|
fi
|
|
rm -rf "$tmp"
|
|
isSuccessful "Fetched LibrePortal $want_ver ($channel) and verified its checksum."
|
|
}
|
|
|
|
# Mode-aware runtime fetch. git/local stay with their existing helpers; this adds
|
|
# the release path. Callers (updater/reinstall/reset) decide pre/post (config
|
|
# backup, redeploy) around it.
|
|
lpFetchSource() {
|
|
case "${CFG_INSTALL_MODE:-release}" in
|
|
release) lpFetchRelease "$@" ;;
|
|
local) declare -f copyFilesFromLocal >/dev/null 2>&1 && copyFilesFromLocal ;;
|
|
git) declare -f gitFolderResetAndBackup >/dev/null 2>&1 && gitFolderResetAndBackup ;;
|
|
*) isError "lpFetchSource: unknown CFG_INSTALL_MODE '${CFG_INSTALL_MODE}'"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Semver-ish compare: lpVersionGt A B → true if A > B (numeric dotted compare,
|
|
# trailing non-numeric ignored). Used by the updater + the badge generator.
|
|
lpVersionGt() {
|
|
local a="${1%%-*}" b="${2%%-*}" i x y
|
|
local -a A B; IFS='.' read -r -a A <<< "$a"; IFS='.' read -r -a B <<< "$b"
|
|
for ((i=0; i<${#A[@]} || i<${#B[@]}; i++)); do
|
|
x=$((10#${A[i]:-0} + 0)) 2>/dev/null; y=$((10#${B[i]:-0} + 0)) 2>/dev/null
|
|
(( x > y )) && return 0
|
|
(( x < y )) && return 1
|
|
done
|
|
return 1
|
|
}
|