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.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
// Throwaway HTTPS origin server for scripts/smoke-test.sh — cache.js only
|
||||
// proxies https:// origins, so the smoke test needs *something* self-signed
|
||||
// to fetch-through, not a real oxmc.me dependency. Not used outside the
|
||||
// smoke test.
|
||||
const { createServer } = require("node:https");
|
||||
const selfsigned = require("selfsigned");
|
||||
|
||||
const port = Number(process.argv[2] || 9443);
|
||||
const body = Buffer.from("hello from dummy origin\n");
|
||||
|
||||
const pems = selfsigned.generate([{ name: "commonName", value: "localhost" }], { days: 1 });
|
||||
|
||||
createServer({ cert: pems.cert, key: pems.private }, (req, res) => {
|
||||
res.writeHead(200, { "content-type": "text/plain", "content-length": body.length });
|
||||
res.end(body);
|
||||
}).listen(port, () => {
|
||||
console.log(`dummy-origin listening on https://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user