From d98127115b916ac49d27e4971eeaf61fa2913a97 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 13 Jun 2026 01:24:36 +0200 Subject: [PATCH] =?UTF-8?q?test(davx5):=20script=20reproducible=20de=20ver?= =?UTF-8?q?ificaci=C3=B3n=20DAVx5=20=E2=86=94=20Xandikos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/test_davx5_sync.sh | 165 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100755 tools/test_davx5_sync.sh diff --git a/tools/test_davx5_sync.sh b/tools/test_davx5_sync.sh new file mode 100755 index 0000000..f0dcf0e --- /dev/null +++ b/tools/test_davx5_sync.sh @@ -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='' +REPORT_BODY='' + +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' | 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