#!/usr/bin/env bash set -Eeuo pipefail ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) SRC="$ROOT/opnsense/plugin/src" WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT RC="$SRC/etc/rc.d/rustproxy" HOOK="$SRC/etc/rc.syshook.d/start/50-rustproxy.start" SERVICE="$SRC/opnsense/scripts/rustproxy/service.sh" MODEL="$SRC/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml" FORM="$SRC/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml" TEMPLATE="$SRC/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf" [[ -f "$HOOK" && -x "$HOOK" ]] || { echo 'Boot syshook must exist and be executable' >&2; exit 1; } [[ ! -e "$SRC/etc/rc.syshook.d/10-rustproxy.start" ]] || { echo 'Old hook path still exists' >&2; exit 1; } for script in "$RC" "$HOOK" "$SERVICE"; do sh -n "$script" done php -l "$SRC/opnsense/mvc/app/models/OPNsense/RustProxy/Migrations/M1_5_0.php" >/dev/null # Ensure GUI, model and Jinja template agree about the new setting. python3 - "$MODEL" "$FORM" "$TEMPLATE" <<'PY' import pathlib, sys, xml.etree.ElementTree as ET model = ET.parse(sys.argv[1]).getroot() form = ET.parse(sys.argv[2]).getroot() template = pathlib.Path(sys.argv[3]).read_text() assert model.find('version').text == '1.5.0' field = model.find('./items/general/updateCheckIntervalSecs') assert field is not None and field.attrib['type'] == 'IntegerField' assert field.findtext('Default') == '3600' assert field.findtext('MinimumValue') == '60' assert field.findtext('MaximumValue') == '604800' assert 'rustproxy.general.updateCheckIntervalSecs' in [e.text for e in form.iter('id')] assert 'rustproxy_update_check_interval_secs=' in template assert 'OPNsense.rustproxy.general.updateCheckIntervalSecs' in template PY # Isolate the rc.d argument composition without running FreeBSD-only commands. cat > "$WORK/rc.subr" <<'EOF' load_rc_config() { :; } run_rc_command() { if [ "$1" = reload ]; then "$reload_cmd"; else printf '%s\n' "$command_args"; fi } EOF python3 - "$RC" "$WORK/rc-test" "$WORK" <<'PY' from pathlib import Path import sys src = Path(sys.argv[1]).read_text() work = Path(sys.argv[3]) src = src.replace('. /etc/rc.subr', '. "'+str(work/'rc.subr')+'"') src = src.replace('config_file="/usr/local/etc/rustproxy/opnsense.conf"', 'config_file="'+str(work/'opnsense.conf')+'"') Path(sys.argv[2]).write_text(src) PY assert_interval() { local val=$1 printf 'enabled="1"\nrustproxy_update_check_interval_secs="%s"\n' "$val" > "$WORK/opnsense.conf" local args args=$(sh "$WORK/rc-test" start) [[ "$args" == *"--update-check-interval-secs $val serve"* ]] || { echo "Wrong update interval arguments: $args" >&2; exit 1; } } assert_interval 3600 assert_interval 7200 assert_interval 60 assert_interval 604800 printf 'enabled="1"\n' > "$WORK/opnsense.conf" args=$(sh "$WORK/rc-test" start) [[ "$args" == *'--update-check-interval-secs 3600 serve'* ]] || exit 1 # Exercise the rc.d soft reload itself with a live mock child and CLI. mkdir -p "$WORK/bin" cat > "$WORK/bin/mock-cli" <<'EOF' #!/bin/sh printf '%s\n' "$*" > "$TEST_CLI_ARGS" EOF chmod +x "$WORK/bin/mock-cli" printf 'enabled="1"\nrustproxy_config_path="/usr/local/etc/rustproxy/config.json"\n' > "$WORK/opnsense.conf" echo "$$" > "$WORK/rustproxy.pid" export TEST_CLI_ARGS="$WORK/cli-args" rustproxy_run_dir="$WORK" rustproxy_command="$WORK/bin/mock-cli" sh "$WORK/rc-test" reload args=$(cat "$TEST_CLI_ARGS") [[ "$args" == *'--data-dir /var/db/rustproxy'* ]] [[ "$args" == *'--config /usr/local/etc/rustproxy/config.json'* ]] [[ "$args" == *'--admin-listen 127.0.0.1:9090 reload'* ]] printf 'enabled="1"\nrustproxy_config_path="/usr/local/etc/rustproxy/config.json;touch /tmp/bad"\n' > "$WORK/opnsense.conf" if rustproxy_run_dir="$WORK" rustproxy_command="$WORK/bin/mock-cli" sh "$WORK/rc-test" reload >/dev/null 2>&1; then echo 'Unsafe OPNsense config path was accepted' >&2; exit 1 fi for invalid in 0 59 604801 nope '1;touch /tmp/pwn'; do printf 'enabled="1"\nrustproxy_update_check_interval_secs="%s"\n' "$invalid" > "$WORK/opnsense.conf" if sh "$WORK/rc-test" start >/dev/null 2>&1; then echo "Interval '$invalid' should have failed" >&2; exit 1 fi done # Mock configd + rc.d so we can exercise the actual hook and service wrapper. mkdir -p "$WORK/bin" cat > "$WORK/bin/configctl" <<'EOF' #!/bin/sh printf 'template %s\n' "$*" >> "$TEST_LOG" if [ "${FAIL_RENDER:-0}" = 1 ]; then exit 2 fi printf 'enabled="%s"\n' "${ENABLED:-1}" > "$TEST_CONFIG" EOF cat > "$WORK/bin/rustproxy-rc" <<'EOF' #!/bin/sh case "$1" in status) [ -f "$TEST_PID" ] ;; onestart) printf 'start\n' >> "$TEST_LOG"; touch "$TEST_PID" ;; onestop) printf 'stop\n' >> "$TEST_LOG"; rm -f "$TEST_PID" ;; reload) printf 'reload\n' >> "$TEST_LOG" ;; *) exit 1 ;; esac EOF chmod +x "$WORK/bin/configctl" "$WORK/bin/rustproxy-rc" python3 - "$HOOK" "$SERVICE" "$WORK" <<'PY' from pathlib import Path import sys hook = Path(sys.argv[1]).read_text() service = Path(sys.argv[2]).read_text() w = Path(sys.argv[3]) hook = hook.replace('/usr/local/sbin/configctl', str(w/'bin/configctl')) hook = hook.replace('/usr/local/opnsense/scripts/rustproxy/service.sh', str(w/'service-test')) service = service.replace('/usr/local/etc/rustproxy/opnsense.conf', str(w/'opnsense.conf')) service = service.replace('/usr/local/etc/rc.d/rustproxy', str(w/'bin/rustproxy-rc')) (w/'hook-test').write_text(hook) (w/'service-test').write_text(service) PY chmod +x "$WORK/hook-test" "$WORK/service-test" export TEST_LOG="$WORK/actions.log" TEST_CONFIG="$WORK/opnsense.conf" TEST_PID="$WORK/pidfile" : > "$TEST_LOG" ENABLED=1 "$WORK/hook-test" [[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Boot did not start service' >&2; exit 1; } ENABLED=1 "$WORK/hook-test" [[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Duplicate supervisor start' >&2; exit 1; } ENABLED=1 "$WORK/service-test" reload [[ $(grep -cx reload "$TEST_LOG") == 1 ]] || { echo 'Soft reload not passed through' >&2; exit 1; } [[ $(grep -cx stop "$TEST_LOG" || true) == 0 ]] || { echo 'Soft reload restarted service' >&2; exit 1; } rm -f "$TEST_PID" ENABLED=0 "$WORK/hook-test" [[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Disabled service started' >&2; exit 1; } if FAIL_RENDER=1 ENABLED=1 "$WORK/hook-test" >/dev/null 2>&1; then echo 'Broken configuration reload was not reported' >&2; exit 1 fi [[ $(grep -cx start "$TEST_LOG") == 1 ]] || { echo 'Started despite failed template rendering' >&2; exit 1; } echo 'OPNsense boot / update interval / soft reload checks: OK'