Related to #698: harden installer release detection

This commit is contained in:
rcourtman 2025-11-12 17:56:16 +00:00
parent 798f91792b
commit 4a0637957c
2 changed files with 119 additions and 8 deletions

View file

@ -36,6 +36,41 @@ DETECTED_CTID=""
DEBIAN_TEMPLATE_FALLBACK="debian-12-standard_12.12-1_amd64.tar.zst"
DEBIAN_TEMPLATE=""
get_latest_release_from_redirect() {
# Follow the GitHub "latest" redirect and extract the tag in a way that
# tolerates intermediate redirects that omit /tag/ (issue #698).
local target_url="${1:-https://github.com/$GITHUB_REPO/releases/latest}"
local effective_url=""
local curl_cmd=(curl -fsSL --connect-timeout 5 --max-time 10 -o /dev/null -w '%{url_effective}' "$target_url")
if command -v timeout >/dev/null 2>&1; then
effective_url=$(timeout 10 "${curl_cmd[@]}" 2>/dev/null || true)
else
effective_url=$("${curl_cmd[@]}" 2>/dev/null || true)
fi
# Strip stray carriage returns so string comparisons behave under set -u
effective_url="${effective_url//$'\r'/}"
if [[ -z "$effective_url" ]]; then
return 1
fi
local tag=""
if [[ "$effective_url" =~ /tag/([^/?#]+) ]]; then
tag="${BASH_REMATCH[1]}"
elif [[ "$effective_url" =~ /download/([^/?#]+)/ ]]; then
tag="${BASH_REMATCH[1]}"
fi
if [[ -z "$tag" ]]; then
return 1
fi
printf '%s\n' "$tag"
return 0
}
detect_lxc_ctid() {
local ctid=""
@ -1655,10 +1690,10 @@ download_pulse() {
# Fallback: Try direct GitHub redirect if API fails
if [[ -z "$LATEST_RELEASE" ]]; then
print_info "GitHub API unavailable, trying alternative method..."
if command -v timeout >/dev/null 2>&1; then
LATEST_RELEASE=$(timeout 10 curl -sI --connect-timeout 5 --max-time 10 https://github.com/$GITHUB_REPO/releases/latest 2>/dev/null | grep -i '^location:.*\/tag\/' | sed -E 's|.*tag/([^[:space:]]+).*|\1|' | tr -d '\r' || true)
else
LATEST_RELEASE=$(curl -sI --connect-timeout 5 --max-time 10 https://github.com/$GITHUB_REPO/releases/latest 2>/dev/null | grep -i '^location:.*\/tag\/' | sed -E 's|.*tag/([^[:space:]]+).*|\1|' | tr -d '\r' || true)
local redirect_version=""
redirect_version=$(get_latest_release_from_redirect 2>/dev/null || true)
if [[ -n "$redirect_version" ]]; then
LATEST_RELEASE="$redirect_version"
fi
fi
@ -2797,10 +2832,10 @@ main() {
# If rate limited or failed, try direct GitHub latest URL
if [[ -z "$STABLE_VERSION" ]] || [[ "$STABLE_VERSION" == *"rate limit"* ]]; then
# Use the GitHub latest release redirect to get version
if command -v timeout >/dev/null 2>&1; then
STABLE_VERSION=$(timeout 10 curl -sI --connect-timeout 5 --max-time 10 https://github.com/$GITHUB_REPO/releases/latest 2>/dev/null | grep -i '^location:' | sed -E 's|.*tag/([^[:space:]]+).*|\1|' | tr -d '\r' || true)
else
STABLE_VERSION=$(curl -sI --connect-timeout 5 --max-time 10 https://github.com/$GITHUB_REPO/releases/latest 2>/dev/null | grep -i '^location:' | sed -E 's|.*tag/([^[:space:]]+).*|\1|' | tr -d '\r' || true)
local redirect_version=""
redirect_version=$(get_latest_release_from_redirect 2>/dev/null || true)
if [[ -n "$redirect_version" ]]; then
STABLE_VERSION="$redirect_version"
fi
fi

View file

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Remote release validator.
# Downloads the published (or draft) assets straight from GitHub Releases,
# recalculates their SHA256 sums, and ensures checksums.txt and the *.sha256
# helper files match what is actually live. This prevents broken updates when
# artifacts are re-uploaded without regenerating checksums (see issue #698).
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <tag> [owner/repo]" >&2
echo "Example: $0 v4.28.0 rcourtman/Pulse" >&2
exit 1
fi
TAG="$1"
REPO="${2:-rcourtman/Pulse}"
BASE_URL="https://github.com/${REPO}/releases/download/${TAG}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
curl_args=(curl -fsSL --connect-timeout 10 --max-time 600 --retry 3 --retry-delay 2 --retry-all-errors)
CHECKSUMS_PATH="${TMP_DIR}/checksums.txt"
echo "Downloading ${BASE_URL}/checksums.txt"
if ! "${curl_args[@]}" "${BASE_URL}/checksums.txt" >"$CHECKSUMS_PATH"; then
echo "Failed to download checksums.txt for ${TAG}" >&2
exit 1
fi
status=0
while read -r checksum filename _; do
[[ -z "${checksum:-}" ]] && continue
[[ "$checksum" =~ ^# ]] && continue
if [[ -z "${filename:-}" ]]; then
echo "Malformed checksums line (missing filename): $checksum" >&2
status=1
continue
fi
artifact_url="${BASE_URL}/${filename}"
echo "Verifying ${filename}..."
if ! actual_checksum=$("${curl_args[@]}" "$artifact_url" | sha256sum | awk '{print $1}'); then
echo "Failed to download ${filename}" >&2
status=$((status + 1))
continue
fi
if [[ "$actual_checksum" != "$checksum" ]]; then
echo "Checksum mismatch for ${filename}: expected ${checksum}, got ${actual_checksum}" >&2
status=$((status + 1))
fi
sha_url="${artifact_url}.sha256"
if ! sha_content=$("${curl_args[@]}" "$sha_url" | tr -d '\r' | sed 's/[[:space:]]*$//'); then
echo "Failed to download ${filename}.sha256" >&2
status=$((status + 1))
continue
fi
expected_line="${checksum} ${filename}"
if [[ "$sha_content" != "$expected_line" ]]; then
echo "${filename}.sha256 content mismatch (expected '${expected_line}', got '${sha_content}')" >&2
status=$((status + 1))
fi
done < "$CHECKSUMS_PATH"
if [[ "$status" -ne 0 ]]; then
echo "Published release validation failed for ${TAG} (${status} error(s))." >&2
exit 1
fi
echo "Published release assets for ${TAG} match checksums.txt and *.sha256 files."