#!/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 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]+$'; } # 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: # 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}"; } 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 }