#!/usr/bin/env bash
# js-sec-byhash-snapshot-v3.sh
# Jammy-security: Release의 SHA256 기준 by-hash 정확 동기화
# - CNF(Commands-*), binary-{amd64,i386}/Packages.*, i18n(Translation-*), dep11/Components-*.yml(.gz) 포함
# - 평문 Packages / Translation-* / Components-*.yml 자동 생성
# - 원자적 교체로 배포
set -euo pipefail
umask 022
# ====== CONFIG ======
BASE_DIR="${BASE_DIR:-/var/www/html/ubuntu/mirror/archive.ubuntu.com/ubuntu}"
DIST="jammy-security"
COMPS=("main" "universe") # 필요 시 restricted multiverse 추가
ARCHES=("amd64" "i386")
HOST="security.ubuntu.com"
STAGE="$(mktemp -d /var/tmp/js-sec.XXXXXX)"
# ====================
need(){ command -v "$1" >/dev/null || { echo "필요 명령 누락: $1"; exit 1; }; }
need wget; need awk; need sha256sum; need stat; need xz; need gzip; need grep; need sed; need cp; need mkdir
echo "[INFO] BASE_DIR=$BASE_DIR"
mkdir -p "$BASE_DIR/dists" "$STAGE/dists/$DIST"
die(){ echo "ERROR: $*" >&2; exit 1; }
rel_lookup_sha256() {
# $1: relpath (예: main/dep11/Components-amd64.yml.gz)
local relpath="$1" line
line=$(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" | grep -F " $relpath" || true)
[[ -n "$line" ]] || return 1
# <hash> <size> <path>
local h s p
h="$(awk '{print $1}' <<<"$line")"
s="$(awk '{print $2}' <<<"$line")"
p="$(awk '{print $3}' <<<"$line")"
[[ "$p" == "$relpath" ]] || return 1
printf "%s %s" "$h" "$s"
}
fetch_byhash_to() {
# $1: relpath, $2: hash
local relpath="$1" hash="$2"
local url="http://$HOST/ubuntu/dists/$DIST/$(dirname "$relpath")/by-hash/SHA256/$hash"
local out="$STAGE/dists/$DIST/$relpath"
mkdir -p "$(dirname "$out")"
wget -q -O "$out" "$url" || die "다운로드 실패: $url"
# by-hash 경로에도 동일 파일 저장
local bh="$STAGE/dists/$DIST/$(dirname "$relpath")/by-hash/SHA256/$hash"
mkdir -p "$(dirname "$bh")"
cp -a "$out" "$bh"
}
verify_exact() {
# $1: relpath, $2: expected hash, $3: expected size
local relpath="$1" exp_h="$2" exp_s="$3"
local full="$STAGE/dists/$DIST/$relpath"
[[ -s "$full" ]] || die "누락: $relpath"
local act_s act_h
act_s="$(stat -c %s "$full")"
act_h="$(sha256sum "$full" | awk '{print $1}')"
[[ "$act_s" == "$exp_s" && "$act_h" == "$exp_h" ]] || {
echo "불일치: $relpath"
echo " exp size/hash: $exp_s / $exp_h"
echo " act size/hash: $act_s / $act_h"
return 1
}
echo "OK $relpath"
}
# 1) Release/InRelease
echo "[STEP1] fetch Release/InRelease"
wget -q -O "$STAGE/dists/$DIST/InRelease" "http://$HOST/ubuntu/dists/$DIST/InRelease" || true
wget -q -O "$STAGE/dists/$DIST/Release" "http://$HOST/ubuntu/dists/$DIST/Release" || die "Release 없음"
wget -q -O "$STAGE/dists/$DIST/Release.gpg" "http://$HOST/ubuntu/dists/$DIST/Release.gpg" || true
REL="$STAGE/dists/$DIST/Release"
# 2) 대상 목록 구성 (CNF/Packages/i18n/dep11 Components)
echo "[STEP2] build target list"
declare -a TARGETS=()
# CNF + Packages
for comp in "${COMPS[@]}"; do
TARGETS+=("$comp/cnf/Commands-amd64.xz")
TARGETS+=("$comp/cnf/Commands-i386.xz")
for arch in "${ARCHES[@]}"; do
TARGETS+=("$comp/binary-$arch/Packages.xz")
TARGETS+=("$comp/binary-$arch/Packages.gz")
done
done
# i18n: Release에 있는 것만
mapfile -t I18N_PATHS < <(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" \
| awk '{print $3}' | grep -E '^(main|universe)/i18n/Translation-')
TARGETS+=("${I18N_PATHS[@]}")
# dep11 Components: .yml 또는 .yml.gz (Release에 있는 항목만)
mapfile -t DEP11_COMPONENTS < <(awk '/^SHA256:/{f=1;next}/^[A-Z0-9-]+:/{f=0} f{print}' "$REL" \
| awk '{print $3}' | grep -E '^(main|universe)/dep11/Components-.*\.yml(\.gz)?$')
TARGETS+=("${DEP11_COMPONENTS[@]}")
# 3) by-hash로 받고 검증 + 평문 생성
echo "[STEP3] fetch via by-hash & verify"
for relpath in "${TARGETS[@]}"; do
info="$(rel_lookup_sha256 "$relpath" || true)"
if [[ -z "${info:-}" ]]; then
echo "SKIP (Release에 없음): $relpath"
continue
fi
exp_h="$(awk '{print $1}' <<<"$info")"
exp_s="$(awk '{print $2}' <<<"$info")"
fetch_byhash_to "$relpath" "$exp_h"
verify_exact "$relpath" "$exp_h" "$exp_s" || die "검증 실패: $relpath"
# 평문 생성: Packages / Translation-* / Components-*.yml
case "$relpath" in
*/Packages.xz) plain="$STAGE/dists/$DIST/$(dirname "$relpath")/Packages"; [[ -s "$plain" ]] || xz -dc "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
*/Packages.gz) plain="$STAGE/dists/$DIST/$(dirname "$relpath")/Packages"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
*/i18n/Translation-*.xz) plain="${STAGE}/dists/${DIST}/${relpath%.xz}"; [[ -s "$plain" ]] || xz -dc "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
*/i18n/Translation-*.gz) plain="${STAGE}/dists/${DIST}/${relpath%.gz}"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
*/dep11/Components-*.yml.gz) plain="${STAGE}/dists/${DIST}/${relpath%.gz}"; [[ -s "$plain" ]] || gzip -cd "$STAGE/dists/$DIST/$relpath" > "$plain" || true ;;
esac
done
# 4) (옵션) dep11 아이콘
echo "[STEP4] dep11 icons (optional)"
for comp in "${COMPS[@]}"; do
base="http://$HOST/ubuntu/dists/$DIST/$comp/dep11"
dst="$STAGE/dists/$DIST/$comp/dep11"
mkdir -p "$dst"
for s in 48 64 128; do
for suf in "" "@2"; do
wget -q -c -O "$dst/icons-${s}x${s}${suf}.tar.gz" "$base/icons-${s}x${s}${suf}.tar.gz" || true
done
done
done
# 5) 원자적 교체
echo "[STEP5] publish atomically"
PUB="$BASE_DIR/dists/$DIST"
TS="$(date +%Y%m%d%H%M%S)"
sudo mkdir -p "$(dirname "$PUB")"
[[ -e "$PUB" && ! -L "$PUB" ]] && sudo mv "$PUB" "${PUB}.bak.${TS}" || true
sudo rm -rf "$PUB"
sudo mv "$STAGE/dists/$DIST" "$PUB"
echo "[DONE] published to $PUB"
echo "클라이언트에서 갱신:"
echo " sudo rm -rf /var/lib/apt/lists/*"
echo " sudo apt-get clean"
echo " sudo apt-get update -o Acquire::By-Hash=true"