test(davx5): script reproducible de verificación DAVx5 ↔ Xandikos
12 checks: auth requerida (401 sin/con credencial mala), TLS Let's Encrypt + no-cleartext, recepción de los 1065 contactos via PROPFIND+REPORT, integridad TEL/EMAIL de un contacto conocido. Lee la credencial de pass en runtime (sin secretos hardcodeados). Validado en emulador Pixel_API34 con DAVx5 4.5.14: recibió los 1065 contactos.
This commit is contained in:
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test_davx5_sync.sh — Protocol-level test of the Xandikos CardDAV server that
|
||||
# DAVx5 (Android) syncs against. Exercises exactly what DAVx5 does under the
|
||||
# hood: PROPFIND discovery, addressbook-query REPORT, plus auth and TLS hardening.
|
||||
#
|
||||
# This is the reproducible baseline that does NOT need the Android emulator: it
|
||||
# validates the server contract DAVx5 depends on. The emulator-side verification
|
||||
# (1065 raw_contacts in the device contacts provider) is documented in the task
|
||||
# report but is inherently interactive (UI-driven account setup).
|
||||
#
|
||||
# Credentials are read from `pass` at runtime — NEVER hardcoded.
|
||||
# pass entry: dav/xandikos-enmanuel (first line = password)
|
||||
#
|
||||
# Usage:
|
||||
# ./test_davx5_sync.sh # run all checks
|
||||
# ./test_davx5_sync.sh -v # verbose (show curl details)
|
||||
#
|
||||
# Exit code 0 = all checks passed; non-zero = at least one failure.
|
||||
|
||||
set -u
|
||||
|
||||
# ---- Config -----------------------------------------------------------------
|
||||
BASE="https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com"
|
||||
PRINCIPAL_PATH="/enmanuel/"
|
||||
ADDRESSBOOK_PATH="/enmanuel/contacts/addressbook/"
|
||||
USER="enmanuel"
|
||||
PASS_ENTRY="dav/xandikos-enmanuel"
|
||||
|
||||
# Expected number of contacts (vCards) currently served. Allow a tolerance band
|
||||
# so the test survives small day-to-day changes without going green on a
|
||||
# catastrophic loss (e.g. empty collection).
|
||||
EXPECTED_VCARDS=1065
|
||||
TOLERANCE=50 # accept EXPECTED +/- TOLERANCE
|
||||
|
||||
# A known contact that must round-trip with its TEL and EMAIL intact.
|
||||
KNOWN_NAME="Nieves"
|
||||
KNOWN_TEL="676 95 90 40"
|
||||
KNOWN_EMAIL="nieves@gomezdeseguraabogados.com"
|
||||
|
||||
VERBOSE=0
|
||||
[ "${1:-}" = "-v" ] && VERBOSE=1
|
||||
|
||||
# ---- Helpers ----------------------------------------------------------------
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RESET=$'\e[0m'
|
||||
|
||||
ok() { echo "${GREEN}PASS${RESET} $1"; PASS_COUNT=$((PASS_COUNT+1)); }
|
||||
fail() { echo "${RED}FAIL${RESET} $1"; FAIL_COUNT=$((FAIL_COUNT+1)); }
|
||||
info() { [ "$VERBOSE" = 1 ] && echo " $1"; return 0; }
|
||||
|
||||
# Read the DAV password from pass into a variable. Never echo it.
|
||||
DAV_PASS="$(pass "$PASS_ENTRY" 2>/dev/null | head -1)"
|
||||
if [ -z "$DAV_PASS" ]; then
|
||||
echo "${RED}ABORT${RESET}: could not read password from 'pass $PASS_ENTRY'"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
AB_URL="${BASE}${ADDRESSBOOK_PATH}"
|
||||
HTTP_URL="http://${BASE#https://}${ADDRESSBOOK_PATH}"
|
||||
|
||||
PROPFIND_BODY='<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getetag/></d:prop></d:propfind>'
|
||||
REPORT_BODY='<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
|
||||
|
||||
echo "=== DAVx5 / Xandikos CardDAV protocol test ==="
|
||||
echo "Server: $BASE"
|
||||
echo "Collection: $ADDRESSBOOK_PATH"
|
||||
echo
|
||||
|
||||
# ---- (a) Auth required -------------------------------------------------------
|
||||
echo "--- (a) Authentication ---"
|
||||
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -X PROPFIND -H "Depth: 0" "$AB_URL")
|
||||
[ "$code" = "401" ] && ok "no auth -> 401 (got $code)" || fail "no auth -> expected 401, got $code"
|
||||
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${USER}:definitely-wrong-password" -X PROPFIND -H "Depth: 0" "$AB_URL")
|
||||
[ "$code" = "401" ] && ok "bad password -> 401 (got $code)" || fail "bad password -> expected 401, got $code"
|
||||
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -u "ghost:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL")
|
||||
# Xandikos returns 401 for an unknown user too.
|
||||
{ [ "$code" = "401" ] || [ "$code" = "403" ] || [ "$code" = "404" ]; } \
|
||||
&& ok "unknown user -> $code (rejected)" || fail "unknown user -> expected 401/403/404, got $code"
|
||||
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL")
|
||||
[ "$code" = "207" ] && ok "valid auth -> 207 Multi-Status (got $code)" || fail "valid auth -> expected 207, got $code"
|
||||
echo
|
||||
|
||||
# ---- (b) TLS -----------------------------------------------------------------
|
||||
echo "--- (b) TLS / transport hardening ---"
|
||||
|
||||
# Valid cert: curl WITHOUT -k must succeed.
|
||||
if curl -sf -o /dev/null -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 0" "$AB_URL"; then
|
||||
ok "valid TLS chain (no -k needed)"
|
||||
else
|
||||
fail "TLS verification failed without -k"
|
||||
fi
|
||||
|
||||
# Cert issuer should be a real CA (Let's Encrypt here), not self-signed.
|
||||
issuer=$(echo | openssl s_client -connect "${BASE#https://}:443" -servername "${BASE#https://}" 2>/dev/null \
|
||||
| openssl x509 -noout -issuer 2>/dev/null)
|
||||
info "issuer: $issuer"
|
||||
echo "$issuer" | grep -qi "Let's Encrypt" \
|
||||
&& ok "cert issued by Let's Encrypt" || fail "unexpected cert issuer: $issuer"
|
||||
|
||||
# Plain HTTP must NOT serve data in cleartext: expect a redirect to HTTPS (3xx)
|
||||
# or refusal — never a 200 with contact data.
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X PROPFIND -H "Depth: 0" "$HTTP_URL" 2>/dev/null)
|
||||
case "$code" in
|
||||
301|302|307|308) ok "plain HTTP -> $code redirect to HTTPS (no cleartext)";;
|
||||
000) ok "plain HTTP refused/unreachable (no cleartext)";;
|
||||
200) fail "plain HTTP served 200 in CLEARTEXT — server leaks data over http!";;
|
||||
*) ok "plain HTTP -> $code (not 200, no cleartext data)";;
|
||||
esac
|
||||
echo
|
||||
|
||||
# ---- (c) Reception: N contacts via REPORT -----------------------------------
|
||||
echo "--- (c) Contact reception (DAVx5's sync mechanism) ---"
|
||||
|
||||
# PROPFIND Depth:1 -> count .vcf hrefs
|
||||
pf=$(curl -s -u "${USER}:${DAV_PASS}" -X PROPFIND -H "Depth: 1" \
|
||||
-H "Content-Type: application/xml" --data "$PROPFIND_BODY" "$AB_URL")
|
||||
hrefs=$(printf '%s' "$pf" | grep -oE '<[a-zA-Z0-9]+:href>[^<]+\.vcf</[a-zA-Z0-9]+:href>' | wc -l | tr -d ' ')
|
||||
info "PROPFIND .vcf hrefs: $hrefs"
|
||||
if [ "$hrefs" -ge $((EXPECTED_VCARDS - TOLERANCE)) ] && [ "$hrefs" -le $((EXPECTED_VCARDS + TOLERANCE)) ]; then
|
||||
ok "PROPFIND Depth:1 lists $hrefs vCards (expected ~$EXPECTED_VCARDS)"
|
||||
else
|
||||
fail "PROPFIND Depth:1 listed $hrefs vCards (expected ~$EXPECTED_VCARDS +/-$TOLERANCE)"
|
||||
fi
|
||||
|
||||
# REPORT addressbook-query -> count BEGIN:VCARD (actual data download)
|
||||
rep=$(curl -s -u "${USER}:${DAV_PASS}" -X REPORT -H "Depth: 1" \
|
||||
-H "Content-Type: application/xml" --data "$REPORT_BODY" "$AB_URL")
|
||||
vcards=$(printf '%s' "$rep" | grep -c 'BEGIN:VCARD')
|
||||
info "REPORT BEGIN:VCARD count: $vcards"
|
||||
if [ "$vcards" -ge $((EXPECTED_VCARDS - TOLERANCE)) ] && [ "$vcards" -le $((EXPECTED_VCARDS + TOLERANCE)) ]; then
|
||||
ok "REPORT addressbook-query returns $vcards vCards (expected ~$EXPECTED_VCARDS)"
|
||||
else
|
||||
fail "REPORT returned $vcards vCards (expected ~$EXPECTED_VCARDS +/-$TOLERANCE)"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ---- (d) Known contact integrity --------------------------------------------
|
||||
echo "--- (d) Known-contact integrity (TEL + EMAIL) ---"
|
||||
|
||||
# Extract the single vCard block that matches KNOWN_NAME and check fields.
|
||||
block=$(printf '%s' "$rep" | awk -v name="$KNOWN_NAME" '
|
||||
/BEGIN:VCARD/ {buf=""}
|
||||
{buf=buf"\n"$0}
|
||||
/END:VCARD/ { if (buf ~ ("FN:.*"name)) {print buf; exit} }')
|
||||
|
||||
if [ -z "$block" ]; then
|
||||
fail "known contact matching '$KNOWN_NAME' not found in REPORT"
|
||||
else
|
||||
ok "known contact '$KNOWN_NAME' present in addressbook"
|
||||
printf '%s' "$block" | grep -qF "$KNOWN_TEL" \
|
||||
&& ok " -> TEL '$KNOWN_TEL' intact" || fail " -> TEL '$KNOWN_TEL' MISSING"
|
||||
printf '%s' "$block" | grep -qiF "$KNOWN_EMAIL" \
|
||||
&& ok " -> EMAIL '$KNOWN_EMAIL' intact" || fail " -> EMAIL '$KNOWN_EMAIL' MISSING"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ---- Summary ----------------------------------------------------------------
|
||||
echo "=== Summary: ${GREEN}${PASS_COUNT} passed${RESET}, ${RED}${FAIL_COUNT} failed${RESET} ==="
|
||||
[ "$FAIL_COUNT" -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user