# Shared by interop.sh and the scenarios: check helpers, PrivaPub personas and tokens, the statistics check. P=http://127.0.0.1:${PASTURE_PORT:-6971} work=$(mktemp -d); trap 'rm -rf "$work"' EXIT pass=0; fail=0; expected=0 # every check is also appended to $INTEROP_JSONL (out/scenarios.jsonl), which the town's report reads next to its own INTEROP_JSONL=${INTEROP_JSONL:-$here/out/scenarios.jsonl} mkdir -p "$(dirname "$INTEROP_JSONL")" record() { python3 -c 'import json,sys,time; print(json.dumps({"source":"scenario","peer":sys.argv[1],"check":sys.argv[2],"status":sys.argv[3],"t":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime())}))' \ "${INTEROP_PEER:-}" "$1" "$2" >> "$INTEROP_JSONL"; } ok() { echo " ok $*"; pass=$((pass+1)); record "$*" pass; } ko() { echo " FAIL $*"; fail=$((fail+1)); record "$*" fail; } xf() { echo " xf $* (expected to fail until a later phase)"; expected=$((expected+1)); record "$*" xfail; } j() { python3 -c "import sys,json try: d=json.load(sys.stdin) except Exception: d=None $1" 2>/dev/null; } until_true() { local tries=$1; shift; for _ in $(seq 1 "$tries"); do if eval "$@"; then return 0; fi; sleep 2; done; return 1; } site() { curl -k --resolve "$1:6443:127.0.0.1" "${@:2}"; } # fetches one of PrivaPub's own https URIs (ids, scribbles) from the workstation, through Caddy pfetch() { curl -sk --connect-to privapub.test:443:127.0.0.1:6443 "$@"; } # make_png : an 8x8 red PNG, for uploads # the status an unsigned ActivityPub GET of a PrivaPub document gets pstatus() { pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$1"; } # SecureMode (PRIVAPUB_ENV="Federation__SecureMode=true") answers every unsigned GET but the instance actor's 401, so a # post's author answering 401 means it is on; then 401 is what "not served" and "gone" look like to an unsigned reader secure_mode() { [ "$(pstatus "${1%%/scribbles/*}")" = "401" ]; } unserved() { local code; code=$(pstatus "$1"); if secure_mode "$1"; then [ "$code" = 401 ]; else [ "$code" = 404 ]; fi; } gone_unsigned() { local code; code=$(pstatus "$1"); if secure_mode "$1"; then [ "$code" = 401 ]; else [ "$code" = 410 ]; fi; } make_png() { python3 -c " import struct,zlib w=h=8 raw=b''.join(b'\x00'+bytes([200,60,60])*w for _ in range(h)) png=b'\x89PNG\r\n\x1a\n'+b''.join(struct.pack('>I',len(c))+t+c+struct.pack('>I',zlib.crc32(t+c)&0xffffffff) for t,c in [(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0)),(b'IDAT',zlib.compress(raw)),(b'IEND',b'')]) open('$1','wb').write(png)"; } ROOT_USER=pastureroot; ROOT_PASS='Pasture-Pass-1!' privapub_root() { local root root=$(curl -s -X POST $P/clientapi/user/signup -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}") [ -n "$(echo "$root" | j "print(d['token'])")" ] || root=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}") echo "$root" | j "print(d['token'])" } # privapub_token : creates the persona under the pasture root if needed and returns a Mastodon token for it, # through the same OAuth flow the deploy's smoke check uses (tools/smoke/oauth.sh). privapub_token() { local persona=$1 jwt jwt=$(privapub_root) curl -s -o /dev/null -X POST $P/clientapi/avatar/private/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \ -d "{\"userName\":\"$persona\",\"name\":\"$persona of PrivaPub\",\"biography\":\"testing federation\"}" "$here/../smoke/oauth.sh" "$P" "$ROOT_USER" "$ROOT_PASS" "$persona" "read write follow" | cut -d' ' -f1 } # p_report : a persona reports an account's posts and asks for # the report to be forwarded; prints whether PrivaPub says it was (Mastodon's "forwarded") p_report() { local auth=$1 account=$2 reason=$3; shift 3 local ids=() id for id in "$@"; do ids+=(-d "status_ids[]=$id"); done curl -s -X POST -H "$auth" "$P/api/v1/reports" -d "account_id=$account" "${ids[@]}" --data-urlencode "comment=$reason" \ -d 'category=other' -d 'forward=true' | j "print(d['forwarded'])" } # the anonymous Service that carries PrivaPub's reports to the servers that take them only from a person or a service P_REPORTER=https://privapub.test/peasants/privapub_reports # stats_check : the admin statistics name the peer's software and count traffic both ways. stats_check() { local host=$1 software=$2 admin found podman exec -w /app pasture-privapub /app/PrivaPub admin promote "$ROOT_USER" >/dev/null 2>&1 || true admin=$(curl -s -X POST $P/clientapi/user/login -H 'Content-Type: application/json' -d "{\"userName\":\"$ROOT_USER\",\"password\":\"$ROOT_PASS\"}" | j "print(d['token'])") until_true 30 'found=$(curl -s -H "Authorization: Bearer $admin" "$P/clientapi/admin/statistics/hosts/$host?days=1"); [ "$(echo "$found" | j "print((d[\"instance\"] or {}).get(\"software\"))")" = "$software" ]' \ && ok "statistics describe $host as $software" || ko "statistics do not describe $host as $software" found=$(curl -s -H "Authorization: Bearer $admin" "$P/clientapi/admin/statistics/hosts/$host?days=1") [ "$(echo "$found" | j "print(any(k.startswith('in:') for day in d['days'] for k in day['counters']))")" = "True" ] \ && ok "statistics count what $host sent" || ko "no inbound statistics for $host" [ "$(echo "$found" | j "print(any(k.startswith('out:') and ':ok' in k for day in d['days'] for k in day['counters']))")" = "True" ] \ && ok "statistics count what we delivered to $host" || ko "no outbound statistics for $host" [ "$(echo "$found" | j "print('$ROOT_USER' not in json.dumps(d['events']) and 'alice' not in json.dumps(d['events']))")" = "True" ] \ && ok "statistics for $host name no account" || ko "statistics for $host name an account" }