Files
pezkuwi-telegram-miniapp/ops/host/test-site-receive.sh
T
Satoshi Qazi Muhammed f6d53f51b1 ci: deploy over pinned, single-purpose keys; root installs the gate
The deploy reached VPS3 as root with one key, handed to appleboy actions
referenced by tag, and on every run it reinstalled the edge-function
ownership gate and its registry as root. That gate guards every project's
functions in the shared volume, so any change merged here could rewrite the
rule that protects pwap's functions.

Now CI connects as miniapp-deploy with two keys, each pinned on the host
(authorized_keys restrict,command=) to a script in ops/host:

- MINIAPP_SITE_DEPLOY_KEY -> miniapp-site-receive: the site tar on stdin,
  into /var/www/telegram.pezkiwi.app, nothing else.
- MINIAPP_FUNCTIONS_DEPLOY_KEY -> miniapp-functions-ssh: `gate-sha`, or
  `deploy` with functions.tgz on stdin, run through sudo for exactly those
  arguments.

Root installs the gate and registry from ops/; the deploy checks the host's
copies against them and refuses to deploy functions while they differ.

The asset cleanup that ran after each deploy (/opt/cleanup-miniapp.sh, only
on the host) protected lazy chunks by mtime and so removed live ones when a
build was shipped more than two hours after it was made; it took seven pages
down for about two minutes on 2026-10-09 while this was being tested, and
they were restored from the same tar. The receive script now keeps every
file of the new build and removes other assets only after four hours. Its
tests cover that (red with the keep rule removed) and the refusals; CI runs
them with shellcheck, and apply-repo-settings.sh requires the job.

VPS3's host key is pinned. The commented-out VPS1 deploy is gone; that host
no longer exists. Every action is pinned to a commit SHA.
2026-10-09 06:09:22 +04:00

58 lines
3.3 KiB
Bash
Executable File

#!/bin/bash
# The rc/before variables are read inside the check expressions, which
# check() runs with eval; shellcheck cannot see into those strings.
# shellcheck disable=SC2034
# Exercises miniapp-site-receive against a scratch web root: what it
# installs, with which modes, and what it must refuse. The script under test
# is copied with its web root pointed at the scratch directory.
set -uo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
T=$(mktemp -d)
trap 'rm -rf "$T"' EXIT
mkdir -p "$T/web" "$T/tmp/stage"
export TMPDIR="$T/tmp/stage" # stage = $T/tmp/stage/tmp.X, so ../../.. is $T
sed -e "s#/var/www/telegram.pezkiwi.app#$T/web#" "$HERE/miniapp-site-receive" > "$T/receive"
chmod +x "$T/receive"
pass=0; fail=0
check () { if eval "$2"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL $1"; fail=$((fail+1)); fi; }
site () { mkdir -p "$1/assets"; echo "<html>new</html>" > "$1/index.html"; echo "x" > "$1/assets/index-NEW.js"; }
run () { ( cd "$2" && tar -cf - . ) | SSH_ORIGINAL_COMMAND="$1" "$T/receive" >/dev/null 2>&1; }
mkdir -p "$T/web/assets"; echo old > "$T/web/assets/index-OLD.js"; echo "<html>old</html>" > "$T/web/index.html"
site "$T/s1"; run deploy "$T/s1"; rc=$?
check "a valid build installs" '[ $rc = 0 ] && grep -q new "$T/web/index.html"'
check "recent chunks of the previous build stay" '[ -f "$T/web/assets/index-OLD.js" ]'
check "files are world-readable (644)" '[ "$(stat -c %a "$T/web/index.html")" = 644 ] && [ "$(stat -c %a "$T/web/assets/index-NEW.js")" = 644 ]'
check "directories are traversable (755)" '[ "$(stat -c %a "$T/web/assets")" = 755 ] && [ "$(stat -c %a "$T/web")" = 755 ]'
before=$(cat "$T/web/index.html")
mkdir -p "$T/s2/assets"; echo x > "$T/s2/assets/a.js"; run deploy "$T/s2"; rc=$?
check "a tar without index.html is refused" '[ $rc != 0 ] && [ "$(cat "$T/web/index.html")" = "$before" ]'
site "$T/s3"; run 'deploy; id' "$T/s3"; rc1=$?; run '' "$T/s3"; rc2=$?
check "any command but deploy is refused" '[ $rc1 = 2 ] && [ $rc2 = 2 ]'
# The cleanup: a file of the new build stays however old it is (the old
# cleanup removed lazy chunks older than two hours); a file not in the build
# stays for four hours, then goes.
site "$T/c1"; echo chunk > "$T/c1/assets/Wallet-OLD.js"; touch -d '3 days ago' "$T/c1/assets/Wallet-OLD.js"
echo prev > "$T/web/assets/prev-recent.js"; echo gone > "$T/web/assets/prev-old.js"; touch -d '1 day ago' "$T/web/assets/prev-old.js"
run deploy "$T/c1"; rc=$?
check "an old file of the new build is kept" '[ $rc = 0 ] && [ -f "$T/web/assets/Wallet-OLD.js" ]'
check "a recent file of an earlier build is kept" '[ -f "$T/web/assets/prev-recent.js" ]'
check "an old file of an earlier build is removed" '[ ! -e "$T/web/assets/prev-old.js" ]'
site "$T/s4"; ln -s /etc/passwd "$T/s4/assets/passwd.js"; run deploy "$T/s4"; rc=$?
check "a symlink in the upload is not installed" '[ $rc = 0 ] && [ ! -e "$T/web/assets/passwd.js" ] && [ ! -L "$T/web/assets/passwd.js" ]'
site "$T/s5"
( cd "$T/s5" && tar -cf "$T/evil.tar" . && tar -rf "$T/evil.tar" --transform 's#^#../../../#' index.html ) 2>/dev/null
SSH_ORIGINAL_COMMAND=deploy "$T/receive" < "$T/evil.tar" >/dev/null 2>&1
check "a ../ member cannot write outside the stage" '[ ! -e "$T/index.html" ] && [ ! -e "$T/tmp/index.html" ]'
echo "RESULT: $pass passed, $fail failed"
[ "$fail" = 0 ]