241 lines
10 KiB
Bash
Executable File
241 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Post-release verification for desfoto.de.
|
|
#
|
|
# .ocauto/verify <release-sha>
|
|
#
|
|
# Proves that (a) the live desfoto.de serves the released build and assets,
|
|
# (b) the security posture matches the promises on /datenschutz/, and
|
|
# (c) the neighbouring sites (dennyschulz.de, dennyapp.de) are untouched.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
root="$(pwd)"
|
|
release_sha="${1:-unknown}"
|
|
remote="${DESFOTO_REMOTE:-prod-main}"
|
|
# DESFOTO_BASE lets the hook be dry-run against a local container
|
|
# (DESFOTO_BASE=http://127.0.0.1:18430 .ocauto/verify unknown); the
|
|
# domain-specific sections (deployed tree, canonical host, neighbours, TLS)
|
|
# only run against the real production origin.
|
|
base="${DESFOTO_BASE:-https://desfoto.de}"
|
|
live=0
|
|
if [ "$base" = "https://desfoto.de" ]; then live=1; fi
|
|
|
|
failures=0
|
|
pass() { printf ' ok %s\n' "$1"; }
|
|
fail() { printf ' FAIL %s\n' "$1" >&2; failures=$((failures + 1)); }
|
|
|
|
check_status() {
|
|
local url="$1" want="$2" got
|
|
got="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 "$url" || echo 000)"
|
|
if [ "$got" = "$want" ]; then pass "$url -> $got"; else fail "$url -> $got (want $want)"; fi
|
|
}
|
|
|
|
check_contains() {
|
|
local url="$1" needle="$2" body
|
|
body="$(curl -sS --max-time 30 "$url" || true)"
|
|
if printf '%s' "$body" | grep -qF -- "$needle"; then
|
|
pass "$url contains '$needle'"
|
|
else
|
|
fail "$url does not contain '$needle'"
|
|
fi
|
|
}
|
|
|
|
check_header() {
|
|
local url="$1" header="$2" headers
|
|
headers="$(curl -sSI --max-time 30 "$url" || true)"
|
|
if printf '%s' "$headers" | grep -qi "^$header:"; then
|
|
pass "$url sends $header"
|
|
else
|
|
fail "$url is missing the $header header"
|
|
fi
|
|
}
|
|
|
|
check_redirect() {
|
|
local url="$1" want="$2" got target
|
|
got="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 "$url" || echo 000)"
|
|
target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 "$url" || true)"
|
|
if [ "$got" = "301" ] && [ "$target" = "$want" ]; then
|
|
pass "$url -> 301 $want"
|
|
else
|
|
fail "$url -> $got ${target:-<no Location>} (want 301 $want)"
|
|
fi
|
|
}
|
|
|
|
check_not_contains() {
|
|
local url="$1" needle="$2" body
|
|
body="$(curl -sS --max-time 30 "$url" || true)"
|
|
if printf '%s' "$body" | grep -qF -- "$needle"; then
|
|
fail "$url still contains '$needle'"
|
|
else
|
|
pass "$url no longer contains '$needle'"
|
|
fi
|
|
}
|
|
|
|
printf '\n== release identity\n'
|
|
if [ "$live" = 1 ] && [ "$release_sha" != "unknown" ] && git -C "$root" rev-parse --verify --quiet "$release_sha" >/dev/null; then
|
|
local_hash="$(cd "$root/site" && find . -type f ! -name sitemap.xml ! -path './.well-known/*' -print0 |
|
|
sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)"
|
|
remote_hash="$(ssh "$remote" "sudo sh -c 'cd /home/denny/stacks/desfoto/site && find . -type f ! -name sitemap.xml ! -path ./.well-known/\* -print0 | sort -z | xargs -0 sha256sum | sha256sum'" | cut -d' ' -f1)"
|
|
if [ "$local_hash" = "$remote_hash" ]; then
|
|
pass "deployed site tree matches the release ($local_hash)"
|
|
else
|
|
fail "deployed site tree differs from the release ($local_hash != $remote_hash)"
|
|
fi
|
|
remote_release="$(ssh "$remote" 'sudo cat /home/denny/stacks/desfoto/RELEASE' | tr -d '\r\n')"
|
|
if [ "$remote_release" = "$release_sha" ]; then
|
|
pass "release marker is $release_sha"
|
|
else
|
|
fail "release marker is '$remote_release' (want $release_sha)"
|
|
fi
|
|
fi
|
|
|
|
printf '\n== routes\n'
|
|
for route in / /fotografie/ /businessfotografie/ /portrait-und-model/ /familie/ \
|
|
/minishootings/ /video/ /musik-und-buehne/ /social-media/ /projekte/ /ueber/ /kontakt/ /impressum/ /datenschutz/ /gtin/; do
|
|
check_status "$base$route" 200
|
|
done
|
|
check_redirect "$base/gtin" "$base/gtin/"
|
|
|
|
# The retired photo URL must keep working: it is the umbrella page now.
|
|
check_redirect "$base/shootings" "$base/fotografie/"
|
|
check_redirect "$base/shootings/" "$base/fotografie/"
|
|
check_redirect "$base/shootings.html" "$base/fotografie/"
|
|
check_redirect "$base/fotografie.html" "$base/fotografie/"
|
|
|
|
# Familien & Paare was renamed: the remaining family work stays with desfoto.
|
|
check_redirect "$base/familien-und-paare" "$base/familie/"
|
|
check_redirect "$base/familien-und-paare/" "$base/familie/"
|
|
check_redirect "$base/familien-und-paare.html" "$base/familie/"
|
|
|
|
printf '\n== canonical host and redirects\n'
|
|
if [ "$live" = 1 ]; then
|
|
www_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 https://www.desfoto.de/ || echo 000)"
|
|
www_target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 https://www.desfoto.de/ || true)"
|
|
if [ "$www_status" = "301" ] && [ "$www_target" = "https://desfoto.de/" ]; then
|
|
pass "www.desfoto.de -> https://desfoto.de/ (301)"
|
|
else
|
|
fail "www.desfoto.de -> $www_status $www_target"
|
|
fi
|
|
http_target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 http://desfoto.de/ || true)"
|
|
case "$http_target" in
|
|
https://desfoto.de/*) pass "http://desfoto.de/ -> $http_target" ;;
|
|
*) fail "http://desfoto.de/ redirects to '$http_target'" ;;
|
|
esac
|
|
fi
|
|
|
|
printf '\n== content\n'
|
|
check_contains "$base/" 'href="/fotografie/"'
|
|
check_contains "$base/" 'href="/musik-und-buehne/"'
|
|
check_contains "$base/" 'href="/video/"'
|
|
check_contains "$base/" "Mobiles Studio"
|
|
check_contains "$base/" "Studio Neumünster"
|
|
check_contains "$base/" "desfoto ist ein Angebot von Denny Schulz, Neumünster."
|
|
check_not_contains "$base/" 'href="/shootings'
|
|
check_not_contains "$base/" "Paarshooting"
|
|
check_contains "$base/fotografie/" "Autarke Blitzanlage"
|
|
check_contains "$base/fotografie/" "Minishootings"
|
|
check_contains "$base/familie/" "Familienshooting"
|
|
check_contains "$base/musik-und-buehne/" "Thjódrörir"
|
|
check_contains "$base/musik-und-buehne/" "Sagenbringer"
|
|
check_contains "$base/impressum/" "§ 5 DDG"
|
|
check_contains "$base/impressum/" "DE462149560"
|
|
check_contains "$base/datenschutz/" "keine Zugriffsprotokolle"
|
|
check_contains "$base/datenschutz/" "bis zu 20"
|
|
check_contains "$base/impressum/" "GTIN-Tool"
|
|
check_contains "$base/gtin/" 'name="robots" content="noindex, nofollow"'
|
|
check_not_contains "$base/" 'href="/gtin'
|
|
check_not_contains "$base/sitemap.xml" "/gtin/"
|
|
check_contains "$base/gtin/" 'href="/impressum/"'
|
|
check_contains "$base/gtin/" 'href="/datenschutz/"'
|
|
for route in / /fotografie/ /businessfotografie/ /portrait-und-model/ /familie/ \
|
|
/minishootings/ /video/ /musik-und-buehne/ /social-media/ /projekte/ /ueber/ \
|
|
/kontakt/ /impressum/ /datenschutz/; do
|
|
check_not_contains "$base$route" 'href="/gtin'
|
|
done
|
|
check_contains "$base/datenschutz/" "youtube-nocookie.com"
|
|
check_contains "$base/video/" "NWWFTf7l8g0"
|
|
check_contains "$base/" "https://www.dennyschulz.de/"
|
|
check_contains "$base/" "https://dennyapp.de/"
|
|
check_contains "$base/sitemap.xml" "<loc>https://desfoto.de/fotografie/</loc>"
|
|
check_not_contains "$base/sitemap.xml" "<loc>https://desfoto.de/shootings/</loc>"
|
|
|
|
printf '\n== error handling and metadata\n'
|
|
check_status "$base/diese-seite-gibt-es-nicht/" 404
|
|
check_contains "$base/diese-seite-gibt-es-nicht/" "Diese Seite gibt es nicht"
|
|
check_status "$base/.well-known/security.txt" 200
|
|
check_status "$base/robots.txt" 200
|
|
|
|
printf '\n== assets\n'
|
|
check_status "$base/assets/img/logo-160.png" 200
|
|
check_status "$base/assets/img/favicon-32.png" 200
|
|
check_status "$base/assets/img/og-fotografie.jpg" 200
|
|
check_status "$base/assets/site.css" 200
|
|
check_status "$base/gtin/css/app.css" 200
|
|
check_status "$base/gtin/js/app.js" 200
|
|
check_status "$base/gtin/sw.js" 200
|
|
check_status "$base/gtin/manifest.webmanifest" 200
|
|
check_status "$base/gtin/health" 404
|
|
manifest_type="$(curl -sSI --max-time 30 "$base/gtin/manifest.webmanifest" | tr -d '\r' | grep -i '^content-type:' || true)"
|
|
if printf '%s' "$manifest_type" | grep -qi 'application/manifest+json'; then
|
|
pass "$base/gtin/manifest.webmanifest uses the Web App Manifest MIME type"
|
|
else
|
|
fail "$base/gtin/manifest.webmanifest has wrong Content-Type: ${manifest_type:-<missing>}"
|
|
fi
|
|
check_status "$base/assets/fonts/fraunces-latin.woff2" 200
|
|
# The portfolio expansion ships the operator's own public work...
|
|
check_status "$base/assets/img/music-dreadlocks-1200.webp" 200
|
|
check_status "$base/assets/img/business-hotel-window-1200.webp" 200
|
|
check_status "$base/assets/img/free-portrait-light-1200.webp" 200
|
|
check_status "$base/assets/img/editorial-leap-1200.webp" 200
|
|
# ...and the wedding pictures showing the guests stay removed.
|
|
check_status "$base/assets/img/group-feier-1200.webp" 404
|
|
check_status "$base/assets/img/group-outdoor-1200.webp" 404
|
|
check_status "$base/assets/img/group-posiert-1200.webp" 404
|
|
# Couple imagery and the studio shot with the old business card ship nowhere.
|
|
check_status "$base/assets/img/couple-park-1200.webp" 404
|
|
check_status "$base/assets/img/portrait-couple-1200.webp" 404
|
|
check_status "$base/assets/img/studio-setup-1200.webp" 404
|
|
# Brautpaar-Bilder sind komplett aus dem Bestand: Fotografie- und Eventbilder
|
|
# mit Paaren gehören zu dennyschulz.de, nicht zu desfoto.
|
|
check_status "$base/assets/img/portrait-natural-1200.webp" 404
|
|
check_status "$base/assets/img/event-motion-1200.webp" 404
|
|
check_status "$base/assets/img/event-dance-1200.webp" 404
|
|
check_contains "$base/minishootings/" "free-modern-portrait-1200.webp"
|
|
check_contains "$base/social-media/" "event-speaker-1200.webp"
|
|
check_contains "$base/projekte/" "music-dreadlocks-1200.webp"
|
|
check_contains "$base/familie/" "free-portrait-light-1200.webp"
|
|
|
|
printf '\n== privacy promises and headers\n'
|
|
check_header "$base/" "Strict-Transport-Security"
|
|
check_header "$base/" "Content-Security-Policy"
|
|
check_header "$base/" "X-Content-Type-Options"
|
|
check_header "$base/" "Referrer-Policy"
|
|
if curl -sSI --max-time 30 "$base/" | grep -qi '^set-cookie:'; then
|
|
fail "a cookie is set on the homepage"
|
|
else
|
|
pass "no Set-Cookie on the homepage"
|
|
fi
|
|
|
|
printf '\n== neighbouring sites untouched\n'
|
|
if [ "$live" = 1 ]; then
|
|
check_status "https://www.dennyschulz.de/" 200
|
|
check_contains "https://www.dennyschulz.de/impressum" "Denny Schulz"
|
|
check_status "https://dennyapp.de/" 200
|
|
fi
|
|
|
|
printf '\n== TLS\n'
|
|
if [ "$live" = 1 ] && command -v openssl >/dev/null 2>&1; then
|
|
if echo | openssl s_client -servername desfoto.de -connect desfoto.de:443 2>/dev/null |
|
|
openssl x509 -noout -checkend 604800 -subject 2>/dev/null; then
|
|
pass "certificate for desfoto.de is valid for at least 7 more days"
|
|
else
|
|
fail "certificate for desfoto.de is missing or expires within 7 days"
|
|
fi
|
|
fi
|
|
|
|
if [ "$failures" -ne 0 ]; then
|
|
printf '\nVERIFY FAIL (%d problem(s)) release=%s\n' "$failures" "$release_sha" >&2
|
|
exit 1
|
|
fi
|
|
printf '\nVERIFY PASS release=%s\n' "$release_sha"
|