mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-28 12:01:13 -04:00
153 lines
6.0 KiB
Python
153 lines
6.0 KiB
Python
"""
|
|
NetAlertX app.conf String Escaping Tests
|
|
|
|
Dispatches the real front/php/server/util.php savesettings path through the PHP
|
|
CLI against a temporary config directory, then checks that the generated
|
|
app.conf compiles without warnings and parses back to the typed values (with '
|
|
mapped to {s-quote}) for both scalar string and array string settings.
|
|
|
|
License: GNU GPLv3
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import warnings
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
FRONT_DIR = Path(__file__).resolve().parents[2] / "front"
|
|
PHP_BIN = shutil.which("php") or shutil.which("php83")
|
|
|
|
pytestmark = pytest.mark.skipif(PHP_BIN is None, reason="PHP CLI (php or php83) not available")
|
|
|
|
# Stands in for the web request: reads {"front": ..., "settings": [...]} from stdin,
|
|
# satisfies security.php's request-only dependencies and lets util.php dispatch savesettings.
|
|
# util.php only reads function/settings from $_POST (not $_REQUEST) and requires a matching
|
|
# csrf_token - seed $_SESSION directly (same PHP process, so security.php's session_start()
|
|
# just resumes it) rather than $_REQUEST, to mirror a real authenticated POST.
|
|
PHP_RUNNER = (
|
|
'$in = json_decode(stream_get_contents(STDIN), true);'
|
|
'if (!function_exists("apache_request_headers")) { function apache_request_headers() { return []; } }'
|
|
'$_SERVER["DOCUMENT_ROOT"] = $in["front"];'
|
|
'$_SERVER["HTTP_HOST"] = "localhost";'
|
|
'$_SERVER["REQUEST_URI"] = "/php/server/util.php";'
|
|
'session_start();'
|
|
'$_SESSION["csrf_token"] = "test-harness-csrf-token";'
|
|
'$_POST = ["function" => "savesettings", "csrf_token" => "test-harness-csrf-token", '
|
|
'"settings" => json_encode($in["settings"])];'
|
|
'require $in["front"] . "/php/server/util.php";'
|
|
)
|
|
|
|
# Minimal app.conf that lets globals.php and security.php load without a password prompt.
|
|
SEED_APP_CONF = "TIMEZONE='UTC'\nSETPWD_enable_password=False\n"
|
|
|
|
CASES = {
|
|
"ordinary_text": "hello world",
|
|
"doc_regex": r"192\.0\.2\..*",
|
|
"consecutive_backslashes": r"a\\b",
|
|
"backslashes_only": "\\" * 3,
|
|
"trailing_backslash": "trail" + "\\",
|
|
"existing_s_quote": "x{s-quote}y",
|
|
"literal_single_quote": "it's",
|
|
"regex_with_quote": r"\d+\s*'",
|
|
"backslash_before_quote": r"a\'b",
|
|
}
|
|
|
|
|
|
def scalar_key(case_id):
|
|
"""Return the app.conf key used for the scalar string setting of a case."""
|
|
return f"S_{case_id.upper()}"
|
|
|
|
|
|
def array_key(case_id):
|
|
"""Return the app.conf key used for the array string setting of a case."""
|
|
return f"A_{case_id.upper()}"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def app_conf(tmp_path_factory):
|
|
"""Run util.php saveSettings() once for all cases and return the generated app.conf source."""
|
|
root = tmp_path_factory.mktemp("app_conf")
|
|
config_dir = root / "config"
|
|
api_dir = root / "api"
|
|
session_dir = root / "session"
|
|
for folder in (config_dir, api_dir, session_dir):
|
|
folder.mkdir()
|
|
(config_dir / "app.conf").write_text(SEED_APP_CONF)
|
|
|
|
# UI_WAIT_FOR_SETTINGS=True keeps getReloadWaitRequired() from reading the (absent) API files.
|
|
settings = [["General", "UI_WAIT_FOR_SETTINGS", "boolean", True]]
|
|
for case_id, typed in CASES.items():
|
|
settings.append(["Test", scalar_key(case_id), "string", typed])
|
|
settings.append(["Test", array_key(case_id), "array", ["plain", typed]])
|
|
|
|
env = dict(os.environ, NETALERTX_CONFIG=str(config_dir), NETALERTX_API=str(api_dir))
|
|
result = subprocess.run(
|
|
[PHP_BIN, "-d", f"session.save_path={session_dir}", "-d", "display_errors=stderr", "-r", PHP_RUNNER],
|
|
input=json.dumps({"front": str(FRONT_DIR), "settings": settings}),
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=60,
|
|
check=True,
|
|
)
|
|
assert json.loads(result.stdout)["success"] is True, result.stdout + result.stderr
|
|
return (config_dir / "app.conf").read_text()
|
|
|
|
|
|
def setting_line(source, key):
|
|
"""Return the single app.conf line that assigns key."""
|
|
lines = [line for line in source.splitlines() if line.startswith(f"{key}=")]
|
|
assert len(lines) == 1, f"expected one {key}= line, got {lines}"
|
|
return lines[0]
|
|
|
|
|
|
def parse_app_conf(source):
|
|
"""Compile source with all warnings as errors and exec it like the backend app.conf readers."""
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error")
|
|
code = compile(source, "app.conf", "exec")
|
|
conf = {}
|
|
exec(code, {"__builtins__": {}}, conf)
|
|
return conf
|
|
|
|
|
|
def test_warning_check_rejects_unescaped_backslash():
|
|
"""The warnings-as-errors compile rejects the unescaped regex source that util.php emitted before escaping."""
|
|
with pytest.raises(SyntaxError):
|
|
parse_app_conf(r"X='192\.0\.2\..*'")
|
|
|
|
|
|
def test_generated_app_conf_compiles(app_conf):
|
|
"""The whole app.conf written by saveSettings() compiles without warnings."""
|
|
parse_app_conf(app_conf)
|
|
|
|
|
|
@pytest.mark.parametrize("case_id", CASES.keys())
|
|
def test_scalar_string_round_trip(app_conf, case_id):
|
|
"""A scalar string setting written by saveSettings() compiles cleanly and parses back to the typed value."""
|
|
key = scalar_key(case_id)
|
|
conf = parse_app_conf(setting_line(app_conf, key))
|
|
assert conf[key] == CASES[case_id].replace("'", "{s-quote}")
|
|
|
|
|
|
@pytest.mark.parametrize("case_id", CASES.keys())
|
|
def test_array_string_round_trip(app_conf, case_id):
|
|
"""An array string setting written by saveSettings() compiles cleanly and parses back to the typed values."""
|
|
key = array_key(case_id)
|
|
conf = parse_app_conf(setting_line(app_conf, key))
|
|
assert conf[key] == ["plain", CASES[case_id].replace("'", "{s-quote}")]
|
|
|
|
|
|
def test_doc_regex_exact_source(app_conf):
|
|
"""The documentation regex is emitted with doubled backslashes and parses back unchanged."""
|
|
scalar = setting_line(app_conf, scalar_key("doc_regex"))
|
|
array = setting_line(app_conf, array_key("doc_regex"))
|
|
assert scalar == r"S_DOC_REGEX='192\\.0\\.2\\..*'"
|
|
assert array == r"A_DOC_REGEX=['plain','192\\.0\\.2\\..*']"
|
|
assert parse_app_conf(scalar)["S_DOC_REGEX"] == r"192\.0\.2\..*"
|
|
assert parse_app_conf(array)["A_DOC_REGEX"] == ["plain", r"192\.0\.2\..*"]
|