#!/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