gitea-selfhost/tests/e2e-binary.sh
leora dcfeb12c68 Re-review fixes: EGRESS_MODE=lax on Gitea 28, basic-auth prod gate, pinned sha256, kit-owned down
- app.ini.template: [security] EGRESS_MODE = lax (ALLOWED_HOST_LIST=external is invalid on 28.1.0)
- README: egress rows for 28 vs 1.23; rollout gate while password basic auth is on (edge 429 on
  Authorization: Basic to /api/v1/ and git HTTP, or tokens first)
- kit.sh: pinned sha256 per arch for 28.1.0; down stops only its own pid or the unit it installed
- e2e: package upload must be 404; 'denied by egress' polled from the Gitea log; 4 MB attachment
  cap; Actions off; fail on [E] / 'invalid entry'; listener cleaned up in trap
2026-10-15 20:57:10 +08:00

113 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end check of the kit in binary mode, in a throwaway directory on a spare port.
# Usage: tests/e2e-binary.sh [PORT] (default 3990). Leaves nothing behind.
set -euo pipefail
PORT="${1:-3990}"
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP="$(mktemp -d)"; WORK="$TMP/kit"; CLONE="$TMP/clone"
pass=0; fail=0
ok() { echo "PASS $*"; pass=$((pass+1)); }
bad() { echo "FAIL $*"; fail=$((fail+1)); }
cleanup() { [ -n "${LPID:-}" ] && kill "$LPID" 2>/dev/null || true; (cd "$WORK" 2>/dev/null && ./kit.sh down >/dev/null 2>&1) || true; rm -rf "$TMP"; }
trap cleanup EXIT
ss -ltn 2>/dev/null | grep -q ":$PORT " && { echo "port $PORT is busy"; exit 2; }
mkdir -p "$WORK"; cp -r "$SRC"/. "$WORK"/; rm -rf "$WORK/.git" "$WORK/.env" "$WORK/secrets" "$WORK/data"
cd "$WORK"
cat > .env <<ENV
KIT_MODE=binary
APP_NAME="Kit E2E"
DOMAIN=127.0.0.1
ROOT_URL=http://127.0.0.1:$PORT/
HTTP_PORT=$PORT
COOKIE_SECURE=false
DISABLE_REGISTRATION=false
MAX_REPOS_PER_USER=10
REGISTER_EMAIL_CONFIRM=false
MAILER_ENABLED=false
ADMIN_USER=kitadmin
ADMIN_EMAIL=kitadmin@example.com
ADMIN_PASSWORD=
PROTECT_BRANCH=main
REQUIRED_APPROVALS=0
GITEA_HOME=$TMP/home
GITEA_CONF=$TMP/home/conf/app.ini
ENV
./kit.sh up && ok "kit.sh up (download + sha256, render config, start, admin + token)" || { bad "kit.sh up"; exit 1; }
A="http://127.0.0.1:$PORT/api/v1"; T="$(cat secrets/admin.token)"; P="$(cat secrets/admin.password)"
H=(-H "Authorization: token $T" -H "Content-Type: application/json")
curl -fsS "${H[@]}" -X POST "$A/user/repos" -d '{"name":"demo","auto_init":true,"default_branch":"main"}' >/dev/null && ok "admin created repo kitadmin/demo" || bad "create repo"
./kit.sh protect kitadmin/demo >/dev/null && ok "branch protection applied to main" || bad "protect"
# Password basic auth is off (ENABLE_BASIC_AUTHENTICATION=false); git over HTTP uses the token.
git -c credential.helper= clone -q "http://kitadmin:$T@127.0.0.1:$PORT/kitadmin/demo.git" "$CLONE"
cd "$CLONE"; git config user.name e2e; git config user.email e2e@example.com
echo direct > direct.txt; git add direct.txt; git commit -qm direct
if git -c credential.helper= push -q origin main 2>/dev/null; then bad "direct push to main was ACCEPTED"; else ok "direct push to main rejected (even for the admin)"; fi
git reset -q --hard origin/main; git checkout -qb feature; echo via-pr > pr.txt; git add pr.txt; git commit -qm "via PR"
git -c credential.helper= push -q origin feature 2>/dev/null && ok "push to a feature branch accepted" || bad "feature push"
cd "$WORK"
N=$(curl -fsS "${H[@]}" -X POST "$A/repos/kitadmin/demo/pulls" -d '{"head":"feature","base":"main","title":"via PR"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["number"])')
curl -fsS "${H[@]}" -X POST "$A/repos/kitadmin/demo/pulls/$N/merge" -d '{"Do":"merge"}' >/dev/null && ok "PR #$N merged into main" || bad "merge"
curl -fsS "${H[@]}" "$A/repos/kitadmin/demo/contents/pr.txt?ref=main" >/dev/null && ok "merged change is on main" || bad "merged file missing"
./kit.sh agent e2e-agent >/dev/null && ok "agent user + scoped token created" || bad "agent"
AT="$(cat secrets/agents/e2e-agent.token)"
curl -fsS -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/user/repos" -d '{"name":"agent-repo","auto_init":true}' >/dev/null && ok "agent token can create its own repo" || bad "agent create repo"
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" "$A/admin/users"); [ "$code" = 403 ] && ok "agent token has no admin access (403)" || bad "agent admin access: $code"
code=$(curl -s -o /dev/null -w '%{http_code}' "${H[@]}" -H "Sudo: e2e-agent" -X POST "$A/orgs" -d '{"username":"e2e-org"}'); [ "$code" = 403 ] && ok "new users cannot create organizations (403)" || bad "org creation by new user: $code"
st=$(curl -fsS "${H[@]}" "$A/settings/repository"); grep -q '"migrations_disabled":true' <<<"$st" && ok "migrations disabled" || bad "migrations"
page=$(curl -fsS "http://127.0.0.1:$PORT/user/sign_up"); ! grep -qi 'captcha' <<<"$page" && ok "public sign-up page has no captcha (agent-native)" || bad "sign-up still shows a captcha"
./kit.sh backup >/dev/null 2>&1 && ls backups/*.zip >/dev/null && ok "backup (gitea dump) written" || bad "backup"
./kit.sh protect-all >/dev/null && ok "protect-all ran" || bad "protect-all"
made=1; for i in $(seq 2 11); do
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/user/repos" -d "{\"name\":\"r$i\"}")
[ "$c" = 201 ] && made=$((made+1))
done
[ "$made" = 10 ] && ok "repo limit: regular user stops at 10 repos" || bad "repo limit: user made $made repos"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/user/repos" -d '{"name":"r-eleventh"}')
[ "$c" = 403 ] && ok "11th repo refused (403)" || bad "11th repo: $c"
# --- adversarial checks (agent-native sign-up, no captcha) ---
c=$(curl -s -o /dev/null -w '%{http_code}' -u "kitadmin:$P" "$A/user"); [ "$c" = 401 ] && ok "password basic auth refused on the API (401)" || bad "password basic auth: $c"
c=$(curl -s -o /dev/null -w '%{http_code}' -u "kitadmin:$T" "$A/user"); [ "$c" = 200 ] && ok "token as basic-auth password still works" || bad "token basic auth: $c"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $T" "$A/user"); [ "$c" = 200 ] && ok "token header works" || bad "token header: $c"
(cd "$CLONE" && git checkout -qb pw-push && if git -c credential.helper= push -q "http://kitadmin:$P@127.0.0.1:$PORT/kitadmin/demo.git" pw-push 2>/dev/null; then exit 1; fi) && ok "git push over HTTP refuses password basic auth" || bad "git HTTP push accepted a password"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/orgs" -d '{"username":"e2e-org2"}'); [ "$c" = 403 ] && ok "agent token cannot create an org (403)" || bad "agent org create: $c"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -X PUT --data-binary 'blob' "http://127.0.0.1:$PORT/api/packages/e2e-agent/generic/p/1.0/f.bin"); [ "$c" = 404 ] && ok "package upload refused (404, registry off)" || bad "package upload: $c (want 404)"
LPORT=$((PORT+7)); HIT="$TMP/webhook-hit"
python3 -c "import http.server,sys
class H(http.server.BaseHTTPRequestHandler):
def do_POST(s): open(sys.argv[2],'w').write('hit'); s.send_response(200); s.end_headers()
def log_message(s,*a): pass
http.server.HTTPServer(('127.0.0.1',int(sys.argv[1])),H).serve_forever()" "$LPORT" "$HIT" >/dev/null 2>&1 & LPID=$!
sleep 0.5
HID=$(curl -s -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/repos/e2e-agent/agent-repo/hooks" \
-d "{\"type\":\"gitea\",\"active\":true,\"config\":{\"url\":\"http://127.0.0.1:$LPORT/\",\"content_type\":\"json\"}}" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)
[ -n "$HID" ] && ok "webhook to 127.0.0.1 created (id $HID); delivering test" || bad "webhook create failed"
[ -n "$HID" ] && curl -s -o /dev/null -H "Authorization: token $AT" -X POST "$A/repos/e2e-agent/agent-repo/hooks/$HID/tests"
LOGS=("$TMP/home")
denied=0; for _ in $(seq 1 30); do
grep -rqs 'denied by egress' "${LOGS[@]}" 2>/dev/null && { denied=1; break; }; sleep 0.5; done
kill $LPID 2>/dev/null || true
[ "$denied" = 1 ] && ok "Gitea logged the webhook as denied by egress" || bad "no 'denied by egress' line within 15s"
[ ! -e "$HIT" ] && ok "webhook to 127.0.0.1 not delivered ([security] EGRESS_MODE=lax)" || bad "webhook reached 127.0.0.1"
# 4 MB attachment cap: a small file is accepted, a 5 MB file is refused
NUM=$(curl -s -H "Authorization: token $AT" -H "Content-Type: application/json" -X POST "$A/repos/e2e-agent/agent-repo/issues" -d '{"title":"attach"}' | python3 -c 'import json,sys;print(json.load(sys.stdin).get("number",""))')
head -c 1024 /dev/urandom > "$TMP/small.txt"; head -c $((5*1024*1024)) /dev/urandom > "$TMP/big.txt"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -F "attachment=@$TMP/small.txt" "$A/repos/e2e-agent/agent-repo/issues/$NUM/assets"); [ "$c" = 201 ] && ok "1 KB attachment accepted" || bad "small attachment: $c"
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" -F "attachment=@$TMP/big.txt" "$A/repos/e2e-agent/agent-repo/issues/$NUM/assets"); [ "$c" != 201 ] && ok "5 MB attachment refused ($c, MAX_SIZE=4)" || bad "5 MB attachment accepted"
# Actions off: the Actions API is not served
ha=$(curl -s -H "Authorization: token $AT" "$A/repos/e2e-agent/agent-repo" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("has_actions"))')
c=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $AT" "http://127.0.0.1:$PORT/e2e-agent/agent-repo/actions")
[ "$ha" = False ] && [ "$c" = 404 ] && ok "Actions disabled (has_actions false, /actions page 404)" || bad "Actions: has_actions=$ha, /actions page $c"
echo "PENDING sign-up burst: throttling is the edge rate-limit rule (Cloudflare/proxy), not Gitea; test once the rule exists"
if grep -q "Deprecation" "$TMP/home/log/gitea.out"; then bad "config uses deprecated keys: $(grep -o 'config option `[^`]*`' "$TMP/home/log/gitea.out" | sort -u | tr '\n' ' ')"; else ok "no deprecated config keys in the Gitea log"; fi
if grep -E '\[E\]|invalid entry' "$TMP/home/log/gitea.out" | grep -v 'denied by egress' | grep -q .; then bad "Gitea logged errors: $(grep -E '\[E\]|invalid entry' "$TMP/home/log/gitea.out" | grep -v 'denied by egress' | head -3)"; else ok "no [E] or 'invalid entry' lines in the Gitea log"; fi
echo; echo "$pass passed, $fail failed"
[ "$fail" = 0 ]