Files
oxmc 685c14ba97 Initial commit: local network content cache for BgUpd component downloads
Node.js daemon: Ed25519 identity + self-signed TLS, signed registration/
renewal with the central registry, mDNS advertising, disk-backed
fetch-through asset cache with an origin allowlist. Includes a local
dev-registry stand-in for the real oxmc.me endpoints and an end-to-end
smoke test (register -> token -> cache miss/hit -> SSRF rejection).

See README.md for the full protocol and trust model.
2026-07-25 00:06:20 -07:00

126 lines
5.2 KiB
Bash

#!/usr/bin/env bash
# End-to-end local smoke test: dev-registry + pawletcache-server + a throwaway
# HTTPS origin, wired together, verifying register -> signed token -> asset
# fetch-through -> disk cache actually works. No real oxmc.me dependency.
#
# Requires: `npm install` already run in pawletcache-server/ (bonjour-service,
# js-yaml, selfsigned). dev-registry/ has zero deps, nothing to install there.
set -euo pipefail
cd "$(dirname "$0")/.." # pawletcache-server/
ORIGIN_PORT=9443
REGISTRY_PORT=4100
CACHE_PORT=8543
ENROLLMENT_TOKEN=smoke-test-token
# Path handling here is fussy on Windows+Git-Bash: native Windows node.exe
# doesn't understand MSYS-style POSIX paths at all (not just /tmp — `pwd`'s
# /c/Users/... gets misread as literal drive-root segments, e.g. resolves to
# C:\c\Users\...). `pwd -W` gives the Windows-style form (C:/Users/...),
# which both node.exe and Bash/`ls` resolve to the same real location — use
# that for anything written into a file Node will read a path back out of.
# (On real POSIX systems `pwd -W` doesn't exist; fall back to plain pwd.)
WORKDIR="$(pwd -W 2>/dev/null || pwd)/.smoke-test-tmp-$$"
mkdir -p "$WORKDIR"
PIDS=()
cleanup() {
for pid in "${PIDS[@]:-}"; do kill "$pid" 2>/dev/null || true; done
rm -rf "$WORKDIR"
}
trap cleanup EXIT
pass() { echo "PASS: $1"; }
fail() { echo "FAIL: $1"; cleanup; exit 1; }
wait_for() {
local url=$1 tries=${2:-30}
for _ in $(seq 1 "$tries"); do
curl -sk -o /dev/null "$url" && return 0
sleep 0.5
done
return 1
}
echo "== starting dummy HTTPS origin on :$ORIGIN_PORT =="
node scripts/dummy-origin.js "$ORIGIN_PORT" > "$WORKDIR/origin.log" 2>&1 &
PIDS+=($!)
wait_for "https://localhost:$ORIGIN_PORT/" || fail "dummy origin never came up"
echo "== starting dev-registry on :$REGISTRY_PORT =="
DEV_REGISTRY_PORT=$REGISTRY_PORT \
DEV_REGISTRY_DATA_DIR="$WORKDIR/registry-data" \
DEV_ENROLLMENT_TOKENS=$ENROLLMENT_TOKEN \
DEV_TOKEN_TTL_MS=600000 \
node dev-registry/src/index.js > "$WORKDIR/registry.log" 2>&1 &
PIDS+=($!)
wait_for "http://localhost:$REGISTRY_PORT/health" || fail "dev-registry never came up"
pass "dev-registry up"
echo "== writing pawletcache-server config =="
cat > "$WORKDIR/config.yml" <<EOF
hostname: localhost
listenAddr: "0.0.0.0"
listenPort: $CACHE_PORT
storageDir: $WORKDIR/assets
identityDir: $WORKDIR/identity
centralUrl: http://localhost:$REGISTRY_PORT
enrollmentToken: $ENROLLMENT_TOKEN
allowedOrigins:
- localhost
EOF
echo "== starting pawletcache-server on :$CACHE_PORT =="
# dummy-origin.js is self-signed on purpose (see its header comment) — cache.js
# rightly rejects that for a real origin, so this smoke test alone tells
# Node's fetch() to skip TLS verification. Never do this outside a scoped
# local test — cache.js's own code has no such override.
NODE_TLS_REJECT_UNAUTHORIZED=0 node src/index.js "$WORKDIR/config.yml" > "$WORKDIR/cache-server.log" 2>&1 &
PIDS+=($!)
wait_for "https://localhost:$CACHE_PORT/asset" || fail "pawletcache-server never came up"
echo "== waiting for registration to land in dev-registry =="
registered=false
for _ in $(seq 1 20); do
lookup=$(curl -s "http://localhost:$REGISTRY_PORT/apis/aosp/cache/lookup")
if echo "$lookup" | grep -q '"available":true'; then
registered=true
break
fi
sleep 0.5
done
[ "$registered" = true ] || fail "pawletcache-server never registered with dev-registry (see $WORKDIR/cache-server.log, $WORKDIR/registry.log)"
pass "pawletcache-server registered, dev-registry issued a signed token"
echo "== verifying the CacheToken carries the right lanHost/port =="
token=$(node -e "console.log(JSON.parse(process.argv[1]).token)" "$lookup")
node -e "
const env = JSON.parse(Buffer.from(process.argv[1], 'base64').toString('utf8'));
const p = env.payload;
if (p.hostname !== 'localhost') throw new Error('hostname mismatch: ' + p.hostname);
if (p.port !== String($CACHE_PORT)) throw new Error('port mismatch: ' + p.port);
if (!p.tlsSpkiSha256) throw new Error('missing tlsSpkiSha256');
console.log('token payload OK:', JSON.stringify(p));
" "$token" || fail "CacheToken payload didn't look right"
pass "CacheToken payload shape OK"
echo "== fetching an asset through the cache (miss, then hit) =="
origin_url="https%3A%2F%2Flocalhost%3A${ORIGIN_PORT}%2Ffile"
body1=$(curl -sk "https://localhost:$CACHE_PORT/asset?url=$origin_url")
[ "$body1" = "hello from dummy origin" ] || fail "first fetch-through returned wrong body: '$body1'"
pass "cache miss fetch-through returned correct content"
body2=$(curl -sk "https://localhost:$CACHE_PORT/asset?url=$origin_url")
[ "$body2" = "hello from dummy origin" ] || fail "second (cached) fetch returned wrong body: '$body2'"
pass "cache hit returned correct content"
[ -n "$(ls -A "$WORKDIR/assets" 2>/dev/null)" ] || fail "nothing landed in storageDir — cache didn't persist to disk"
pass "asset persisted to disk cache"
echo "== verifying the SSRF allowlist actually blocks non-allowlisted origins =="
status=$(curl -sk -o /dev/null -w '%{http_code}' "https://localhost:$CACHE_PORT/asset?url=https%3A%2F%2Fevil.example%2Ffile")
[ "$status" = "403" ] || fail "expected 403 for a non-allowlisted origin, got $status"
pass "non-allowlisted origin correctly rejected (403)"
echo ""
echo "ALL CHECKS PASSED"