testsAndMisc/linux_configuration/scripts/periodic_background/hosts/lib/hosts_cache.sh
Krzysztof kuhy Rudnicki 47478791d8
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
hosts/install: split into lib/ under the 250-line cap
913 lines down to 150. The ten functions were only 202 lines; the other 712
were a single top-level block, now eight phase functions across six libs. As
in the previous split the bodies were wrapped WITHOUT re-indenting, because
five heredocs use quoted terminators that must stay at column 0 (shfmt
indented them afterwards, which is safe).

The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was
321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could
not otherwise fit the cap; a hosts-format data file is also far easier to
audit and diff than a quoted heredoc.

That move broke a guard, which is the important part of this commit:

  extract_custom_entries_from_script sliced the blocklist out of the script
  between "# Custom blocking entries" and the heredoc terminator. With the
  entries in a data file there is no terminator, so it would have extracted
  NOTHING -- and check_custom_entries_protection, the guard whose entire job
  is refusing to drop blocked domains, would have compared against an empty
  list and passed everything. It now reads the data file, and the extracted
  set is identical: 254 domains, diffed.

A second, pre-existing bug surfaced while testing:

  extract_unblock_entries_from_script matched its OWN marker lines.
  PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the
  '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they
  were domains. Both are in this machine's live state file right now.
  Renaming either marker would have read as the whitelist growing and blocked
  the install outright.

There is no dry-run or status mode, so this file cannot be proven by running
it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down.
The proof is textual instead. Every one of the 8 wrapped phase bodies is
byte-identical to its original line range; a line-multiset check found 0 lines
lost; and an indentation-insensitive comparison against the pre-split file
leaves exactly 8 absent lines, each an intentional edit (the rewritten
extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback
made overridable, and the heredoc markers). /etc/hosts is byte-unchanged
across the whole session, and `--help` output is identical.

36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and
49/49), covering both directions of each: removing a blocked domain is
refused and adding one allowed; growing the whitelist is refused and shrinking
it allowed. A same-count swap is refused too, which a count-only check would
have let through.

The install phases themselves are deliberately untested -- running them would
take down the protection they install. That is weaker evidence than the two
previous splits had, and it is inherent to this file rather than a gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:45:04 +02:00

92 lines
2.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# lib/hosts_cache.sh — keep the local copy of the upstream StevenBlack list
# current.
#
# The cache holds the RAW upstream file, without our own entries, so a refresh
# never has to re-derive what we added. Sourced by install.sh.
# Helpers
extract_date_epoch_from_file() {
# Grep "# Date:" line and convert to epoch seconds (UTC)
local f="$1"
local line
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
if [[ -n $line ]]; then
date -u -d "$line" +%s 2>/dev/null || echo ""
else
echo ""
fi
}
fetch_remote_header() {
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
local out="$1"
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
return 0
fi
# Fallback – may download more, but we only keep first lines
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
return 0
fi
return 1
}
download_remote_full_to() {
local out="$1"
curl -LfsS "$URL" -o "$out"
}
# Decide whether the cached upstream blocklist is current, and re-download
# it if not. The cache holds the RAW upstream file, without our additions.
refresh_upstream_cache() {
# Decide whether to use cache or update
TMP_REMOTE_HEAD=$(mktemp)
trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
REMOTE_AVAILABLE=0
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
REMOTE_AVAILABLE=1
fi
NEED_UPDATE=0
if [[ -f $LOCAL_CACHE ]]; then
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
else
local_epoch=""
fi
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
echo "Using cached StevenBlack hosts (up-to-date)."
else
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
NEED_UPDATE=1
fi
else
if [[ -f $LOCAL_CACHE ]]; then
echo "No internet; using cached StevenBlack hosts."
else
echo "Error: No internet and no cached StevenBlack hosts found." >&2
exit 1
fi
fi
# Ensure we have a fresh cache if needed
if [[ $NEED_UPDATE -eq 1 ]]; then
TMP_DL=$(mktemp)
if download_remote_full_to "$TMP_DL"; then
# Save raw upstream to cache
sudo mv "$TMP_DL" "$LOCAL_CACHE"
sudo chmod 644 "$LOCAL_CACHE"
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
else
rm -f "$TMP_DL"
echo "Error: Failed to download latest StevenBlack hosts." >&2
exit 1
fi
fi
}