Keep the graceful final-ACK TCP close and optional FEM control guards from pixitha's browser compatibility fixes. Replace Brotli negotiation and duplicate page assets with one gzip representation compressed by Google Zopfli 0.4.3 using 1,000 iterations. Compress the bootstrap, captive success, compact OTA, and static WiFi setup pages with the same validated build cache. Honor gzip quality values and wildcard precedence. Validate compressed OTA page evidence in the firmware capability gate, and add native and Python regressions plus CI coverage. Remove the unused Brotli build dependency. Validation: 54 native tests, 56 Python/browser checks, full V4.3 build and verified application flash. Actual V4 WiFi responses matched the generated gzip assets; 30 repeat downloads completed without reboot. Chromium loaded the setup and settings pages without script or transfer errors. Physical Android testing is pending because the Pi detects no phone over USB or mDNS.
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import gzip
|
|
from pathlib import Path
|
|
import re
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SERVER = ROOT / "src" / "helpers" / "esp32" / "WebConfigServer.cpp"
|
|
KEYS = ROOT / "src" / "helpers" / "WebConfigKeys.h"
|
|
UI = ROOT / "webui" / "index.html"
|
|
EMBEDDED_UI = ROOT / "src" / "helpers" / "esp32" / "WebConfigHtml.h"
|
|
|
|
|
|
def function_body(source: str, start: str, end: str) -> str:
|
|
return source[source.index(start) : source.index(end, source.index(start))]
|
|
|
|
|
|
def embedded_ui() -> str:
|
|
header = EMBEDDED_UI.read_text(encoding="utf-8")
|
|
length = int(re.search(r"WEBCONFIG_HTML_GZ_LEN = (\d+);", header).group(1))
|
|
array = header.split(
|
|
"const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {", 1
|
|
)[1]
|
|
blob = bytes(
|
|
int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", array)
|
|
)[:length]
|
|
return gzip.decompress(blob).decode("utf-8")
|
|
|
|
|
|
class WebConfigSecretMaskingTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.server = SERVER.read_text(encoding="utf-8")
|
|
cls.keys = KEYS.read_text(encoding="utf-8")
|
|
cls.ui = UI.read_text(encoding="utf-8")
|
|
match = re.search(
|
|
r'static const char SECRET_SENTINEL\[\]\s*=\s*"([^"]*)"\s*;',
|
|
cls.server,
|
|
)
|
|
if match is None:
|
|
raise AssertionError("WebConfigServer has no secret sentinel")
|
|
cls.sentinel = match.group(1)
|
|
|
|
def test_server_and_captive_portal_use_exactly_eight_stars(self):
|
|
self.assertEqual(self.sentinel, "********")
|
|
self.assertEqual(len(self.sentinel), 8)
|
|
|
|
browser = re.search(r'var SENTINEL\s*=\s*"([^"]*)"\s*;', self.ui)
|
|
self.assertIsNotNone(browser)
|
|
self.assertEqual(browser.group(1), self.sentinel)
|
|
|
|
captive_portal = re.search(
|
|
r'var SENTINEL\s*=\s*"([^"]*)"\s*;', embedded_ui()
|
|
)
|
|
self.assertIsNotNone(captive_portal)
|
|
self.assertEqual(captive_portal.group(1), self.sentinel)
|
|
|
|
def test_config_get_masks_only_nonempty_wifi_password(self):
|
|
get_handler = function_body(
|
|
self.server,
|
|
"void WebConfigServer::handleConfigGet(",
|
|
"void WebConfigServer::handleConfigPost(",
|
|
)
|
|
self.assertRegex(
|
|
get_handler,
|
|
r'wifi\["pwd"\]\s*=\s*_wifi_password\[0\]\s*'
|
|
r'\?\s*SECRET_SENTINEL\s*:\s*""\s*;',
|
|
)
|
|
self.assertEqual(get_handler.count('wifi["pwd"]'), 1)
|
|
self.assertNotRegex(
|
|
get_handler,
|
|
r'wifi\["pwd"\]\s*=\s*(?:\(const char\s*\*\)\s*)?'
|
|
r'_wifi_password\s*;',
|
|
)
|
|
|
|
def test_config_post_skips_only_exact_sentinel_for_secret_keys(self):
|
|
post_handler = function_body(
|
|
self.server,
|
|
"void WebConfigServer::handleConfigPost(",
|
|
"void WebConfigServer::handleConfigResult(",
|
|
)
|
|
skip = re.search(
|
|
r"if\s*\(\s*isSecretKey\(key\)\s*&&\s*"
|
|
r"strcmp\(val,\s*SECRET_SENTINEL\)\s*==\s*0\s*\)\s*continue\s*;",
|
|
post_handler,
|
|
)
|
|
self.assertIsNotNone(skip)
|
|
|
|
# The equality guard must run before a BatchEntry/CLI command is built;
|
|
# otherwise an unchanged masked field could persist the placeholder.
|
|
batch_entry = post_handler.index("BatchEntry& e")
|
|
pre_persistence = post_handler[
|
|
post_handler.index("int count = 0;") : batch_entry
|
|
]
|
|
self.assertEqual(pre_persistence.count("continue;"), 1)
|
|
self.assertLess(skip.start(), batch_entry)
|
|
self.assertLess(skip.start(), post_handler.index("count++;"))
|
|
|
|
# strcmp equality means close values remain real password updates, not
|
|
# wildcard matches. Keep representative boundary cases explicit.
|
|
def skipped(key: str, value: str) -> bool:
|
|
return key == "wifi.pwd" and value == self.sentinel
|
|
|
|
self.assertTrue(skipped("wifi.pwd", "********"))
|
|
for value in ("", "*******", "*********", "********x", "x********"):
|
|
self.assertFalse(skipped("wifi.pwd", value), value)
|
|
self.assertFalse(skipped("wifi.ssid", "********"))
|
|
|
|
def test_wifi_password_is_classified_as_a_secret_key(self):
|
|
secret_classifier = function_body(
|
|
self.keys,
|
|
"static inline bool wcIsSecretKey(",
|
|
"static inline bool wcIsSecretReadCommand(",
|
|
)
|
|
self.assertIn('strcmp(key, "wifi.pwd") == 0', secret_classifier)
|
|
|
|
def test_explicit_local_cli_reads_are_not_config_snapshots(self):
|
|
# Both terminal APIs require the authenticated LAN, and neither applies
|
|
# the config form's secret masking to an explicit command reply.
|
|
batch = function_body(self.server, "void WebConfigServer::drainBatch(",
|
|
"void WebConfigServer::attachRoutes(")
|
|
self.assertIn("execAdminCommand(e.cmd, e.reply)", batch)
|
|
self.assertNotIn("wcMaskSecretReply", batch)
|
|
for start, end in [
|
|
("void WebConfigServer::handleCliPost(", "void WebConfigServer::handleCliResult("),
|
|
("bool WebConfigServer::checkTerminalAccess(", "void WebConfigServer::handleTerminalPost("),
|
|
]:
|
|
handler = function_body(self.server, start, end)
|
|
self.assertIn("_mode != MODE_LAN", handler)
|
|
self.assertIn("checkAuth(req)", handler)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|