mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-06 21:13:00 -04:00
BE: address app.conf escaping review feedback
Keep encode_python_string() in front/php/server/util.php, replacing
encode_single_quotes() in place, and remove the standalone
app_conf_encode.php helper file and its require.
Rework the regression test to dispatch the real util.php savesettings
path through the PHP CLI against temporary synthetic config, API and
session directories, covering both scalar string and array string
settings: the generated app.conf must compile with warnings as errors
and parse back to the typed values.
The existing {s-quote} lifecycle is preserved (single quotes are still
written as {s-quote} and converted back by the same consumers), and the
app.conf readers in initialise.py and plugin_helper.py are unchanged.
This commit is contained in:
1 parent
6dbd3f8f29
commit
1c3139bb7b
3 files changed
+88
-48
No files matched your search
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
//------------------------------------------------------------------------------
|
||||
// NetAlertX
|
||||
// Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
//
|
||||
// app_conf_encode.php - Side-effect-free helpers for serializing app.conf values
|
||||
//------------------------------------------------------------------------------
|
||||
# Puche 2021 / 2022+ jokob support@netalertx.com GNU GPLv3
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Encode a string for use inside a single-quoted Python literal in app.conf.
|
||||
* Doubles backslashes so they round-trip unchanged, and replaces ' with the
|
||||
* legacy {s-quote} placeholder that the backend converts back per use.
|
||||
*/
|
||||
function encode_python_string($val) {
|
||||
return str_replace(['\\', '\''], ['\\\\', '{s-quote}'], $val);
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
require dirname(__FILE__).'/../templates/globals.php';
|
||||
require dirname(__FILE__).'/../templates/skinUI.php';
|
||||
require_once dirname(__FILE__).'/app_conf_encode.php';
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -272,6 +271,15 @@ function getSettingValue($setKey) {
|
||||
return 'Could not find setting '.$setKey;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
/**
|
||||
* Encode a string for use inside a single-quoted Python literal in app.conf.
|
||||
* Doubles backslashes so they round-trip unchanged, and replaces ' with the
|
||||
* legacy {s-quote} placeholder that the backend converts back per use.
|
||||
*/
|
||||
function encode_python_string($val) {
|
||||
return str_replace(['\\', '\''], ['\\\\', '{s-quote}'], $val);
|
||||
}
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Helper function to send notifications via the backend API endpoint
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
NetAlertX app.conf String Escaping Tests
|
||||
|
||||
Runs the real PHP encode_python_string() (front/php/server/app_conf_encode.php)
|
||||
through the PHP CLI, embeds its output in app.conf-style Python source the same
|
||||
way util.php saveSettings() does, and checks that the source compiles without
|
||||
warnings and parses back to the typed value (with ' mapped to {s-quote}).
|
||||
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
|
||||
@@ -17,18 +18,26 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ENCODER_PHP = Path(__file__).resolve().parents[2] / "front" / "php" / "server" / "app_conf_encode.php"
|
||||
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")
|
||||
|
||||
# Reads {"path": ..., "cases": [...]} from stdin and prints the encoded cases as JSON.
|
||||
# Stands in for the web request: reads {"front": ..., "settings": [...]} from stdin,
|
||||
# satisfies security.php's request-only dependencies and lets util.php dispatch savesettings.
|
||||
PHP_RUNNER = (
|
||||
'$in = json_decode(stream_get_contents(STDIN), true);'
|
||||
'require $in["path"];'
|
||||
'echo json_encode(array_map("encode_python_string", $in["cases"]));'
|
||||
'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";'
|
||||
'$_REQUEST = ["function" => "savesettings", "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\..*",
|
||||
@@ -42,19 +51,52 @@ CASES = {
|
||||
}
|
||||
|
||||
|
||||
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 encoded():
|
||||
"""Return {case_id: encoded} produced by one PHP CLI run of encode_python_string()."""
|
||||
payload = json.dumps({"path": str(ENCODER_PHP), "cases": list(CASES.values())})
|
||||
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, "-r", PHP_RUNNER],
|
||||
input=payload,
|
||||
[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,
|
||||
)
|
||||
return dict(zip(CASES.keys(), json.loads(result.stdout)))
|
||||
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):
|
||||
@@ -73,24 +115,32 @@ def test_warning_check_rejects_unescaped_backslash():
|
||||
parse_app_conf(r"X='192\.0\.2\..*'")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case_id", CASES.keys())
|
||||
def test_scalar_string_round_trip(encoded, case_id):
|
||||
"""A scalar string setting (X='<encoded>') compiles cleanly and parses back to the typed value."""
|
||||
typed = CASES[case_id]
|
||||
conf = parse_app_conf(f"X='{encoded[case_id]}'\n")
|
||||
assert conf["X"] == typed.replace("'", "{s-quote}")
|
||||
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_array_string_round_trip(encoded, case_id):
|
||||
"""An array setting element (X=['plain','<encoded>']) compiles cleanly and parses back to the typed value."""
|
||||
typed = CASES[case_id]
|
||||
conf = parse_app_conf(f"X=['plain','{encoded[case_id]}']\n")
|
||||
assert conf["X"] == ["plain", typed.replace("'", "{s-quote}")]
|
||||
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}")
|
||||
|
||||
|
||||
def test_doc_regex_exact_source(encoded):
|
||||
@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."""
|
||||
source = f"ICMP_IN_REGEX='{encoded['doc_regex']}'"
|
||||
assert source == r"ICMP_IN_REGEX='192\\.0\\.2\\..*'"
|
||||
assert parse_app_conf(source)["ICMP_IN_REGEX"] == r"192\.0\.2\..*"
|
||||
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\..*"]
|
||||
Reference in new issue
Block a user