diff --git a/images/common/init_command.sh b/images/common/init_command.sh index 5ee4a25a..cc313fbb 100644 --- a/images/common/init_command.sh +++ b/images/common/init_command.sh @@ -1,7 +1,7 @@ #!/bin/sh # OpenWISP common module init script set -e -source utils.sh +. ./utils.sh init_conf @@ -24,26 +24,23 @@ elif [ "$MODULE_NAME" = 'postfix' ]; then elif [ "$MODULE_NAME" = 'freeradius' ]; then wait_nginx_services if [ "$DEBUG_MODE" = 'False' ]; then - source docker-entrypoint.sh + . ./docker-entrypoint.sh else - source docker-entrypoint.sh -X + . ./docker-entrypoint.sh -X fi elif [ "$MODULE_NAME" = 'openvpn' ]; then - if [[ -z "$VPN_DOMAIN" ]]; then exit; fi + if [ -z "$VPN_DOMAIN" ]; then exit; fi + . ./openvpn_utils.sh wait_nginx_services openvpn_preconfig openvpn_config openvpn_config_download crl_download - echo "*/1 * * * * sh /openvpn.sh" | crontab - - ( - crontab -l - echo "0 0 * * * sh /revokelist.sh" - ) | crontab - + crontab /openvpn.crontab crond # Schedule send topology script only when # network topology module is enabled. - if [ "$USE_OPENWISP_TOPOLOGY" == "True" ]; then + if [ "$USE_OPENWISP_TOPOLOGY" = "True" ]; then init_send_network_topology fi # Supervisor is used to start the service because OpenVPN @@ -93,7 +90,7 @@ elif [ "$MODULE_NAME" = 'celery' ]; then ${OPENWISP_CELERY_NETWORK_COMMAND_FLAGS} fi - if [[ "$USE_OPENWISP_FIRMWARE" == "True" && "$USE_OPENWISP_CELERY_FIRMWARE" == "True" ]]; then + if [ "$USE_OPENWISP_FIRMWARE" = "True" ] && [ "$USE_OPENWISP_CELERY_FIRMWARE" = "True" ]; then echo "Starting the 'firmware_upgrader' celery worker" celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues firmware_upgrader \ -n firmware_upgrader@%h --logfile /opt/openwisp/logs/celery_firmware_upgrader.log \ @@ -104,7 +101,7 @@ elif [ "$MODULE_NAME" = 'celery' ]; then tail -f /opt/openwisp/logs/* elif [ "$MODULE_NAME" = 'celery_monitoring' ]; then python services.py database redis dashboard - if [[ "$USE_OPENWISP_MONITORING" == "True" && "$USE_OPENWISP_CELERY_MONITORING" == 'True' ]]; then + if [ "$USE_OPENWISP_MONITORING" = "True" ] && [ "$USE_OPENWISP_CELERY_MONITORING" = 'True' ]; then echo "Starting the 'monitoring' celery worker" celery -A openwisp worker -l ${DJANGO_LOG_LEVEL} --queues monitoring \ -n monitoring@%h --logfile /opt/openwisp/logs/celery_monitoring.log \ diff --git a/images/common/utils.sh b/images/common/utils.sh index ff5ea6da..56fe98bc 100644 --- a/images/common/utils.sh +++ b/images/common/utils.sh @@ -197,67 +197,3 @@ postfix_config() { postmap /etc/aliases newaliases } - -get_redis_value() { - local key="$1" - echo -en "GET $key\r\n" | nc redis 6379 | awk 'NR==2 {gsub(/\r/, ""); print}' -} - -openvpn_preconfig() { - mkdir -p /dev/net - if [ ! -c /dev/net/tun ]; then - mknod /dev/net/tun c 10 200 - fi - ip -6 route show default 2>/dev/null - if [ $? = 0 ]; then - echo "Enabling IPv6 Forwarding" - sysctl -w net.ipv6.conf.all.disable_ipv6=0 || echo "Failed to enable IPv6 support" - sysctl -w net.ipv6.conf.default.forwarding=1 || echo "Failed to enable IPv6 Forwarding default" - sysctl -w net.ipv6.conf.all.forwarding=1 || echo "Failed to enable IPv6 Forwarding" - fi -} - -openvpn_config() { - # Fectch UUID and Key of the default VPN only if they - # are not already set. The user may override the UUID and Key - # by setting them in the environment variables to use deploy - # a different VPN server. - if [ -z "$UUID" ]; then - export UUID=$(get_redis_value "openwisp_default_vpn_uuid") - export KEY=$(get_redis_value "openwisp_default_vpn_key") - export CA_UUID=$(get_redis_value "openwisp_default_vpn_ca_uuid") - fi -} - -openvpn_config_checksum() { - OFILE=$(curl --silent --insecure \ - "${API_INTERNAL}/controller/vpn/checksum/${UUID}/?key=${KEY}") - export OFILE - NFILE=$(cat checksum) - export NFILE -} - -openvpn_config_download() { - curl --silent --retry 10 --retry-delay 5 --retry-max-time 300 --insecure --output vpn.tar.gz \ - "${API_INTERNAL}/controller/vpn/download-config/${UUID}/?key=${KEY}" - curl --silent --insecure --output checksum \ - "${API_INTERNAL}/controller/vpn/checksum/${UUID}/?key=${KEY}" - tar xzf vpn.tar.gz - chmod 600 ./*.pem -} - -crl_download() { - curl --silent --insecure --output revoked.crl \ - "${DASHBOARD_INTERNAL}/admin/pki/ca/x509/ca/${CA_UUID}.crl" -} - -init_send_network_topology() { - if [ -z "$TOPOLOGY_UUID" ]; then - export TOPOLOGY_UUID=$(get_redis_value "default_openvpn_topology_uuid") - export TOPOLOGY_KEY=$(get_redis_value "default_openvpn_topology_key") - fi - ( - crontab -l - echo "*/$TOPOLOGY_UPDATE_INTERVAL * * * * TOPOLOGY_UUID=$TOPOLOGY_UUID TOPOLOGY_KEY=$TOPOLOGY_KEY sh /send-topology.sh" - ) | crontab - -} diff --git a/images/openwisp_openvpn/openvpn.crontab b/images/openwisp_openvpn/openvpn.crontab new file mode 100644 index 00000000..b157072d --- /dev/null +++ b/images/openwisp_openvpn/openvpn.crontab @@ -0,0 +1,2 @@ +*/1 * * * * sh /openvpn.sh +*/5 * * * * sh /revokelist.sh diff --git a/images/openwisp_openvpn/openvpn.sh b/images/openwisp_openvpn/openvpn.sh index a3af89b8..49379780 100644 --- a/images/openwisp_openvpn/openvpn.sh +++ b/images/openwisp_openvpn/openvpn.sh @@ -3,12 +3,19 @@ # This script will be called by cronjob to # update OpenVPN configurations periodically. cd / -source /utils.sh +. /utils.sh +. /openvpn_utils.sh openvpn_config -openvpn_config_checksum +if ! openvpn_config_checksum; then + echo "Failed to fetch or validate OpenVPN checksum" >&2 + exit 1 +fi if [ "${OFILE}" != "${NFILE}" ]; then - openvpn_config_download + if ! openvpn_config_download; then + echo "Failed to download updated OpenVPN configuration" >&2 + exit 1 + fi supervisorctl restart openvpn fi diff --git a/images/openwisp_openvpn/openvpn_utils.sh b/images/openwisp_openvpn/openvpn_utils.sh new file mode 100644 index 00000000..2a73e5f6 --- /dev/null +++ b/images/openwisp_openvpn/openvpn_utils.sh @@ -0,0 +1,200 @@ +#!/bin/sh + +get_redis_value() { + local key="$1" + printf 'GET %s\r\n' "$key" | nc -w 5 redis 6379 | awk 'NR==2 {gsub(/\r/, ""); print}' +} + +openvpn_preconfig() { + mkdir -p /dev/net + if [ ! -c /dev/net/tun ]; then + mknod /dev/net/tun c 10 200 + fi + if ip -6 route show default >/dev/null 2>&1; then + echo "Enabling IPv6 Forwarding" + sysctl -w net.ipv6.conf.all.disable_ipv6=0 || echo "Failed to enable IPv6 support" + sysctl -w net.ipv6.conf.default.forwarding=1 || echo "Failed to enable IPv6 Forwarding default" + sysctl -w net.ipv6.conf.all.forwarding=1 || echo "Failed to enable IPv6 Forwarding" + fi +} + +openvpn_config() { + # Fetch UUID and Key of the default VPN only if they + # are not already set. The user may override the UUID and Key + # by setting them in the environment variables to use deploy + # a different VPN server. + if [ -z "$UUID" ]; then + UUID="$(get_redis_value "openwisp_default_vpn_uuid")" + KEY="$(get_redis_value "openwisp_default_vpn_key")" + CA_UUID="$(get_redis_value "openwisp_default_vpn_ca_uuid")" + export UUID KEY CA_UUID + fi +} + +openvpn_config_checksum() { + local remote_checksum + local local_checksum + + remote_checksum=$(curl --silent --show-error --fail --insecure \ + "${API_INTERNAL}/controller/vpn/checksum/${UUID}/?key=${KEY}") || return 1 + if [ -z "$remote_checksum" ]; then + return 1 + fi + + if [ -s checksum ]; then + local_checksum=$(cat checksum) + fi + if [ -z "$local_checksum" ]; then + local_checksum="" + fi + + export OFILE="$remote_checksum" + export NFILE="$local_checksum" +} + +openvpn_config_download() { + local tmp_tar + local tmp_checksum + local tmp_extract_dir + local expected_checksum + local actual_checksum + + tmp_tar=$(mktemp /tmp/vpn-config.XXXXXX) || return 1 + tmp_checksum=$(mktemp /tmp/vpn-checksum.XXXXXX) || { + rm -f "$tmp_tar" + return 1 + } + tmp_extract_dir=$(mktemp -d /tmp/vpn-config-dir.XXXXXX) || { + rm -f "$tmp_tar" "$tmp_checksum" + return 1 + } + trap 'rm -f "$tmp_tar" "$tmp_checksum"; rm -rf "$tmp_extract_dir"' EXIT HUP INT TERM + + curl --silent --show-error --fail --retry 10 --retry-delay 5 --retry-max-time 300 \ + --insecure --output "$tmp_tar" \ + "${API_INTERNAL}/controller/vpn/download-config/${UUID}/?key=${KEY}" || return 1 + test -s "$tmp_tar" || return 1 + + curl --silent --show-error --fail --insecure --output "$tmp_checksum" \ + "${API_INTERNAL}/controller/vpn/checksum/${UUID}/?key=${KEY}" || return 1 + test -s "$tmp_checksum" || { + echo "Downloaded OpenVPN checksum is empty" >&2 + return 1 + } + + expected_checksum=$(awk 'NF {print $1; exit}' "$tmp_checksum" | tr -d '\r') + if [ -z "$expected_checksum" ]; then + echo "Downloaded OpenVPN checksum is empty" >&2 + return 1 + fi + + case "${#expected_checksum}" in + 32) + actual_checksum=$(md5sum "$tmp_tar" | awk '{print $1}') || return 1 + ;; + 40) + actual_checksum=$(sha1sum "$tmp_tar" | awk '{print $1}') || return 1 + ;; + 64) + actual_checksum=$(sha256sum "$tmp_tar" | awk '{print $1}') || return 1 + ;; + 128) + actual_checksum=$(sha512sum "$tmp_tar" | awk '{print $1}') || return 1 + ;; + *) + echo "Unsupported OpenVPN checksum format: $expected_checksum" >&2 + return 1 + ;; + esac + if [ "$actual_checksum" != "$expected_checksum" ]; then + echo "Downloaded OpenVPN config checksum mismatch" >&2 + return 1 + fi + + tar xzf "$tmp_tar" -C "$tmp_extract_dir" || return 1 + set -- "$tmp_extract_dir"/*.pem + test -e "$1" || return 1 + chmod 600 "$@" || return 1 + cp -R "$tmp_extract_dir"/. / || return 1 + + mv "$tmp_checksum" checksum || return 1 + rm -f "$tmp_tar" + rm -rf "$tmp_extract_dir" + trap - EXIT HUP INT TERM +} + +crl_download_to() { + local output_path="${1:-revoked.crl}" + curl --silent --show-error --fail --retry 10 --retry-delay 5 --retry-max-time 300 \ + --insecure --output "$output_path" \ + "${DASHBOARD_INTERNAL}/admin/pki/ca/x509/ca/${CA_UUID}.crl" || return 1 + test -s "$output_path" +} + +crl_download() { + local tmp_crl + + tmp_crl=$(mktemp /tmp/revoked.crl.XXXXXX) || return 1 + if ! crl_download_to "$tmp_crl"; then + rm -f "$tmp_crl" + return 1 + fi + if [ ! -s "$tmp_crl" ]; then + rm -f "$tmp_crl" + return 1 + fi + mv "$tmp_crl" revoked.crl || { + rm -f "$tmp_crl" + return 1 + } +} + +crl_download_if_changed() { + local tmp_crl + tmp_crl=$(mktemp /tmp/revoked.crl.XXXXXX) || return 2 + trap 'rm -f "$tmp_crl"' EXIT HUP INT TERM + + if ! crl_download_to "$tmp_crl"; then + trap - EXIT HUP INT TERM + rm -f "$tmp_crl" + return 2 + fi + if [ ! -s "$tmp_crl" ]; then + trap - EXIT HUP INT TERM + rm -f "$tmp_crl" + return 2 + fi + + if [ ! -f revoked.crl ] || ! cmp -s "$tmp_crl" revoked.crl; then + mv "$tmp_crl" revoked.crl || return 2 + trap - EXIT HUP INT TERM + return 0 + fi + + trap - EXIT HUP INT TERM + rm -f "$tmp_crl" + return 1 +} + +init_send_network_topology() { + case "$TOPOLOGY_UPDATE_INTERVAL" in + '' | *[!0-9]* | 0) + echo "Skipping topology cron: invalid TOPOLOGY_UPDATE_INTERVAL: $TOPOLOGY_UPDATE_INTERVAL" >&2 + return 0 + ;; + esac + + if [ -z "$TOPOLOGY_UUID" ]; then + TOPOLOGY_UUID="$(get_redis_value "default_openvpn_topology_uuid")" + TOPOLOGY_KEY="$(get_redis_value "default_openvpn_topology_key")" + export TOPOLOGY_UUID TOPOLOGY_KEY + fi + if [ -z "$TOPOLOGY_UUID" ] || [ -z "$TOPOLOGY_KEY" ]; then + echo "Skipping topology cron: missing TOPOLOGY_UUID or TOPOLOGY_KEY" >&2 + return 0 + fi + ( + crontab -l + echo "*/$TOPOLOGY_UPDATE_INTERVAL * * * * TOPOLOGY_UUID=$TOPOLOGY_UUID TOPOLOGY_KEY=$TOPOLOGY_KEY sh /send-topology.sh" + ) | crontab - +} diff --git a/images/openwisp_openvpn/revokelist.sh b/images/openwisp_openvpn/revokelist.sh index 35e93dd2..5df0b6ee 100644 --- a/images/openwisp_openvpn/revokelist.sh +++ b/images/openwisp_openvpn/revokelist.sh @@ -3,8 +3,15 @@ # This script will be called by cronjob to # update CRL periodically. cd / -source /utils.sh +. /utils.sh +. /openvpn_utils.sh openvpn_config -crl_download -supervisorctl restart openvpn +crl_download_if_changed +crl_status=$? + +if [ "$crl_status" -eq 0 ]; then + supervisorctl restart openvpn +elif [ "$crl_status" -eq 2 ]; then + echo "Failed to download CRL, keeping existing revoked.crl" >&2 +fi diff --git a/tests/runtests.py b/tests/runtests.py index ffa47ede..e6a55720 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -6,6 +6,7 @@ from pathlib import Path from urllib import error as urlerror from urllib import request +from uuid import uuid4 import requests from selenium.common.exceptions import TimeoutException @@ -352,10 +353,12 @@ def test_console_errors(self): def test_add_superuser(self): """Create new user to ensure a new user can be added.""" + username = f"test_superuser_{uuid4().hex[:8]}" + email = f"{username}@example.com" self.login() - self.create_superuser() + self.create_superuser(email=email, username=username) self.assertEqual( - "The user “test_superuser” was changed successfully.", + f"The user “{username}” was changed successfully.", self.find_element(By.CLASS_NAME, "success").text, ) @@ -460,14 +463,284 @@ def _test_celery_task_registered(container_name): with self.subTest("Test celery_monitoring container"): _test_celery_task_registered("celery_monitoring") + def test_openvpn_revokelist_restart_logic(self): + """Ensure revokelist restart behavior only triggers on CRL changes.""" + + container_id = self.docker_compose_get_container_id("openvpn") + openvpn_container = self.docker_client.containers.get(container_id) + test_dir = "/tmp/openwisp-crl-test" + + def _run_case(mode, seed_crl=None): + setup_existing = "" + if seed_crl is None: + setup_existing = f"rm -f {test_dir}/work/revoked.crl" + else: + setup_existing = ( + f"printf '%s\\n' '{seed_crl}' > {test_dir}/work/revoked.crl" + ) + + command = f""" +set -eu +rm -rf {test_dir} +mkdir -p {test_dir}/bin {test_dir}/work +cat > {test_dir}/bin/supervisorctl <<'EOF' +#!/bin/sh +printf '%s\\n' "$*" >> {test_dir}/supervisor.log +EOF +chmod +x {test_dir}/bin/supervisorctl +cp /openvpn_utils.sh {test_dir}/openvpn_utils.sh +cat >> {test_dir}/openvpn_utils.sh <<'EOF' + +openvpn_config() {{ :; }} + +crl_download_to() {{ + output_path="${{1:-revoked.crl}}" + case "${{CRL_TEST_MODE}}" in + initial|unchanged) + printf '%s\\n' 'v1-initial-crl' > "$output_path" + ;; + changed) + printf '%s\\n' 'v2-changed-crl' > "$output_path" + ;; + empty) + : > "$output_path" + ;; + fail) + return 1 + ;; + esac +}} +EOF +sed \ + -e 's|^cd /$|cd {test_dir}/work|' \ + -e 's|^source /utils.sh$|:|' \ + -e 's|^[.] /utils.sh$|:|' \ + -e 's|^source /openvpn_utils.sh$|. {test_dir}/openvpn_utils.sh|' \ + -e 's|^[.] /openvpn_utils.sh$|. {test_dir}/openvpn_utils.sh|' \ + /revokelist.sh > {test_dir}/revokelist.sh +grep -Fx "cd {test_dir}/work" {test_dir}/revokelist.sh >/dev/null +grep -Fx ":" {test_dir}/revokelist.sh >/dev/null +grep -Fx ". {test_dir}/openvpn_utils.sh" {test_dir}/revokelist.sh >/dev/null +! grep -Fx "cd /" {test_dir}/revokelist.sh >/dev/null +! grep -Fx "source /utils.sh" {test_dir}/revokelist.sh >/dev/null +! grep -Fx "source /openvpn_utils.sh" {test_dir}/revokelist.sh >/dev/null +! grep -Fx ". /utils.sh" {test_dir}/revokelist.sh >/dev/null +! grep -Fx ". /openvpn_utils.sh" {test_dir}/revokelist.sh >/dev/null +chmod +x {test_dir}/revokelist.sh +{setup_existing} +: > {test_dir}/supervisor.log +CRL_TEST_MODE={mode} PATH={test_dir}/bin:$PATH \ + sh {test_dir}/revokelist.sh > {test_dir}/stdout 2> {test_dir}/stderr +""" + result = openvpn_container.exec_run(["sh", "-lc", command]) + self.assertEqual(result.exit_code, 0, result.output.decode("utf-8")) + + def _read(path): + read_result = openvpn_container.exec_run( + ["sh", "-lc", f"cat {path} 2>/dev/null || true"] + ) + return read_result.output.decode("utf-8") + + return { + "stdout": _read(f"{test_dir}/stdout"), + "stderr": _read(f"{test_dir}/stderr"), + "supervisor": _read(f"{test_dir}/supervisor.log"), + "revoked_crl": _read(f"{test_dir}/work/revoked.crl"), + } + + initial = _run_case("initial") + self.assertEqual(initial["stdout"], "") + self.assertEqual(initial["stderr"], "") + self.assertEqual(initial["supervisor"], "restart openvpn\n") + self.assertEqual(initial["revoked_crl"], "v1-initial-crl\n") + + unchanged = _run_case("unchanged", seed_crl="v1-initial-crl") + self.assertEqual(unchanged["stdout"], "") + self.assertEqual(unchanged["stderr"], "") + self.assertEqual(unchanged["supervisor"], "") + self.assertEqual(unchanged["revoked_crl"], "v1-initial-crl\n") + + changed = _run_case("changed", seed_crl="v1-initial-crl") + self.assertEqual(changed["stdout"], "") + self.assertEqual(changed["stderr"], "") + self.assertEqual(changed["supervisor"], "restart openvpn\n") + self.assertEqual(changed["revoked_crl"], "v2-changed-crl\n") + + failed = _run_case("fail", seed_crl="v1-initial-crl") + self.assertEqual(failed["stdout"], "") + self.assertIn( + "Failed to download CRL, keeping existing revoked.crl", + failed["stderr"], + ) + self.assertEqual(failed["supervisor"], "") + self.assertEqual(failed["revoked_crl"], "v1-initial-crl\n") + + empty = _run_case("empty", seed_crl="v1-initial-crl") + self.assertEqual(empty["stdout"], "") + self.assertIn( + "Failed to download CRL, keeping existing revoked.crl", + empty["stderr"], + ) + self.assertEqual(empty["supervisor"], "") + self.assertEqual(empty["revoked_crl"], "v1-initial-crl\n") + + def test_openvpn_config_update_integrity_logic(self): + """Ensure config update logic recovers local state and rejects mismatches.""" + + container_id = self.docker_compose_get_container_id("openvpn") + openvpn_container = self.docker_client.containers.get(container_id) + test_dir = "/tmp/openwisp-config-test" + + def _run_case(mode): + seed_checksum = "" + if mode == "mismatch": + seed_checksum = ( + f"printf '%s\\n' 'old-checksum' > {test_dir}/work/checksum" + ) + + command = f""" +set -eu +rm -rf {test_dir} +mkdir -p {test_dir}/bin {test_dir}/work {test_dir}/payload +cat > {test_dir}/bin/supervisorctl <<'EOF' +#!/bin/sh +printf '%s\\n' "$*" >> {test_dir}/supervisor.log +EOF +chmod +x {test_dir}/bin/supervisorctl +cat > {test_dir}/bin/curl <<'EOF' +#!/bin/sh +set -eu +output="" +url="" +while [ "$#" -gt 0 ]; do + case "$1" in + --output) + output="$2" + shift 2 + ;; + --retry|--retry-delay|--retry-max-time) + shift 2 + ;; + --silent|--show-error|--fail|--insecure) + shift + ;; + *) + url="$1" + shift + ;; + esac +done +case "$url" in + */download-config/*) + cp "$OPENVPN_TEST_TAR" "$output" + ;; + */checksum/*) + if [ -n "$output" ]; then + printf '%s\\n' "$OPENVPN_REMOTE_CHECKSUM" > "$output" + else + printf '%s\\n' "$OPENVPN_REMOTE_CHECKSUM" + fi + ;; + *) + echo "Unexpected curl URL: $url" >&2 + exit 1 + ;; +esac +EOF +chmod +x {test_dir}/bin/curl +printf '%s\\n' 'old-ca' > {test_dir}/work/ca.pem +printf '%s\\n' 'old-cert' > {test_dir}/work/cert.pem +printf '%s\\n' 'old-key' > {test_dir}/work/key.pem +printf '%s\\n' 'old-dh' > {test_dir}/work/dh.pem +{seed_checksum} +printf '%s\\n' 'new-ca' > {test_dir}/payload/ca.pem +printf '%s\\n' 'new-cert' > {test_dir}/payload/cert.pem +printf '%s\\n' 'new-key' > {test_dir}/payload/key.pem +printf '%s\\n' 'new-dh' > {test_dir}/payload/dh.pem +tar czf {test_dir}/payload.tar.gz -C {test_dir}/payload . +ACTUAL_CHECKSUM=$(sha256sum {test_dir}/payload.tar.gz | awk '{{print $1}}') +case "{mode}" in + mismatch) + REMOTE_CHECKSUM='0000000000000000000000000000000000000000000000000000000000000000' + ;; + missing_local_checksum) + REMOTE_CHECKSUM="$ACTUAL_CHECKSUM" + ;; +esac +sed \ + -e 's|cp -R "\\$tmp_extract_dir"/. /|cp -R "$tmp_extract_dir"/. {test_dir}/work/|' \ + /openvpn_utils.sh > {test_dir}/openvpn_utils.sh +sed \ + -e 's|^cd /$|cd {test_dir}/work|' \ + -e 's|^[.] /utils.sh$|:|' \ + -e 's|^[.] /openvpn_utils.sh$|. {test_dir}/openvpn_utils.sh|' \ + /openvpn.sh > {test_dir}/openvpn.sh +grep -Fx "cd {test_dir}/work" {test_dir}/openvpn.sh >/dev/null +grep -Fx ":" {test_dir}/openvpn.sh >/dev/null +grep -Fx ". {test_dir}/openvpn_utils.sh" {test_dir}/openvpn.sh >/dev/null +grep -F 'cp -R "$tmp_extract_dir"/. {test_dir}/work/' \ + {test_dir}/openvpn_utils.sh >/dev/null +! grep -Fx "cd /" {test_dir}/openvpn.sh >/dev/null +! grep -Fx ". /utils.sh" {test_dir}/openvpn.sh >/dev/null +! grep -Fx ". /openvpn_utils.sh" {test_dir}/openvpn.sh >/dev/null +chmod +x {test_dir}/openvpn.sh +: > {test_dir}/supervisor.log +OPENVPN_TEST_TAR={test_dir}/payload.tar.gz \ +OPENVPN_REMOTE_CHECKSUM="$REMOTE_CHECKSUM" \ +PATH={test_dir}/bin:$PATH \ +UUID=test-vpn KEY=test-key \ + sh {test_dir}/openvpn.sh > {test_dir}/stdout 2> {test_dir}/stderr +""" + result = openvpn_container.exec_run(["sh", "-lc", command]) + + def _read(path): + read_result = openvpn_container.exec_run( + ["sh", "-lc", f"cat {path} 2>/dev/null || true"] + ) + return read_result.output.decode("utf-8") + + return { + "exit_code": result.exit_code, + "stdout": _read(f"{test_dir}/stdout"), + "stderr": _read(f"{test_dir}/stderr"), + "supervisor": _read(f"{test_dir}/supervisor.log"), + "ca_pem": _read(f"{test_dir}/work/ca.pem"), + "checksum": _read(f"{test_dir}/work/checksum"), + } + + mismatch = _run_case("mismatch") + self.assertEqual(mismatch["exit_code"], 1) + self.assertEqual(mismatch["stdout"], "") + self.assertIn( + "Downloaded OpenVPN config checksum mismatch", + mismatch["stderr"], + ) + self.assertIn( + "Failed to download updated OpenVPN configuration", + mismatch["stderr"], + ) + self.assertEqual(mismatch["supervisor"], "") + self.assertEqual(mismatch["ca_pem"], "old-ca\n") + self.assertEqual(mismatch["checksum"], "old-checksum\n") + + missing_local_checksum = _run_case("missing_local_checksum") + self.assertEqual(missing_local_checksum["exit_code"], 0) + self.assertEqual(missing_local_checksum["stdout"], "") + self.assertEqual(missing_local_checksum["stderr"], "") + self.assertEqual(missing_local_checksum["supervisor"], "restart openvpn\n") + self.assertEqual(missing_local_checksum["ca_pem"], "new-ca\n") + self.assertRegex(missing_local_checksum["checksum"].strip(), r"^[0-9a-f]{64}$") + def test_radius_user_registration(self): """Ensure users can register using the RADIUS API.""" - url = f"{self.config['api_url']}/api/v1/radius/organization/default/account/" + username = f"signup-user-{uuid4().hex[:8]}" + email = f"{username}@signup.com" + url = f'{self.config["api_url"]}/api/v1/radius/organization/default/account/' response = requests.post( url, json={ - "username": "signup-user", - "email": "user@signup.com", + "username": username, + "email": email, "password1": "rLx6OH%[", "password2": "rLx6OH%[", }, @@ -476,9 +749,7 @@ def test_radius_user_registration(self): self.assertEqual(response.status_code, 201) # Delete the created user self.login() - self.get_resource( - "signup-user", "/admin/openwisp_users/user/", "field-username" - ) + self.get_resource(username, "/admin/openwisp_users/user/", "field-username") self.objects_to_delete.append(self.base_driver.current_url) def test_freeradius(self):